Compare commits

...
Sign in to create a new pull request.

23 commits

Author SHA1 Message Date
frosty
96b57648dd fix(locale): changed wording on en_uk, en_us to be more accurate 2026-04-02 19:19:37 -04:00
rdjx3
37f18d148f Added Latvian and Russian translations 2026-04-03 02:10:57 +03:00
frosty
62e6d0c3d6 fix: fixed issue with last commit 2026-04-02 09:59:44 +03:00
frosty
08c1aa8abe fix: prioritise theme files 2026-04-02 09:55:12 +03:00
frosty
b9f775fc2d fix: prevent SIGSEGV on NULL extract in infobox handler 2026-04-02 07:39:11 +03:00
frosty
821e670ddd fix: copy locale folder in install process 2026-04-02 07:28:23 +03:00
frosty
2fb5f975de optimise: improve duplicate URL detection 2026-04-02 07:23:23 +03:00
frosty
d2e0c7f481 fix: improved speed in ImagesProxy.c 2026-04-02 06:44:40 +03:00
violet
f66686a959 fix: startpage captcha detection 2026-04-02 01:25:00 +03:00
frosty
f29fa38398 design: changed appearance of arrows on pagination 2026-04-02 01:16:39 +03:00
frosty
478aafcf87 fix: fixed unused 'locale' parameter on pagination 2026-04-02 01:06:31 +03:00
frosty
1382d73d53 feat: configure search engines in user settings p2 2026-04-01 22:40:10 +03:00
frosty
8176078105 feat: configure search engines in user settings 2026-04-01 22:39:22 +03:00
frosty
614bd26cb3 refactor: internationalise pagination and clean up related code 2026-04-01 05:49:18 +03:00
frosty
c6bdeecb2a test: made ca_ca locale rtl for testing purposes 2026-04-01 04:05:22 +03:00
frosty
116069c8e9 feat: add more locale keys 2026-04-01 04:01:07 +03:00
frosty
c41ab84738 feat: begin working on localisation 2026-04-01 00:37:15 +03:00
frosty
335b6f4683 feat: make clicking on logo in header bring you back to homepage 2026-03-31 06:03:57 +03:00
frosty
71d3d0dcb0 feat: improve navigation behaviour for settings 2026-03-31 06:03:50 +03:00
frosty
0ea4bc726c fix: make check for X-Forwarded-For case insensitive in RateLimit.c 2026-03-31 05:22:42 +03:00
stab
f38fe3c42e Added rate limiting and settings fixes. 2026-03-31 05:10:22 +03:00
frosty
c3ed901738 feat: begin adding settings menu, move theme to settings 2026-03-30 10:37:46 +03:00
Ansari
9e6e763064 docker: use multi-stage build to reduce image size 2026-03-30 03:01:16 +03:00
32 changed files with 1880 additions and 294 deletions

View file

@ -1,39 +1,46 @@
FROM alpine:latest # ---------- Build stage ----------
FROM alpine:3.20 AS builder
# Install required dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
libxml2-dev \ build-base \
curl-dev \ curl-dev \
shadow \ libxml2-dev \
git \
make \
gcc \
musl-dev \
pkgconf \
openssl-dev \ openssl-dev \
openrc git \
pkgconf
# 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 WORKDIR /app
COPY . /app COPY . /app
# Clone and install omnisearch RUN git clone https://git.bwaaa.monster/beaker /tmp/beaker && \
RUN cd /app \ cd /tmp/beaker && \
&& make \ make && make install && \
&& make install-openrc rm -rf /tmp/beaker
# Enable OpenRC and start the service RUN make clean && make && strip bin/omnisearch
RUN rc-update add omnisearch default
# ---------- 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
# Expose the default port
EXPOSE 5000 EXPOSE 5000
# Start OpenRC and the service CMD ["/app/omnisearch"]
CMD sh -c "openrc default && touch /run/openrc/softlevel && omnisearch"

View file

@ -95,9 +95,10 @@ install:
@echo "Example: doas/sudo make install-openrc" @echo "Example: doas/sudo make install-openrc"
install-launchd: $(TARGET) install-launchd: $(TARGET)
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(INSTALL_BIN_DIR) $(LOG_DIR) @mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(INSTALL_BIN_DIR) $(LOG_DIR)
@cp -rf templates/* $(DATA_DIR)/templates/ @cp -rf templates/* $(DATA_DIR)/templates/
@cp -rf static/* $(DATA_DIR)/static/ @cp -rf static/* $(DATA_DIR)/static/
@cp -rf locales/* $(DATA_DIR)/locales/
@cp -n example-config.ini $(DATA_DIR)/config.ini || true @cp -n example-config.ini $(DATA_DIR)/config.ini || true
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
@mkdir -p $(LAUNCHD_DIR) @mkdir -p $(LAUNCHD_DIR)
@ -116,9 +117,10 @@ install-launchd: $(TARGET)
@echo "Start with: sudo launchctl kickstart -k system/$(LAUNCHD_LABEL)" @echo "Start with: sudo launchctl kickstart -k system/$(LAUNCHD_LABEL)"
install-systemd: $(TARGET) install-systemd: $(TARGET)
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR) @mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
@cp -rf templates/* $(DATA_DIR)/templates/ @cp -rf templates/* $(DATA_DIR)/templates/
@cp -rf static/* $(DATA_DIR)/static/ @cp -rf static/* $(DATA_DIR)/static/
@cp -rf locales/* $(DATA_DIR)/locales/
@cp -n example-config.ini $(DATA_DIR)/config.ini || true @cp -n example-config.ini $(DATA_DIR)/config.ini || true
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
@echo "Setting up user '$(USER)'..." @echo "Setting up user '$(USER)'..."
@ -134,9 +136,10 @@ install-systemd: $(TARGET)
@echo "Run 'systemctl enable --now omnisearch' to start" @echo "Run 'systemctl enable --now omnisearch' to start"
install-openrc: $(TARGET) install-openrc: $(TARGET)
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR) @mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
@cp -rf templates/* $(DATA_DIR)/templates/ @cp -rf templates/* $(DATA_DIR)/templates/
@cp -rf static/* $(DATA_DIR)/static/ @cp -rf static/* $(DATA_DIR)/static/
@cp -rf locales/* $(DATA_DIR)/locales/
@cp -n example-config.ini $(DATA_DIR)/config.ini || true @cp -n example-config.ini $(DATA_DIR)/config.ini || true
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
@echo "Setting up user '$(USER)'..." @echo "Setting up user '$(USER)'..."
@ -152,9 +155,10 @@ install-openrc: $(TARGET)
@echo "Run 'rc-update add omnisearch default' to enable" @echo "Run 'rc-update add omnisearch default' to enable"
install-runit: $(TARGET) install-runit: $(TARGET)
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR) $(RUNIT_DIR)/omnisearch/log/ @mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR) $(RUNIT_DIR)/omnisearch/log/
@cp -rf templates/* $(DATA_DIR)/templates/ @cp -rf templates/* $(DATA_DIR)/templates/
@cp -rf static/* $(DATA_DIR)/static/ @cp -rf static/* $(DATA_DIR)/static/
@cp -rf locales/* $(DATA_DIR)/locales/
@cp -n example-config.ini $(DATA_DIR)/config.ini || true @cp -n example-config.ini $(DATA_DIR)/config.ini || true
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
@echo "Setting up user '$(USER)'..." @echo "Setting up user '$(USER)'..."
@ -173,9 +177,10 @@ install-runit: $(TARGET)
@echo "Artix: ln -s $(RUNIT_DIR)/omnisearch/ /run/runit/" @echo "Artix: ln -s $(RUNIT_DIR)/omnisearch/ /run/runit/"
install-s6: $(TARGET) install-s6: $(TARGET)
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR) @mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
@cp -rf templates/* $(DATA_DIR)/templates/ @cp -rf templates/* $(DATA_DIR)/templates/
@cp -rf static/* $(DATA_DIR)/static/ @cp -rf static/* $(DATA_DIR)/static/
@cp -rf locales/* $(DATA_DIR)/locales/
@cp -n example-config.ini $(DATA_DIR)/config.ini || true @cp -n example-config.ini $(DATA_DIR)/config.ini || true
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
@echo "Setting up user '$(USER)'..." @echo "Setting up user '$(USER)'..."
@ -194,9 +199,10 @@ install-s6: $(TARGET)
@echo "Service will start automatically" @echo "Service will start automatically"
install-dinit: $(TARGET) install-dinit: $(TARGET)
@mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(LOG_DIR) $(CACHE_DIR) @mkdir -p $(DATA_DIR)/templates $(DATA_DIR)/static $(DATA_DIR)/locales $(LOG_DIR) $(CACHE_DIR)
@cp -rf templates/* $(DATA_DIR)/templates/ @cp -rf templates/* $(DATA_DIR)/templates/
@cp -rf static/* $(DATA_DIR)/static/ @cp -rf static/* $(DATA_DIR)/static/
@cp -rf locales/* $(DATA_DIR)/locales/
@cp -n example-config.ini $(DATA_DIR)/config.ini || true @cp -n example-config.ini $(DATA_DIR)/config.ini || true
install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch
@echo "Setting up user '$(USER)'..." @echo "Setting up user '$(USER)'..."

View file

@ -31,3 +31,14 @@ domain = https://search.example.com
# Use *,-engine to exclude specific engines (e.g., *,-startpage) # Use *,-engine to exclude specific engines (e.g., *,-startpage)
# Available engines: ddg, startpage, yahoo, mojeek # Available engines: ddg, startpage, yahoo, mojeek
engines="*" engines="*"
[rate_limit]
# Rate limit searches per interval
# /search
#search_requests = 10
#search_interval = 60
# /images
#images_requests = 20
#images_interval = 60

34
locales/ca_ca.ini Normal file
View file

@ -0,0 +1,34 @@
[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"

34
locales/en_uk.ini Normal file
View file

@ -0,0 +1,34 @@
[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"

34
locales/en_us.ini Normal file
View file

@ -0,0 +1,34 @@
[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"

32
locales/lv_lv.ini Normal file
View file

@ -0,0 +1,32 @@
[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"

32
locales/ru_ru.ini Normal file
View file

@ -0,0 +1,32 @@
[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 = "Перейти на сайт"

View file

@ -100,6 +100,16 @@ int load_config(const char *filename, Config *config) {
strncpy(config->engines, value, sizeof(config->engines) - 1); strncpy(config->engines, value, sizeof(config->engines) - 1);
config->engines[sizeof(config->engines) - 1] = '\0'; 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);
}
} }
} }
} }

View file

@ -45,6 +45,10 @@ typedef struct {
int cache_ttl_infobox; int cache_ttl_infobox;
int cache_ttl_image; int cache_ttl_image;
char engines[512]; char engines[512];
int rate_limit_search_requests;
int rate_limit_search_interval;
int rate_limit_images_requests;
int rate_limit_images_interval;
} Config; } Config;
int load_config(const char *filename, Config *config); int load_config(const char *filename, Config *config);

210
src/Limiter/RateLimit.c Normal file
View file

@ -0,0 +1,210 @@
#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;
}

20
src/Limiter/RateLimit.h Normal file
View file

@ -0,0 +1,20 @@
#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

View file

@ -13,6 +13,8 @@
#include "Routes/ImageProxy.h" #include "Routes/ImageProxy.h"
#include "Routes/Images.h" #include "Routes/Images.h"
#include "Routes/Search.h" #include "Routes/Search.h"
#include "Routes/Settings.h"
#include "Routes/SettingsSave.h"
#include "Scraping/Scraping.h" #include "Scraping/Scraping.h"
Config global_config; Config global_config;
@ -53,7 +55,11 @@ int main() {
.cache_ttl_search = DEFAULT_CACHE_TTL_SEARCH, .cache_ttl_search = DEFAULT_CACHE_TTL_SEARCH,
.cache_ttl_infobox = DEFAULT_CACHE_TTL_INFOBOX, .cache_ttl_infobox = DEFAULT_CACHE_TTL_INFOBOX,
.cache_ttl_image = DEFAULT_CACHE_TTL_IMAGE, .cache_ttl_image = DEFAULT_CACHE_TTL_IMAGE,
.engines = ""}; .engines = "",
.rate_limit_search_requests = 0,
.rate_limit_search_interval = 0,
.rate_limit_images_requests = 0,
.rate_limit_images_interval = 0};
if (load_config("config.ini", &cfg) != 0) { if (load_config("config.ini", &cfg) != 0) {
fprintf(stderr, "[WARN] Could not load config file, using defaults\n"); fprintf(stderr, "[WARN] Could not load config file, using defaults\n");
@ -61,6 +67,13 @@ int main() {
global_config = cfg; 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); apply_engines_config(cfg.engines);
if (cache_init(cfg.cache_dir) != 0) { if (cache_init(cfg.cache_dir) != 0) {
@ -97,6 +110,8 @@ int main() {
set_handler("/search", results_handler); set_handler("/search", results_handler);
set_handler("/images", images_handler); set_handler("/images", images_handler);
set_handler("/proxy", image_proxy_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); fprintf(stderr, "[INFO] Starting Omnisearch on %s:%d\n", cfg.host, cfg.port);
@ -111,6 +126,7 @@ int main() {
curl_global_cleanup(); curl_global_cleanup();
xmlCleanupParser(); xmlCleanupParser();
beaker_free_locales();
free_proxy_list(); free_proxy_list();
cache_shutdown(); cache_shutdown();
return EXIT_SUCCESS; return EXIT_SUCCESS;

View file

@ -1,14 +1,23 @@
#include "Home.h" #include "Home.h"
#include "../Utility/Utility.h"
#include <beaker.h>
#include <stdlib.h> #include <stdlib.h>
int home_handler(UrlParams *params) { int home_handler(UrlParams *params) {
(void)params; (void)params;
char *theme = get_theme("");
char *locale = get_locale("en_uk");
TemplateContext ctx = new_context(); TemplateContext ctx = new_context();
context_set(&ctx, "theme", theme);
beaker_set_locale(&ctx, locale);
char *rendered_html = render_template("home.html", &ctx); char *rendered_html = render_template("home.html", &ctx);
send_response(rendered_html); send_response(rendered_html);
free(rendered_html); free(rendered_html);
free_context(&ctx); free_context(&ctx);
free(theme);
free(locale);
return 0; return 0;
} }

View file

@ -7,12 +7,25 @@
#include <netdb.h> #include <netdb.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <openssl/evp.h> #include <openssl/evp.h>
#include <pthread.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <time.h>
#define MAX_IMAGE_SIZE (10 * 1024 * 1024) #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 { typedef struct {
char *data; char *data;
@ -20,6 +33,57 @@ typedef struct {
size_t capacity; size_t capacity;
} MemoryBuffer; } 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) { static int is_private_ip(const char *ip_str) {
struct in_addr addr; struct in_addr addr;
if (inet_pton(AF_INET, ip_str, &addr) != 1) { if (inet_pton(AF_INET, ip_str, &addr) != 1) {
@ -59,7 +123,11 @@ static int is_private_ip(const char *ip_str) {
return 0; return 0;
} }
static int is_private_hostname(const char *hostname) { static const char *is_private_hostname(const char *hostname, char *out_ip) {
if (dns_cache_lookup(hostname, out_ip) == 0) {
return out_ip;
}
struct addrinfo hints, *res, *p; struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof(hints)); memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET; hints.ai_family = AF_INET;
@ -67,7 +135,7 @@ static int is_private_hostname(const char *hostname) {
int err = getaddrinfo(hostname, NULL, &hints, &res); int err = getaddrinfo(hostname, NULL, &hints, &res);
if (err != 0) { if (err != 0) {
return 0; return NULL;
} }
for (p = res; p != NULL; p = p->ai_next) { for (p = res; p != NULL; p = p->ai_next) {
@ -78,16 +146,21 @@ static int is_private_hostname(const char *hostname) {
if (is_private_ip(ip_str)) { if (is_private_ip(ip_str)) {
freeaddrinfo(res); freeaddrinfo(res);
return 1; return NULL;
} }
freeaddrinfo(res);
strcpy(out_ip, ip_str);
dns_cache_insert(hostname, ip_str);
return out_ip;
} }
} }
freeaddrinfo(res); freeaddrinfo(res);
return 0; return NULL;
} }
static int is_allowed_domain(const char *url) { static int is_allowed_domain(const char *url, char *resolved_ip) {
CURLU *h = curl_url(); CURLU *h = curl_url();
if (!h) { if (!h) {
return -1; return -1;
@ -132,7 +205,7 @@ static int is_allowed_domain(const char *url) {
*colon = '\0'; *colon = '\0';
} }
if (is_private_hostname(host)) { if (!is_private_hostname(host, resolved_ip)) {
curl_url_cleanup(h); curl_url_cleanup(h);
return 0; return 0;
} }
@ -206,7 +279,8 @@ int image_proxy_handler(UrlParams *params) {
return 0; return 0;
} }
int domain_check = is_allowed_domain(url); char resolved_ip[INET_ADDRSTRLEN] = {0};
int domain_check = is_allowed_domain(url, resolved_ip);
if (domain_check == -1) { if (domain_check == -1) {
send_response("Invalid URL scheme"); send_response("Invalid URL scheme");
return 0; return 0;
@ -283,6 +357,31 @@ int image_proxy_handler(UrlParams *params) {
"Chrome/120.0.0.0 Safari/537.36"); "Chrome/120.0.0.0 Safari/537.36");
apply_proxy_settings(curl); 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); CURLcode res = curl_easy_perform(curl);
long response_code; long response_code;
@ -296,6 +395,8 @@ int image_proxy_handler(UrlParams *params) {
strncpy(content_type, content_type_ptr, sizeof(content_type) - 1); strncpy(content_type, content_type_ptr, sizeof(content_type) - 1);
} }
if (resolves)
curl_slist_free_all(resolves);
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
if (res != CURLE_OK || response_code != 200) { if (res != CURLE_OK || response_code != 200) {

View file

@ -1,9 +1,46 @@
#include "Images.h" #include "Images.h"
#include "../Cache/Cache.h"
#include "../Limiter/RateLimit.h"
#include "../Scraping/ImageScraping.h" #include "../Scraping/ImageScraping.h"
#include "../Utility/Unescape.h" #include "../Utility/Unescape.h"
#include "../Utility/Utility.h"
#include "Config.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) { int images_handler(UrlParams *params) {
extern Config global_config;
TemplateContext ctx = new_context(); TemplateContext ctx = new_context();
char *raw_query = ""; char *raw_query = "";
int page = 1; int page = 1;
@ -20,38 +57,91 @@ 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); context_set(&ctx, "query", raw_query);
context_set(&ctx, "page", page_str);
context_set(&ctx, "prev_page", prev_str); char *theme = get_theme("");
context_set(&ctx, "next_page", next_str); context_set(&ctx, "theme", theme);
context_set(&ctx, "two_prev_page", two_prev_str); free(theme);
context_set(&ctx, "two_next_page", two_next_str);
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);
}
char *display_query = url_decode_query(raw_query); char *display_query = url_decode_query(raw_query);
context_set(&ctx, "query", display_query); context_set(&ctx, "query", display_query);
if (!raw_query || strlen(raw_query) == 0) { if (!raw_query || strlen(raw_query) == 0) {
send_response("<h1>No query provided</h1>"); send_redirect("/");
if (display_query) if (display_query)
free(display_query); free(display_query);
free_context(&ctx); free_context(&ctx);
return -1; 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; ImageResult *results = NULL;
int result_count = 0; int result_count = 0;
if (scrape_images(raw_query, page, &results, &result_count) != 0 || if (scrape_images(raw_query, page, &results, &result_count) != 0 ||
!results) { !results) {
send_response("<h1>Error fetching images</h1>"); char error_html[128];
snprintf(error_html, sizeof(error_html), "<h1>%s</h1>", error_images_msg);
send_response(error_html);
free(request_cache_key);
free(display_query); free(display_query);
free_context(&ctx); free_context(&ctx);
return -1; return -1;
@ -66,6 +156,7 @@ int images_handler(UrlParams *params) {
if (inner_counts) if (inner_counts)
free(inner_counts); free(inner_counts);
free_image_results(results, result_count); free_image_results(results, result_count);
free(request_cache_key);
free(display_query); free(display_query);
free_context(&ctx); free_context(&ctx);
return -1; return -1;
@ -99,7 +190,18 @@ int images_handler(UrlParams *params) {
free(image_matrix); free(image_matrix);
free(inner_counts); 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_image_results(results, result_count);
free(request_cache_key);
free(display_query); free(display_query);
free_context(&ctx); free_context(&ctx);

View file

@ -1,20 +1,101 @@
#include "Search.h" #include "Search.h"
#include "../Cache/Cache.h"
#include "../Infobox/Calculator.h" #include "../Infobox/Calculator.h"
#include "../Infobox/CurrencyConversion.h" #include "../Infobox/CurrencyConversion.h"
#include "../Infobox/Dictionary.h" #include "../Infobox/Dictionary.h"
#include "../Infobox/UnitConversion.h" #include "../Infobox/UnitConversion.h"
#include "../Infobox/Wikipedia.h" #include "../Infobox/Wikipedia.h"
#include "../Limiter/RateLimit.h"
#include "../Scraping/Scraping.h" #include "../Scraping/Scraping.h"
#include "../Utility/Display.h" #include "../Utility/Display.h"
#include "../Utility/Unescape.h" #include "../Utility/Unescape.h"
#include "../Utility/Utility.h"
#include "Config.h" #include "Config.h"
#include <ctype.h> #include <ctype.h>
#include <openssl/evp.h>
#include <pthread.h> #include <pthread.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.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 { typedef struct {
const char *query; const char *query;
InfoBox result; InfoBox result;
@ -29,8 +110,6 @@ typedef struct {
enum { enum {
RESULT_FIELD_COUNT = 6, RESULT_FIELD_COUNT = 6,
LINK_FIELD_COUNT = 3,
PAGER_WINDOW_SIZE = 5,
}; };
static InfoBox fetch_wiki_wrapper(char *query) { static InfoBox fetch_wiki_wrapper(char *query) {
@ -160,7 +239,7 @@ static void *infobox_thread_func(void *arg) {
data->result = h->fetch_fn((char *)data->query); data->result = h->fetch_fn((char *)data->query);
data->success = (data->result.title != NULL && data->result.extract != NULL && data->success = (data->result.title != NULL && data->result.extract != NULL &&
strlen(data->result.extract) > 10); data->result.extract[0] != '\0');
return NULL; return NULL;
} }
@ -186,66 +265,6 @@ static int add_infobox_to_collection(InfoBox *infobox, char ****collection,
return current_count + 1; 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, static int add_warning_to_collection(const char *engine_name,
const char *warning_message, const char *warning_message,
char ****collection, int **inner_counts, char ****collection, int **inner_counts,
@ -293,15 +312,20 @@ static int add_warning_to_collection(const char *engine_name,
return current_count + 1; return current_count + 1;
} }
static const char *warning_message_for_job(const ScrapeJob *job) { static const char *warning_message_for_job(const ScrapeJob *job, const char *locale) {
switch (job->status) { switch (job->status) {
case SCRAPE_STATUS_FETCH_ERROR: case SCRAPE_STATUS_FETCH_ERROR: {
return "request failed before OmniSearch could read search results."; const char *msg = beaker_get_locale_value(locale, "warning_fetch_error");
case SCRAPE_STATUS_PARSE_MISMATCH: return msg ? msg : "request failed before OmniSearch could read search results.";
return "returned search results in a format OmniSearch could not parse."; }
case SCRAPE_STATUS_BLOCKED: case SCRAPE_STATUS_PARSE_MISMATCH: {
return "returned a captcha or another blocking page instead of search " const char *msg = beaker_get_locale_value(locale, "warning_parse_mismatch");
"results."; 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.";
}
default: default:
return NULL; return NULL;
} }
@ -344,6 +368,13 @@ static const SearchEngine *find_enabled_engine(const char *engine_id) {
return NULL; 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, static char *build_search_href(const char *query, const char *engine_id,
int page) { int page) {
const char *safe_query = query ? query : ""; const char *safe_query = query ? query : "";
@ -377,13 +408,37 @@ static char *build_search_href(const char *query, const char *engine_id,
return href; 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) { int results_handler(UrlParams *params) {
extern Config global_config;
TemplateContext ctx = new_context(); TemplateContext ctx = new_context();
char *raw_query = ""; char *raw_query = "";
const char *selected_engine_id = "all"; const char *selected_engine_id = "all";
int page = 1; int page = 1;
int btnI = 0; 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) { if (params) {
for (int i = 0; i < params->count; i++) { for (int i = 0; i < params->count; i++) {
if (strcmp(params->params[i].key, "q") == 0) { if (strcmp(params->params[i].key, "q") == 0) {
@ -401,12 +456,40 @@ int results_handler(UrlParams *params) {
} }
context_set(&ctx, "query", raw_query); 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]; char page_str[16];
snprintf(page_str, sizeof(page_str), "%d", page); snprintf(page_str, sizeof(page_str), "%d", page);
context_set(&ctx, "page", page_str); 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) { if (!raw_query || strlen(raw_query) == 0) {
send_response("<h1>No query provided</h1>"); send_redirect("/");
if (has_user_pref) {
for (int i = 0; i < user_engine_count; i++)
free(user_engines[i]);
free(user_engines);
}
free_context(&ctx); free_context(&ctx);
return -1; return -1;
} }
@ -423,7 +506,9 @@ int results_handler(UrlParams *params) {
int enabled_engine_count = 0; int enabled_engine_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) { for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled && if (ENGINE_REGISTRY[i].enabled &&
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine)) { (!selected_engine || &ENGINE_REGISTRY[i] == selected_engine) &&
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref)) {
enabled_engine_count++; enabled_engine_count++;
} }
} }
@ -450,7 +535,9 @@ int results_handler(UrlParams *params) {
int engine_idx = 0; int engine_idx = 0;
for (int i = 0; i < ENGINE_COUNT; i++) { for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled && if (ENGINE_REGISTRY[i].enabled &&
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine)) { (!selected_engine || &ENGINE_REGISTRY[i] == selected_engine) &&
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref)) {
all_results[engine_idx] = NULL; all_results[engine_idx] = NULL;
jobs[engine_idx].engine = &ENGINE_REGISTRY[i]; jobs[engine_idx].engine = &ENGINE_REGISTRY[i];
jobs[engine_idx].query = raw_query; jobs[engine_idx].query = raw_query;
@ -468,9 +555,56 @@ 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; int filter_engine_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) { for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled) if (ENGINE_REGISTRY[i].enabled &&
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref))
filter_engine_count++; filter_engine_count++;
} }
@ -487,7 +621,9 @@ int results_handler(UrlParams *params) {
free(all_href); free(all_href);
for (int i = 0; i < ENGINE_COUNT; i++) { for (int i = 0; i < ENGINE_COUNT; i++) {
if (!ENGINE_REGISTRY[i].enabled) if (!ENGINE_REGISTRY[i].enabled ||
!engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref))
continue; continue;
char *filter_href = char *filter_href =
@ -545,6 +681,12 @@ 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); free_context(&ctx);
if (redirect_url) { if (redirect_url) {
send_redirect(redirect_url); send_redirect(redirect_url);
@ -563,8 +705,16 @@ 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); free_context(&ctx);
send_response("<h1>No results found</h1>"); char no_results_html[128];
snprintf(no_results_html, sizeof(no_results_html), "<h1>%s</h1>", no_results_msg);
send_response(no_results_html);
return 0; return 0;
} }
@ -596,7 +746,7 @@ int results_handler(UrlParams *params) {
int warning_count = 0; int warning_count = 0;
for (int i = 0; i < enabled_engine_count; i++) { for (int i = 0; i < enabled_engine_count; i++) {
if (warning_message_for_job(&jobs[i])) if (warning_message_for_job(&jobs[i], locale))
warning_count++; warning_count++;
} }
@ -606,7 +756,7 @@ int results_handler(UrlParams *params) {
int warning_index = 0; int warning_index = 0;
for (int i = 0; i < enabled_engine_count; i++) { for (int i = 0; i < enabled_engine_count; i++) {
const char *warning_message = warning_message_for_job(&jobs[i]); const char *warning_message = warning_message_for_job(&jobs[i], locale);
if (!warning_message) if (!warning_message)
continue; continue;
@ -640,14 +790,7 @@ int results_handler(UrlParams *params) {
if (total_results > 0) { if (total_results > 0) {
char ***results_matrix = (char ***)malloc(sizeof(char **) * total_results); char ***results_matrix = (char ***)malloc(sizeof(char **) * total_results);
int *results_inner_counts = (int *)malloc(sizeof(int) * total_results); int *results_inner_counts = (int *)malloc(sizeof(int) * total_results);
char **seen_urls = (char **)malloc(sizeof(char *) * total_results); if (!results_matrix || !results_inner_counts) {
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); char *html = render_template("results.html", &ctx);
if (html) { if (html) {
send_response(html); send_response(html);
@ -662,41 +805,35 @@ 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); free_context(&ctx);
return 0; return 0;
} }
int unique_count = 0; int unique_count = 0;
UrlHashTable url_table;
url_hash_init(&url_table);
for (int i = 0; i < enabled_engine_count; i++) { for (int i = 0; i < enabled_engine_count; i++) {
for (int j = 0; j < jobs[i].results_count; j++) { for (int j = 0; j < jobs[i].results_count; j++) {
char *display_url = all_results[i][j].url; char *display_url = all_results[i][j].url;
int is_duplicate = 0; if (url_hash_contains(&url_table, display_url)) {
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].url);
free(all_results[i][j].title); free(all_results[i][j].title);
free(all_results[i][j].snippet); free(all_results[i][j].snippet);
continue; continue;
} }
seen_urls[unique_count] = strdup(display_url); url_hash_insert(&url_table, 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] = results_matrix[unique_count] =
(char **)malloc(sizeof(char *) * RESULT_FIELD_COUNT); (char **)malloc(sizeof(char *) * RESULT_FIELD_COUNT);
if (!results_matrix[unique_count]) { if (!results_matrix[unique_count]) {
free(seen_urls[unique_count]);
free(all_results[i][j].url); free(all_results[i][j].url);
free(all_results[i][j].title); free(all_results[i][j].title);
free(all_results[i][j].snippet); free(all_results[i][j].snippet);
@ -734,43 +871,10 @@ int results_handler(UrlParams *params) {
char ***pager_matrix = NULL; char ***pager_matrix = NULL;
int *pager_inner_counts = NULL; int *pager_inner_counts = NULL;
int pager_count = 0; SearchHrefData href_data = { .query = raw_query, .engine_id = selected_engine_id };
int pager_start = page <= 3 ? 1 : page - 2; int pager_count = build_pagination(page, search_href_builder,
int pager_end = pager_start + PAGER_WINDOW_SIZE - 1; &href_data, &pager_matrix,
&pager_inner_counts);
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) { if (pager_count > 0) {
context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix, context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix,
@ -794,11 +898,10 @@ int results_handler(UrlParams *params) {
for (int j = 0; j < RESULT_FIELD_COUNT; j++) for (int j = 0; j < RESULT_FIELD_COUNT; j++)
free(results_matrix[i][j]); free(results_matrix[i][j]);
free(results_matrix[i]); free(results_matrix[i]);
free(seen_urls[i]);
} }
free(seen_urls);
free(results_matrix); free(results_matrix);
free(results_inner_counts); free(results_inner_counts);
url_hash_free(&url_table);
} else { } else {
char *html = render_template("results.html", &ctx); char *html = render_template("results.html", &ctx);
if (html) { if (html) {
@ -811,6 +914,8 @@ int results_handler(UrlParams *params) {
} }
} }
free(request_cache_key);
if (page == 1) { if (page == 1) {
for (int i = 0; i < HANDLER_COUNT; i++) { for (int i = 0; i < HANDLER_COUNT; i++) {
if (infobox_data[i].success) { if (infobox_data[i].success) {
@ -818,6 +923,12 @@ 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); free_context(&ctx);
return 0; return 0;

112
src/Routes/Settings.c Normal file
View file

@ -0,0 +1,112 @@
#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;
}

8
src/Routes/Settings.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <beaker.h>
int settings_handler(UrlParams *params);
#endif

66
src/Routes/SettingsSave.c Normal file
View file

@ -0,0 +1,66 @@
#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;
}

View file

@ -0,0 +1,8 @@
#ifndef SETTINGS_SAVE_H
#define SETTINGS_SAVE_H
#include <beaker.h>
int settings_save_handler(UrlParams *params);
#endif

View file

@ -24,7 +24,8 @@ static int response_is_startpage_captcha(const ScrapeJob *job,
return response_contains(response, "<title>Startpage Captcha</title>") || return response_contains(response, "<title>Startpage Captcha</title>") ||
response_contains(response, "Startpage Captcha") || response_contains(response, "Startpage Captcha") ||
response_contains(response, "/static-pages-assets/page-data/captcha/"); response_contains(response, "/static-pages-assets/page-data/captcha/") ||
response_contains(response, ">Startpage Blocked</title>");
} }
static int response_looks_like_results_page(const ScrapeJob *job, static int response_looks_like_results_page(const ScrapeJob *job,

View file

@ -1,4 +1,9 @@
#include "Utility.h" #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) { int hex_to_int(char c) {
if (c >= '0' && c <= '9') if (c >= '0' && c <= '9')
@ -9,3 +14,204 @@ int hex_to_int(char c) {
return c - 'A' + 10; return c - 'A' + 10;
return -1; 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;
}

View file

@ -1,6 +1,24 @@
#ifndef UTILITY_H #ifndef UTILITY_H
#define UTILITY_H #define UTILITY_H
#include <beaker.h>
#define LINK_FIELD_COUNT 3
int hex_to_int(char c); 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 #endif

View file

@ -25,12 +25,19 @@
box-sizing: border-box; box-sizing: border-box;
} }
html {
height:100%;
}
body { body {
background-color:var(--bg-main); 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); color:var(--text-primary);
font-family:system-ui,-apple-system,sans-serif; font-family:system-ui,-apple-system,sans-serif;
margin:0; margin:0;
padding:0; padding:0;
min-height:100%;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
@ -44,7 +51,6 @@ img[src=""] {
align-items: center; align-items: center;
min-height: 100vh; min-height: 100vh;
padding: 20px; padding: 20px;
background: radial-gradient(circle at top right, var(--bg-card) 0%, var(--bg-main) 100%);
} }
.view-home .container { .view-home .container {
@ -100,18 +106,65 @@ img[src=""] {
background:var(--bg-card); background:var(--bg-card);
color:var(--text-primary); color:var(--text-primary);
border-color:var(--border); 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 { .view-home .btn-secondary:hover {
background:var(--border); background:var(--border);
border-color:var(--text-secondary); 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 { header {
display:flex; display:flex;
align-items:center; align-items:center;
gap:20px; gap:20px;
padding:15px 60px; padding-block:15px;
padding-inline:60px;
border-bottom:1px solid var(--border); border-bottom:1px solid var(--border);
background:var(--bg-main); background:var(--bg-main);
width:100%;
} }
.search-form { .search-form {
flex-grow:1; flex-grow:1;
@ -126,6 +179,16 @@ h1 {
h1 span { h1 span {
color:var(--accent); 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 { .search-box {
width: 100%; width: 100%;
padding: 12px 24px; padding: 12px 24px;
@ -142,14 +205,14 @@ h1 span {
box-shadow:0 0 0 4px var(--accent-glow); box-shadow:0 0 0 4px var(--accent-glow);
} }
.nav-tabs { .nav-tabs {
padding:0 60px; padding-inline:60px;
border-bottom:1px solid var(--border); border-bottom:1px solid var(--border);
background:var(--bg-main); background:var(--bg-main);
width:100%;
} }
.nav-container { .nav-container {
display:flex; display:flex;
gap:30px; gap:30px;
max-width:1200px;
} }
.nav-tabs a { .nav-tabs a {
padding:14px 0; padding:14px 0;
@ -168,6 +231,9 @@ h1 span {
color:var(--accent); color:var(--accent);
border-bottom-color:var(--accent); border-bottom-color:var(--accent);
} }
.nav-right {
margin-inline-start:auto;
}
.image-results-container { .image-results-container {
padding:30px 60px; padding:30px 60px;
} }
@ -270,7 +336,8 @@ h1 span {
display:grid; display:grid;
grid-template-columns:140px minmax(0,700px) 450px; grid-template-columns:140px minmax(0,700px) 450px;
gap:60px; gap:60px;
padding:30px 60px; padding-block:30px;
padding-inline:60px;
} }
.result-header { .result-header {
display: flex; display: flex;
@ -286,7 +353,7 @@ h1 span {
background-size: cover; background-size: cover;
background-position: center; background-position: center;
position: absolute; position: absolute;
left: -24px; inset-inline-start: -24px;
} }
.url { .url {
color: var(--text-secondary); color: var(--text-secondary);
@ -300,7 +367,7 @@ h1 span {
.result-favicon { .result-favicon {
width: 14px; width: 14px;
height: 14px; height: 14px;
left: -20px; inset-inline-start: -20px;
} }
} }
@ -308,7 +375,7 @@ h1 span {
.result-favicon { .result-favicon {
width: 12px; width: 12px;
height: 12px; height: 12px;
left: -16px; inset-inline-start: -16px;
} }
} }
.results-container { .results-container {
@ -471,41 +538,59 @@ h1 span {
border-color: var(--accent); border-color: var(--accent);
} }
[dir="rtl"] .pagination-btn.prev {
transform: scaleX(-1);
}
[dir="rtl"] .pagination-btn.next {
transform: scaleX(-1);
}
@media (max-width:1200px) { @media (max-width:1200px) {
body {
padding-left: 16px;
padding-right: 16px;
}
.content-layout { .content-layout {
grid-template-columns:1fr; grid-template-columns:1fr;
padding:20px 30px; padding-block:20px;
padding-inline:30px;
gap:20px;
}
header {
gap:20px; gap:20px;
} }
.results-container,.infobox-sidebar { .results-container,.infobox-sidebar {
grid-column:1; grid-column:1;
max-width:100%; max-width:100%;
} }
.settings-layout {
padding-block:20px;
padding-inline:30px;
display:flex;
justify-content:center;
}
.infobox-sidebar { .infobox-sidebar {
order:-1; order:-1;
} }
.nav-tabs,.image-results-container { .nav-tabs,.image-results-container {
padding:0 30px; padding-inline:30px;
} }
header { header {
padding:15px 30px; padding-block:15px;
padding-inline:30px;
} }
} }
@media (max-width:768px) { @media (max-width:768px) {
body { .nav-settings-icon {
padding-left: 16px; display:none;
padding-right: 16px; }
.nav-settings-link {
display:inline;
} }
header { header {
flex-direction:column; flex-direction:column;
gap:12px; gap:12px;
padding:12px 16px; padding-block:12px;
padding-inline:16px;
text-align:center; text-align:center;
} }
h1 { h1 {
@ -521,7 +606,7 @@ h1 span {
.nav-tabs { .nav-tabs {
overflow-x:auto; overflow-x:auto;
-webkit-overflow-scrolling:touch; -webkit-overflow-scrolling:touch;
padding:0 16px; padding-inline:16px;
} }
.nav-container { .nav-container {
gap:24px; gap:24px;
@ -532,7 +617,9 @@ h1 span {
font-size:0.95rem; font-size:0.95rem;
} }
.content-layout { .content-layout {
padding:16px; padding-inline-start:40px;
padding-inline-end:16px;
padding-block:16px;
gap:16px; gap:16px;
} }
.result { .result {
@ -576,7 +663,7 @@ h1 span {
max-width:200px; max-width:200px;
} }
.image-results-container { .image-results-container {
padding:16px; padding-inline:16px;
} }
.pagination { .pagination {
flex-wrap:wrap; flex-wrap:wrap;
@ -591,7 +678,6 @@ h1 span {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transform: translateY(-5vh);
padding:20px 16px; padding:20px 16px;
min-height: 100vh; min-height: 100vh;
} }
@ -621,8 +707,17 @@ h1 span {
} }
@media (max-width:600px) { @media (max-width:600px) {
.content-layout {
padding-inline-start:28px;
padding-inline-end:16px;
padding-block:16px;
}
.settings-layout {
padding:0;
}
header { header {
padding:12px 12px; padding-inline:12px;
padding-block:12px;
} }
.search-box { .search-box {
font-size:0.95rem; font-size:0.95rem;
@ -657,3 +752,195 @@ h1 span {
font-size:0.75rem; 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;
}
}

4
static/settings.svg Normal file
View file

@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 829 B

10
static/theme-dark.css Normal file
View file

@ -0,0 +1,10 @@
: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);
}

10
static/theme-light.css Normal file
View file

@ -0,0 +1,10 @@
: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);
}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{__locale_id}}" dir="{{__locale_direction}}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -8,6 +8,8 @@
OmniSearch OmniSearch
</title> </title>
<link rel="stylesheet" href="static/main.css"> <link rel="stylesheet" href="static/main.css">
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="search" <link rel="search"
type="application/opensearchdescription+xml" type="application/opensearchdescription+xml"
@ -22,19 +24,21 @@
</h1> </h1>
<form action="/search" class="home-search-form"> <form action="/search" class="home-search-form">
<div class="search-input-wrapper"> <div class="search-input-wrapper">
<input name="q" type="text" class="search-box" placeholder="Search the web..." <input name="q" type="text" class="search-box" placeholder="{{l("search_placeholder")}}"
autofocus autocomplete="off"> autofocus autocomplete="off">
</div> </div>
<div class="buttons"> <div class="buttons">
<button type="submit" class="btn-primary"> <button type="submit" class="btn-primary">
Search {{l("search_button")}}
</button> </button>
<button type="submit" name="btnI" value="1" class="btn-secondary"> <button type="submit" name="btnI" value="1" class="btn-secondary">
Surprise me {{l("surprise_me_button")}}
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<a href="/settings" class="home-settings-btn" title="{{l("settings_tab")}}"></a>
</body> </body>
</html> </html>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{__locale_id}}" dir="{{__locale_direction}}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -9,25 +9,31 @@
</title> </title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="static/main.css"> <link rel="stylesheet" href="static/main.css">
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
</head> </head>
<body class="images-view"> <body class="images-view">
<header> <header>
<h1> <a href="/" class="logo-link"><h1>
Omni<span>Search</span> Omni<span>Search</span>
</h1> </h1></a>
<form action="/images" method="GET" class="search-form"> <form action="/images" method="GET" class="search-form">
<input name="q" autocomplete="off"="text" class="search-box" placeholder="Search for images..." <input name="q" autocomplete="off"="text" class="search-box" placeholder="{{l("search_placeholder")}}"
value="{{query}}"> value="{{query}}">
</form> </form>
<a href="/settings?q={{query}}" class="nav-settings-icon" title="{{l("settings_tab")}}"></a>
</header> </header>
<nav class="nav-tabs"> <nav class="nav-tabs">
<div class="nav-container"> <div class="nav-container">
<a href="/search?q={{query}}"> <a href="/search?q={{query}}">
All {{l("all_tab")}}
</a> </a>
<a href="/images?q={{query}}" class="active"> <a href="/images?q={{query}}" class="active">
Images {{l("images_tab")}}
</a>
<a href="/settings?q={{query}}" class="nav-settings-link">
{{l("settings_tab")}}
</a> </a>
</div> </div>
</nav> </nav>
@ -40,10 +46,10 @@
<div class="image-overlay"> <div class="image-overlay">
<div class="overlay-buttons"> <div class="overlay-buttons">
<a href="{{img[3]}}" target="_blank" class="overlay-btn primary"> <a href="{{img[3]}}" target="_blank" class="overlay-btn primary">
View Image {{l("view_image")}}
</a> </a>
<a href="{{img[2]}}" target="_blank" class="overlay-btn secondary"> <a href="{{img[2]}}" target="_blank" class="overlay-btn secondary">
Visit Site {{l("visit_site")}}
</a> </a>
</div> </div>
</div> </div>
@ -59,48 +65,15 @@
</div> </div>
{{endfor}} {{endfor}}
</div> </div>
{{if exists pagination_links}}
<nav class="pagination"> <nav class="pagination">
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}"> {{for link in pagination_links}}
&larr; <a class="{{link[2]}}" href="{{link[0]}}">
</a> {{link[1]}}
{{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}}">
&rarr;
</a> </a>
{{endfor}}
</nav> </nav>
{{endif}}
</main> </main>
</body> </body>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="{{__locale_id}}" dir="{{__locale_direction}}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -8,6 +8,8 @@
OmniSearch - {{query}} OmniSearch - {{query}}
</title> </title>
<link rel="stylesheet" href="static/main.css"> <link rel="stylesheet" href="static/main.css">
{{if theme == "light"}}<link rel="preload" href="static/theme-light.css" as="style"><link rel="stylesheet" href="static/theme-light.css">{{endif}}
{{if theme == "dark"}}<link rel="preload" href="static/theme-dark.css" as="style"><link rel="stylesheet" href="static/theme-dark.css">{{endif}}
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="search" <link rel="search"
type="application/opensearchdescription+xml" type="application/opensearchdescription+xml"
@ -16,22 +18,26 @@
<body class="results-view"> <body class="results-view">
<header> <header>
<h1> <a href="/" class="logo-link"><h1>
Omni<span>Search</span> Omni<span>Search</span>
</h1> </h1></a>
<form action="/search" method="GET" class="search-form"> <form action="/search" method="GET" class="search-form">
<input name="engine" type="hidden" value="{{selected_engine}}"> <input name="engine" type="hidden" value="{{selected_engine}}">
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="Search the web..." <input name="q" type="text" class="search-box" autocomplete="off" placeholder="{{l("search_placeholder")}}"
value="{{query}}"> value="{{query}}">
</form> </form>
<a href="/settings?q={{query}}" class="nav-settings-icon" title="{{l("settings_tab")}}"></a>
</header> </header>
<nav class="nav-tabs"> <nav class="nav-tabs">
<div class="nav-container"> <div class="nav-container">
<a href="{{search_href}}" class="active"> <a href="{{search_href}}" class="active">
All {{l("all_tab")}}
</a> </a>
<a href="/images?q={{query}}"> <a href="/images?q={{query}}">
Images {{l("images_tab")}}
</a>
<a href="/settings?q={{query}}" class="nav-settings-link">
{{l("settings_tab")}}
</a> </a>
</div> </div>
</nav> </nav>
@ -82,7 +88,7 @@
{{result[3]}} {{result[3]}}
</p> </p>
<span> <span>
<a class="cached" href="https://web.archive.org/web/{{result[0]|safe}}">View Cached</a> <a class="cached" href="https://web.archive.org/web/{{result[0]|safe}}">{{l("view_cached")}}</a>
</span> </span>
</div> </div>
{{endfor}} {{endfor}}
@ -112,7 +118,7 @@
{{info[2]|safe}} {{info[2]|safe}}
</p> </p>
<a class="read-more" href="{{info[3]}}"> <a class="read-more" href="{{info[3]}}">
Read More {{l("read_more")}}
</a> </a>
</div> </div>
</div> </div>

100
templates/settings.html Normal file
View file

@ -0,0 +1,100 @@
<!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>