Compare commits
No commits in common. "indev" and "master" have entirely different histories.
32 changed files with 294 additions and 1880 deletions
61
Dockerfile
61
Dockerfile
|
|
@ -1,46 +1,39 @@
|
|||
# ---------- Build stage ----------
|
||||
FROM alpine:3.20 AS builder
|
||||
FROM alpine:latest
|
||||
|
||||
# Install required dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
curl-dev \
|
||||
libxml2-dev \
|
||||
openssl-dev \
|
||||
curl-dev \
|
||||
shadow \
|
||||
git \
|
||||
pkgconf
|
||||
make \
|
||||
gcc \
|
||||
musl-dev \
|
||||
pkgconf \
|
||||
openssl-dev \
|
||||
openrc
|
||||
|
||||
# Clone and install beaker
|
||||
RUN git clone https://git.bwaaa.monster/beaker /tmp/beaker \
|
||||
&& cd /tmp/beaker \
|
||||
&& make \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/beaker
|
||||
|
||||
# Import omnisearch source
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
RUN git clone https://git.bwaaa.monster/beaker /tmp/beaker && \
|
||||
cd /tmp/beaker && \
|
||||
make && make install && \
|
||||
rm -rf /tmp/beaker
|
||||
# Clone and install omnisearch
|
||||
RUN cd /app \
|
||||
&& make \
|
||||
&& make install-openrc
|
||||
|
||||
RUN make clean && make && strip bin/omnisearch
|
||||
|
||||
# ---------- Runtime stage ----------
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libcurl \
|
||||
libxml2 \
|
||||
openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only required artifacts
|
||||
COPY --from=builder /app/bin/omnisearch /app/omnisearch
|
||||
COPY --from=builder /usr/lib/libbeaker.so /usr/lib/libbeaker.so
|
||||
COPY --from=builder /app/templates /app/templates
|
||||
COPY --from=builder /app/static /app/static
|
||||
|
||||
# Security: non-root user
|
||||
RUN adduser -D appuser && chmod +x /app/omnisearch
|
||||
USER appuser
|
||||
|
||||
ENV LD_LIBRARY_PATH=/usr/lib
|
||||
# Enable OpenRC and start the service
|
||||
RUN rc-update add omnisearch default
|
||||
|
||||
# Expose the default port
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["/app/omnisearch"]
|
||||
# Start OpenRC and the service
|
||||
CMD sh -c "openrc default && touch /run/openrc/softlevel && omnisearch"
|
||||
|
|
|
|||
18
Makefile
18
Makefile
|
|
@ -95,10 +95,9 @@ install:
|
|||
@echo "Example: doas/sudo make install-openrc"
|
||||
|
||||
install-launchd: $(TARGET)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(INSTALL_BIN_DIR) $(LOG_DIR)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(INSTALL_BIN_DIR) $(LOG_DIR)
|
||||
@cp -rf templates/* $(DATA_DIR)/templates/
|
||||
@cp -rf static/* $(DATA_DIR)/static/
|
||||
@cp -rf locales/* $(DATA_DIR)/locales/
|
||||
@cp -n example-config.ini $(DATA_DIR)/config.ini || true
|
||||
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
|
||||
@mkdir -p $(LAUNCHD_DIR)
|
||||
|
|
@ -117,10 +116,9 @@ install-launchd: $(TARGET)
|
|||
@echo "Start with: sudo launchctl kickstart -k system/$(LAUNCHD_LABEL)"
|
||||
|
||||
install-systemd: $(TARGET)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR)
|
||||
@cp -rf templates/* $(DATA_DIR)/templates/
|
||||
@cp -rf static/* $(DATA_DIR)/static/
|
||||
@cp -rf locales/* $(DATA_DIR)/locales/
|
||||
@cp -n example-config.ini $(DATA_DIR)/config.ini || true
|
||||
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
|
||||
@echo "Setting up user '$(USER)'..."
|
||||
|
|
@ -136,10 +134,9 @@ install-systemd: $(TARGET)
|
|||
@echo "Run 'systemctl enable --now omnisearch' to start"
|
||||
|
||||
install-openrc: $(TARGET)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR)
|
||||
@cp -rf templates/* $(DATA_DIR)/templates/
|
||||
@cp -rf static/* $(DATA_DIR)/static/
|
||||
@cp -rf locales/* $(DATA_DIR)/locales/
|
||||
@cp -n example-config.ini $(DATA_DIR)/config.ini || true
|
||||
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
|
||||
@echo "Setting up user '$(USER)'..."
|
||||
|
|
@ -155,10 +152,9 @@ install-openrc: $(TARGET)
|
|||
@echo "Run 'rc-update add omnisearch default' to enable"
|
||||
|
||||
install-runit: $(TARGET)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR) $(RUNIT_DIR)/omnisearch/log/
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR) $(RUNIT_DIR)/omnisearch/log/
|
||||
@cp -rf templates/* $(DATA_DIR)/templates/
|
||||
@cp -rf static/* $(DATA_DIR)/static/
|
||||
@cp -rf locales/* $(DATA_DIR)/locales/
|
||||
@cp -n example-config.ini $(DATA_DIR)/config.ini || true
|
||||
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
|
||||
@echo "Setting up user '$(USER)'..."
|
||||
|
|
@ -177,10 +173,9 @@ install-runit: $(TARGET)
|
|||
@echo "Artix: ln -s $(RUNIT_DIR)/omnisearch/ /run/runit/"
|
||||
|
||||
install-s6: $(TARGET)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR)
|
||||
@cp -rf templates/* $(DATA_DIR)/templates/
|
||||
@cp -rf static/* $(DATA_DIR)/static/
|
||||
@cp -rf locales/* $(DATA_DIR)/locales/
|
||||
@cp -n example-config.ini $(DATA_DIR)/config.ini || true
|
||||
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
|
||||
@echo "Setting up user '$(USER)'..."
|
||||
|
|
@ -199,10 +194,9 @@ install-s6: $(TARGET)
|
|||
@echo "Service will start automatically"
|
||||
|
||||
install-dinit: $(TARGET)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
|
||||
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR)
|
||||
@cp -rf templates/* $(DATA_DIR)/templates/
|
||||
@cp -rf static/* $(DATA_DIR)/static/
|
||||
@cp -rf locales/* $(DATA_DIR)/locales/
|
||||
@cp -n example-config.ini $(DATA_DIR)/config.ini || true
|
||||
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
|
||||
@echo "Setting up user '$(USER)'..."
|
||||
|
|
|
|||
|
|
@ -31,14 +31,3 @@ domain = https://search.example.com
|
|||
# Use *,-engine to exclude specific engines (e.g., *,-startpage)
|
||||
# Available engines: ddg, startpage, yahoo, mojeek
|
||||
engines="*"
|
||||
|
||||
[rate_limit]
|
||||
# Rate limit searches per interval
|
||||
|
||||
# /search
|
||||
#search_requests = 10
|
||||
#search_interval = 60
|
||||
|
||||
# /images
|
||||
#images_requests = 20
|
||||
#images_interval = 60
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
[Meta]
|
||||
Id = "ca_ca"
|
||||
Name = "Cat (Demo)"
|
||||
Direction = "rtl"
|
||||
|
||||
[Keys]
|
||||
search_placeholder = "meow"
|
||||
search_button = "meow"
|
||||
surprise_me_button = "meow"
|
||||
all_tab = "meow"
|
||||
images_tab = "meow"
|
||||
settings_tab = "meow"
|
||||
settings_title = "meow"
|
||||
theme_label = "meow"
|
||||
theme_desc = "meow"
|
||||
theme_system = "meow"
|
||||
theme_light = "meow"
|
||||
theme_dark = "meow"
|
||||
language_label = "meow"
|
||||
display_language_label = "meow"
|
||||
language_desc = "meow"
|
||||
engines_label = "meow"
|
||||
engines_desc = "meow"
|
||||
save_settings_button = "meow"
|
||||
no_results = "meow"
|
||||
error_images = "meow"
|
||||
rate_limit = "meow"
|
||||
warning_fetch_error = "meow"
|
||||
warning_parse_mismatch = "meow"
|
||||
warning_blocked = "meow"
|
||||
read_more = "meow"
|
||||
view_cached = "meow"
|
||||
view_image = "meow"
|
||||
visit_site = "meow"
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
[Meta]
|
||||
Id = "en_uk"
|
||||
Name = "English (Traditional)"
|
||||
Direction = "ltr"
|
||||
|
||||
[Keys]
|
||||
search_placeholder = "Search the web..."
|
||||
search_button = "Search"
|
||||
surprise_me_button = "Surprise me"
|
||||
all_tab = "All"
|
||||
images_tab = "Images"
|
||||
settings_tab = "Settings"
|
||||
settings_title = "Settings"
|
||||
theme_label = "Appearance"
|
||||
theme_desc = "Choose your preferred colour scheme."
|
||||
theme_system = "System"
|
||||
theme_light = "Light"
|
||||
theme_dark = "Dark"
|
||||
language_label = "Language"
|
||||
display_language_label = "Display Language"
|
||||
language_desc = "Choose your preferred language."
|
||||
engines_label = "Search Engines"
|
||||
engines_desc = "Choose which search engines to use. Only engines enabled on the server are shown."
|
||||
save_settings_button = "Save Settings"
|
||||
no_results = "No results found"
|
||||
error_images = "Error fetching images"
|
||||
rate_limit = "Slow down! Too many searches from you!"
|
||||
warning_fetch_error = "request failed before OmniSearch could read search results."
|
||||
warning_parse_mismatch = "returned search results in a format OmniSearch could not parse."
|
||||
warning_blocked = "returned a captcha or another blocking page instead of search results."
|
||||
read_more = "Read More"
|
||||
view_cached = "Cached"
|
||||
view_image = "Image"
|
||||
visit_site = "Site"
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
[Meta]
|
||||
Id = "en_us"
|
||||
Name = "English (Simplified)"
|
||||
Direction = "ltr"
|
||||
|
||||
[Keys]
|
||||
search_placeholder = "Search the web..."
|
||||
search_button = "Search"
|
||||
surprise_me_button = "Surprise me"
|
||||
all_tab = "All"
|
||||
images_tab = "Images"
|
||||
settings_tab = "Settings"
|
||||
settings_title = "Settings"
|
||||
theme_label = "Appearance"
|
||||
theme_desc = "Choose your preferred color scheme."
|
||||
theme_system = "System"
|
||||
theme_light = "Light"
|
||||
theme_dark = "Dark"
|
||||
language_label = "Language"
|
||||
display_language_label = "Display Language"
|
||||
language_desc = "Choose your preferred language."
|
||||
engines_label = "Search Engines"
|
||||
engines_desc = "Choose which search engines to use. Only engines enabled on the server are shown."
|
||||
save_settings_button = "Save Settings"
|
||||
no_results = "No results found"
|
||||
error_images = "Error fetching images"
|
||||
rate_limit = "Slow down! Too many searches from you!"
|
||||
warning_fetch_error = "request failed before OmniSearch could read search results."
|
||||
warning_parse_mismatch = "returned search results in a format OmniSearch could not parse."
|
||||
warning_blocked = "returned a captcha or another blocking page instead of search results."
|
||||
read_more = "Read More"
|
||||
view_cached = "Cached"
|
||||
view_image = "Image"
|
||||
visit_site = "Site"
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
[Meta]
|
||||
Id = "lv_lv"
|
||||
Name = "Latviešu"
|
||||
Direction = "ltr"
|
||||
|
||||
[Keys]
|
||||
search_placeholder = "Meklēt tīmeklī..."
|
||||
search_button = "Meklēt"
|
||||
surprise_me_button = "Pārsteidz mani"
|
||||
all_tab = "Viss"
|
||||
images_tab = "Attēli"
|
||||
settings_tab = "Iestatījumi"
|
||||
settings_title = "Iestatījumi"
|
||||
theme_label = "Izskats"
|
||||
theme_desc = "Izvēlieties vēlamo krāsu shēmu."
|
||||
theme_system = "Sistēmas"
|
||||
theme_light = "Gaišs"
|
||||
theme_dark = "Tumšs"
|
||||
language_label = "Valoda"
|
||||
display_language_label = "Saskarnes valoda"
|
||||
language_desc = "Izvēlieties vēlamo valodu."
|
||||
save_settings_button = "Saglabāt iestatījumus"
|
||||
no_results = "Rezultāti nav atrasti"
|
||||
error_images = "Kļūda, ielādējot attēlus"
|
||||
rate_limit = "Lēnāk! Pārāk daudz jūsu meklēšanas vaicājumu!"
|
||||
warning_fetch_error = "neizdevās izpildīt pieprasījumu, pirms OmniSearch varēja nolasīt meklēšanas rezultātus."
|
||||
warning_parse_mismatch = "atgrieza meklēšanas rezultātus formātā, kuru OmniSearch nevarēja apstrādāt."
|
||||
warning_blocked = "atgrieza captcha vai citu bloķēšanas lapu meklēšanas rezultātu vietā."
|
||||
read_more = "Lasīt vairāk"
|
||||
view_cached = "Kešēts"
|
||||
view_image = "Skatīt attēlu"
|
||||
visit_site = "Apmeklēt vietni"
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
[Meta]
|
||||
Id = "ru_ru"
|
||||
Name = "Русский"
|
||||
Direction = "ltr"
|
||||
|
||||
[Keys]
|
||||
search_placeholder = "Поиск в интернете..."
|
||||
search_button = "Найти"
|
||||
surprise_me_button = "Удиви меня"
|
||||
all_tab = "Все"
|
||||
images_tab = "Изображения"
|
||||
settings_tab = "Настройки"
|
||||
settings_title = "Настройки"
|
||||
theme_label = "Внешний вид"
|
||||
theme_desc = "Выберите предпочитаемую цветовую схему."
|
||||
theme_system = "Системная"
|
||||
theme_light = "Светлая"
|
||||
theme_dark = "Тёмная"
|
||||
language_label = "Язык"
|
||||
display_language_label = "Язык интерфейса"
|
||||
language_desc = "Выберите предпочитаемый язык."
|
||||
save_settings_button = "Сохранить настройки"
|
||||
no_results = "Ничего не найдено"
|
||||
error_images = "Ошибка загрузки изображений"
|
||||
rate_limit = "Не так быстро! Слишком много поисковых запросов от вас!"
|
||||
warning_fetch_error = "не удалось выполнить запрос до того, как OmniSearch смог прочитать результаты поиска."
|
||||
warning_parse_mismatch = "вернул результаты поиска в формате, который OmniSearch не смог обработать."
|
||||
warning_blocked = "вернул капчу или другую блокирующую страницу вместо результатов поиска."
|
||||
read_more = "Подробнее"
|
||||
view_cached = "Сохранённая копия"
|
||||
view_image = "Просмотр изображения"
|
||||
visit_site = "Перейти на сайт"
|
||||
10
src/Config.c
10
src/Config.c
|
|
@ -100,16 +100,6 @@ int load_config(const char *filename, Config *config) {
|
|||
strncpy(config->engines, value, sizeof(config->engines) - 1);
|
||||
config->engines[sizeof(config->engines) - 1] = '\0';
|
||||
}
|
||||
} else if (strcmp(section, "rate_limit") == 0) {
|
||||
if (strcmp(key, "search_requests") == 0) {
|
||||
config->rate_limit_search_requests = atoi(value);
|
||||
} else if (strcmp(key, "search_interval") == 0) {
|
||||
config->rate_limit_search_interval = atoi(value);
|
||||
} else if (strcmp(key, "images_requests") == 0) {
|
||||
config->rate_limit_images_requests = atoi(value);
|
||||
} else if (strcmp(key, "images_interval") == 0) {
|
||||
config->rate_limit_images_interval = atoi(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,6 @@ typedef struct {
|
|||
int cache_ttl_infobox;
|
||||
int cache_ttl_image;
|
||||
char engines[512];
|
||||
int rate_limit_search_requests;
|
||||
int rate_limit_search_interval;
|
||||
int rate_limit_images_requests;
|
||||
int rate_limit_images_interval;
|
||||
} Config;
|
||||
|
||||
int load_config(const char *filename, Config *config);
|
||||
|
|
|
|||
|
|
@ -1,210 +0,0 @@
|
|||
#include "RateLimit.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <beaker.h>
|
||||
#include <ctype.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct RateLimitEntry {
|
||||
char client_key[64];
|
||||
char scope[32];
|
||||
time_t window_start;
|
||||
time_t last_seen;
|
||||
int count;
|
||||
struct RateLimitEntry *next;
|
||||
} RateLimitEntry;
|
||||
|
||||
extern __thread int current_client_socket;
|
||||
extern __thread char current_request_buffer[];
|
||||
|
||||
static pthread_mutex_t rate_limit_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static RateLimitEntry *rate_limit_entries = NULL;
|
||||
|
||||
static int is_blank_char(char c) {
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
static const char *str_case_str(const char *haystack, const char *needle) {
|
||||
size_t nlen = strlen(needle);
|
||||
for (; *haystack; haystack++) {
|
||||
if (tolower((unsigned char)*haystack) == tolower((unsigned char)*needle)) {
|
||||
size_t i;
|
||||
for (i = 1; i < nlen; i++) {
|
||||
if (tolower((unsigned char)haystack[i]) !=
|
||||
tolower((unsigned char)needle[i]))
|
||||
break;
|
||||
}
|
||||
if (i == nlen)
|
||||
return haystack;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void trim_copy(char *dest, size_t dest_size, const char *src,
|
||||
size_t src_len) {
|
||||
while (src_len > 0 && is_blank_char(*src)) {
|
||||
src++;
|
||||
src_len--;
|
||||
}
|
||||
|
||||
while (src_len > 0 && is_blank_char(src[src_len - 1])) {
|
||||
src_len--;
|
||||
}
|
||||
|
||||
if (dest_size == 0)
|
||||
return;
|
||||
|
||||
if (src_len >= dest_size)
|
||||
src_len = dest_size - 1;
|
||||
|
||||
memcpy(dest, src, src_len);
|
||||
dest[src_len] = '\0';
|
||||
}
|
||||
|
||||
static void get_client_key(char *client_key, size_t client_key_size) {
|
||||
const char *header = str_case_str(current_request_buffer, "x-forwarded-for:");
|
||||
if (!header)
|
||||
return;
|
||||
|
||||
header += strlen("X-Forwarded-For:");
|
||||
const char *line_end = strpbrk(header, "\r\n");
|
||||
size_t line_len = line_end ? (size_t)(line_end - header) : strlen(header);
|
||||
const char *comma = memchr(header, ',', line_len);
|
||||
size_t value_len = comma ? (size_t)(comma - header) : line_len;
|
||||
|
||||
trim_copy(client_key, client_key_size, header, value_len);
|
||||
}
|
||||
|
||||
static void get_client_key_from_socket(char *client_key,
|
||||
size_t client_key_size) {
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addr_len = sizeof(addr);
|
||||
|
||||
if (getpeername(current_client_socket, (struct sockaddr *)&addr, &addr_len) !=
|
||||
0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addr.ss_family == AF_INET) {
|
||||
struct sockaddr_in *ipv4 = (struct sockaddr_in *)&addr;
|
||||
inet_ntop(AF_INET, &ipv4->sin_addr, client_key, client_key_size);
|
||||
} else if (addr.ss_family == AF_INET6) {
|
||||
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)&addr;
|
||||
inet_ntop(AF_INET6, &ipv6->sin6_addr, client_key, client_key_size);
|
||||
} else if (addr.ss_family == AF_UNIX) {
|
||||
snprintf(client_key, client_key_size, "unix:%d", current_client_socket);
|
||||
}
|
||||
}
|
||||
|
||||
void rate_limit_get_client_key(char *client_key, size_t client_key_size) {
|
||||
if (!client_key || client_key_size == 0)
|
||||
return;
|
||||
|
||||
client_key[0] = '\0';
|
||||
get_client_key(client_key, client_key_size);
|
||||
|
||||
if (client_key[0] == '\0') {
|
||||
get_client_key_from_socket(client_key, client_key_size);
|
||||
}
|
||||
|
||||
if (client_key[0] == '\0') {
|
||||
snprintf(client_key, client_key_size, "nun");
|
||||
}
|
||||
}
|
||||
|
||||
static void prune_stale_entries(time_t now) {
|
||||
RateLimitEntry **cursor = &rate_limit_entries;
|
||||
|
||||
while (*cursor) {
|
||||
RateLimitEntry *entry = *cursor;
|
||||
if (now - entry->last_seen > 9999) {
|
||||
*cursor = entry->next;
|
||||
free(entry);
|
||||
continue;
|
||||
}
|
||||
cursor = &entry->next;
|
||||
}
|
||||
}
|
||||
|
||||
static RateLimitEntry *find_entry(const char *client_key, const char *scope) {
|
||||
for (RateLimitEntry *entry = rate_limit_entries; entry; entry = entry->next) {
|
||||
if (strcmp(entry->client_key, client_key) == 0 &&
|
||||
strcmp(entry->scope, scope) == 0) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static RateLimitEntry *create_entry(const char *client_key, const char *scope,
|
||||
time_t now) {
|
||||
RateLimitEntry *entry = (RateLimitEntry *)calloc(1, sizeof(RateLimitEntry));
|
||||
if (!entry)
|
||||
return NULL;
|
||||
|
||||
snprintf(entry->client_key, sizeof(entry->client_key), "%s", client_key);
|
||||
snprintf(entry->scope, sizeof(entry->scope), "%s", scope);
|
||||
entry->window_start = now;
|
||||
entry->last_seen = now;
|
||||
entry->next = rate_limit_entries;
|
||||
rate_limit_entries = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
RateLimitResult rate_limit_check(const char *scope,
|
||||
const RateLimitConfig *config) {
|
||||
RateLimitResult result = {.limited = 0, .retry_after_seconds = 0};
|
||||
|
||||
if (!scope || !config || config->max_requests <= 0 ||
|
||||
config->interval_seconds <= 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
char client_key[64];
|
||||
time_t now = time(NULL);
|
||||
|
||||
rate_limit_get_client_key(client_key, sizeof(client_key));
|
||||
|
||||
pthread_mutex_lock(&rate_limit_mutex);
|
||||
|
||||
prune_stale_entries(now);
|
||||
|
||||
RateLimitEntry *entry = find_entry(client_key, scope);
|
||||
if (!entry) {
|
||||
entry = create_entry(client_key, scope, now);
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&rate_limit_mutex);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
entry->last_seen = now;
|
||||
|
||||
if (now - entry->window_start >= config->interval_seconds) {
|
||||
entry->window_start = now;
|
||||
entry->count = 0;
|
||||
}
|
||||
|
||||
if (entry->count >= config->max_requests) {
|
||||
result.limited = 1;
|
||||
result.retry_after_seconds =
|
||||
config->interval_seconds - (int)(now - entry->window_start);
|
||||
if (result.retry_after_seconds < 1) {
|
||||
result.retry_after_seconds = 1;
|
||||
}
|
||||
pthread_mutex_unlock(&rate_limit_mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
entry->count++;
|
||||
pthread_mutex_unlock(&rate_limit_mutex);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#ifndef RATE_LIMIT_H
|
||||
#define RATE_LIMIT_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
int max_requests;
|
||||
int interval_seconds;
|
||||
} RateLimitConfig;
|
||||
|
||||
typedef struct {
|
||||
int limited;
|
||||
int retry_after_seconds;
|
||||
} RateLimitResult;
|
||||
|
||||
void rate_limit_get_client_key(char *client_key, size_t client_key_size);
|
||||
RateLimitResult rate_limit_check(const char *scope,
|
||||
const RateLimitConfig *config);
|
||||
|
||||
#endif
|
||||
18
src/Main.c
18
src/Main.c
|
|
@ -13,8 +13,6 @@
|
|||
#include "Routes/ImageProxy.h"
|
||||
#include "Routes/Images.h"
|
||||
#include "Routes/Search.h"
|
||||
#include "Routes/Settings.h"
|
||||
#include "Routes/SettingsSave.h"
|
||||
#include "Scraping/Scraping.h"
|
||||
|
||||
Config global_config;
|
||||
|
|
@ -55,11 +53,7 @@ int main() {
|
|||
.cache_ttl_search = DEFAULT_CACHE_TTL_SEARCH,
|
||||
.cache_ttl_infobox = DEFAULT_CACHE_TTL_INFOBOX,
|
||||
.cache_ttl_image = DEFAULT_CACHE_TTL_IMAGE,
|
||||
.engines = "",
|
||||
.rate_limit_search_requests = 0,
|
||||
.rate_limit_search_interval = 0,
|
||||
.rate_limit_images_requests = 0,
|
||||
.rate_limit_images_interval = 0};
|
||||
.engines = ""};
|
||||
|
||||
if (load_config("config.ini", &cfg) != 0) {
|
||||
fprintf(stderr, "[WARN] Could not load config file, using defaults\n");
|
||||
|
|
@ -67,13 +61,6 @@ int main() {
|
|||
|
||||
global_config = cfg;
|
||||
|
||||
int loaded = beaker_load_locales();
|
||||
if (loaded > 0) {
|
||||
fprintf(stderr, "[INFO] Loaded %d locales\n", loaded);
|
||||
} else {
|
||||
fprintf(stderr, "[WARN] No locales loaded (make sure to run from omnisearch directory)\n");
|
||||
}
|
||||
|
||||
apply_engines_config(cfg.engines);
|
||||
|
||||
if (cache_init(cfg.cache_dir) != 0) {
|
||||
|
|
@ -110,8 +97,6 @@ int main() {
|
|||
set_handler("/search", results_handler);
|
||||
set_handler("/images", images_handler);
|
||||
set_handler("/proxy", image_proxy_handler);
|
||||
set_handler("/settings", settings_handler);
|
||||
set_handler("/save_settings", settings_save_handler);
|
||||
|
||||
fprintf(stderr, "[INFO] Starting Omnisearch on %s:%d\n", cfg.host, cfg.port);
|
||||
|
||||
|
|
@ -126,7 +111,6 @@ int main() {
|
|||
|
||||
curl_global_cleanup();
|
||||
xmlCleanupParser();
|
||||
beaker_free_locales();
|
||||
free_proxy_list();
|
||||
cache_shutdown();
|
||||
return EXIT_SUCCESS;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
#include "Home.h"
|
||||
#include "../Utility/Utility.h"
|
||||
#include <beaker.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int home_handler(UrlParams *params) {
|
||||
(void)params;
|
||||
char *theme = get_theme("");
|
||||
char *locale = get_locale("en_uk");
|
||||
|
||||
TemplateContext ctx = new_context();
|
||||
context_set(&ctx, "theme", theme);
|
||||
beaker_set_locale(&ctx, locale);
|
||||
char *rendered_html = render_template("home.html", &ctx);
|
||||
send_response(rendered_html);
|
||||
|
||||
free(rendered_html);
|
||||
free_context(&ctx);
|
||||
free(theme);
|
||||
free(locale);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,25 +7,12 @@
|
|||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
|
||||
#define MAX_IMAGE_SIZE (10 * 1024 * 1024)
|
||||
#define DNS_CACHE_TTL 300
|
||||
|
||||
typedef struct DnsCacheEntry {
|
||||
char hostname[256];
|
||||
char ip_str[INET_ADDRSTRLEN];
|
||||
time_t resolved_at;
|
||||
struct DnsCacheEntry *next;
|
||||
} DnsCacheEntry;
|
||||
|
||||
static DnsCacheEntry *dns_cache = NULL;
|
||||
static pthread_mutex_t dns_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
|
|
@ -33,57 +20,6 @@ typedef struct {
|
|||
size_t capacity;
|
||||
} MemoryBuffer;
|
||||
|
||||
static int dns_cache_lookup(const char *hostname, char *out_ip) {
|
||||
time_t now = time(NULL);
|
||||
pthread_mutex_lock(&dns_cache_mutex);
|
||||
for (DnsCacheEntry *e = dns_cache; e; e = e->next) {
|
||||
if (strcmp(e->hostname, hostname) == 0) {
|
||||
if ((now - e->resolved_at) < DNS_CACHE_TTL) {
|
||||
strcpy(out_ip, e->ip_str);
|
||||
pthread_mutex_unlock(&dns_cache_mutex);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&dns_cache_mutex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void dns_cache_insert(const char *hostname, const char *ip_str) {
|
||||
time_t now = time(NULL);
|
||||
pthread_mutex_lock(&dns_cache_mutex);
|
||||
|
||||
DnsCacheEntry **cursor = &dns_cache;
|
||||
while (*cursor) {
|
||||
DnsCacheEntry *entry = *cursor;
|
||||
if ((now - entry->resolved_at) >= DNS_CACHE_TTL) {
|
||||
*cursor = entry->next;
|
||||
free(entry);
|
||||
continue;
|
||||
}
|
||||
if (strcmp(entry->hostname, hostname) == 0) {
|
||||
strcpy(entry->ip_str, ip_str);
|
||||
entry->resolved_at = now;
|
||||
pthread_mutex_unlock(&dns_cache_mutex);
|
||||
return;
|
||||
}
|
||||
cursor = &entry->next;
|
||||
}
|
||||
|
||||
DnsCacheEntry *new_entry = malloc(sizeof(DnsCacheEntry));
|
||||
if (new_entry) {
|
||||
strncpy(new_entry->hostname, hostname, sizeof(new_entry->hostname) - 1);
|
||||
new_entry->hostname[sizeof(new_entry->hostname) - 1] = '\0';
|
||||
strcpy(new_entry->ip_str, ip_str);
|
||||
new_entry->resolved_at = now;
|
||||
new_entry->next = dns_cache;
|
||||
dns_cache = new_entry;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&dns_cache_mutex);
|
||||
}
|
||||
|
||||
static int is_private_ip(const char *ip_str) {
|
||||
struct in_addr addr;
|
||||
if (inet_pton(AF_INET, ip_str, &addr) != 1) {
|
||||
|
|
@ -123,11 +59,7 @@ static int is_private_ip(const char *ip_str) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
static const char *is_private_hostname(const char *hostname, char *out_ip) {
|
||||
if (dns_cache_lookup(hostname, out_ip) == 0) {
|
||||
return out_ip;
|
||||
}
|
||||
|
||||
static int is_private_hostname(const char *hostname) {
|
||||
struct addrinfo hints, *res, *p;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
|
|
@ -135,7 +67,7 @@ static const char *is_private_hostname(const char *hostname, char *out_ip) {
|
|||
|
||||
int err = getaddrinfo(hostname, NULL, &hints, &res);
|
||||
if (err != 0) {
|
||||
return NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (p = res; p != NULL; p = p->ai_next) {
|
||||
|
|
@ -146,21 +78,16 @@ static const char *is_private_hostname(const char *hostname, char *out_ip) {
|
|||
|
||||
if (is_private_ip(ip_str)) {
|
||||
freeaddrinfo(res);
|
||||
return NULL;
|
||||
return 1;
|
||||
}
|
||||
|
||||
freeaddrinfo(res);
|
||||
strcpy(out_ip, ip_str);
|
||||
dns_cache_insert(hostname, ip_str);
|
||||
return out_ip;
|
||||
}
|
||||
}
|
||||
|
||||
freeaddrinfo(res);
|
||||
return NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_allowed_domain(const char *url, char *resolved_ip) {
|
||||
static int is_allowed_domain(const char *url) {
|
||||
CURLU *h = curl_url();
|
||||
if (!h) {
|
||||
return -1;
|
||||
|
|
@ -205,7 +132,7 @@ static int is_allowed_domain(const char *url, char *resolved_ip) {
|
|||
*colon = '\0';
|
||||
}
|
||||
|
||||
if (!is_private_hostname(host, resolved_ip)) {
|
||||
if (is_private_hostname(host)) {
|
||||
curl_url_cleanup(h);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -279,8 +206,7 @@ int image_proxy_handler(UrlParams *params) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
char resolved_ip[INET_ADDRSTRLEN] = {0};
|
||||
int domain_check = is_allowed_domain(url, resolved_ip);
|
||||
int domain_check = is_allowed_domain(url);
|
||||
if (domain_check == -1) {
|
||||
send_response("Invalid URL scheme");
|
||||
return 0;
|
||||
|
|
@ -357,31 +283,6 @@ int image_proxy_handler(UrlParams *params) {
|
|||
"Chrome/120.0.0.0 Safari/537.36");
|
||||
apply_proxy_settings(curl);
|
||||
|
||||
struct curl_slist *resolves = NULL;
|
||||
if (resolved_ip[0] != '\0') {
|
||||
CURLU *u = curl_url();
|
||||
if (u) {
|
||||
curl_url_set(u, CURLUPART_URL, url, 0);
|
||||
char *rhost = NULL;
|
||||
curl_url_get(u, CURLUPART_HOST, &rhost, 0);
|
||||
if (rhost) {
|
||||
char *rscheme = NULL;
|
||||
curl_url_get(u, CURLUPART_SCHEME, &rscheme, 0);
|
||||
int port = (rscheme && strcasecmp(rscheme, "https") == 0) ? 443 : 80;
|
||||
if (rscheme)
|
||||
curl_free(rscheme);
|
||||
|
||||
char resolve_str[512];
|
||||
snprintf(resolve_str, sizeof(resolve_str), "%s:%d:%s", rhost, port,
|
||||
resolved_ip);
|
||||
resolves = curl_slist_append(NULL, resolve_str);
|
||||
curl_easy_setopt(curl, CURLOPT_RESOLVE, resolves);
|
||||
curl_free(rhost);
|
||||
}
|
||||
curl_url_cleanup(u);
|
||||
}
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
long response_code;
|
||||
|
|
@ -395,8 +296,6 @@ int image_proxy_handler(UrlParams *params) {
|
|||
strncpy(content_type, content_type_ptr, sizeof(content_type) - 1);
|
||||
}
|
||||
|
||||
if (resolves)
|
||||
curl_slist_free_all(resolves);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK || response_code != 200) {
|
||||
|
|
|
|||
|
|
@ -1,46 +1,9 @@
|
|||
#include "Images.h"
|
||||
#include "../Cache/Cache.h"
|
||||
#include "../Limiter/RateLimit.h"
|
||||
#include "../Scraping/ImageScraping.h"
|
||||
#include "../Utility/Unescape.h"
|
||||
#include "../Utility/Utility.h"
|
||||
#include "Config.h"
|
||||
#include <beaker.h>
|
||||
|
||||
static char *build_images_request_cache_key(const char *query, int page,
|
||||
const char *client_key) {
|
||||
char scope_key[BUFFER_SIZE_MEDIUM];
|
||||
snprintf(scope_key, sizeof(scope_key), "images_request:%s",
|
||||
client_key ? client_key : "unknown");
|
||||
return cache_compute_key(query, page, scope_key);
|
||||
}
|
||||
|
||||
static char *build_images_href(const char *query, int page) {
|
||||
const char *safe_query = query ? query : "";
|
||||
size_t needed = strlen("/images?q=") + strlen(safe_query) + 1;
|
||||
if (page > 1)
|
||||
needed += strlen("&p=") + 16;
|
||||
|
||||
char *href = (char *)malloc(needed);
|
||||
if (!href)
|
||||
return NULL;
|
||||
|
||||
snprintf(href, needed, "/images?q=%s", safe_query);
|
||||
if (page > 1) {
|
||||
char page_buf[16];
|
||||
snprintf(page_buf, sizeof(page_buf), "%d", page);
|
||||
strcat(href, "&p=");
|
||||
strcat(href, page_buf);
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
static char *images_href_builder(int page, void *data) {
|
||||
return build_images_href((const char *)data, page);
|
||||
}
|
||||
|
||||
int images_handler(UrlParams *params) {
|
||||
extern Config global_config;
|
||||
TemplateContext ctx = new_context();
|
||||
char *raw_query = "";
|
||||
int page = 1;
|
||||
|
|
@ -57,91 +20,38 @@ int images_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
|
||||
char page_str[16], prev_str[16], next_str[16], two_prev_str[16],
|
||||
two_next_str[16];
|
||||
|
||||
snprintf(page_str, sizeof(page_str), "%d", page);
|
||||
snprintf(prev_str, sizeof(prev_str), "%d", page > 1 ? page - 1 : 0);
|
||||
snprintf(next_str, sizeof(next_str), "%d", page + 1);
|
||||
snprintf(two_prev_str, sizeof(two_prev_str), "%d", page > 2 ? page - 2 : 0);
|
||||
snprintf(two_next_str, sizeof(two_next_str), "%d", page + 2);
|
||||
context_set(&ctx, "query", raw_query);
|
||||
|
||||
char *theme = get_theme("");
|
||||
context_set(&ctx, "theme", theme);
|
||||
free(theme);
|
||||
|
||||
char *locale = get_locale("en_uk");
|
||||
beaker_set_locale(&ctx, locale);
|
||||
|
||||
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!";
|
||||
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;
|
||||
int *pager_inner_counts = NULL;
|
||||
int pager_count = build_pagination(page, images_href_builder,
|
||||
(void *)raw_query, &pager_matrix,
|
||||
&pager_inner_counts);
|
||||
if (pager_count > 0) {
|
||||
context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix,
|
||||
pager_count, pager_inner_counts);
|
||||
}
|
||||
context_set(&ctx, "page", page_str);
|
||||
context_set(&ctx, "prev_page", prev_str);
|
||||
context_set(&ctx, "next_page", next_str);
|
||||
context_set(&ctx, "two_prev_page", two_prev_str);
|
||||
context_set(&ctx, "two_next_page", two_next_str);
|
||||
|
||||
char *display_query = url_decode_query(raw_query);
|
||||
context_set(&ctx, "query", display_query);
|
||||
|
||||
if (!raw_query || strlen(raw_query) == 0) {
|
||||
send_redirect("/");
|
||||
send_response("<h1>No query provided</h1>");
|
||||
if (display_query)
|
||||
free(display_query);
|
||||
free_context(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char client_key[BUFFER_SIZE_SMALL];
|
||||
rate_limit_get_client_key(client_key, sizeof(client_key));
|
||||
|
||||
char *request_cache_key =
|
||||
build_images_request_cache_key(raw_query, page, client_key);
|
||||
int request_is_cached = 0;
|
||||
|
||||
if (request_cache_key && get_cache_ttl_image() > 0) {
|
||||
char *cached_marker = NULL;
|
||||
size_t cached_marker_size = 0;
|
||||
|
||||
if (cache_get(request_cache_key, (time_t)get_cache_ttl_image(),
|
||||
&cached_marker, &cached_marker_size) == 0) {
|
||||
request_is_cached = 1;
|
||||
}
|
||||
|
||||
free(cached_marker);
|
||||
}
|
||||
|
||||
if (!request_is_cached) {
|
||||
RateLimitConfig rate_limit_config = {
|
||||
.max_requests = global_config.rate_limit_images_requests,
|
||||
.interval_seconds = global_config.rate_limit_images_interval,
|
||||
};
|
||||
RateLimitResult rate_limit_result =
|
||||
rate_limit_check("images", &rate_limit_config);
|
||||
if (rate_limit_result.limited) {
|
||||
char response[256];
|
||||
snprintf(response, sizeof(response), "<h1>%s</h1>", rate_limit_msg);
|
||||
send_response(response);
|
||||
free(request_cache_key);
|
||||
free(display_query);
|
||||
free_context(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (request_cache_key && get_cache_ttl_image() > 0) {
|
||||
cache_set(request_cache_key, "1", 1);
|
||||
}
|
||||
}
|
||||
|
||||
ImageResult *results = NULL;
|
||||
int result_count = 0;
|
||||
|
||||
if (scrape_images(raw_query, page, &results, &result_count) != 0 ||
|
||||
!results) {
|
||||
char error_html[128];
|
||||
snprintf(error_html, sizeof(error_html), "<h1>%s</h1>", error_images_msg);
|
||||
send_response(error_html);
|
||||
free(request_cache_key);
|
||||
send_response("<h1>Error fetching images</h1>");
|
||||
free(display_query);
|
||||
free_context(&ctx);
|
||||
return -1;
|
||||
|
|
@ -156,7 +66,6 @@ int images_handler(UrlParams *params) {
|
|||
if (inner_counts)
|
||||
free(inner_counts);
|
||||
free_image_results(results, result_count);
|
||||
free(request_cache_key);
|
||||
free(display_query);
|
||||
free_context(&ctx);
|
||||
return -1;
|
||||
|
|
@ -190,18 +99,7 @@ int images_handler(UrlParams *params) {
|
|||
free(image_matrix);
|
||||
free(inner_counts);
|
||||
|
||||
if (pager_count > 0) {
|
||||
for (int i = 0; i < pager_count; i++) {
|
||||
for (int j = 0; j < LINK_FIELD_COUNT; j++)
|
||||
free(pager_matrix[i][j]);
|
||||
free(pager_matrix[i]);
|
||||
}
|
||||
free(pager_matrix);
|
||||
free(pager_inner_counts);
|
||||
}
|
||||
|
||||
free_image_results(results, result_count);
|
||||
free(request_cache_key);
|
||||
free(display_query);
|
||||
free_context(&ctx);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,101 +1,20 @@
|
|||
#include "Search.h"
|
||||
#include "../Cache/Cache.h"
|
||||
#include "../Infobox/Calculator.h"
|
||||
#include "../Infobox/CurrencyConversion.h"
|
||||
#include "../Infobox/Dictionary.h"
|
||||
#include "../Infobox/UnitConversion.h"
|
||||
#include "../Infobox/Wikipedia.h"
|
||||
#include "../Limiter/RateLimit.h"
|
||||
#include "../Scraping/Scraping.h"
|
||||
#include "../Utility/Display.h"
|
||||
#include "../Utility/Unescape.h"
|
||||
#include "../Utility/Utility.h"
|
||||
#include "Config.h"
|
||||
#include <ctype.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define URL_HASH_TABLE_SIZE 64
|
||||
|
||||
typedef struct UrlHashEntry {
|
||||
char *url;
|
||||
struct UrlHashEntry *next;
|
||||
} UrlHashEntry;
|
||||
|
||||
typedef struct {
|
||||
UrlHashEntry *buckets[URL_HASH_TABLE_SIZE];
|
||||
} UrlHashTable;
|
||||
|
||||
static void url_hash_init(UrlHashTable *ht) {
|
||||
for (int i = 0; i < URL_HASH_TABLE_SIZE; i++) {
|
||||
ht->buckets[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned int url_hash(const char *url) {
|
||||
unsigned char hash[EVP_MAX_MD_SIZE];
|
||||
unsigned int hash_len;
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
|
||||
if (!ctx)
|
||||
return 0;
|
||||
EVP_DigestInit_ex(ctx, EVP_md5(), NULL);
|
||||
EVP_DigestUpdate(ctx, url, strlen(url));
|
||||
EVP_DigestFinal_ex(ctx, hash, &hash_len);
|
||||
EVP_MD_CTX_free(ctx);
|
||||
unsigned int h = 0;
|
||||
for (unsigned int i = 0; i < hash_len; i++) {
|
||||
h = h * 31 + hash[i];
|
||||
}
|
||||
return h % URL_HASH_TABLE_SIZE;
|
||||
}
|
||||
|
||||
static int url_hash_contains(UrlHashTable *ht, const char *url) {
|
||||
unsigned int idx = url_hash(url);
|
||||
for (UrlHashEntry *e = ht->buckets[idx]; e; e = e->next) {
|
||||
if (strcmp(e->url, url) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int url_hash_insert(UrlHashTable *ht, const char *url) {
|
||||
unsigned int idx = url_hash(url);
|
||||
for (UrlHashEntry *e = ht->buckets[idx]; e; e = e->next) {
|
||||
if (strcmp(e->url, url) == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
UrlHashEntry *new_entry = malloc(sizeof(UrlHashEntry));
|
||||
if (!new_entry)
|
||||
return -1;
|
||||
new_entry->url = strdup(url);
|
||||
if (!new_entry->url) {
|
||||
free(new_entry);
|
||||
return -1;
|
||||
}
|
||||
new_entry->next = ht->buckets[idx];
|
||||
ht->buckets[idx] = new_entry;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void url_hash_free(UrlHashTable *ht) {
|
||||
for (int i = 0; i < URL_HASH_TABLE_SIZE; i++) {
|
||||
UrlHashEntry *e = ht->buckets[i];
|
||||
while (e) {
|
||||
UrlHashEntry *next = e->next;
|
||||
free(e->url);
|
||||
free(e);
|
||||
e = next;
|
||||
}
|
||||
ht->buckets[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *query;
|
||||
InfoBox result;
|
||||
|
|
@ -110,6 +29,8 @@ typedef struct {
|
|||
|
||||
enum {
|
||||
RESULT_FIELD_COUNT = 6,
|
||||
LINK_FIELD_COUNT = 3,
|
||||
PAGER_WINDOW_SIZE = 5,
|
||||
};
|
||||
|
||||
static InfoBox fetch_wiki_wrapper(char *query) {
|
||||
|
|
@ -239,7 +160,7 @@ static void *infobox_thread_func(void *arg) {
|
|||
|
||||
data->result = h->fetch_fn((char *)data->query);
|
||||
data->success = (data->result.title != NULL && data->result.extract != NULL &&
|
||||
data->result.extract[0] != '\0');
|
||||
strlen(data->result.extract) > 10);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -265,6 +186,66 @@ static int add_infobox_to_collection(InfoBox *infobox, char ****collection,
|
|||
return current_count + 1;
|
||||
}
|
||||
|
||||
static int add_link_to_collection(const char *href, const char *label,
|
||||
const char *class_name, char ****collection,
|
||||
int **inner_counts, int current_count) {
|
||||
char ***old_collection = *collection;
|
||||
int *old_inner_counts = *inner_counts;
|
||||
char ***new_collection =
|
||||
(char ***)malloc(sizeof(char **) * (current_count + 1));
|
||||
int *new_inner_counts =
|
||||
(int *)malloc(sizeof(int) * (current_count + 1));
|
||||
|
||||
if (!new_collection || !new_inner_counts) {
|
||||
free(new_collection);
|
||||
free(new_inner_counts);
|
||||
return current_count;
|
||||
}
|
||||
|
||||
if (*collection && current_count > 0) {
|
||||
memcpy(new_collection, *collection, sizeof(char **) * current_count);
|
||||
}
|
||||
if (*inner_counts && current_count > 0) {
|
||||
memcpy(new_inner_counts, *inner_counts, sizeof(int) * current_count);
|
||||
}
|
||||
|
||||
*collection = new_collection;
|
||||
*inner_counts = new_inner_counts;
|
||||
|
||||
(*collection)[current_count] =
|
||||
(char **)malloc(sizeof(char *) * LINK_FIELD_COUNT);
|
||||
if (!(*collection)[current_count]) {
|
||||
*collection = old_collection;
|
||||
*inner_counts = old_inner_counts;
|
||||
free(new_collection);
|
||||
free(new_inner_counts);
|
||||
return current_count;
|
||||
}
|
||||
|
||||
(*collection)[current_count][0] = strdup(href ? href : "");
|
||||
(*collection)[current_count][1] = strdup(label ? label : "");
|
||||
(*collection)[current_count][2] = strdup(class_name ? class_name : "");
|
||||
|
||||
if (!(*collection)[current_count][0] || !(*collection)[current_count][1] ||
|
||||
!(*collection)[current_count][2]) {
|
||||
free((*collection)[current_count][0]);
|
||||
free((*collection)[current_count][1]);
|
||||
free((*collection)[current_count][2]);
|
||||
free((*collection)[current_count]);
|
||||
*collection = old_collection;
|
||||
*inner_counts = old_inner_counts;
|
||||
free(new_collection);
|
||||
free(new_inner_counts);
|
||||
return current_count;
|
||||
}
|
||||
|
||||
(*inner_counts)[current_count] = LINK_FIELD_COUNT;
|
||||
|
||||
free(old_collection);
|
||||
free(old_inner_counts);
|
||||
return current_count + 1;
|
||||
}
|
||||
|
||||
static int add_warning_to_collection(const char *engine_name,
|
||||
const char *warning_message,
|
||||
char ****collection, int **inner_counts,
|
||||
|
|
@ -312,20 +293,15 @@ static int add_warning_to_collection(const char *engine_name,
|
|||
return current_count + 1;
|
||||
}
|
||||
|
||||
static const char *warning_message_for_job(const ScrapeJob *job, const char *locale) {
|
||||
static const char *warning_message_for_job(const ScrapeJob *job) {
|
||||
switch (job->status) {
|
||||
case SCRAPE_STATUS_FETCH_ERROR: {
|
||||
const char *msg = beaker_get_locale_value(locale, "warning_fetch_error");
|
||||
return msg ? msg : "request failed before OmniSearch could read search results.";
|
||||
}
|
||||
case SCRAPE_STATUS_PARSE_MISMATCH: {
|
||||
const char *msg = beaker_get_locale_value(locale, "warning_parse_mismatch");
|
||||
return msg ? msg : "returned search results in a format OmniSearch could not parse.";
|
||||
}
|
||||
case SCRAPE_STATUS_BLOCKED: {
|
||||
const char *msg = beaker_get_locale_value(locale, "warning_blocked");
|
||||
return msg ? msg : "returned a captcha or another blocking page instead of search results.";
|
||||
}
|
||||
case SCRAPE_STATUS_FETCH_ERROR:
|
||||
return "request failed before OmniSearch could read search results.";
|
||||
case SCRAPE_STATUS_PARSE_MISMATCH:
|
||||
return "returned search results in a format OmniSearch could not parse.";
|
||||
case SCRAPE_STATUS_BLOCKED:
|
||||
return "returned a captcha or another blocking page instead of search "
|
||||
"results.";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -368,13 +344,6 @@ static const SearchEngine *find_enabled_engine(const char *engine_id) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static int engine_allowed_for_user(const SearchEngine *eng, char **user_ids,
|
||||
int user_count, int has_pref) {
|
||||
if (!has_pref)
|
||||
return 1;
|
||||
return user_engines_contains(eng->id, user_ids, user_count);
|
||||
}
|
||||
|
||||
static char *build_search_href(const char *query, const char *engine_id,
|
||||
int page) {
|
||||
const char *safe_query = query ? query : "";
|
||||
|
|
@ -408,37 +377,13 @@ static char *build_search_href(const char *query, const char *engine_id,
|
|||
return href;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *query;
|
||||
const char *engine_id;
|
||||
} SearchHrefData;
|
||||
|
||||
static char *search_href_builder(int page, void *data) {
|
||||
SearchHrefData *d = (SearchHrefData *)data;
|
||||
return build_search_href(d->query, d->engine_id, page);
|
||||
}
|
||||
|
||||
static char *build_search_request_cache_key(const char *query,
|
||||
const char *engine_id, int page,
|
||||
const char *client_key) {
|
||||
char scope_key[BUFFER_SIZE_MEDIUM];
|
||||
snprintf(scope_key, sizeof(scope_key), "search_request:%s:%s",
|
||||
engine_id ? engine_id : "all", client_key ? client_key : "unknown");
|
||||
return cache_compute_key(query, page, scope_key);
|
||||
}
|
||||
|
||||
int results_handler(UrlParams *params) {
|
||||
extern Config global_config;
|
||||
TemplateContext ctx = new_context();
|
||||
char *raw_query = "";
|
||||
const char *selected_engine_id = "all";
|
||||
int page = 1;
|
||||
int btnI = 0;
|
||||
|
||||
char **user_engines = NULL;
|
||||
int user_engine_count = 0;
|
||||
int has_user_pref = (get_user_engines(&user_engines, &user_engine_count) == 0);
|
||||
|
||||
if (params) {
|
||||
for (int i = 0; i < params->count; i++) {
|
||||
if (strcmp(params->params[i].key, "q") == 0) {
|
||||
|
|
@ -456,40 +401,12 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
|
||||
context_set(&ctx, "query", raw_query);
|
||||
|
||||
char *theme = get_theme("");
|
||||
context_set(&ctx, "theme", theme);
|
||||
free(theme);
|
||||
|
||||
char *locale = get_locale("en_uk");
|
||||
beaker_set_locale(&ctx, locale);
|
||||
|
||||
const char *rate_limit_msg = beaker_get_locale_value(locale, "rate_limit");
|
||||
if (!rate_limit_msg) rate_limit_msg = "Slow down! Too many searches from you!";
|
||||
const char *no_results_msg = beaker_get_locale_value(locale, "no_results");
|
||||
if (!no_results_msg) no_results_msg = "No results found";
|
||||
|
||||
char page_str[16];
|
||||
snprintf(page_str, sizeof(page_str), "%d", page);
|
||||
context_set(&ctx, "page", page_str);
|
||||
|
||||
char prev_str[16], next_str[16], two_prev_str[16], two_next_str[16];
|
||||
snprintf(prev_str, sizeof(prev_str), "%d", page > 1 ? page - 1 : 0);
|
||||
snprintf(next_str, sizeof(next_str), "%d", page + 1);
|
||||
snprintf(two_prev_str, sizeof(two_prev_str), "%d", page > 2 ? page - 2 : 0);
|
||||
snprintf(two_next_str, sizeof(two_next_str), "%d", page + 2);
|
||||
context_set(&ctx, "prev_page", prev_str);
|
||||
context_set(&ctx, "next_page", next_str);
|
||||
context_set(&ctx, "two_prev_page", two_prev_str);
|
||||
context_set(&ctx, "two_next_page", two_next_str);
|
||||
|
||||
if (!raw_query || strlen(raw_query) == 0) {
|
||||
send_redirect("/");
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
send_response("<h1>No query provided</h1>");
|
||||
free_context(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -506,9 +423,7 @@ int results_handler(UrlParams *params) {
|
|||
int enabled_engine_count = 0;
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (ENGINE_REGISTRY[i].enabled &&
|
||||
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine) &&
|
||||
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
|
||||
user_engine_count, has_user_pref)) {
|
||||
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine)) {
|
||||
enabled_engine_count++;
|
||||
}
|
||||
}
|
||||
|
|
@ -535,9 +450,7 @@ int results_handler(UrlParams *params) {
|
|||
int engine_idx = 0;
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (ENGINE_REGISTRY[i].enabled &&
|
||||
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine) &&
|
||||
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
|
||||
user_engine_count, has_user_pref)) {
|
||||
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine)) {
|
||||
all_results[engine_idx] = NULL;
|
||||
jobs[engine_idx].engine = &ENGINE_REGISTRY[i];
|
||||
jobs[engine_idx].query = raw_query;
|
||||
|
|
@ -555,56 +468,9 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
|
||||
char client_key[BUFFER_SIZE_SMALL];
|
||||
rate_limit_get_client_key(client_key, sizeof(client_key));
|
||||
|
||||
char *request_cache_key = build_search_request_cache_key(
|
||||
raw_query, selected_engine_id, page, client_key);
|
||||
int request_is_cached = 0;
|
||||
|
||||
if (request_cache_key && get_cache_ttl_search() > 0) {
|
||||
char *cached_marker = NULL;
|
||||
size_t cached_marker_size = 0;
|
||||
|
||||
if (cache_get(request_cache_key, (time_t)get_cache_ttl_search(),
|
||||
&cached_marker, &cached_marker_size) == 0) {
|
||||
request_is_cached = 1;
|
||||
}
|
||||
|
||||
free(cached_marker);
|
||||
}
|
||||
|
||||
if (engine_idx > 0 && !request_is_cached) {
|
||||
RateLimitConfig rate_limit_config = {
|
||||
.max_requests = global_config.rate_limit_search_requests,
|
||||
.interval_seconds = global_config.rate_limit_search_interval,
|
||||
};
|
||||
RateLimitResult rate_limit_result =
|
||||
rate_limit_check("search", &rate_limit_config);
|
||||
if (rate_limit_result.limited) {
|
||||
char response[256];
|
||||
snprintf(response, sizeof(response), "<h1>%s</h1>", rate_limit_msg);
|
||||
send_response(response);
|
||||
free(request_cache_key);
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
free_context(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (request_cache_key && get_cache_ttl_search() > 0) {
|
||||
cache_set(request_cache_key, "1", 1);
|
||||
}
|
||||
}
|
||||
|
||||
int filter_engine_count = 0;
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (ENGINE_REGISTRY[i].enabled &&
|
||||
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
|
||||
user_engine_count, has_user_pref))
|
||||
if (ENGINE_REGISTRY[i].enabled)
|
||||
filter_engine_count++;
|
||||
}
|
||||
|
||||
|
|
@ -621,9 +487,7 @@ int results_handler(UrlParams *params) {
|
|||
free(all_href);
|
||||
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (!ENGINE_REGISTRY[i].enabled ||
|
||||
!engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
|
||||
user_engine_count, has_user_pref))
|
||||
if (!ENGINE_REGISTRY[i].enabled)
|
||||
continue;
|
||||
|
||||
char *filter_href =
|
||||
|
|
@ -681,12 +545,6 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
}
|
||||
free(request_cache_key);
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
free_context(&ctx);
|
||||
if (redirect_url) {
|
||||
send_redirect(redirect_url);
|
||||
|
|
@ -705,16 +563,8 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
}
|
||||
free(request_cache_key);
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
free_context(&ctx);
|
||||
char no_results_html[128];
|
||||
snprintf(no_results_html, sizeof(no_results_html), "<h1>%s</h1>", no_results_msg);
|
||||
send_response(no_results_html);
|
||||
send_response("<h1>No results found</h1>");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -746,7 +596,7 @@ int results_handler(UrlParams *params) {
|
|||
|
||||
int warning_count = 0;
|
||||
for (int i = 0; i < enabled_engine_count; i++) {
|
||||
if (warning_message_for_job(&jobs[i], locale))
|
||||
if (warning_message_for_job(&jobs[i]))
|
||||
warning_count++;
|
||||
}
|
||||
|
||||
|
|
@ -756,7 +606,7 @@ int results_handler(UrlParams *params) {
|
|||
int warning_index = 0;
|
||||
|
||||
for (int i = 0; i < enabled_engine_count; i++) {
|
||||
const char *warning_message = warning_message_for_job(&jobs[i], locale);
|
||||
const char *warning_message = warning_message_for_job(&jobs[i]);
|
||||
if (!warning_message)
|
||||
continue;
|
||||
|
||||
|
|
@ -790,7 +640,14 @@ int results_handler(UrlParams *params) {
|
|||
if (total_results > 0) {
|
||||
char ***results_matrix = (char ***)malloc(sizeof(char **) * total_results);
|
||||
int *results_inner_counts = (int *)malloc(sizeof(int) * total_results);
|
||||
if (!results_matrix || !results_inner_counts) {
|
||||
char **seen_urls = (char **)malloc(sizeof(char *) * total_results);
|
||||
if (!results_matrix || !results_inner_counts || !seen_urls) {
|
||||
if (results_matrix)
|
||||
free(results_matrix);
|
||||
if (results_inner_counts)
|
||||
free(results_inner_counts);
|
||||
if (seen_urls)
|
||||
free(seen_urls);
|
||||
char *html = render_template("results.html", &ctx);
|
||||
if (html) {
|
||||
send_response(html);
|
||||
|
|
@ -805,35 +662,41 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
}
|
||||
free(request_cache_key);
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
free_context(&ctx);
|
||||
return 0;
|
||||
}
|
||||
int unique_count = 0;
|
||||
UrlHashTable url_table;
|
||||
url_hash_init(&url_table);
|
||||
|
||||
for (int i = 0; i < enabled_engine_count; i++) {
|
||||
for (int j = 0; j < jobs[i].results_count; j++) {
|
||||
char *display_url = all_results[i][j].url;
|
||||
|
||||
if (url_hash_contains(&url_table, display_url)) {
|
||||
int is_duplicate = 0;
|
||||
for (int k = 0; k < unique_count; k++) {
|
||||
if (strcmp(seen_urls[k], display_url) == 0) {
|
||||
is_duplicate = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_duplicate) {
|
||||
free(all_results[i][j].url);
|
||||
free(all_results[i][j].title);
|
||||
free(all_results[i][j].snippet);
|
||||
continue;
|
||||
}
|
||||
|
||||
url_hash_insert(&url_table, display_url);
|
||||
|
||||
seen_urls[unique_count] = strdup(display_url);
|
||||
if (!seen_urls[unique_count]) {
|
||||
free(all_results[i][j].url);
|
||||
free(all_results[i][j].title);
|
||||
free(all_results[i][j].snippet);
|
||||
continue;
|
||||
}
|
||||
results_matrix[unique_count] =
|
||||
(char **)malloc(sizeof(char *) * RESULT_FIELD_COUNT);
|
||||
if (!results_matrix[unique_count]) {
|
||||
free(seen_urls[unique_count]);
|
||||
free(all_results[i][j].url);
|
||||
free(all_results[i][j].title);
|
||||
free(all_results[i][j].snippet);
|
||||
|
|
@ -871,10 +734,43 @@ int results_handler(UrlParams *params) {
|
|||
|
||||
char ***pager_matrix = NULL;
|
||||
int *pager_inner_counts = NULL;
|
||||
SearchHrefData href_data = { .query = raw_query, .engine_id = selected_engine_id };
|
||||
int pager_count = build_pagination(page, search_href_builder,
|
||||
&href_data, &pager_matrix,
|
||||
&pager_inner_counts);
|
||||
int pager_count = 0;
|
||||
int pager_start = page <= 3 ? 1 : page - 2;
|
||||
int pager_end = pager_start + PAGER_WINDOW_SIZE - 1;
|
||||
|
||||
if (page > 3) {
|
||||
char *first_href = build_search_href(raw_query, selected_engine_id, 1);
|
||||
pager_count = add_link_to_collection(first_href, "First", "pagination-btn",
|
||||
&pager_matrix, &pager_inner_counts,
|
||||
pager_count);
|
||||
free(first_href);
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
char *prev_href =
|
||||
build_search_href(raw_query, selected_engine_id, page - 1);
|
||||
pager_count = add_link_to_collection(prev_href, "Prev", "pagination-btn",
|
||||
&pager_matrix, &pager_inner_counts,
|
||||
pager_count);
|
||||
free(prev_href);
|
||||
}
|
||||
|
||||
for (int i = pager_start; i <= pager_end; i++) {
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%d", i);
|
||||
char *page_href = build_search_href(raw_query, selected_engine_id, i);
|
||||
pager_count = add_link_to_collection(
|
||||
page_href, label,
|
||||
i == page ? "pagination-btn pagination-current" : "pagination-btn",
|
||||
&pager_matrix, &pager_inner_counts, pager_count);
|
||||
free(page_href);
|
||||
}
|
||||
|
||||
char *next_href = build_search_href(raw_query, selected_engine_id, page + 1);
|
||||
pager_count = add_link_to_collection(next_href, "Next", "pagination-btn",
|
||||
&pager_matrix, &pager_inner_counts,
|
||||
pager_count);
|
||||
free(next_href);
|
||||
|
||||
if (pager_count > 0) {
|
||||
context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix,
|
||||
|
|
@ -898,10 +794,11 @@ int results_handler(UrlParams *params) {
|
|||
for (int j = 0; j < RESULT_FIELD_COUNT; j++)
|
||||
free(results_matrix[i][j]);
|
||||
free(results_matrix[i]);
|
||||
free(seen_urls[i]);
|
||||
}
|
||||
free(seen_urls);
|
||||
free(results_matrix);
|
||||
free(results_inner_counts);
|
||||
url_hash_free(&url_table);
|
||||
} else {
|
||||
char *html = render_template("results.html", &ctx);
|
||||
if (html) {
|
||||
|
|
@ -914,8 +811,6 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
|
||||
free(request_cache_key);
|
||||
|
||||
if (page == 1) {
|
||||
for (int i = 0; i < HANDLER_COUNT; i++) {
|
||||
if (infobox_data[i].success) {
|
||||
|
|
@ -923,12 +818,6 @@ int results_handler(UrlParams *params) {
|
|||
}
|
||||
}
|
||||
}
|
||||
free(locale);
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
free_context(&ctx);
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
#include "Settings.h"
|
||||
#include "../Scraping/Scraping.h"
|
||||
#include "../Utility/Utility.h"
|
||||
#include <beaker.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int settings_handler(UrlParams *params) {
|
||||
const char *query = "";
|
||||
if (params) {
|
||||
for (int i = 0; i < params->count; i++) {
|
||||
if (strcmp(params->params[i].key, "q") == 0) {
|
||||
query = params->params[i].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char *theme = get_theme("system");
|
||||
char *locale = get_locale("en_uk");
|
||||
|
||||
LocaleInfo locales[32];
|
||||
int locale_count = beaker_get_all_locales(locales, 32);
|
||||
|
||||
char **locale_data[32];
|
||||
int inner_counts[32];
|
||||
for (int i = 0; i < locale_count; i++) {
|
||||
locale_data[i] = malloc(sizeof(char *) * 2);
|
||||
locale_data[i][0] = locales[i].meta.id;
|
||||
locale_data[i][1] = locales[i].meta.name;
|
||||
inner_counts[i] = 2;
|
||||
}
|
||||
|
||||
char **user_engines = NULL;
|
||||
int user_engine_count = 0;
|
||||
int has_user_pref = (get_user_engines(&user_engines, &user_engine_count) == 0);
|
||||
|
||||
int enabled_count = 0;
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (ENGINE_REGISTRY[i].enabled)
|
||||
enabled_count++;
|
||||
}
|
||||
|
||||
char ***engine_data = NULL;
|
||||
int *engine_inner = NULL;
|
||||
|
||||
if (enabled_count > 0) {
|
||||
engine_data = malloc(sizeof(char **) * enabled_count);
|
||||
engine_inner = malloc(sizeof(int) * enabled_count);
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (!ENGINE_REGISTRY[i].enabled)
|
||||
continue;
|
||||
|
||||
int is_selected = !has_user_pref;
|
||||
if (has_user_pref) {
|
||||
for (int j = 0; j < user_engine_count; j++) {
|
||||
if (strcmp(user_engines[j], ENGINE_REGISTRY[i].id) == 0) {
|
||||
is_selected = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine_data[idx] = malloc(sizeof(char *) * 3);
|
||||
engine_data[idx][0] = (char *)ENGINE_REGISTRY[i].id;
|
||||
engine_data[idx][1] = (char *)ENGINE_REGISTRY[i].name;
|
||||
engine_data[idx][2] = is_selected ? "checked" : "";
|
||||
engine_inner[idx] = 3;
|
||||
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
TemplateContext ctx = new_context();
|
||||
beaker_set_locale(&ctx, locale);
|
||||
context_set(&ctx, "query", query);
|
||||
context_set(&ctx, "theme", theme);
|
||||
context_set(&ctx, "locale", locale);
|
||||
context_set_array_of_arrays(&ctx, "locales", locale_data, locale_count, inner_counts);
|
||||
|
||||
if (enabled_count > 0) {
|
||||
context_set_array_of_arrays(&ctx, "enabled_engines", engine_data,
|
||||
enabled_count, engine_inner);
|
||||
context_set(&ctx, "has_enabled_engines", "1");
|
||||
}
|
||||
|
||||
for (int i = 0; i < locale_count; i++) {
|
||||
free(locale_data[i]);
|
||||
}
|
||||
if (engine_data) {
|
||||
for (int i = 0; i < enabled_count; i++)
|
||||
free(engine_data[i]);
|
||||
free(engine_data);
|
||||
}
|
||||
free(engine_inner);
|
||||
if (has_user_pref) {
|
||||
for (int i = 0; i < user_engine_count; i++)
|
||||
free(user_engines[i]);
|
||||
free(user_engines);
|
||||
}
|
||||
|
||||
char *rendered_html = render_template("settings.html", &ctx);
|
||||
send_response(rendered_html);
|
||||
|
||||
free(rendered_html);
|
||||
free(theme);
|
||||
free(locale);
|
||||
free_context(&ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include <beaker.h>
|
||||
|
||||
int settings_handler(UrlParams *params);
|
||||
|
||||
#endif
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
#include "SettingsSave.h"
|
||||
#include "../Scraping/Scraping.h"
|
||||
#include "../Utility/Utility.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MAX_ENGINE_IDS ENGINE_COUNT
|
||||
|
||||
int settings_save_handler(UrlParams *params) {
|
||||
const char *theme = "";
|
||||
const char *locale = "";
|
||||
const char *query = "";
|
||||
int engines_present = 0;
|
||||
char selected_ids[ENGINE_COUNT][32];
|
||||
int selected_count = 0;
|
||||
|
||||
if (params) {
|
||||
for (int i = 0; i < params->count; i++) {
|
||||
if (strcmp(params->params[i].key, "theme") == 0) {
|
||||
theme = params->params[i].value;
|
||||
} else if (strcmp(params->params[i].key, "locale") == 0) {
|
||||
locale = params->params[i].value;
|
||||
} else if (strcmp(params->params[i].key, "q") == 0) {
|
||||
query = params->params[i].value;
|
||||
} else if (strcmp(params->params[i].key, "engines_present") == 0) {
|
||||
engines_present = 1;
|
||||
} else if (strncmp(params->params[i].key, "engine_", 7) == 0 &&
|
||||
strcmp(params->params[i].value, "1") == 0) {
|
||||
const char *engine_id = params->params[i].key + 7;
|
||||
if (engine_id[0] != '\0' && is_engine_id_enabled(engine_id) &&
|
||||
selected_count < ENGINE_COUNT) {
|
||||
strncpy(selected_ids[selected_count], engine_id,
|
||||
sizeof(selected_ids[selected_count]) - 1);
|
||||
selected_ids[selected_count][sizeof(selected_ids[selected_count]) - 1] =
|
||||
'\0';
|
||||
selected_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(theme) > 0) {
|
||||
set_cookie("theme", theme, "Fri, 31 Dec 2038 23:59:59 GMT", "/", false, false);
|
||||
}
|
||||
if (strlen(locale) > 0) {
|
||||
set_cookie("locale", locale, "Fri, 31 Dec 2038 23:59:59 GMT", "/", false, false);
|
||||
}
|
||||
|
||||
if (engines_present) {
|
||||
char cookie_value[512];
|
||||
cookie_value[0] = '\0';
|
||||
for (int i = 0; i < selected_count; i++) {
|
||||
if (i > 0)
|
||||
strcat(cookie_value, ",");
|
||||
strcat(cookie_value, selected_ids[i]);
|
||||
}
|
||||
set_cookie("engines", cookie_value, "Fri, 31 Dec 2038 23:59:59 GMT", "/",
|
||||
false, false);
|
||||
}
|
||||
|
||||
char redirect_url[512];
|
||||
snprintf(redirect_url, sizeof(redirect_url), "/settings?q=%s", query);
|
||||
send_redirect(redirect_url);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#ifndef SETTINGS_SAVE_H
|
||||
#define SETTINGS_SAVE_H
|
||||
|
||||
#include <beaker.h>
|
||||
|
||||
int settings_save_handler(UrlParams *params);
|
||||
|
||||
#endif
|
||||
|
|
@ -24,8 +24,7 @@ static int response_is_startpage_captcha(const ScrapeJob *job,
|
|||
|
||||
return response_contains(response, "<title>Startpage Captcha</title>") ||
|
||||
response_contains(response, "Startpage Captcha") ||
|
||||
response_contains(response, "/static-pages-assets/page-data/captcha/") ||
|
||||
response_contains(response, ">Startpage Blocked</title>");
|
||||
response_contains(response, "/static-pages-assets/page-data/captcha/");
|
||||
}
|
||||
|
||||
static int response_looks_like_results_page(const ScrapeJob *job,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
#include "Utility.h"
|
||||
#include "../Scraping/Scraping.h"
|
||||
#include <beaker.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int hex_to_int(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
|
|
@ -14,204 +9,3 @@ int hex_to_int(char c) {
|
|||
return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *get_theme(const char *default_theme) {
|
||||
char *cookie = get_cookie("theme");
|
||||
if (cookie &&
|
||||
(strcmp(cookie, "light") == 0 ||
|
||||
strcmp(cookie, "dark") == 0)) {
|
||||
return cookie;
|
||||
}
|
||||
free(cookie);
|
||||
return strdup(default_theme);
|
||||
}
|
||||
|
||||
char *get_locale(const char *default_locale) {
|
||||
char *cookie = get_cookie("locale");
|
||||
if (cookie && beaker_get_locale_meta(cookie) != NULL) {
|
||||
return cookie;
|
||||
}
|
||||
free(cookie);
|
||||
return strdup(default_locale);
|
||||
}
|
||||
|
||||
static int engine_id_casecmp(const char *a, const char *b) {
|
||||
while (*a && *b) {
|
||||
char la = *a;
|
||||
char lb = *b;
|
||||
if (la >= 'A' && la <= 'Z') la = la - 'A' + 'a';
|
||||
if (lb >= 'A' && lb <= 'Z') lb = lb - 'A' + 'a';
|
||||
if (la != lb) return 0;
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
int is_engine_id_enabled(const char *engine_id) {
|
||||
if (!engine_id) return 0;
|
||||
for (int i = 0; i < ENGINE_COUNT; i++) {
|
||||
if (ENGINE_REGISTRY[i].enabled &&
|
||||
engine_id_casecmp(ENGINE_REGISTRY[i].id, engine_id)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_user_engines(char ***out_ids, int *out_count) {
|
||||
*out_ids = NULL;
|
||||
*out_count = 0;
|
||||
|
||||
char *cookie = get_cookie("engines");
|
||||
if (!cookie || cookie[0] == '\0') {
|
||||
free(cookie);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char **ids = NULL;
|
||||
int count = 0;
|
||||
|
||||
char *copy = strdup(cookie);
|
||||
if (!copy) {
|
||||
free(cookie);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *saveptr;
|
||||
char *token = strtok_r(copy, ",", &saveptr);
|
||||
while (token) {
|
||||
while (*token == ' ' || *token == '\t')
|
||||
token++;
|
||||
|
||||
if (token[0] != '\0' && is_engine_id_enabled(token)) {
|
||||
char **new_ids = realloc(ids, sizeof(char *) * (count + 1));
|
||||
if (new_ids) {
|
||||
ids = new_ids;
|
||||
ids[count] = strdup(token);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
token = strtok_r(NULL, ",", &saveptr);
|
||||
}
|
||||
|
||||
free(copy);
|
||||
free(cookie);
|
||||
|
||||
if (count == 0) {
|
||||
free(ids);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_ids = ids;
|
||||
*out_count = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int user_engines_contains(const char *engine_id, char **ids, int count) {
|
||||
if (!engine_id || !ids) return 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (engine_id_casecmp(ids[i], engine_id))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int add_link_to_collection(const char *href, const char *label,
|
||||
const char *class_name, char ****collection,
|
||||
int **inner_counts, int current_count) {
|
||||
char ***old_collection = *collection;
|
||||
int *old_inner_counts = *inner_counts;
|
||||
char ***new_collection =
|
||||
(char ***)malloc(sizeof(char **) * (current_count + 1));
|
||||
int *new_inner_counts =
|
||||
(int *)malloc(sizeof(int) * (current_count + 1));
|
||||
|
||||
if (!new_collection || !new_inner_counts) {
|
||||
free(new_collection);
|
||||
free(new_inner_counts);
|
||||
return current_count;
|
||||
}
|
||||
|
||||
if (*collection && current_count > 0) {
|
||||
memcpy(new_collection, *collection, sizeof(char **) * current_count);
|
||||
}
|
||||
if (*inner_counts && current_count > 0) {
|
||||
memcpy(new_inner_counts, *inner_counts, sizeof(int) * current_count);
|
||||
}
|
||||
|
||||
*collection = new_collection;
|
||||
*inner_counts = new_inner_counts;
|
||||
|
||||
(*collection)[current_count] =
|
||||
(char **)malloc(sizeof(char *) * LINK_FIELD_COUNT);
|
||||
if (!(*collection)[current_count]) {
|
||||
*collection = old_collection;
|
||||
*inner_counts = old_inner_counts;
|
||||
free(new_collection);
|
||||
free(new_inner_counts);
|
||||
return current_count;
|
||||
}
|
||||
|
||||
(*collection)[current_count][0] = strdup(href ? href : "");
|
||||
(*collection)[current_count][1] = strdup(label ? label : "");
|
||||
(*collection)[current_count][2] = strdup(class_name ? class_name : "");
|
||||
|
||||
if (!(*collection)[current_count][0] || !(*collection)[current_count][1] ||
|
||||
!(*collection)[current_count][2]) {
|
||||
free((*collection)[current_count][0]);
|
||||
free((*collection)[current_count][1]);
|
||||
free((*collection)[current_count][2]);
|
||||
free((*collection)[current_count]);
|
||||
*collection = old_collection;
|
||||
*inner_counts = old_inner_counts;
|
||||
free(new_collection);
|
||||
free(new_inner_counts);
|
||||
return current_count;
|
||||
}
|
||||
|
||||
(*inner_counts)[current_count] = LINK_FIELD_COUNT;
|
||||
|
||||
free(old_collection);
|
||||
free(old_inner_counts);
|
||||
return current_count + 1;
|
||||
}
|
||||
|
||||
int build_pagination(int page,
|
||||
char *(*href_builder)(int page, void *data), void *data,
|
||||
char ****out_matrix, int **out_inner_counts) {
|
||||
enum { PAGER_WINDOW_SIZE = 5 };
|
||||
|
||||
*out_matrix = NULL;
|
||||
*out_inner_counts = NULL;
|
||||
int count = 0;
|
||||
|
||||
int pager_start = page <= 3 ? 1 : page - 2;
|
||||
int pager_end = pager_start + PAGER_WINDOW_SIZE - 1;
|
||||
|
||||
if (page > 1) {
|
||||
char *href = href_builder(page - 1, data);
|
||||
count = add_link_to_collection(href, "←", "pagination-btn prev",
|
||||
out_matrix, out_inner_counts, count);
|
||||
free(href);
|
||||
}
|
||||
|
||||
for (int i = pager_start; i <= pager_end; i++) {
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%d", i);
|
||||
char *href = href_builder(i, data);
|
||||
count = add_link_to_collection(
|
||||
href, label,
|
||||
i == page ? "pagination-btn pagination-current" : "pagination-btn",
|
||||
out_matrix, out_inner_counts, count);
|
||||
free(href);
|
||||
}
|
||||
|
||||
char *href = href_builder(page + 1, data);
|
||||
count = add_link_to_collection(href, "→", "pagination-btn next",
|
||||
out_matrix, out_inner_counts, count);
|
||||
free(href);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,6 @@
|
|||
#ifndef UTILITY_H
|
||||
#define UTILITY_H
|
||||
|
||||
#include <beaker.h>
|
||||
|
||||
#define LINK_FIELD_COUNT 3
|
||||
|
||||
int hex_to_int(char c);
|
||||
char *get_theme(const char *default_theme);
|
||||
char *get_locale(const char *default_locale);
|
||||
|
||||
int is_engine_id_enabled(const char *engine_id);
|
||||
int get_user_engines(char ***out_ids, int *out_count);
|
||||
int user_engines_contains(const char *engine_id, char **ids, int count);
|
||||
|
||||
int add_link_to_collection(const char *href, const char *label,
|
||||
const char *class_name, char ****collection,
|
||||
int **inner_counts, int current_count);
|
||||
|
||||
int build_pagination(int page,
|
||||
char *(*href_builder)(int page, void *data), void *data,
|
||||
char ****out_matrix, int **out_inner_counts);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
335
static/main.css
335
static/main.css
|
|
@ -25,19 +25,12 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height:100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color:var(--bg-main);
|
||||
background-image:radial-gradient(circle at top end, var(--bg-card) 0%, var(--bg-main) 100%);
|
||||
background-attachment:fixed;
|
||||
color:var(--text-primary);
|
||||
font-family:system-ui,-apple-system,sans-serif;
|
||||
margin:0;
|
||||
padding:0;
|
||||
min-height:100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +44,7 @@ img[src=""] {
|
|||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background: radial-gradient(circle at top right, var(--bg-card) 0%, var(--bg-main) 100%);
|
||||
}
|
||||
|
||||
.view-home .container {
|
||||
|
|
@ -106,65 +100,18 @@ img[src=""] {
|
|||
background:var(--bg-card);
|
||||
color:var(--text-primary);
|
||||
border-color:var(--border);
|
||||
text-decoration:none;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
padding:10px 24px;
|
||||
border-radius:8px;
|
||||
font-weight:600;
|
||||
font-size:0.9rem;
|
||||
cursor:pointer;
|
||||
transition:all 0.2s;
|
||||
border:1px solid var(--border);
|
||||
}
|
||||
.view-home .btn-secondary:hover {
|
||||
background:var(--border);
|
||||
border-color:var(--text-secondary);
|
||||
}
|
||||
.home-settings-btn {
|
||||
position:fixed;
|
||||
top:27px;
|
||||
inset-inline-end:60px;
|
||||
width:24px;
|
||||
height:24px;
|
||||
background-color:var(--text-primary);
|
||||
-webkit-mask-image:url('/static/settings.svg');
|
||||
mask-image:url('/static/settings.svg');
|
||||
mask-size:contain;
|
||||
mask-repeat:no-repeat;
|
||||
mask-position:center;
|
||||
text-decoration:none;
|
||||
}
|
||||
.nav-settings-icon {
|
||||
width:24px;
|
||||
height:24px;
|
||||
flex-shrink:0;
|
||||
margin-inline-start:auto;
|
||||
margin-top:3px;
|
||||
background-color:var(--text-secondary);
|
||||
-webkit-mask-image:url('/static/settings.svg');
|
||||
mask-image:url('/static/settings.svg');
|
||||
mask-size:100% 100%;
|
||||
transition:background-color 0.2s;
|
||||
text-decoration:none;
|
||||
}
|
||||
.nav-settings-icon:hover,
|
||||
.nav-settings-icon.active {
|
||||
background-color:var(--text-primary);
|
||||
}
|
||||
.nav-settings-link {
|
||||
display:none;
|
||||
margin-inline-start:auto;
|
||||
}
|
||||
header {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:20px;
|
||||
padding-block:15px;
|
||||
padding-inline:60px;
|
||||
padding:15px 60px;
|
||||
border-bottom:1px solid var(--border);
|
||||
background:var(--bg-main);
|
||||
width:100%;
|
||||
}
|
||||
.search-form {
|
||||
flex-grow:1;
|
||||
|
|
@ -179,16 +126,6 @@ h1 {
|
|||
h1 span {
|
||||
color:var(--accent);
|
||||
}
|
||||
.logo-link {
|
||||
text-decoration:none;
|
||||
color:inherit;
|
||||
}
|
||||
header .logo-link {
|
||||
transition:transform 0.2s;
|
||||
}
|
||||
header .logo-link:hover {
|
||||
transform:scale(1.03);
|
||||
}
|
||||
.search-box {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
|
|
@ -205,14 +142,14 @@ header .logo-link:hover {
|
|||
box-shadow:0 0 0 4px var(--accent-glow);
|
||||
}
|
||||
.nav-tabs {
|
||||
padding-inline:60px;
|
||||
padding:0 60px;
|
||||
border-bottom:1px solid var(--border);
|
||||
background:var(--bg-main);
|
||||
width:100%;
|
||||
}
|
||||
.nav-container {
|
||||
display:flex;
|
||||
gap:30px;
|
||||
max-width:1200px;
|
||||
}
|
||||
.nav-tabs a {
|
||||
padding:14px 0;
|
||||
|
|
@ -231,9 +168,6 @@ header .logo-link:hover {
|
|||
color:var(--accent);
|
||||
border-bottom-color:var(--accent);
|
||||
}
|
||||
.nav-right {
|
||||
margin-inline-start:auto;
|
||||
}
|
||||
.image-results-container {
|
||||
padding:30px 60px;
|
||||
}
|
||||
|
|
@ -336,8 +270,7 @@ header .logo-link:hover {
|
|||
display:grid;
|
||||
grid-template-columns:140px minmax(0,700px) 450px;
|
||||
gap:60px;
|
||||
padding-block:30px;
|
||||
padding-inline:60px;
|
||||
padding:30px 60px;
|
||||
}
|
||||
.result-header {
|
||||
display: flex;
|
||||
|
|
@ -353,7 +286,7 @@ header .logo-link:hover {
|
|||
background-size: cover;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
inset-inline-start: -24px;
|
||||
left: -24px;
|
||||
}
|
||||
.url {
|
||||
color: var(--text-secondary);
|
||||
|
|
@ -367,7 +300,7 @@ header .logo-link:hover {
|
|||
.result-favicon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
inset-inline-start: -20px;
|
||||
left: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +308,7 @@ header .logo-link:hover {
|
|||
.result-favicon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
inset-inline-start: -16px;
|
||||
left: -16px;
|
||||
}
|
||||
}
|
||||
.results-container {
|
||||
|
|
@ -538,59 +471,41 @@ header .logo-link:hover {
|
|||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
[dir="rtl"] .pagination-btn.prev {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
[dir="rtl"] .pagination-btn.next {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
|
||||
@media (max-width:1200px) {
|
||||
body {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.content-layout {
|
||||
grid-template-columns:1fr;
|
||||
padding-block:20px;
|
||||
padding-inline:30px;
|
||||
gap:20px;
|
||||
}
|
||||
header {
|
||||
padding:20px 30px;
|
||||
gap:20px;
|
||||
}
|
||||
.results-container,.infobox-sidebar {
|
||||
grid-column:1;
|
||||
max-width:100%;
|
||||
}
|
||||
.settings-layout {
|
||||
padding-block:20px;
|
||||
padding-inline:30px;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
}
|
||||
.infobox-sidebar {
|
||||
order:-1;
|
||||
}
|
||||
.nav-tabs,.image-results-container {
|
||||
padding-inline:30px;
|
||||
padding:0 30px;
|
||||
}
|
||||
header {
|
||||
padding-block:15px;
|
||||
padding-inline:30px;
|
||||
padding:15px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:768px) {
|
||||
.nav-settings-icon {
|
||||
display:none;
|
||||
}
|
||||
.nav-settings-link {
|
||||
display:inline;
|
||||
body {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
header {
|
||||
flex-direction:column;
|
||||
gap:12px;
|
||||
padding-block:12px;
|
||||
padding-inline:16px;
|
||||
padding:12px 16px;
|
||||
text-align:center;
|
||||
}
|
||||
h1 {
|
||||
|
|
@ -606,7 +521,7 @@ header .logo-link:hover {
|
|||
.nav-tabs {
|
||||
overflow-x:auto;
|
||||
-webkit-overflow-scrolling:touch;
|
||||
padding-inline:16px;
|
||||
padding:0 16px;
|
||||
}
|
||||
.nav-container {
|
||||
gap:24px;
|
||||
|
|
@ -617,9 +532,7 @@ header .logo-link:hover {
|
|||
font-size:0.95rem;
|
||||
}
|
||||
.content-layout {
|
||||
padding-inline-start:40px;
|
||||
padding-inline-end:16px;
|
||||
padding-block:16px;
|
||||
padding:16px;
|
||||
gap:16px;
|
||||
}
|
||||
.result {
|
||||
|
|
@ -663,7 +576,7 @@ header .logo-link:hover {
|
|||
max-width:200px;
|
||||
}
|
||||
.image-results-container {
|
||||
padding-inline:16px;
|
||||
padding:16px;
|
||||
}
|
||||
.pagination {
|
||||
flex-wrap:wrap;
|
||||
|
|
@ -678,6 +591,7 @@ header .logo-link:hover {
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateY(-5vh);
|
||||
padding:20px 16px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
|
@ -707,17 +621,8 @@ header .logo-link:hover {
|
|||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.content-layout {
|
||||
padding-inline-start:28px;
|
||||
padding-inline-end:16px;
|
||||
padding-block:16px;
|
||||
}
|
||||
.settings-layout {
|
||||
padding:0;
|
||||
}
|
||||
header {
|
||||
padding-inline:12px;
|
||||
padding-block:12px;
|
||||
padding:12px 12px;
|
||||
}
|
||||
.search-box {
|
||||
font-size:0.95rem;
|
||||
|
|
@ -752,195 +657,3 @@ header .logo-link:hover {
|
|||
font-size:0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-layout {
|
||||
padding-block: 30px;
|
||||
padding-inline-start: 260px;
|
||||
padding-inline-end: 60px;
|
||||
}
|
||||
|
||||
.settings-container {
|
||||
max-width:700px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-size:1.8rem;
|
||||
font-weight:700;
|
||||
margin:0 0 32px 0;
|
||||
letter-spacing:-0.5px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background:var(--bg-card);
|
||||
border:1px solid var(--border);
|
||||
border-radius:12px;
|
||||
padding:24px;
|
||||
margin-bottom:32px;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
font-size:1.1rem;
|
||||
font-weight:700;
|
||||
margin:0 0 4px 0;
|
||||
}
|
||||
|
||||
.settings-section-desc {
|
||||
color:var(--text-secondary);
|
||||
font-size:0.9rem;
|
||||
margin:0 0 20px 0;
|
||||
line-height:1.4;
|
||||
}
|
||||
|
||||
.settings-field {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
padding:10px 0;
|
||||
}
|
||||
|
||||
.settings-field + .settings-field {
|
||||
border-top:1px solid var(--border);
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-size:0.95rem;
|
||||
color:var(--text-primary);
|
||||
}
|
||||
|
||||
.settings-select {
|
||||
padding:8px 12px;
|
||||
border-radius:8px;
|
||||
border:1px solid var(--border);
|
||||
background:var(--bg-main);
|
||||
color:var(--text-primary);
|
||||
font-size:0.9rem;
|
||||
outline:none;
|
||||
cursor:pointer;
|
||||
transition:border-color 0.2s;
|
||||
}
|
||||
|
||||
.settings-select:focus {
|
||||
border-color:var(--accent);
|
||||
}
|
||||
|
||||
.settings-checkbox {
|
||||
width:18px;
|
||||
height:18px;
|
||||
accent-color:var(--accent);
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display:flex;
|
||||
gap:12px;
|
||||
margin-top:8px;
|
||||
padding-bottom:40px;
|
||||
justify-content:flex-start;
|
||||
}
|
||||
|
||||
.settings-actions .btn-primary {
|
||||
background:var(--accent);
|
||||
color:var(--bg-main);
|
||||
border:1px solid transparent;
|
||||
padding:10px 24px;
|
||||
border-radius:8px;
|
||||
font-weight:600;
|
||||
font-size:0.9rem;
|
||||
cursor:pointer;
|
||||
transition:all 0.2s;
|
||||
touch-action:manipulation;
|
||||
}
|
||||
|
||||
.settings-actions .btn-primary:hover {
|
||||
filter:brightness(1.1);
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
|
||||
.settings-actions .btn-secondary {
|
||||
background:var(--bg-card);
|
||||
color:var(--text-primary);
|
||||
border:1px solid var(--border);
|
||||
padding:10px 24px;
|
||||
border-radius:8px;
|
||||
font-weight:600;
|
||||
font-size:0.9rem;
|
||||
cursor:pointer;
|
||||
transition:all 0.2s;
|
||||
touch-action:manipulation;
|
||||
}
|
||||
|
||||
.settings-actions .btn-secondary:hover {
|
||||
background:var(--border);
|
||||
border-color:var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width:768px) {
|
||||
.settings-layout {
|
||||
padding:12px;
|
||||
display:block;
|
||||
}
|
||||
.settings-container {
|
||||
max-width:100%;
|
||||
}
|
||||
.settings-title {
|
||||
font-size:1.4rem;
|
||||
margin-bottom:24px;
|
||||
}
|
||||
.settings-section {
|
||||
padding:16px;
|
||||
}
|
||||
.settings-field {
|
||||
flex-direction:column;
|
||||
align-items:stretch;
|
||||
gap:8px;
|
||||
}
|
||||
.settings-actions {
|
||||
flex-direction:column;
|
||||
}
|
||||
.settings-actions .btn-primary,
|
||||
.settings-actions .btn-secondary {
|
||||
width:100%;
|
||||
text-align:center;
|
||||
}
|
||||
}
|
||||
|
||||
[dir="rtl"] {
|
||||
direction: rtl;
|
||||
unicode-bidi: embed;
|
||||
}
|
||||
|
||||
[dir="rtl"] header {
|
||||
flex-direction: row-reverse;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
[dir="rtl"] .nav-container {
|
||||
flex-direction: row-reverse;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
[dir="rtl"] .search-box {
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
[dir="rtl"] .url {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
[dir="rtl"] .nav-settings-icon {
|
||||
margin-inline-start: unset;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
[dir="rtl"] .settings-actions .btn-primary {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
[dir="rtl"] header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 829 B |
|
|
@ -1,10 +0,0 @@
|
|||
: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);
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
: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);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
|
@ -8,9 +8,7 @@
|
|||
OmniSearch
|
||||
</title>
|
||||
<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}}
|
||||
{{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"
|
||||
type="application/opensearchdescription+xml"
|
||||
title="OmniSearch" href="/opensearch.xml">
|
||||
|
|
@ -24,21 +22,19 @@
|
|||
</h1>
|
||||
<form action="/search" class="home-search-form">
|
||||
<div class="search-input-wrapper">
|
||||
<input name="q" type="text" class="search-box" placeholder="{{l("search_placeholder")}}"
|
||||
<input name="q" type="text" class="search-box" placeholder="Search the web..."
|
||||
autofocus autocomplete="off">
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button type="submit" class="btn-primary">
|
||||
{{l("search_button")}}
|
||||
Search
|
||||
</button>
|
||||
<button type="submit" name="btnI" value="1" class="btn-secondary">
|
||||
{{l("surprise_me_button")}}
|
||||
</button>
|
||||
</div>
|
||||
Surprise me
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/settings" class="home-settings-btn" title="{{l("settings_tab")}}"></a>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
|
@ -7,33 +7,27 @@
|
|||
<title>
|
||||
OmniSearch Images - {{query}}
|
||||
</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">
|
||||
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
|
||||
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
|
||||
</head>
|
||||
|
||||
<body class="images-view">
|
||||
<header>
|
||||
<a href="/" class="logo-link"><h1>
|
||||
<h1>
|
||||
Omni<span>Search</span>
|
||||
</h1></a>
|
||||
</h1>
|
||||
<form action="/images" method="GET" class="search-form">
|
||||
<input name="q" autocomplete="off"="text" class="search-box" placeholder="{{l("search_placeholder")}}"
|
||||
<input name="q" autocomplete="off"="text" class="search-box" placeholder="Search for images..."
|
||||
value="{{query}}">
|
||||
</form>
|
||||
<a href="/settings?q={{query}}" class="nav-settings-icon" title="{{l("settings_tab")}}"></a>
|
||||
</header>
|
||||
<nav class="nav-tabs">
|
||||
<div class="nav-container">
|
||||
<a href="/search?q={{query}}">
|
||||
{{l("all_tab")}}
|
||||
All
|
||||
</a>
|
||||
<a href="/images?q={{query}}" class="active">
|
||||
{{l("images_tab")}}
|
||||
</a>
|
||||
<a href="/settings?q={{query}}" class="nav-settings-link">
|
||||
{{l("settings_tab")}}
|
||||
Images
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -46,10 +40,10 @@
|
|||
<div class="image-overlay">
|
||||
<div class="overlay-buttons">
|
||||
<a href="{{img[3]}}" target="_blank" class="overlay-btn primary">
|
||||
{{l("view_image")}}
|
||||
View Image
|
||||
</a>
|
||||
<a href="{{img[2]}}" target="_blank" class="overlay-btn secondary">
|
||||
{{l("visit_site")}}
|
||||
Visit Site
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -65,15 +59,48 @@
|
|||
</div>
|
||||
{{endfor}}
|
||||
</div>
|
||||
{{if exists pagination_links}}
|
||||
<nav class="pagination">
|
||||
{{for link in pagination_links}}
|
||||
<a class="{{link[2]}}" href="{{link[0]}}">
|
||||
{{link[1]}}
|
||||
</a>
|
||||
{{endfor}}
|
||||
</nav>
|
||||
{{endif}}
|
||||
<nav class="pagination">
|
||||
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}">
|
||||
←
|
||||
</a>
|
||||
|
||||
{{if two_prev_page != 0}}
|
||||
<a class="pagination-btn prev" href="/images?q={{query}}&p={{two_prev_page}}">
|
||||
{{two_prev_page}}
|
||||
</a>
|
||||
{{endif}}
|
||||
|
||||
{{if prev_page != 0}}
|
||||
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}">
|
||||
{{prev_page}}
|
||||
</a>
|
||||
{{endif}}
|
||||
|
||||
<a class="pagination-btn pagination-current" href="/images?q={{query}}&p={{page}}">
|
||||
{{page}}
|
||||
</a>
|
||||
<a class="pagination-btn next" href="/images?q={{query}}&p={{next_page}}">
|
||||
{{next_page}}
|
||||
</a>
|
||||
<a class="pagination-btn next" href="/images?q={{query}}&p={{two_next_page}}">
|
||||
{{two_next_page}}
|
||||
</a>
|
||||
|
||||
{{if prev_page == 0}}
|
||||
<a class="pagination-btn prev" href="/images?q={{query}}&p=4">
|
||||
4
|
||||
</a>
|
||||
{{endif}}
|
||||
|
||||
{{if two_prev_page == 0}}
|
||||
<a class="pagination-btn prev" href="/images?q={{query}}&p=5">
|
||||
5
|
||||
</a>
|
||||
{{endif}}
|
||||
<a class="pagination-btn next" href="/images?q={{query}}&p={{next_page}}">
|
||||
→
|
||||
</a>
|
||||
</nav>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
|
@ -8,9 +8,7 @@
|
|||
OmniSearch - {{query}}
|
||||
</title>
|
||||
<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}}
|
||||
{{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"
|
||||
type="application/opensearchdescription+xml"
|
||||
title="OmniSearch" href="/opensearch.xml">
|
||||
|
|
@ -18,26 +16,22 @@
|
|||
|
||||
<body class="results-view">
|
||||
<header>
|
||||
<a href="/" class="logo-link"><h1>
|
||||
<h1>
|
||||
Omni<span>Search</span>
|
||||
</h1></a>
|
||||
</h1>
|
||||
<form action="/search" method="GET" class="search-form">
|
||||
<input name="engine" type="hidden" value="{{selected_engine}}">
|
||||
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="{{l("search_placeholder")}}"
|
||||
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="Search the web..."
|
||||
value="{{query}}">
|
||||
</form>
|
||||
<a href="/settings?q={{query}}" class="nav-settings-icon" title="{{l("settings_tab")}}"></a>
|
||||
</header>
|
||||
<nav class="nav-tabs">
|
||||
<div class="nav-container">
|
||||
<a href="{{search_href}}" class="active">
|
||||
{{l("all_tab")}}
|
||||
All
|
||||
</a>
|
||||
<a href="/images?q={{query}}">
|
||||
{{l("images_tab")}}
|
||||
</a>
|
||||
<a href="/settings?q={{query}}" class="nav-settings-link">
|
||||
{{l("settings_tab")}}
|
||||
Images
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -88,7 +82,7 @@
|
|||
{{result[3]}}
|
||||
</p>
|
||||
<span>
|
||||
<a class="cached" href="https://web.archive.org/web/{{result[0]|safe}}">{{l("view_cached")}}</a>
|
||||
<a class="cached" href="https://web.archive.org/web/{{result[0]|safe}}">View Cached</a>
|
||||
</span>
|
||||
</div>
|
||||
{{endfor}}
|
||||
|
|
@ -117,8 +111,8 @@
|
|||
<div class="infobox-content"> <p class="infobox-summary">
|
||||
{{info[2]|safe}}
|
||||
</p>
|
||||
<a class="read-more" href="{{info[3]}}">
|
||||
{{l("read_more")}}
|
||||
<a class="read-more" href="{{info[3]}}">
|
||||
Read More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
||||
<title>
|
||||
OmniSearch - {{l("settings_title")}}
|
||||
</title>
|
||||
<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}}
|
||||
{{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="search"
|
||||
type="application/opensearchdescription+xml"
|
||||
title="OmniSearch" href="/opensearch.xml">
|
||||
</head>
|
||||
|
||||
<body class="settings-view">
|
||||
<header>
|
||||
<a href="/" class="logo-link"><h1>
|
||||
Omni<span>Search</span>
|
||||
</h1></a>
|
||||
{{if query != ""}}
|
||||
<form action="/search" method="GET" class="search-form">
|
||||
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="{{l("search_placeholder")}}"
|
||||
value="{{query}}">
|
||||
</form>
|
||||
{{endif}}
|
||||
{{if query != ""}}
|
||||
<a href="/search?q={{query}}" class="nav-settings-icon active" title="{{l("settings_tab")}}"></a>
|
||||
{{else}}
|
||||
<a href="/" class="nav-settings-icon active" title="{{l("settings_tab")}}"></a>
|
||||
{{endif}}
|
||||
</header>
|
||||
{{if query != ""}}
|
||||
<nav class="nav-tabs">
|
||||
<div class="nav-container">
|
||||
<a href="/search?q={{query}}">
|
||||
{{l("all_tab")}}
|
||||
</a>
|
||||
<a href="/images?q={{query}}">
|
||||
{{l("images_tab")}}
|
||||
</a>
|
||||
<a href="/settings" class="active nav-settings-link">
|
||||
{{l("settings_tab")}}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
{{endif}}
|
||||
<div class="settings-layout">
|
||||
<main class="settings-container">
|
||||
<form action="/save_settings" method="GET">
|
||||
<input type="hidden" name="q" value="{{query}}">
|
||||
<section class="settings-section">
|
||||
<h3 class="settings-section-title">{{l("theme_label")}}</h3>
|
||||
<p class="settings-section-desc">{{l("theme_desc")}}</p>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="theme">{{l("theme_label")}}</label>
|
||||
<select id="theme" name="theme" class="settings-select">
|
||||
<option value="system" {{if theme == "system"}}selected{{endif}}>{{l("theme_system")}}</option>
|
||||
<option value="light" {{if theme == "light"}}selected{{endif}}>{{l("theme_light")}}</option>
|
||||
<option value="dark" {{if theme == "dark"}}selected{{endif}}>{{l("theme_dark")}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h3 class="settings-section-title">{{l("language_label")}}</h3>
|
||||
<p class="settings-section-desc">{{l("language_desc")}}</p>
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="locale">{{l("display_language_label")}}</label>
|
||||
<select id="locale" name="locale" class="settings-select">
|
||||
{{for loc in locales}}
|
||||
<option value="{{loc[0]}}" {{if __locale_id == loc[0]}}selected{{endif}}>{{loc[1]}}</option>
|
||||
{{endfor}}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
{{if has_enabled_engines}}
|
||||
<section class="settings-section">
|
||||
<h3 class="settings-section-title">{{l("engines_label")}}</h3>
|
||||
<p class="settings-section-desc">{{l("engines_desc")}}</p>
|
||||
<input type="hidden" name="engines_present" value="1">
|
||||
{{for eng in enabled_engines}}
|
||||
<div class="settings-field">
|
||||
<label class="settings-label" for="engine_{{eng[0]}}">{{eng[1]}}</label>
|
||||
<input type="checkbox" id="engine_{{eng[0]}}" name="engine_{{eng[0]}}" value="1" class="settings-checkbox" {{eng[2]}}>
|
||||
</div>
|
||||
{{endfor}}
|
||||
</section>
|
||||
{{endif}}
|
||||
<div class="settings-actions">
|
||||
<button type="submit" class="btn-primary">{{l("save_settings_button")}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue