chore: merge indev into master
This commit is contained in:
commit
ad949a452f
20 changed files with 315 additions and 185 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
[server]
|
[server]
|
||||||
host = 0.0.0.0
|
host = 0.0.0.0
|
||||||
port = 8087
|
port = 8087
|
||||||
domain = https://search.example.com
|
|
||||||
|
|
||||||
# Default locale (default: en_gb)
|
# Default locale (default: en_gb)
|
||||||
#locale = en_gb
|
#locale = en_gb
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,9 @@ int load_config(const char *filename, Config *config) {
|
||||||
config->host[sizeof(config->host) - 1] = '\0';
|
config->host[sizeof(config->host) - 1] = '\0';
|
||||||
} else if (strcmp(key, "port") == 0) {
|
} else if (strcmp(key, "port") == 0) {
|
||||||
config->port = atoi(value);
|
config->port = atoi(value);
|
||||||
} else if (strcmp(key, "domain") == 0) {
|
|
||||||
strncpy(config->domain, value, sizeof(config->domain) - 1);
|
|
||||||
config->domain[sizeof(config->domain) - 1] = '\0';
|
|
||||||
} else if (strcmp(key, "locale") == 0) {
|
} else if (strcmp(key, "locale") == 0) {
|
||||||
strncpy(config->default_locale, value, sizeof(config->default_locale) - 1);
|
strncpy(config->default_locale, value,
|
||||||
|
sizeof(config->default_locale) - 1);
|
||||||
config->default_locale[sizeof(config->default_locale) - 1] = '\0';
|
config->default_locale[sizeof(config->default_locale) - 1] = '\0';
|
||||||
}
|
}
|
||||||
} else if (strcmp(section, "proxy") == 0) {
|
} else if (strcmp(section, "proxy") == 0) {
|
||||||
|
|
|
||||||
32
src/Main.c
32
src/Main.c
|
|
@ -22,14 +22,35 @@ Config global_config;
|
||||||
|
|
||||||
int handle_opensearch(UrlParams *params) {
|
int handle_opensearch(UrlParams *params) {
|
||||||
(void)params;
|
(void)params;
|
||||||
extern Config global_config;
|
|
||||||
TemplateContext ctx = new_context();
|
TemplateContext ctx = new_context();
|
||||||
context_set(&ctx, "domain", global_config.domain);
|
|
||||||
|
const char *http_host = beaker_get_header("Host");
|
||||||
|
if (http_host == NULL) {
|
||||||
|
http_host = "localhost";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *req_scheme =
|
||||||
|
"https"; // not sure if it's a good idea to just assume https, but you
|
||||||
|
// should probably be using https for anything other than testing
|
||||||
|
// or local network anyways.
|
||||||
|
|
||||||
|
if (strncmp(http_host, "localhost", 9) == 0 ||
|
||||||
|
strncmp(http_host, "127.", 4) == 0 ||
|
||||||
|
strncmp(http_host, "192.168.", 8) == 0 ||
|
||||||
|
strncmp(http_host, "10.", 3) == 0) {
|
||||||
|
req_scheme = "http";
|
||||||
|
}
|
||||||
|
|
||||||
|
context_set(&ctx, "domain", http_host);
|
||||||
|
context_set(&ctx, "scheme", req_scheme);
|
||||||
|
|
||||||
char *rendered = render_template("opensearch.xml", &ctx);
|
char *rendered = render_template("opensearch.xml", &ctx);
|
||||||
serve_data(rendered, strlen(rendered), "application/opensearchdescription+xml");
|
serve_data(rendered, strlen(rendered),
|
||||||
|
"application/opensearchdescription+xml");
|
||||||
|
|
||||||
free(rendered);
|
free(rendered);
|
||||||
free_context(&ctx);
|
free_context(&ctx);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +67,6 @@ int main() {
|
||||||
|
|
||||||
Config cfg = {.host = DEFAULT_HOST,
|
Config cfg = {.host = DEFAULT_HOST,
|
||||||
.port = DEFAULT_PORT,
|
.port = DEFAULT_PORT,
|
||||||
.domain = "",
|
|
||||||
.default_locale = "en_gb",
|
.default_locale = "en_gb",
|
||||||
.proxy = "",
|
.proxy = "",
|
||||||
.proxy_list_file = "",
|
.proxy_list_file = "",
|
||||||
|
|
@ -68,6 +88,7 @@ int main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
set_default_locale(cfg.default_locale);
|
set_default_locale(cfg.default_locale);
|
||||||
|
init_themes("static");
|
||||||
|
|
||||||
global_config = cfg;
|
global_config = cfg;
|
||||||
|
|
||||||
|
|
@ -75,7 +96,8 @@ int main() {
|
||||||
if (loaded > 0) {
|
if (loaded > 0) {
|
||||||
fprintf(stderr, "[INFO] Loaded %d locales\n", loaded);
|
fprintf(stderr, "[INFO] Loaded %d locales\n", loaded);
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "[WARN] No locales loaded (make sure to run from omnisearch directory)\n");
|
fprintf(stderr, "[WARN] No locales loaded (make sure to run from "
|
||||||
|
"omnisearch directory)\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_engines_config(cfg.engines);
|
apply_engines_config(cfg.engines);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@
|
||||||
#include "../Utility/Utility.h"
|
#include "../Utility/Utility.h"
|
||||||
#include <beaker.h>
|
#include <beaker.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
int home_handler(UrlParams *params) {
|
int home_handler(UrlParams *params) {
|
||||||
(void)params;
|
(void)params;
|
||||||
char *theme = get_theme("");
|
char *theme = get_theme("");
|
||||||
char *locale = get_locale(NULL);
|
char *locale = get_locale(NULL);
|
||||||
|
|
||||||
|
char **themes = NULL;
|
||||||
|
int themes_count = 0;
|
||||||
|
get_available_themes(&themes, &themes_count);
|
||||||
|
|
||||||
TemplateContext ctx = new_context();
|
TemplateContext ctx = new_context();
|
||||||
context_set(&ctx, "theme", theme);
|
context_set(&ctx, "theme", theme);
|
||||||
context_set(&ctx, "version", VERSION);
|
context_set(&ctx, "version", VERSION);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
#include "../Utility/Utility.h"
|
#include "../Utility/Utility.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include <beaker.h>
|
#include <beaker.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
static char *build_images_request_cache_key(const char *query, int page,
|
static char *build_images_request_cache_key(const char *query, int page,
|
||||||
const char *client_key) {
|
const char *client_key) {
|
||||||
|
|
@ -67,15 +69,18 @@ int images_handler(UrlParams *params) {
|
||||||
beaker_set_locale(&ctx, locale);
|
beaker_set_locale(&ctx, locale);
|
||||||
|
|
||||||
const char *rate_limit_msg = beaker_get_locale_value(locale, "rate_limit");
|
const char *rate_limit_msg = beaker_get_locale_value(locale, "rate_limit");
|
||||||
if (!rate_limit_msg) rate_limit_msg = "Slow down! Too many image searches from you!";
|
if (!rate_limit_msg)
|
||||||
const char *error_images_msg = beaker_get_locale_value(locale, "error_images");
|
rate_limit_msg = "Slow down! Too many image searches from you!";
|
||||||
if (!error_images_msg) error_images_msg = "Error fetching images";
|
const char *error_images_msg =
|
||||||
|
beaker_get_locale_value(locale, "error_images");
|
||||||
|
if (!error_images_msg)
|
||||||
|
error_images_msg = "Error fetching images";
|
||||||
|
|
||||||
char ***pager_matrix = NULL;
|
char ***pager_matrix = NULL;
|
||||||
int *pager_inner_counts = NULL;
|
int *pager_inner_counts = NULL;
|
||||||
int pager_count = build_pagination(page, images_href_builder,
|
int pager_count =
|
||||||
(void *)raw_query, &pager_matrix,
|
build_pagination(page, images_href_builder, (void *)raw_query,
|
||||||
&pager_inner_counts);
|
&pager_matrix, &pager_inner_counts);
|
||||||
if (pager_count > 0) {
|
if (pager_count > 0) {
|
||||||
context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix,
|
context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix,
|
||||||
pager_count, pager_inner_counts);
|
pager_count, pager_inner_counts);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ int settings_handler(UrlParams *params) {
|
||||||
|
|
||||||
char **user_engines = NULL;
|
char **user_engines = NULL;
|
||||||
int user_engine_count = 0;
|
int user_engine_count = 0;
|
||||||
int has_user_pref = (get_user_engines(&user_engines, &user_engine_count) == 0);
|
int has_user_pref =
|
||||||
|
(get_user_engines(&user_engines, &user_engine_count) == 0);
|
||||||
|
|
||||||
int enabled_count = 0;
|
int enabled_count = 0;
|
||||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||||
|
|
@ -77,7 +78,34 @@ int settings_handler(UrlParams *params) {
|
||||||
context_set(&ctx, "query", query);
|
context_set(&ctx, "query", query);
|
||||||
context_set(&ctx, "theme", theme);
|
context_set(&ctx, "theme", theme);
|
||||||
context_set(&ctx, "locale", locale);
|
context_set(&ctx, "locale", locale);
|
||||||
context_set_array_of_arrays(&ctx, "locales", locale_data, locale_count, inner_counts);
|
context_set_array_of_arrays(&ctx, "locales", locale_data, locale_count,
|
||||||
|
inner_counts);
|
||||||
|
|
||||||
|
char **themes = NULL;
|
||||||
|
int themes_count = 0;
|
||||||
|
get_available_themes(&themes, &themes_count);
|
||||||
|
|
||||||
|
if (themes_count > 0) {
|
||||||
|
char ***theme_ptrs = malloc(sizeof(char **) * themes_count);
|
||||||
|
int *theme_inner = malloc(sizeof(int) * themes_count);
|
||||||
|
for (int i = 0; i < themes_count; i++) {
|
||||||
|
theme_ptrs[i] = malloc(sizeof(char *) * 2);
|
||||||
|
theme_ptrs[i][0] = themes[i];
|
||||||
|
theme_ptrs[i][1] = strdup(themes[i]);
|
||||||
|
if (theme_ptrs[i][1][0] >= 'a' && theme_ptrs[i][1][0] <= 'z') {
|
||||||
|
theme_ptrs[i][1][0] = theme_ptrs[i][1][0] - 'a' + 'A';
|
||||||
|
}
|
||||||
|
theme_inner[i] = 2;
|
||||||
|
}
|
||||||
|
context_set_array_of_arrays(&ctx, "themes", theme_ptrs, themes_count,
|
||||||
|
theme_inner);
|
||||||
|
for (int i = 0; i < themes_count; i++) {
|
||||||
|
free(theme_ptrs[i][1]);
|
||||||
|
free(theme_ptrs[i]);
|
||||||
|
}
|
||||||
|
free(theme_ptrs);
|
||||||
|
free(theme_inner);
|
||||||
|
}
|
||||||
|
|
||||||
if (enabled_count > 0) {
|
if (enabled_count > 0) {
|
||||||
context_set_array_of_arrays(&ctx, "enabled_engines", engine_data,
|
context_set_array_of_arrays(&ctx, "enabled_engines", engine_data,
|
||||||
|
|
|
||||||
|
|
@ -28,113 +28,82 @@ static char *build_proxy_url(const char *image_url) {
|
||||||
return proxy_url;
|
return proxy_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int parse_image_node(xmlNodePtr node, ImageResult *result) {
|
static char *extract_json_string(const char *json, const char *key) {
|
||||||
xmlNodePtr img_node = NULL;
|
if (!json || !key)
|
||||||
xmlNodePtr tit_node = NULL;
|
return NULL;
|
||||||
xmlNodePtr des_node = NULL;
|
|
||||||
xmlNodePtr thumb_link = NULL;
|
|
||||||
|
|
||||||
for (xmlNodePtr child = node->children; child; child = child->next) {
|
char search_key[64];
|
||||||
if (child->type != XML_ELEMENT_NODE)
|
snprintf(search_key, sizeof(search_key), "\"%s\"", key);
|
||||||
continue;
|
|
||||||
|
|
||||||
if (xmlStrcmp(child->name, (const xmlChar *)"a") == 0) {
|
const char *key_pos = strstr(json, search_key);
|
||||||
xmlChar *class = xmlGetProp(child, (const xmlChar *)"class");
|
if (!key_pos)
|
||||||
if (class) {
|
return NULL;
|
||||||
if (xmlStrstr(class, (const xmlChar *)"thumb") != NULL) {
|
|
||||||
thumb_link = child;
|
const char *colon = strchr(key_pos + strlen(search_key), ':');
|
||||||
for (xmlNodePtr thumb_child = child->children; thumb_child;
|
if (!colon)
|
||||||
thumb_child = thumb_child->next) {
|
return NULL;
|
||||||
if (xmlStrcmp(thumb_child->name, (const xmlChar *)"div") == 0) {
|
|
||||||
xmlChar *div_class =
|
colon++;
|
||||||
xmlGetProp(thumb_child, (const xmlChar *)"class");
|
while (*colon == ' ' || *colon == '\t' || *colon == '\n' || *colon == '\r')
|
||||||
if (div_class &&
|
colon++;
|
||||||
xmlStrcmp(div_class, (const xmlChar *)"cico") == 0) {
|
|
||||||
for (xmlNodePtr cico_child = thumb_child->children; cico_child;
|
if (*colon != '"')
|
||||||
cico_child = cico_child->next) {
|
return NULL;
|
||||||
if (xmlStrcmp(cico_child->name, (const xmlChar *)"img") ==
|
colon++;
|
||||||
0) {
|
|
||||||
img_node = cico_child;
|
size_t len = 0;
|
||||||
break;
|
const char *start = colon;
|
||||||
}
|
while (*colon && *colon != '"') {
|
||||||
}
|
if (*colon == '\\' && *(colon + 1))
|
||||||
}
|
colon++;
|
||||||
if (div_class)
|
colon++;
|
||||||
xmlFree(div_class);
|
len++;
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (xmlStrstr(class, (const xmlChar *)"tit") != NULL) {
|
|
||||||
tit_node = child;
|
|
||||||
}
|
|
||||||
xmlFree(class);
|
|
||||||
}
|
|
||||||
} else if (xmlStrcmp(child->name, (const xmlChar *)"div") == 0) {
|
|
||||||
xmlChar *class = xmlGetProp(child, (const xmlChar *)"class");
|
|
||||||
if (class && xmlStrcmp(class, (const xmlChar *)"meta") == 0) {
|
|
||||||
for (xmlNodePtr meta_child = child->children; meta_child;
|
|
||||||
meta_child = meta_child->next) {
|
|
||||||
if (xmlStrcmp(meta_child->name, (const xmlChar *)"div") == 0) {
|
|
||||||
xmlChar *div_class =
|
|
||||||
xmlGetProp(meta_child, (const xmlChar *)"class");
|
|
||||||
if (div_class) {
|
|
||||||
if (xmlStrcmp(div_class, (const xmlChar *)"des") == 0) {
|
|
||||||
des_node = meta_child;
|
|
||||||
}
|
|
||||||
xmlFree(div_class);
|
|
||||||
}
|
|
||||||
} else if (xmlStrcmp(meta_child->name, (const xmlChar *)"a") == 0) {
|
|
||||||
xmlChar *a_class = xmlGetProp(meta_child, (const xmlChar *)"class");
|
|
||||||
if (a_class && xmlStrstr(a_class, (const xmlChar *)"tit") != NULL) {
|
|
||||||
tit_node = meta_child;
|
|
||||||
}
|
|
||||||
if (a_class)
|
|
||||||
xmlFree(a_class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (class)
|
|
||||||
xmlFree(class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xmlChar *iurl =
|
char *result = malloc(len + 1);
|
||||||
img_node ? xmlGetProp(img_node, (const xmlChar *)"src") : NULL;
|
if (!result)
|
||||||
xmlChar *full_url =
|
return NULL;
|
||||||
thumb_link ? xmlGetProp(thumb_link, (const xmlChar *)"href") : NULL;
|
|
||||||
xmlChar *title = des_node ? xmlNodeGetContent(des_node)
|
|
||||||
: (tit_node ? xmlNodeGetContent(tit_node) : NULL);
|
|
||||||
xmlChar *rurl =
|
|
||||||
tit_node ? xmlGetProp(tit_node, (const xmlChar *)"href") : NULL;
|
|
||||||
|
|
||||||
if (!iurl || strlen((char *)iurl) == 0) {
|
colon = start;
|
||||||
if (iurl)
|
size_t i = 0;
|
||||||
xmlFree(iurl);
|
while (*colon && *colon != '"') {
|
||||||
if (title)
|
if (*colon == '\\' && *(colon + 1))
|
||||||
xmlFree(title);
|
colon++;
|
||||||
if (rurl)
|
result[i++] = *colon++;
|
||||||
xmlFree(rurl);
|
}
|
||||||
if (full_url)
|
result[i] = '\0';
|
||||||
xmlFree(full_url);
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_iusc_node(xmlNodePtr node, ImageResult *result) {
|
||||||
|
xmlChar *m_attr = xmlGetProp(node, (const xmlChar *)"m");
|
||||||
|
if (!m_attr)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
char *turl = extract_json_string((const char *)m_attr, "turl");
|
||||||
|
char *murl = extract_json_string((const char *)m_attr, "murl");
|
||||||
|
char *purl = extract_json_string((const char *)m_attr, "purl");
|
||||||
|
char *title = extract_json_string((const char *)m_attr, "t");
|
||||||
|
|
||||||
|
int ok = (turl != NULL && strlen(turl) > 0);
|
||||||
|
if (ok) {
|
||||||
|
char *proxy_url = build_proxy_url(turl);
|
||||||
|
result->thumbnail_url = proxy_url ? strdup(proxy_url) : strdup(turl);
|
||||||
|
free(proxy_url);
|
||||||
|
result->title =
|
||||||
|
title && strlen(title) > 0 ? strdup(title) : strdup("Image");
|
||||||
|
result->page_url = purl && strlen(purl) > 0 ? strdup(purl) : strdup("#");
|
||||||
|
result->full_url = murl && strlen(murl) > 0 ? strdup(murl) : strdup("#");
|
||||||
}
|
}
|
||||||
|
|
||||||
char *proxy_url = build_proxy_url((char *)iurl);
|
free(turl);
|
||||||
result->thumbnail_url = proxy_url ? strdup(proxy_url) : strdup((char *)iurl);
|
free(murl);
|
||||||
free(proxy_url);
|
free(purl);
|
||||||
result->title = strdup(title ? (char *)title : "Image");
|
free(title);
|
||||||
result->page_url = strdup(rurl ? (char *)rurl : "#");
|
|
||||||
result->full_url = strdup(full_url ? (char *)full_url : "#");
|
|
||||||
|
|
||||||
if (iurl)
|
xmlFree(m_attr);
|
||||||
xmlFree(iurl);
|
return ok;
|
||||||
if (title)
|
|
||||||
xmlFree(title);
|
|
||||||
if (rurl)
|
|
||||||
xmlFree(rurl);
|
|
||||||
if (full_url)
|
|
||||||
xmlFree(full_url);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int scrape_images(const char *query, int page, ImageResult **out_results,
|
int scrape_images(const char *query, int page, ImageResult **out_results,
|
||||||
|
|
@ -157,13 +126,16 @@ int scrape_images(const char *query, int page, ImageResult **out_results,
|
||||||
|
|
||||||
char url[BUFFER_SIZE_LARGE];
|
char url[BUFFER_SIZE_LARGE];
|
||||||
int first = (page - 1) * IMAGE_RESULTS_PER_PAGE + 1;
|
int first = (page - 1) * IMAGE_RESULTS_PER_PAGE + 1;
|
||||||
snprintf(url, sizeof(url), "%s?q=%s&first=%d", BING_IMAGE_URL, encoded_query,
|
snprintf(
|
||||||
first);
|
url, sizeof(url),
|
||||||
|
"https://www.bing.com/images/async?q=%s&async=content&first=%d&count=%d",
|
||||||
|
encoded_query, first, 35);
|
||||||
free(encoded_query);
|
free(encoded_query);
|
||||||
|
|
||||||
HttpResponse resp = http_get(
|
HttpResponse resp = http_get(
|
||||||
url,
|
url,
|
||||||
"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko");
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, "
|
||||||
|
"like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
if (!resp.memory) {
|
if (!resp.memory) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +155,7 @@ int scrape_images(const char *query, int page, ImageResult **out_results,
|
||||||
}
|
}
|
||||||
|
|
||||||
xmlXPathObjectPtr xpathObj =
|
xmlXPathObjectPtr xpathObj =
|
||||||
xmlXPathEvalExpression((const xmlChar *)"//div[@class='item']", xpathCtx);
|
xmlXPathEvalExpression((const xmlChar *)"//a[@class='iusc']", xpathCtx);
|
||||||
|
|
||||||
if (!xpathObj || !xpathObj->nodesetval) {
|
if (!xpathObj || !xpathObj->nodesetval) {
|
||||||
if (xpathObj)
|
if (xpathObj)
|
||||||
|
|
@ -210,7 +182,7 @@ int scrape_images(const char *query, int page, ImageResult **out_results,
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < nodes && count < IMAGE_RESULTS_PER_PAGE; i++) {
|
for (int i = 0; i < nodes && count < IMAGE_RESULTS_PER_PAGE; i++) {
|
||||||
xmlNodePtr node = xpathObj->nodesetval->nodeTab[i];
|
xmlNodePtr node = xpathObj->nodesetval->nodeTab[i];
|
||||||
if (parse_image_node(node, &results[count])) {
|
if (parse_iusc_node(node, &results[count])) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,17 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb,
|
||||||
return realsize;
|
return realsize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static struct curl_slist *build_http_headers(void) {
|
||||||
|
struct curl_slist *headers = NULL;
|
||||||
|
headers = curl_slist_append(
|
||||||
|
headers,
|
||||||
|
"Accept: "
|
||||||
|
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
|
||||||
|
headers = curl_slist_append(headers, "Accept-Language: en-US,en;q=0.5");
|
||||||
|
headers = curl_slist_append(headers, "DNT: 1");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
HttpResponse http_get(const char *url, const char *user_agent) {
|
HttpResponse http_get(const char *url, const char *user_agent) {
|
||||||
HttpResponse resp = {.memory = NULL, .size = 0, .capacity = 0};
|
HttpResponse resp = {.memory = NULL, .size = 0, .capacity = 0};
|
||||||
|
|
||||||
|
|
@ -51,16 +62,24 @@ HttpResponse http_get(const char *url, const char *user_agent) {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct curl_slist *headers = build_http_headers();
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT,
|
curl_easy_setopt(curl, CURLOPT_USERAGENT,
|
||||||
user_agent ? user_agent : "libcurl-agent/1.0");
|
user_agent ? user_agent : "libcurl-agent/1.0");
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, CURL_TIMEOUT_SECS);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, CURL_DNS_TIMEOUT_SECS);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
apply_proxy_settings(curl);
|
apply_proxy_settings(curl);
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
curl_slist_free_all(headers);
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
if (res != CURLE_OK) {
|
if (res != CURLE_OK) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,80 @@
|
||||||
#include "Utility.h"
|
#include "Utility.h"
|
||||||
#include "../Scraping/Scraping.h"
|
#include "../Scraping/Scraping.h"
|
||||||
#include <beaker.h>
|
#include <beaker.h>
|
||||||
|
#include <dirent.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
static char global_default_locale[32] = "en_gb";
|
static char global_default_locale[32] = "en_gb";
|
||||||
|
static char **themes_list = NULL;
|
||||||
|
static int themes_count = 0;
|
||||||
|
static int themes_initialized = 0;
|
||||||
|
|
||||||
|
void init_themes(const char *static_path) {
|
||||||
|
if (themes_initialized)
|
||||||
|
return;
|
||||||
|
themes_initialized = 1;
|
||||||
|
|
||||||
|
char themes_dir[512];
|
||||||
|
snprintf(themes_dir, sizeof(themes_dir), "%s/themes", static_path);
|
||||||
|
|
||||||
|
DIR *dir = opendir(themes_dir);
|
||||||
|
if (!dir)
|
||||||
|
return;
|
||||||
|
|
||||||
|
struct dirent *entry;
|
||||||
|
int capacity = 4;
|
||||||
|
themes_list = malloc(sizeof(char *) * capacity);
|
||||||
|
themes_count = 0;
|
||||||
|
|
||||||
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
|
size_t len = strlen(entry->d_name);
|
||||||
|
if (len > 4 && strcmp(entry->d_name + len - 4, ".css") == 0) {
|
||||||
|
if (themes_count >= capacity) {
|
||||||
|
capacity *= 2;
|
||||||
|
themes_list = realloc(themes_list, sizeof(char *) * capacity);
|
||||||
|
}
|
||||||
|
themes_list[themes_count] = strndup(entry->d_name, len - 4);
|
||||||
|
themes_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
|
||||||
|
for (int i = 0; i < themes_count; i++) {
|
||||||
|
for (int j = i + 1; j < themes_count; j++) {
|
||||||
|
int priority_i = 0, priority_j = 0;
|
||||||
|
if (strcmp(themes_list[i], "system") == 0)
|
||||||
|
priority_i = 0;
|
||||||
|
else if (strcmp(themes_list[i], "light") == 0)
|
||||||
|
priority_i = 1;
|
||||||
|
else if (strcmp(themes_list[i], "dark") == 0)
|
||||||
|
priority_i = 2;
|
||||||
|
else
|
||||||
|
priority_i = 3;
|
||||||
|
if (strcmp(themes_list[j], "system") == 0)
|
||||||
|
priority_j = 0;
|
||||||
|
else if (strcmp(themes_list[j], "light") == 0)
|
||||||
|
priority_j = 1;
|
||||||
|
else if (strcmp(themes_list[j], "dark") == 0)
|
||||||
|
priority_j = 2;
|
||||||
|
else
|
||||||
|
priority_j = 3;
|
||||||
|
if (priority_i > priority_j ||
|
||||||
|
(priority_i == priority_j &&
|
||||||
|
strcmp(themes_list[i], themes_list[j]) > 0)) {
|
||||||
|
char *tmp = themes_list[i];
|
||||||
|
themes_list[i] = themes_list[j];
|
||||||
|
themes_list[j] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_available_themes(char ***out_themes, int *out_count) {
|
||||||
|
*out_themes = themes_list;
|
||||||
|
*out_count = themes_count;
|
||||||
|
}
|
||||||
|
|
||||||
void set_default_locale(const char *locale) {
|
void set_default_locale(const char *locale) {
|
||||||
if (locale && strlen(locale) > 0) {
|
if (locale && strlen(locale) > 0) {
|
||||||
|
|
@ -26,13 +95,16 @@ int hex_to_int(char c) {
|
||||||
|
|
||||||
char *get_theme(const char *default_theme) {
|
char *get_theme(const char *default_theme) {
|
||||||
char *cookie = get_cookie("theme");
|
char *cookie = get_cookie("theme");
|
||||||
if (cookie &&
|
if (cookie && strlen(cookie) > 0) {
|
||||||
(strcmp(cookie, "light") == 0 ||
|
for (int i = 0; i < themes_count; i++) {
|
||||||
strcmp(cookie, "dark") == 0)) {
|
if (strcmp(cookie, themes_list[i]) == 0) {
|
||||||
return cookie;
|
return cookie;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
free(cookie);
|
free(cookie);
|
||||||
return strdup(default_theme);
|
return strdup(default_theme && strlen(default_theme) > 0 ? default_theme
|
||||||
|
: "system");
|
||||||
}
|
}
|
||||||
|
|
||||||
char *get_locale(const char *default_locale) {
|
char *get_locale(const char *default_locale) {
|
||||||
|
|
@ -41,7 +113,8 @@ char *get_locale(const char *default_locale) {
|
||||||
return cookie;
|
return cookie;
|
||||||
}
|
}
|
||||||
free(cookie);
|
free(cookie);
|
||||||
const char *fallback = default_locale ? default_locale : global_default_locale;
|
const char *fallback =
|
||||||
|
default_locale ? default_locale : global_default_locale;
|
||||||
return strdup(fallback);
|
return strdup(fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,9 +122,12 @@ static int engine_id_casecmp(const char *a, const char *b) {
|
||||||
while (*a && *b) {
|
while (*a && *b) {
|
||||||
char la = *a;
|
char la = *a;
|
||||||
char lb = *b;
|
char lb = *b;
|
||||||
if (la >= 'A' && la <= 'Z') la = la - 'A' + 'a';
|
if (la >= 'A' && la <= 'Z')
|
||||||
if (lb >= 'A' && lb <= 'Z') lb = lb - 'A' + 'a';
|
la = la - 'A' + 'a';
|
||||||
if (la != lb) return 0;
|
if (lb >= 'A' && lb <= 'Z')
|
||||||
|
lb = lb - 'A' + 'a';
|
||||||
|
if (la != lb)
|
||||||
|
return 0;
|
||||||
a++;
|
a++;
|
||||||
b++;
|
b++;
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +135,8 @@ static int engine_id_casecmp(const char *a, const char *b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int is_engine_id_enabled(const char *engine_id) {
|
int is_engine_id_enabled(const char *engine_id) {
|
||||||
if (!engine_id) return 0;
|
if (!engine_id)
|
||||||
|
return 0;
|
||||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||||
if (ENGINE_REGISTRY[i].enabled &&
|
if (ENGINE_REGISTRY[i].enabled &&
|
||||||
engine_id_casecmp(ENGINE_REGISTRY[i].id, engine_id)) {
|
engine_id_casecmp(ENGINE_REGISTRY[i].id, engine_id)) {
|
||||||
|
|
@ -120,7 +197,8 @@ int get_user_engines(char ***out_ids, int *out_count) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int user_engines_contains(const char *engine_id, char **ids, int count) {
|
int user_engines_contains(const char *engine_id, char **ids, int count) {
|
||||||
if (!engine_id || !ids) return 0;
|
if (!engine_id || !ids)
|
||||||
|
return 0;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
if (engine_id_casecmp(ids[i], engine_id))
|
if (engine_id_casecmp(ids[i], engine_id))
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -135,8 +213,7 @@ int add_link_to_collection(const char *href, const char *label,
|
||||||
int *old_inner_counts = *inner_counts;
|
int *old_inner_counts = *inner_counts;
|
||||||
char ***new_collection =
|
char ***new_collection =
|
||||||
(char ***)malloc(sizeof(char **) * (current_count + 1));
|
(char ***)malloc(sizeof(char **) * (current_count + 1));
|
||||||
int *new_inner_counts =
|
int *new_inner_counts = (int *)malloc(sizeof(int) * (current_count + 1));
|
||||||
(int *)malloc(sizeof(int) * (current_count + 1));
|
|
||||||
|
|
||||||
if (!new_collection || !new_inner_counts) {
|
if (!new_collection || !new_inner_counts) {
|
||||||
free(new_collection);
|
free(new_collection);
|
||||||
|
|
@ -188,9 +265,8 @@ int add_link_to_collection(const char *href, const char *label,
|
||||||
return current_count + 1;
|
return current_count + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int build_pagination(int page,
|
int build_pagination(int page, char *(*href_builder)(int page, void *data),
|
||||||
char *(*href_builder)(int page, void *data), void *data,
|
void *data, char ****out_matrix, int **out_inner_counts) {
|
||||||
char ****out_matrix, int **out_inner_counts) {
|
|
||||||
enum { PAGER_WINDOW_SIZE = 5 };
|
enum { PAGER_WINDOW_SIZE = 5 };
|
||||||
|
|
||||||
*out_matrix = NULL;
|
*out_matrix = NULL;
|
||||||
|
|
@ -202,8 +278,8 @@ int build_pagination(int page,
|
||||||
|
|
||||||
if (page > 1) {
|
if (page > 1) {
|
||||||
char *href = href_builder(page - 1, data);
|
char *href = href_builder(page - 1, data);
|
||||||
count = add_link_to_collection(href, "←", "pagination-btn prev",
|
count = add_link_to_collection(href, "←", "pagination-btn prev", out_matrix,
|
||||||
out_matrix, out_inner_counts, count);
|
out_inner_counts, count);
|
||||||
free(href);
|
free(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,8 +295,8 @@ int build_pagination(int page,
|
||||||
}
|
}
|
||||||
|
|
||||||
char *href = href_builder(page + 1, data);
|
char *href = href_builder(page + 1, data);
|
||||||
count = add_link_to_collection(href, "→", "pagination-btn next",
|
count = add_link_to_collection(href, "→", "pagination-btn next", out_matrix,
|
||||||
out_matrix, out_inner_counts, count);
|
out_inner_counts, count);
|
||||||
free(href);
|
free(href);
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
int hex_to_int(char c);
|
int hex_to_int(char c);
|
||||||
char *get_theme(const char *default_theme);
|
char *get_theme(const char *default_theme);
|
||||||
|
void init_themes(const char *static_path);
|
||||||
|
void get_available_themes(char ***out_themes, int *out_count);
|
||||||
void set_default_locale(const char *locale);
|
void set_default_locale(const char *locale);
|
||||||
char *get_locale(const char *default_locale);
|
char *get_locale(const char *default_locale);
|
||||||
|
|
||||||
|
|
@ -26,8 +28,7 @@ int add_link_to_collection(const char *href, const char *label,
|
||||||
const char *class_name, char ****collection,
|
const char *class_name, char ****collection,
|
||||||
int **inner_counts, int current_count);
|
int **inner_counts, int current_count);
|
||||||
|
|
||||||
int build_pagination(int page,
|
int build_pagination(int page, char *(*href_builder)(int page, void *data),
|
||||||
char *(*href_builder)(int page, void *data), void *data,
|
void *data, char ****out_matrix, int **out_inner_counts);
|
||||||
char ****out_matrix, int **out_inner_counts);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,3 @@
|
||||||
:root {
|
|
||||||
--bg-main: #ffffff;
|
|
||||||
--bg-card: #f8f9fa;
|
|
||||||
--border: #e0e0e0;
|
|
||||||
--text-primary: #1a1a1a;
|
|
||||||
--text-secondary: #5f6368;
|
|
||||||
--text-muted: #757575;
|
|
||||||
--accent: #202124;
|
|
||||||
--accent-glow: rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg-main: #121212;
|
|
||||||
--bg-card: #1e1e1e;
|
|
||||||
--border: #333333;
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #a0a0a0;
|
|
||||||
--text-muted: #d1d1d1;
|
|
||||||
--accent: #e2e2e2;
|
|
||||||
--accent-glow: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
|
|
||||||
10
static/themes/catppuccin mocha.css
Normal file
10
static/themes/catppuccin mocha.css
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
:root {
|
||||||
|
--bg-main: #181825;
|
||||||
|
--bg-card: #1e1e2e;
|
||||||
|
--border: #313244;
|
||||||
|
--text-primary: #cdd6f4;
|
||||||
|
--text-secondary: #a6adc8;
|
||||||
|
--text-muted: #6c7086;
|
||||||
|
--accent: #cba6f7;
|
||||||
|
--accent-glow: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
22
static/themes/system.css
Normal file
22
static/themes/system.css
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
:root {
|
||||||
|
--bg-main: #ffffff;
|
||||||
|
--bg-card: #f8f9fa;
|
||||||
|
--border: #e0e0e0;
|
||||||
|
--text-primary: #1a1a1a;
|
||||||
|
--text-secondary: #5f6368;
|
||||||
|
--text-muted: #757575;
|
||||||
|
--accent: #202124;
|
||||||
|
--accent-glow: rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg-main: #121212;
|
||||||
|
--bg-card: #1e1e1e;
|
||||||
|
--border: #333333;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--text-muted: #d1d1d1;
|
||||||
|
--accent: #e2e2e2;
|
||||||
|
--accent-glow: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
OmniSearch
|
OmniSearch
|
||||||
</title>
|
</title>
|
||||||
<link rel="stylesheet" href="static/main.css">
|
<link rel="stylesheet" href="static/main.css">
|
||||||
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
|
<link rel="stylesheet" href="static/themes/{{theme}}.css">
|
||||||
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
<link rel="search"
|
<link rel="search"
|
||||||
type="application/opensearchdescription+xml"
|
type="application/opensearchdescription+xml"
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@
|
||||||
</title>
|
</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
<link rel="stylesheet" href="static/main.css">
|
<link rel="stylesheet" href="static/main.css">
|
||||||
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
|
<link rel="stylesheet" href="static/themes/{{theme}}.css">
|
||||||
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="images-view">
|
<body class="images-view">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
|
||||||
<ShortName>OmniSearch</ShortName>
|
<ShortName>OmniSearch</ShortName>
|
||||||
<Description>Lightweight metasearch engine</Description>
|
<Description>Lightweight metasearch engine</Description>
|
||||||
<Url type="text/html" method="get" template="{{domain}}/search?q={searchTerms}"/>
|
<Url type="text/html" method="get" template="{{scheme}}://{{domain}}/search?q={searchTerms}"/>
|
||||||
<InputEncoding>UTF-8</InputEncoding>
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
<OutputEncoding>UTF-8</OutputEncoding>
|
<OutputEncoding>UTF-8</OutputEncoding>
|
||||||
<moz:SearchForm>{{domain}}/</moz:SearchForm>
|
<moz:SearchForm>{{domain}}/</moz:SearchForm>
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
OmniSearch - {{query}}
|
OmniSearch - {{query}}
|
||||||
</title>
|
</title>
|
||||||
<link rel="stylesheet" href="static/main.css">
|
<link rel="stylesheet" href="static/main.css">
|
||||||
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
|
<link rel="stylesheet" href="static/themes/{{theme}}.css">
|
||||||
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
<link rel="search"
|
<link rel="search"
|
||||||
type="application/opensearchdescription+xml"
|
type="application/opensearchdescription+xml"
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
OmniSearch - {{l("settings_title")}}
|
OmniSearch - {{l("settings_title")}}
|
||||||
</title>
|
</title>
|
||||||
<link rel="stylesheet" href="static/main.css">
|
<link rel="stylesheet" href="static/main.css">
|
||||||
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
|
<link rel="stylesheet" href="static/themes/{{theme}}.css">
|
||||||
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
<link rel="search"
|
<link rel="search"
|
||||||
type="application/opensearchdescription+xml"
|
type="application/opensearchdescription+xml"
|
||||||
|
|
@ -58,9 +57,9 @@
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label class="settings-label" for="theme">{{l("theme_label")}}</label>
|
<label class="settings-label" for="theme">{{l("theme_label")}}</label>
|
||||||
<select id="theme" name="theme" class="settings-select">
|
<select id="theme" name="theme" class="settings-select">
|
||||||
<option value="system" {{if theme == "system"}}selected{{endif}}>{{l("theme_system")}}</option>
|
{{for t in themes}}
|
||||||
<option value="light" {{if theme == "light"}}selected{{endif}}>{{l("theme_light")}}</option>
|
<option value="{{t[0]}}" {{if theme == t[0]}}selected{{endif}}>{{t[1]}}</option>
|
||||||
<option value="dark" {{if theme == "dark"}}selected{{endif}}>{{l("theme_dark")}}</option>
|
{{endfor}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue