This commit is contained in:
2026-06-03 18:02:39 +01:00
52 changed files with 3318 additions and 601 deletions

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
# ---------- Build stage ----------
FROM alpine:3.20 AS builder
RUN apk add --no-cache \
build-base \
curl-dev \
libxml2-dev \
openssl-dev \
git \
pkgconf
WORKDIR /app
COPY . /app
RUN git clone https://git.bwaaa.monster/beaker /tmp/beaker && \
cd /tmp/beaker && \
make && make install && \
rm -rf /tmp/beaker
RUN make clean && make && strip bin/omnisearch
# ---------- Runtime stage ----------
FROM alpine:3.20
RUN apk add --no-cache \
libcurl \
libxml2 \
openssl
WORKDIR /app
# Copy only required artifacts
COPY --from=builder /app/bin/omnisearch /app/omnisearch
COPY --from=builder /usr/lib/libbeaker.so /usr/lib/libbeaker.so
COPY --from=builder /app/templates /app/templates
COPY --from=builder /app/static /app/static
# Security: non-root user
RUN adduser -D appuser && chmod +x /app/omnisearch
USER appuser
ENV LD_LIBRARY_PATH=/usr/lib
EXPOSE 5000
CMD ["/app/omnisearch"]

View File

@@ -3,13 +3,22 @@ UNAME_S := $(shell uname -s)
PKG_CONFIG ?= pkg-config PKG_CONFIG ?= pkg-config
PKG_DEPS := libxml-2.0 libcurl openssl PKG_DEPS := libxml-2.0 libcurl openssl
GIT_HASH := $(shell git rev-parse --short HEAD)
GIT_DATE := $(shell git log -1 --format='%ad' --date='format:%y.%m.%d')
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
GIT_REMOTE := $(shell git remote get-url origin)
VERSION := $(GIT_DATE)+$(GIT_HASH)_$(GIT_BRANCH)
CFLAGS := -Wall -Wextra -O2 -Isrc -DVERSION='"$(VERSION)"' -DGIT_REMOTE='"$(GIT_REMOTE)"'
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
DEP_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(PKG_DEPS) 2>/dev/null) DEP_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(PKG_DEPS) 2>/dev/null)
DEP_LIBS := $(shell $(PKG_CONFIG) --libs $(PKG_DEPS) 2>/dev/null) DEP_LIBS := $(shell $(PKG_CONFIG) --libs $(PKG_DEPS) 2>/dev/null)
CFLAGS := -Wall -Wextra -O2 -Isrc $(DEP_CFLAGS) CFLAGS += $(DEP_CFLAGS)
LIBS := -lbeaker $(DEP_LIBS) -lpthread -lm LIBS := -lbeaker $(DEP_LIBS) -lpthread -lm
else else
CFLAGS := -Wall -Wextra -O2 -Isrc -I/usr/include/libxml2 CFLAGS += -I/usr/include/libxml2
LIBS := -lbeaker -lcurl -lxml2 -lpthread -lm -lssl -lcrypto LIBS := -lbeaker -lcurl -lxml2 -lpthread -lm -lssl -lcrypto
endif endif
@@ -76,6 +85,7 @@ USER := omnisearch
GROUP := omnisearch GROUP := omnisearch
SYSTEMD_DIR := /etc/systemd/system SYSTEMD_DIR := /etc/systemd/system
RUNIT_DIR ?= $(error Please set RUNIT_DIR to your services directory)
OPENRC_DIR := /etc/init.d OPENRC_DIR := /etc/init.d
DINIT_DIR := /etc/dinit.d DINIT_DIR := /etc/dinit.d
LAUNCHD_DIR ?= /Library/LaunchDaemons LAUNCHD_DIR ?= /Library/LaunchDaemons
@@ -94,9 +104,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)
@@ -115,9 +126,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)'..."
@@ -133,9 +145,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)'..."
@@ -151,9 +164,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) /etc/service/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)'..."
@@ -161,20 +175,21 @@ install-runit: $(TARGET)
@id -u $(USER) >/dev/null 2>&1 || useradd --system --home $(DATA_DIR) --shell /usr/sbin/nologin -g $(GROUP) $(USER) @id -u $(USER) >/dev/null 2>&1 || useradd --system --home $(DATA_DIR) --shell /usr/sbin/nologin -g $(GROUP) $(USER)
@chown -R $(USER):$(GROUP) $(LOG_DIR) $(CACHE_DIR) $(VAR_DIR) $(DATA_DIR) 2>/dev/null || true @chown -R $(USER):$(GROUP) $(LOG_DIR) $(CACHE_DIR) $(VAR_DIR) $(DATA_DIR) 2>/dev/null || true
@chown $(USER):$(GROUP) $(DATA_DIR)/config.ini 2>/dev/null || true @chown $(USER):$(GROUP) $(DATA_DIR)/config.ini 2>/dev/null || true
install -m 755 init/runit/run /etc/service/omnisearch/run install -m 755 init/runit/run $(RUNIT_DIR)/omnisearch/run
install -m 755 init/runit/log/run /etc/service/omnisearch/log/run install -m 755 init/runit/log/run $(RUNIT_DIR)/omnisearch/log/run
@echo "" @echo ""
@echo "Config: $(DATA_DIR)/config.ini" @echo "Config: $(DATA_DIR)/config.ini"
@echo "Edit config with: nano $(DATA_DIR)/config.ini" @echo "Edit config with: nano $(DATA_DIR)/config.ini"
@echo "Installed runit service to /etc/service/omnisearch" @echo "Installed runit service to $(RUNIT_DIR)/omnisearch"
@echo "You need to start the service manually" @echo "You need to start the service manually"
@echo "Void: ln -s /etc/service/omnisearch/ /var/service" @echo "Void: ln -s $(RUNIT_DIR)/omnisearch/ /var/service/"
@echo "Artix: ln -s /etc/service/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)'..."
@@ -193,9 +208,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)'..."
@@ -218,6 +234,7 @@ uninstall:
rm -f $(DINIT_DIR)/omnisearch rm -f $(DINIT_DIR)/omnisearch
rm -rf /etc/service/omnisearch rm -rf /etc/service/omnisearch
rm -rf /var/service/omnisearch rm -rf /var/service/omnisearch
@echo "You might need to unlink omnisearch if you're using runit"
@id -u $(USER) >/dev/null 2>&1 && userdel $(USER) 2>/dev/null || true @id -u $(USER) >/dev/null 2>&1 && userdel $(USER) 2>/dev/null || true
@grep -q '^$(GROUP):' /etc/group 2>/dev/null && groupdel $(GROUP) 2>/dev/null || true @grep -q '^$(GROUP):' /etc/group 2>/dev/null && groupdel $(GROUP) 2>/dev/null || true
@echo "Uninstalled omnisearch" @echo "Uninstalled omnisearch"

View File

@@ -23,7 +23,7 @@ Depending on your system, you may first need to install libcurl and libxml2.
### Debian/Ubuntu ### Debian/Ubuntu
``` ```
# apt install libxml2-dev libcurl4-openssl-dev # apt install build-essential libssl-dev libxml2-dev libcurl4-openssl-dev
``` ```
### Fedora ### Fedora
@@ -50,7 +50,7 @@ On Alpine, `shadow` is needed for the user creation process during the install.
### NixOS ### NixOS
Add the flake to your inputs and import the module. That is all you need. Add the flake to your inputs and import the module. That is all you need.
Here's an example of using the modules in a flake: Here's an example of using the modules in a flake:
``` ```nix
# flake.nix # flake.nix
{ {
inputs = { inputs = {
@@ -108,6 +108,20 @@ On macOS, use `install-launchd`.
## Hosting ## Hosting
Run it normally behind a reverse proxy (like nginx) Run it normally behind a reverse proxy (like nginx)
## Deploy with Docker Compose
You need Docker or Podman and Docker Compose installed on your system.
Run the container:
```
$ git clone https://git.bwaaa.monster/omnisearch
$ cd omnisearch
$ docker compose up -d --build
```
By default it can be reached on port 8087.
## Customisation ## Customisation
To make your own changes while still being able to receive upstream updates: To make your own changes while still being able to receive upstream updates:

10
docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
services:
omnisearch:
build:
context: .
dockerfile: Dockerfile
ports:
- "8087:8087"
volumes:
- ./locales:/app/locales
- ./example-config.ini:/etc/omnisearch/config.ini

View File

@@ -1,7 +1,9 @@
[server] [server]
host = 0.0.0.0 host = 0.0.0.0
port = 8087 port = 8087
domain = https://search.example.com
# Default locale (default: en_gb)
#locale = en_gb
[proxy] [proxy]
# Single proxy (comment out to use list_file instead) # Single proxy (comment out to use list_file instead)
@@ -31,3 +33,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

8
flake.lock generated
View File

@@ -3,11 +3,11 @@
"beaker-src": { "beaker-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1773884524, "lastModified": 1775244490,
"narHash": "sha256-1dnlofWaxI/YRID+WPz2jHZNDyloBubDt/bAQk9ePLU=", "narHash": "sha256-4TJv7X6D0l4rEbTRKf47gU43L8G5uJgxxtsqMkVixQY=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "abc598baf15d6f8a4de395a27ba34b1e769558e1", "rev": "3fab89ecf8f4c664477a82add660d28db87357b4",
"revCount": 21, "revCount": 27,
"shallow": false, "shallow": false,
"type": "git", "type": "git",
"url": "https://git.bwaaa.monster/beaker" "url": "https://git.bwaaa.monster/beaker"

View File

@@ -61,7 +61,7 @@
installPhase = '' installPhase = ''
mkdir -p $out/bin $out/share/omnisearch mkdir -p $out/bin $out/share/omnisearch
install -Dm755 bin/omnisearch $out/bin/omnisearch install -Dm755 bin/omnisearch $out/bin/omnisearch
cp -r templates static -t $out/share/omnisearch/ cp -r templates static locales -t $out/share/omnisearch/
''; '';
meta = { meta = {

34
locales/af_za.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "af_za"
Name = "Afrikaans (ZA)"
Direction = "ltr"
[Keys]
search_placeholder = "Soek op die web..."
search_button = "Soek"
surprise_me_button = "Verras my"
all_tab = "Alles"
images_tab = "Foto's"
settings_tab = "Instellings"
settings_title = "Instellings"
theme_label = "Kleurinstelling"
theme_desc = "Kies jou voorkeur kleurskema."
theme_system = "Stelsel"
theme_light = "Lig"
theme_dark = "Donker"
language_label = "Taal"
display_language_label = "Vertoontaal"
language_desc = "Kies jou voorkeurtaal."
engines_label = "Soekenjins"
engines_desc = "Kies watter soekenjins om te gebruik. Slegs enjins wat op die bediener geaktiveer is, word gewys.."
save_settings_button = "Stoor Instellings"
no_results = "Geen resultate gevind nie"
error_images = "Fout tydens die haal van foto's"
rate_limit = "Stadiger! Te veel soektogte van jou!"
warning_fetch_error = "versoek het misluk voordat OmniSearch soekresultate kon lees."
warning_parse_mismatch = "het soekresultate in 'n formaat teruggegee wat OmniSearch nie kon ontleed nie."
warning_blocked = "het 'n captcha of 'n ander blokkerende bladsy in plaas van soekresultate teruggegee."
read_more = "Lees Meer"
view_cached = "Gekasgeheue"
view_image = "Foto"
visit_site = "Webwerf"

34
locales/bn_bd.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "bn_bd"
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 = "আপনার পছন্দের ভাষা নির্ধারণ করুন।"
engines_label = "সার্চ ইঞ্জিন"
engines_desc = "ব্যবহৃত সার্চ ইঞ্জিন নির্ধারণ করুন। শুধু সার্ভারে চলনকৃত সার্চ ইঞ্জিন দেখানো।"
save_settings_button = "সেটিংস সংরক্ষণ করুন"
no_results = "কোনো ফলাফল পাওয়া যায়নি"
error_images = "ছবি অনুসন্ধান করায় অক্ষম"
rate_limit = "আস্তে-ধীরে! বহুত অনুসন্ধান করছেন দেখি!"
warning_fetch_error = "অমনিসার্চ ফলাফল বোঝার আগেই আবেদন ব্যার্থ হয়েছে।"
warning_parse_mismatch = "অমনিসার্চ বুঝতে অক্ষম এমনপ্রকার ফলাফল ফিরেছে।"
warning_blocked = "অনুসন্ধানের ফলাফলের বদলে ক্যাপ্চা অথবা অন্য ব্লককারী পেজ ফিরেছে।"
read_more = "পড়তে থাকুন"
view_cached = "ক্যাশকৃত"
view_image = "ছবি"
visit_site = "সাইট"

34
locales/en_gb.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "en_gb"
Name = "English"
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 (US)"
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"

34
locales/fr_fr.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "fr_fr"
Name = "Français"
Direction = "ltr"
[Keys]
search_placeholder = "Rechercher le web..."
search_button = "Rechercher"
surprise_me_button = "Surprenez-moi"
all_tab = "Tout"
images_tab = "Images"
settings_tab = "Paramètres"
settings_title = "Paramètres"
theme_label = "Apparence"
theme_desc = "Choisissez votre palette de couleurs préférée."
theme_system = "Système"
theme_light = "Clair"
theme_dark = "Sombre"
language_label = "Langue"
display_language_label = "Afficher les Langues"
language_desc = "Choisissez votre langue."
engines_label = "Moteur de Recherche"
engines_desc = "Choisissez les moteurs de recherche que vous souhaitez utiliser. Seulement les moteurs de recherche activer sur le serveur sont affichés."
save_settings_button = "Enregistrer les paramètres"
no_results = "Aucun résultats"
error_images = "Erreur lors du chargement des images"
rate_limit = "Woah ralentissez! Vous effectuer trop de Rechercher!"
warning_fetch_error = "La requête a échoué avant que OmniSearch puisse lire les résultats de la recherche."
warning_parse_mismatch = "Les résultats de la recherche ont été retourné dans un format que OmniSearch ne peux pas analyser."
warning_blocked = "Un captcha ou une autre page bloquante a été retourné au lieu d'un résultat de recherche."
read_more = "Lire Plus"
view_cached = "En cache"
view_image = "Image"
visit_site = "Site"

34
locales/lv_lv.ini Normal file
View File

@@ -0,0 +1,34 @@
[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ēma"
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 = "Meklēšanas rezultāti tika atgriezti formātā, kuru OmniSearch nevarēja apstrādāt."
warning_blocked = "Tika atgriezta captcha vai cita bloķējoša lapa meklēšanas rezultātu vietā."
read_more = "Lasīt vairāk"
view_cached = "Saglabātā kopija"
view_image = "Skatīt attēlu"
visit_site = "Apmeklēt vietni"
engines_label = "Meklēšanas dzinēji"
engines_desc = "Izvēlieties, kurus meklēšanas dzinējus izmantot. Tiek rādīti tikai serverī iespējotie."

34
locales/nl_nl.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "nl_nl"
Name = "Nederlands"
Direction = "ltr"
[Keys]
search_placeholder = "Zoek op het web..."
search_button = "Zoek"
surprise_me_button = "Verras me"
all_tab = "Alle"
images_tab = "Plaatjes"
settings_tab = "Instellingen"
settings_title = "Instellingen"
theme_label = "Kleurinstelling"
theme_desc = "Kies welke stijl je gebruikt."
theme_system = "Systeemkleur"
theme_light = "Licht"
theme_dark = "Donker"
language_label = "Taal"
display_language_label = "Geselecteerde taal"
language_desc = "Kies de taal van OmniSearch."
engines_label = "Zoekmachines"
engines_desc = "Kies welke zoekmachines je gebruikt. Alleen de ingeschakelde zoekmachines worden gebruikt."
save_settings_button = "Opslaan"
no_results = "Geen resultaten"
error_images = "Foutmelding bij het zoeken naar plaatjes."
rate_limit = "Te veel zoekopdrachten! Ga wat langzamer!"
warning_fetch_error = "stuurde een foutmelding voordat OmniSearch de resultaten kon lezen."
warning_parse_mismatch = "stuurde een foutmelding in een format dat OmniSearch niet kon lezen."
warning_blocked = "stuurde een captcha of een andere pagina, maar geen zoekresultaten."
read_more = "Lees meer"
view_cached = "Tijdelijk Opgeslagen"
view_image = "Bekijk"
visit_site = "Website"

34
locales/pt_br.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "pt_br"
Name = "Portuguese (Brazil)"
Direction = "ltr"
[Keys]
search_placeholder = "Pesquisar na internet..."
search_button = "Pesquisar"
surprise_me_button = "Me surpreenda"
all_tab = "Tudo"
images_tab = "Imagens"
settings_tab = "Configurações"
settings_title = "Configurações"
theme_label = "Aparência"
theme_desc = "Escolha o esquema de cores de sua preferência."
theme_system = "Padrão do Sistema"
theme_light = "Claro"
theme_dark = "Escuro"
language_label = "Idioma"
display_language_label = "Idioma de exibição"
language_desc = "Escolha o idioma de sua preferência"
engines_label = "Mecanismos de pesquisa"
engines_desc = "Escolha quais mecanismos de pesquisa serão utilizados. Apenas mecanismos habilitados no servidor serão mostrados."
save_settings_button = "Salvar configurações"
no_results = "Nenhum resultado encontrado"
error_images = "Erro ao buscar imagens"
rate_limit = "Calma aí! Muitas pesquisas vindo de você!"
warning_fetch_error = "pedido falhou antes que o Omnisearch pudesse ler resultados de pesquisa."
warning_parse_mismatch = "retornou resultados em um formato que o Omnisearch não pode processar."
warning_blocked = "retornou um captcha ou outra página de bloqueio ao invés de resultados de pesquisa."
read_more = "Ler mais"
view_cached = "Armazenado em cache"
view_image = "Imagem"
visit_site = "Site"

34
locales/ru_ru.ini Normal file
View File

@@ -0,0 +1,34 @@
[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 = "Перейти на сайт"
engines_label = "Поисковые системы"
engines_desc = "Выберите, какие поисковые системы использовать. Отображаются только включённые на сервере."

34
locales/vi_vn.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "vi_vn"
Name = "tiếng việt"
Direction = "ltr"
[Keys]
search_placeholder = "tìm kiếm trên web..."
search_button = "tìm kiếm"
surprise_me_button = "bất ngờ"
all_tab = "tất cả"
images_tab = "hình ảnh"
settings_tab = "cài đặt"
settings_title = "cài đặt"
theme_label = "giao diện"
theme_desc = "chọn bảng màu mà bạn muốn"
theme_system = "hệ thống"
theme_light = "sáng"
theme_dark = "tối"
language_label = "ngôn ngữ"
display_language_label = "ngôn ngữ hiển thị"
language_desc = "chọn ngôn ngữ mà bn muốn"
engines_label = "công cụ tìm kiếm"
engines_desc = "chọn các công cụ tìm kiếm để sử dụng, chỉ những công cụ dc bật trên máy chủ ms đc hiển thị."
save_settings_button = "lưu cài đặt"
no_results = "k tìm thấy kết quả"
error_images = "lỗi khi tải hình ảnh"
rate_limit = "chậm lại! dừng lại! bạn đã tìm kiếm quá nhiều lần!"
warning_fetch_error = "yêu cầu thất bại trước khi omnisearch có thể đọc kết quả tìm kiếm."
warning_parse_mismatch = "kết quả tìm kiếm được trả về ở định dạng omnisearch k thể phân tích."
warning_blocked = "trang trả về captcha hoặc trang chặn khác thay vì kết quả tìm kiếm."
read_more = "đọc thêm"
view_cached = "bản lưu"
view_image = "hình ảnh"
visit_site = "truy cập trang"

View File

@@ -20,6 +20,7 @@ let
host = ${cfg.settings.server.host} host = ${cfg.settings.server.host}
port = ${toString cfg.settings.server.port} port = ${toString cfg.settings.server.port}
domain = ${cfg.settings.server.domain} domain = ${cfg.settings.server.domain}
${lib.optionalString (cfg.settings.server.locale != null) "locale = ${cfg.settings.server.locale}"}
[proxy] [proxy]
${lib.optionalString (cfg.settings.proxy.proxy != null) "proxy = \"${cfg.settings.proxy.proxy}\""} ${lib.optionalString (cfg.settings.proxy.proxy != null) "proxy = \"${cfg.settings.proxy.proxy}\""}
@@ -64,7 +65,11 @@ in
}; };
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "http://localhost:8087"; default = "http://localhost:${toString cfg.settings.server.port}";
};
locale = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
}; };
}; };
proxy = { proxy = {
@@ -122,6 +127,7 @@ in
BindReadOnlyPaths = [ BindReadOnlyPaths = [
"${pkg}/share/omnisearch/templates:/var/lib/omnisearch/templates" "${pkg}/share/omnisearch/templates:/var/lib/omnisearch/templates"
"${pkg}/share/omnisearch/static:/var/lib/omnisearch/static" "${pkg}/share/omnisearch/static:/var/lib/omnisearch/static"
"${pkg}/share/omnisearch/locales:/var/lib/omnisearch/locales"
"${finalConfigFile}:/var/lib/omnisearch/config.ini" "${finalConfigFile}:/var/lib/omnisearch/config.ini"
]; ];

View File

@@ -11,15 +11,20 @@
static char cache_dir[BUFFER_SIZE_MEDIUM] = {0}; static char cache_dir[BUFFER_SIZE_MEDIUM] = {0};
static int cache_ttl_search_val = DEFAULT_CACHE_TTL_SEARCH; static int cache_ttl_search_val = DEFAULT_CACHE_TTL_SEARCH;
static int cache_ttl_infobox_val = DEFAULT_CACHE_TTL_INFOBOX; static int cache_ttl_infobox_val = DEFAULT_CACHE_TTL_INFOBOX;
static int cache_ttl_image_val = DEFAULT_CACHE_TTL_IMAGE;
void set_cache_ttl_search(int ttl) { cache_ttl_search_val = ttl; } void set_cache_ttl_search(int ttl) { cache_ttl_search_val = ttl; }
void set_cache_ttl_infobox(int ttl) { cache_ttl_infobox_val = ttl; } void set_cache_ttl_infobox(int ttl) { cache_ttl_infobox_val = ttl; }
void set_cache_ttl_image(int ttl) { cache_ttl_image_val = ttl; }
int get_cache_ttl_search(void) { return cache_ttl_search_val; } int get_cache_ttl_search(void) { return cache_ttl_search_val; }
int get_cache_ttl_infobox(void) { return cache_ttl_infobox_val; } int get_cache_ttl_infobox(void) { return cache_ttl_infobox_val; }
int get_cache_ttl_image(void) { return cache_ttl_image_val; }
static void md5_hash(const char *str, char *output) { static void md5_hash(const char *str, char *output) {
unsigned char hash[EVP_MAX_MD_SIZE]; unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len; unsigned int hash_len;

View File

@@ -17,7 +17,9 @@ char *cache_compute_key(const char *query, int page, const char *engine_name);
void set_cache_ttl_search(int ttl); void set_cache_ttl_search(int ttl);
void set_cache_ttl_infobox(int ttl); void set_cache_ttl_infobox(int ttl);
void set_cache_ttl_image(int ttl);
int get_cache_ttl_search(void); int get_cache_ttl_search(void);
int get_cache_ttl_infobox(void); int get_cache_ttl_infobox(void);
int get_cache_ttl_image(void);
#endif #endif

View File

@@ -65,9 +65,10 @@ int load_config(const char *filename, Config *config) {
config->host[sizeof(config->host) - 1] = '\0'; config->host[sizeof(config->host) - 1] = '\0';
} else if (strcmp(key, "port") == 0) { } else if (strcmp(key, "port") == 0) {
config->port = atoi(value); config->port = atoi(value);
} else if (strcmp(key, "domain") == 0) { } else if (strcmp(key, "locale") == 0) {
strncpy(config->domain, value, sizeof(config->domain) - 1); strncpy(config->default_locale, value,
config->domain[sizeof(config->domain) - 1] = '\0'; sizeof(config->default_locale) - 1);
config->default_locale[sizeof(config->default_locale) - 1] = '\0';
} }
} else if (strcmp(section, "proxy") == 0) { } else if (strcmp(section, "proxy") == 0) {
if (strcmp(key, "proxy") == 0) { if (strcmp(key, "proxy") == 0) {
@@ -92,12 +93,24 @@ int load_config(const char *filename, Config *config) {
config->cache_ttl_search = atoi(value); config->cache_ttl_search = atoi(value);
} else if (strcmp(key, "ttl_infobox") == 0) { } else if (strcmp(key, "ttl_infobox") == 0) {
config->cache_ttl_infobox = atoi(value); config->cache_ttl_infobox = atoi(value);
} else if (strcmp(key, "ttl_image") == 0) {
config->cache_ttl_image = atoi(value);
} }
} else if (strcmp(section, "engines") == 0) { } else if (strcmp(section, "engines") == 0) {
if (strcmp(key, "engines") == 0) { if (strcmp(key, "engines") == 0) {
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

@@ -6,6 +6,7 @@
#define DEFAULT_CACHE_DIR "/tmp/omnisearch_cache" #define DEFAULT_CACHE_DIR "/tmp/omnisearch_cache"
#define DEFAULT_CACHE_TTL_SEARCH 3600 #define DEFAULT_CACHE_TTL_SEARCH 3600
#define DEFAULT_CACHE_TTL_INFOBOX 86400 #define DEFAULT_CACHE_TTL_INFOBOX 86400
#define DEFAULT_CACHE_TTL_IMAGE 604800
#define DEFAULT_MAX_PROXY_RETRIES 3 #define DEFAULT_MAX_PROXY_RETRIES 3
#define BUFFER_SIZE_SMALL 256 #define BUFFER_SIZE_SMALL 256
@@ -20,7 +21,7 @@
#define MD5_HASH_LEN 32 #define MD5_HASH_LEN 32
#define HEX_CHARS "0123456789abcdef" #define HEX_CHARS "0123456789abcdef"
#define INFOBOX_FIELD_COUNT 4 #define INFOBOX_FIELD_COUNT 5
#define MAX_RESULTS_PER_ENGINE 10 #define MAX_RESULTS_PER_ENGINE 10
#define CURL_TIMEOUT_SECS 15L #define CURL_TIMEOUT_SECS 15L
@@ -34,6 +35,7 @@ typedef struct {
char host[256]; char host[256];
int port; int port;
char domain[256]; char domain[256];
char default_locale[32];
char proxy[256]; char proxy[256];
char proxy_list_file[256]; char proxy_list_file[256];
int max_proxy_retries; int max_proxy_retries;
@@ -42,7 +44,12 @@ typedef struct {
char cache_dir[512]; char cache_dir[512];
int cache_ttl_search; int cache_ttl_search;
int cache_ttl_infobox; int cache_ttl_infobox;
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);

166
src/Infobox/ColourCode.c Normal file
View File

@@ -0,0 +1,166 @@
#include "ColourCode.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const unsigned char HEX_VALS[256] = {
['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5,
['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9, ['a'] = 10, ['b'] = 11,
['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15, ['A'] = 10, ['B'] = 11,
['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15,
};
static int is_hex_digit(char c) {
return HEX_VALS[(unsigned char)c] || c == '0';
}
static int is_valid_hex(const char *s, int len) {
if (len != 3 && len != 6)
return 0;
for (int i = 0; i < len; i++) {
if (!is_hex_digit(s[i]))
return 0;
}
return 1;
}
static void hex_to_rgb(const char *hex, int *r, int *g, int *b, int len) {
*r = *g = *b = 0;
if (len == 3) {
*r = HEX_VALS[(unsigned char)hex[0]] * 17;
*g = HEX_VALS[(unsigned char)hex[1]] * 17;
*b = HEX_VALS[(unsigned char)hex[2]] * 17;
} else if (len == 6) {
*r = HEX_VALS[(unsigned char)hex[0]] * 16 + HEX_VALS[(unsigned char)hex[1]];
*g = HEX_VALS[(unsigned char)hex[2]] * 16 + HEX_VALS[(unsigned char)hex[3]];
*b = HEX_VALS[(unsigned char)hex[4]] * 16 + HEX_VALS[(unsigned char)hex[5]];
}
}
static void rgb_to_hsl(int r, int g, int b, int *h, int *s, int *l) {
int max = r > g ? (r > b ? r : b) : (g > b ? g : b);
int min = r < g ? (r < b ? r : b) : (g < b ? g : b);
int delta = max - min;
*l = (max + min) / 2;
*s = 0;
*h = 0;
if (delta > 0) {
*s = (int)((double)delta / (1.0 - fabs(2.0 * *l / 255.0 - 1.0)) * 100.0);
if (max == r) {
*h = (int)(60.0 * fmod((double)(g - b) / delta, 6.0));
} else if (max == g) {
*h = (int)(60.0 * (((b - r) / delta) + 2.0));
} else {
*h = (int)(60.0 * (((r - g) / delta) + 4.0));
}
if (*h < 0)
*h += 360;
}
}
static void rgb_to_hsv(int r, int g, int b, int *h, int *s, int *v) {
int max = r > g ? (r > b ? r : b) : (g > b ? g : b);
int min = r < g ? (r < b ? r : b) : (g < b ? g : b);
int delta = max - min;
*v = max * 100 / 255;
*s = max > 0 ? delta * 100 / max : 0;
*h = 0;
if (delta > 0) {
if (max == r) {
*h = (int)(60.0 * fmod((double)(g - b) / delta, 6.0));
} else if (max == g) {
*h = (int)(60.0 * (((b - r) / delta) + 2.0));
} else {
*h = (int)(60.0 * (((r - g) / delta) + 4.0));
}
if (*h < 0)
*h += 360;
}
}
static void rgb_to_cmyk(int r, int g, int b, int *c, int *m, int *y, int *k) {
int max = r > g ? (r > b ? r : b) : (g > b ? g : b);
double kf = 1.0 - max / 255.0;
if (kf >= 0.99) {
*c = *m = *y = *k = 100;
} else {
*c = (int)((1.0 - r / 255.0 - kf) / (1.0 - kf) * 100);
*m = (int)((1.0 - g / 255.0 - kf) / (1.0 - kf) * 100);
*y = (int)((1.0 - b / 255.0 - kf) / (1.0 - kf) * 100);
*k = (int)(kf * 100);
}
}
int is_colour_code_query(const char *query) {
if (!query)
return 0;
const char *p = query;
if (p[0] == '#') {
p++;
while (*p == ' ')
p++;
const char *end = p;
while (is_hex_digit(*end))
end++;
int len = end - p;
return is_valid_hex(p, len);
}
return 0;
}
InfoBox fetch_colour_data(char *query) {
InfoBox info = {NULL, NULL, NULL, NULL};
if (!query)
return info;
const char *p = query;
if (p[0] != '#')
return info;
p++;
while (*p == ' ')
p++;
const char *end = p;
while (is_hex_digit(*end))
end++;
int len = end - p;
if (!is_valid_hex(p, len))
return info;
int r, g, b;
hex_to_rgb(p, &r, &g, &b, len);
int h, s, l;
rgb_to_hsl(r, g, b, &h, &s, &l);
int h2, s2, v;
rgb_to_hsv(r, g, b, &h2, &s2, &v);
int c, m, y, k;
rgb_to_cmyk(r, g, b, &c, &m, &y, &k);
char html[1024];
snprintf(html, sizeof(html),
"<div class='colour-container' style='line-height: 1.6;'>"
"<div style='display: flex; align-items: flex-start; gap: 16px;'>"
"<div style='width: 100px; height: 100px; border-radius: 12px; "
"border: 1px solid #ddd; background-color: #%.*s; flex-shrink: 0;'></div>"
"<div>"
"<div style='font-size: 1.3em; font-weight: bold; margin-bottom: 8px;'>"
"#%.*s</div>"
"<div style='color: #666; margin-bottom: 4px;'>RGB(%d, %d, %d)</div>"
"<div style='color: #666; margin-bottom: 4px;'>HSL(%d, %d%%, %d%%)</div>"
"<div style='color: #666; margin-bottom: 4px;'>HSV(%d, %d%%, %d%%)</div>"
"<div style='color: #666;'>CMYK(%d%%, %d%%, %d%%, %d%%)</div>"
"</div>"
"</div>"
"</div>",
len, p, len, p, r, g, b, h, s, l, h2, s2, v, c, m, y, k);
info.title = strdup("Colour");
info.extract = strdup(html);
info.thumbnail_url = NULL;
info.url = strdup("#");
return info;
}

9
src/Infobox/ColourCode.h Normal file
View File

@@ -0,0 +1,9 @@
#ifndef COLOURCODE_H
#define COLOURCODE_H
#include "Infobox.h"
int is_colour_code_query(const char *query);
InfoBox fetch_colour_data(char *query);
#endif

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,20 +13,44 @@
#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"
#include "Utility/Utility.h"
Config global_config; Config global_config;
int handle_opensearch(UrlParams *params) { int handle_opensearch(UrlParams *params) {
(void)params; (void)params;
extern Config global_config;
TemplateContext ctx = new_context(); TemplateContext ctx = new_context();
context_set(&ctx, "domain", global_config.domain);
const char *http_host = beaker_get_header("Host");
if (http_host == NULL) {
http_host = "localhost";
}
const char *req_scheme =
"https"; // not sure if it's a good idea to just assume https, but you
// should probably be using https for anything other than testing
// or local network anyways.
if (strncmp(http_host, "localhost", 9) == 0 ||
strncmp(http_host, "127.", 4) == 0 ||
strncmp(http_host, "192.168.", 8) == 0 ||
strncmp(http_host, "10.", 3) == 0) {
req_scheme = "http";
}
context_set(&ctx, "domain", http_host);
context_set(&ctx, "scheme", req_scheme);
char *rendered = render_template("opensearch.xml", &ctx); char *rendered = render_template("opensearch.xml", &ctx);
serve_data(rendered, strlen(rendered), "application/opensearchdescription+xml"); serve_data(rendered, strlen(rendered),
"application/opensearchdescription+xml");
free(rendered); free(rendered);
free_context(&ctx); free_context(&ctx);
return 0; return 0;
} }
@@ -43,7 +67,7 @@ int main() {
Config cfg = {.host = DEFAULT_HOST, Config cfg = {.host = DEFAULT_HOST,
.port = DEFAULT_PORT, .port = DEFAULT_PORT,
.domain = "", .default_locale = "en_gb",
.proxy = "", .proxy = "",
.proxy_list_file = "", .proxy_list_file = "",
.max_proxy_retries = DEFAULT_MAX_PROXY_RETRIES, .max_proxy_retries = DEFAULT_MAX_PROXY_RETRIES,
@@ -52,14 +76,30 @@ int main() {
.cache_dir = DEFAULT_CACHE_DIR, .cache_dir = DEFAULT_CACHE_DIR,
.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,
.engines = ""}; .cache_ttl_image = DEFAULT_CACHE_TTL_IMAGE,
.engines = "",
.rate_limit_search_requests = 0,
.rate_limit_search_interval = 0,
.rate_limit_images_requests = 0,
.rate_limit_images_interval = 0};
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");
} }
set_default_locale(cfg.default_locale);
init_themes("static");
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) {
@@ -72,6 +112,7 @@ int main() {
set_cache_ttl_search(cfg.cache_ttl_search); set_cache_ttl_search(cfg.cache_ttl_search);
set_cache_ttl_infobox(cfg.cache_ttl_infobox); set_cache_ttl_infobox(cfg.cache_ttl_infobox);
set_cache_ttl_image(cfg.cache_ttl_image);
if (cfg.proxy_list_file[0] != '\0') { if (cfg.proxy_list_file[0] != '\0') {
if (load_proxy_list(cfg.proxy_list_file) < 0) { if (load_proxy_list(cfg.proxy_list_file) < 0) {
@@ -95,6 +136,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);
@@ -109,6 +152,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,30 @@
#include "Home.h" #include "Home.h"
#include "../Utility/Utility.h"
#include <beaker.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
int home_handler(UrlParams *params) { int home_handler(UrlParams *params) {
(void)params; (void)params;
char *theme = get_theme("");
char *locale = get_locale(NULL);
char **themes = NULL;
int themes_count = 0;
get_available_themes(&themes, &themes_count);
TemplateContext ctx = new_context(); TemplateContext ctx = new_context();
context_set(&ctx, "theme", theme);
context_set(&ctx, "version", VERSION);
context_set(&ctx, "git_remote", GIT_REMOTE);
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

@@ -1,11 +1,31 @@
#include "ImageProxy.h" #include "ImageProxy.h"
#include "../Cache/Cache.h"
#include "../Proxy/Proxy.h" #include "../Proxy/Proxy.h"
#include <arpa/inet.h>
#include <curl/curl.h> #include <curl/curl.h>
#include <curl/urlapi.h>
#include <netdb.h>
#include <netinet/in.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 <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;
@@ -13,7 +33,157 @@ typedef struct {
size_t capacity; size_t capacity;
} MemoryBuffer; } MemoryBuffer;
static int is_allowed_domain(const char *url) { static int dns_cache_lookup(const char *hostname, char *out_ip) {
time_t now = time(NULL);
pthread_mutex_lock(&dns_cache_mutex);
for (DnsCacheEntry *e = dns_cache; e; e = e->next) {
if (strcmp(e->hostname, hostname) == 0) {
if ((now - e->resolved_at) < DNS_CACHE_TTL) {
strcpy(out_ip, e->ip_str);
pthread_mutex_unlock(&dns_cache_mutex);
return 0;
}
break;
}
}
pthread_mutex_unlock(&dns_cache_mutex);
return -1;
}
static void dns_cache_insert(const char *hostname, const char *ip_str) {
time_t now = time(NULL);
pthread_mutex_lock(&dns_cache_mutex);
DnsCacheEntry **cursor = &dns_cache;
while (*cursor) {
DnsCacheEntry *entry = *cursor;
if ((now - entry->resolved_at) >= DNS_CACHE_TTL) {
*cursor = entry->next;
free(entry);
continue;
}
if (strcmp(entry->hostname, hostname) == 0) {
strcpy(entry->ip_str, ip_str);
entry->resolved_at = now;
pthread_mutex_unlock(&dns_cache_mutex);
return;
}
cursor = &entry->next;
}
DnsCacheEntry *new_entry = malloc(sizeof(DnsCacheEntry));
if (new_entry) {
strncpy(new_entry->hostname, hostname, sizeof(new_entry->hostname) - 1);
new_entry->hostname[sizeof(new_entry->hostname) - 1] = '\0';
strcpy(new_entry->ip_str, ip_str);
new_entry->resolved_at = now;
new_entry->next = dns_cache;
dns_cache = new_entry;
}
pthread_mutex_unlock(&dns_cache_mutex);
}
static int is_private_ip(const char *ip_str) {
struct in_addr addr;
if (inet_pton(AF_INET, ip_str, &addr) != 1) {
return 0;
}
uint32_t ip = ntohl(addr.s_addr);
// 10.0.0.0/8
if ((ip >> 24) == 10) {
return 1;
}
// 172.16.0.0/12
if ((ip >> 20) == 0xAC) {
uint8_t second = (ip >> 16) & 0xFF;
if (second >= 16 && second <= 31) {
return 1;
}
}
// 192.168.0.0/16
if ((ip >> 16) == 0xC0A8) {
return 1;
}
// 127.0.0.0/8
if ((ip >> 24) == 127) {
return 1;
}
// 169.254.0.0/16
if ((ip >> 16) == 0xA9FE) {
return 1;
}
return 0;
}
static const char *is_private_hostname(const char *hostname, char *out_ip) {
if (dns_cache_lookup(hostname, out_ip) == 0) {
return out_ip;
}
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int err = getaddrinfo(hostname, NULL, &hints, &res);
if (err != 0) {
return NULL;
}
for (p = res; p != NULL; p = p->ai_next) {
if (p->ai_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(ipv4->sin_addr), ip_str, INET_ADDRSTRLEN);
if (is_private_ip(ip_str)) {
freeaddrinfo(res);
return NULL;
}
freeaddrinfo(res);
strcpy(out_ip, ip_str);
dns_cache_insert(hostname, ip_str);
return out_ip;
}
}
freeaddrinfo(res);
return NULL;
}
static int is_allowed_domain(const char *url, char *resolved_ip) {
CURLU *h = curl_url();
if (!h) {
return -1;
}
curl_url_set(h, CURLUPART_URL, url, 0);
char *scheme = NULL;
curl_url_get(h, CURLUPART_SCHEME, &scheme, 0);
int valid_scheme = 0;
if (scheme && (strcasecmp(scheme, "http") == 0 || strcasecmp(scheme, "https") == 0)) {
valid_scheme = 1;
}
if (scheme)
curl_free(scheme);
if (!valid_scheme) {
curl_url_cleanup(h);
return -1;
}
const char *protocol = strstr(url, "://"); const char *protocol = strstr(url, "://");
if (!protocol) { if (!protocol) {
protocol = url; protocol = url;
@@ -30,21 +200,18 @@ static int is_allowed_domain(const char *url) {
} }
strncpy(host, protocol, host_len); strncpy(host, protocol, host_len);
const char *allowed_domains[] = {"mm.bing.net", "th.bing.com", NULL}; char *colon = strchr(host, ':');
if (colon) {
for (int i = 0; allowed_domains[i] != NULL; i++) { *colon = '\0';
size_t domain_len = strlen(allowed_domains[i]);
size_t host_str_len = strlen(host);
if (host_str_len >= domain_len) {
const char *suffix = host + host_str_len - domain_len;
if (strcmp(suffix, allowed_domains[i]) == 0) {
return 1;
}
}
} }
return 0; if (!is_private_hostname(host, resolved_ip)) {
curl_url_cleanup(h);
return 0;
}
curl_url_cleanup(h);
return 1;
} }
static size_t write_callback(void *contents, size_t size, size_t nmemb, static size_t write_callback(void *contents, size_t size, size_t nmemb,
@@ -73,6 +240,31 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb,
return realsize; return realsize;
} }
static char *url_encode_key(const char *url) {
char *hash = malloc(33);
if (!hash)
return NULL;
unsigned char md5hash[16];
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) {
free(hash);
return NULL;
}
EVP_DigestInit_ex(ctx, EVP_md5(), NULL);
EVP_DigestUpdate(ctx, url, strlen(url));
EVP_DigestFinal_ex(ctx, md5hash, NULL);
EVP_MD_CTX_free(ctx);
for (int i = 0; i < 16; i++) {
sprintf(hash + (i * 2), "%02x", md5hash[i]);
}
hash[32] = '\0';
return hash;
}
int image_proxy_handler(UrlParams *params) { int image_proxy_handler(UrlParams *params) {
const char *url = NULL; const char *url = NULL;
for (int i = 0; i < params->count; i++) { for (int i = 0; i < params->count; i++) {
@@ -87,13 +279,67 @@ int image_proxy_handler(UrlParams *params) {
return 0; return 0;
} }
if (!is_allowed_domain(url)) { char resolved_ip[INET_ADDRSTRLEN] = {0};
send_response("Domain not allowed"); int domain_check = is_allowed_domain(url, resolved_ip);
if (domain_check == -1) {
send_response("Invalid URL scheme");
return 0;
}
if (domain_check == 0) {
send_response("Private addresses are not allowed");
return 0;
}
char *cache_key = url_encode_key(url);
if (!cache_key) {
send_response("Failed to generate cache key");
return 0;
}
char *cached_data = NULL;
size_t cached_size = 0;
int cache_ttl = get_cache_ttl_image();
if (cache_get(cache_key, cache_ttl, &cached_data, &cached_size) == 0) {
if (!cached_data || cached_size == 0) {
free(cached_data);
free(cache_key);
send_response("Empty cached image response");
return 0;
}
char content_type[64] = {0};
const char *ext = strrchr(url, '.');
if (ext) {
if (strcasecmp(ext, ".png") == 0) {
strncpy(content_type, "image/png", sizeof(content_type) - 1);
} else if (strcasecmp(ext, ".gif") == 0) {
strncpy(content_type, "image/gif", sizeof(content_type) - 1);
} else if (strcasecmp(ext, ".webp") == 0) {
strncpy(content_type, "image/webp", sizeof(content_type) - 1);
} else if (strcasecmp(ext, ".svg") == 0) {
strncpy(content_type, "image/svg+xml", sizeof(content_type) - 1);
} else if (strcasecmp(ext, ".ico") == 0) {
strncpy(content_type, "image/x-icon", sizeof(content_type) - 1);
} else if (strcasecmp(ext, ".bmp") == 0) {
strncpy(content_type, "image/bmp", sizeof(content_type) - 1);
}
}
if (strlen(content_type) == 0) {
strncpy(content_type, "image/jpeg", sizeof(content_type) - 1);
}
serve_data(cached_data, cached_size, content_type);
free(cached_data);
free(cache_key);
return 0; return 0;
} }
CURL *curl = curl_easy_init(); CURL *curl = curl_easy_init();
if (!curl) { if (!curl) {
free(cache_key);
send_response("Failed to initialize curl"); send_response("Failed to initialize curl");
return 0; return 0;
} }
@@ -102,6 +348,7 @@ int image_proxy_handler(UrlParams *params) {
if (!buf.data) { if (!buf.data) {
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
free(cache_key);
send_response("Memory allocation failed"); send_response("Memory allocation failed");
return 0; return 0;
} }
@@ -111,8 +358,37 @@ int image_proxy_handler(UrlParams *params) {
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_USERAGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"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;
@@ -126,18 +402,40 @@ 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) {
free(buf.data); free(buf.data);
free(cache_key);
send_response("Failed to fetch image"); send_response("Failed to fetch image");
return 0; return 0;
} }
if (buf.size == 0) {
free(buf.data);
free(cache_key);
send_response("Empty image response");
return 0;
}
if (strlen(content_type) == 0 ||
strncmp(content_type, "image/", 6) != 0) {
free(buf.data);
free(cache_key);
send_response("Invalid content type");
return 0;
}
const char *mime_type = const char *mime_type =
strlen(content_type) > 0 ? content_type : "image/jpeg"; strlen(content_type) > 0 ? content_type : "image/jpeg";
cache_set(cache_key, buf.data, buf.size);
serve_data(buf.data, buf.size, mime_type); serve_data(buf.data, buf.size, mime_type);
free(buf.data); free(buf.data);
free(cache_key);
return 0; return 0;
} }

View File

@@ -1,9 +1,48 @@
#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>
#include <stdlib.h>
#include <string.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 +59,94 @@ 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(NULL);
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 +161,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 +195,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,102 @@
#include "Search.h" #include "Search.h"
#include "../Cache/Cache.h"
#include "../Infobox/Calculator.h" #include "../Infobox/Calculator.h"
#include "../Infobox/ColourCode.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;
@@ -27,6 +109,10 @@ typedef struct {
char *(*url_construct_fn)(const char *query); char *(*url_construct_fn)(const char *query);
} InfoBoxHandler; } InfoBoxHandler;
enum {
RESULT_FIELD_COUNT = 6,
};
static InfoBox fetch_wiki_wrapper(char *query) { static InfoBox fetch_wiki_wrapper(char *query) {
char *url = construct_wiki_url(query); char *url = construct_wiki_url(query);
if (!url) if (!url)
@@ -53,7 +139,31 @@ static InfoBox fetch_unit_wrapper(char *query) {
static InfoBox fetch_currency_wrapper(char *query) { static InfoBox fetch_currency_wrapper(char *query) {
return fetch_currency_data(query); return fetch_currency_data(query);
} }
char *get_base_url(const char *input) {
if (!input) return NULL;
const char *start = input;
const char *protocol_pos = strstr(input, "://");
if (protocol_pos) {
start = protocol_pos + 3;
}
const char *end = start;
while (*end && *end != '/' && *end != '?' && *end != '#') {
end++;
}
size_t len = end - start;
char *domain = (char *)malloc(len + 1);
if (!domain) return NULL;
strncpy(domain, start, len);
domain[len] = '\0';
return domain;
}
static int is_calculator_query(const char *query) { static int is_calculator_query(const char *query) {
if (!query) if (!query)
return 0; return 0;
@@ -107,11 +217,16 @@ static int is_calculator_query(const char *query) {
return 0; return 0;
} }
static InfoBox fetch_colour_wrapper(char *query) {
return fetch_colour_data(query);
}
static InfoBoxHandler handlers[] = { static InfoBoxHandler handlers[] = {
{is_dictionary_query, fetch_dict_wrapper, NULL}, {is_dictionary_query, fetch_dict_wrapper, NULL},
{is_calculator_query, fetch_calc_wrapper, NULL}, {is_calculator_query, fetch_calc_wrapper, NULL},
{is_unit_conv_query, fetch_unit_wrapper, NULL}, {is_unit_conv_query, fetch_unit_wrapper, NULL},
{is_currency_query, fetch_currency_wrapper, NULL}, {is_currency_query, fetch_currency_wrapper, NULL},
{is_colour_code_query, fetch_colour_wrapper, NULL},
{always_true, fetch_wiki_wrapper, construct_wiki_url}, {always_true, fetch_wiki_wrapper, construct_wiki_url},
}; };
enum { HANDLER_COUNT = sizeof(handlers) / sizeof(handlers[0]) }; enum { HANDLER_COUNT = sizeof(handlers) / sizeof(handlers[0]) };
@@ -130,7 +245,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;
} }
@@ -150,6 +265,7 @@ static int add_infobox_to_collection(InfoBox *infobox, char ****collection,
(*collection)[current_count][2] = (*collection)[current_count][2] =
infobox->extract ? strdup(infobox->extract) : NULL; infobox->extract ? strdup(infobox->extract) : NULL;
(*collection)[current_count][3] = infobox->url ? strdup(infobox->url) : NULL; (*collection)[current_count][3] = infobox->url ? strdup(infobox->url) : NULL;
(*collection)[current_count][4] = infobox->url ? strdup(infobox->url) : NULL;
(*inner_counts)[current_count] = INFOBOX_FIELD_COUNT; (*inner_counts)[current_count] = INFOBOX_FIELD_COUNT;
return current_count + 1; return current_count + 1;
@@ -202,26 +318,133 @@ 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;
} }
} }
static int engine_id_matches(const char *left, const char *right) {
if (!left || !right)
return 0;
while (*left && *right) {
char l = *left;
char r = *right;
if (l >= 'A' && l <= 'Z')
l = l - 'A' + 'a';
if (r >= 'A' && r <= 'Z')
r = r - 'A' + 'a';
if (l != r)
return 0;
left++;
right++;
}
return *left == *right;
}
static const SearchEngine *find_enabled_engine(const char *engine_id) {
if (!engine_id || engine_id[0] == '\0' || engine_id_matches(engine_id, "all"))
return NULL;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled &&
engine_id_matches(ENGINE_REGISTRY[i].id, engine_id)) {
return &ENGINE_REGISTRY[i];
}
}
return NULL;
}
static int engine_allowed_for_user(const SearchEngine *eng, char **user_ids,
int user_count, int has_pref) {
if (!has_pref)
return 1;
return user_engines_contains(eng->id, user_ids, user_count);
}
static char *build_search_href(const char *query, const char *engine_id,
int page) {
const char *safe_query = query ? query : "";
int use_engine = engine_id && engine_id[0] != '\0' &&
!engine_id_matches(engine_id, "all");
size_t needed = strlen("/search?q=") + strlen(safe_query) + 1;
if (use_engine)
needed += strlen("&engine=") + strlen(engine_id);
if (page > 1)
needed += strlen("&p=") + 16;
char *href = (char *)malloc(needed);
if (!href)
return NULL;
snprintf(href, needed, "/search?q=%s", safe_query);
if (use_engine) {
strcat(href, "&engine=");
strcat(href, engine_id);
}
if (page > 1) {
char page_buf[16];
snprintf(page_buf, sizeof(page_buf), "%d", page);
strcat(href, "&p=");
strcat(href, page_buf);
}
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";
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) {
@@ -230,6 +453,8 @@ int results_handler(UrlParams *params) {
int parsed = atoi(params->params[i].value); int parsed = atoi(params->params[i].value);
if (parsed > 1) if (parsed > 1)
page = parsed; page = parsed;
} else if (strcmp(params->params[i].key, "engine") == 0) {
selected_engine_id = params->params[i].value;
} else if (strcmp(params->params[i].key, "btnI") == 0) { } else if (strcmp(params->params[i].key, "btnI") == 0) {
btnI = atoi(params->params[i].value); btnI = atoi(params->params[i].value);
} }
@@ -238,25 +463,62 @@ int results_handler(UrlParams *params) {
context_set(&ctx, "query", raw_query); context_set(&ctx, "query", raw_query);
char page_str[16], prev_str[16], next_str[16], two_prev_str[16], char *theme = get_theme("");
two_next_str[16]; context_set(&ctx, "theme", theme);
free(theme);
char *locale = get_locale(NULL);
beaker_set_locale(&ctx, locale);
const char *rate_limit_msg = beaker_get_locale_value(locale, "rate_limit");
if (!rate_limit_msg) rate_limit_msg = "Slow down! Too many searches from you!";
const char *no_results_msg = beaker_get_locale_value(locale, "no_results");
if (!no_results_msg) no_results_msg = "No results found";
char page_str[16];
snprintf(page_str, sizeof(page_str), "%d", page); snprintf(page_str, sizeof(page_str), "%d", page);
context_set(&ctx, "page", page_str);
char prev_str[16], next_str[16], two_prev_str[16], two_next_str[16];
snprintf(prev_str, sizeof(prev_str), "%d", page > 1 ? page - 1 : 0); snprintf(prev_str, sizeof(prev_str), "%d", page > 1 ? page - 1 : 0);
snprintf(next_str, sizeof(next_str), "%d", page + 1); 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_prev_str, sizeof(two_prev_str), "%d", page > 2 ? page - 2 : 0);
snprintf(two_next_str, sizeof(two_next_str), "%d", page + 2); snprintf(two_next_str, sizeof(two_next_str), "%d", page + 2);
context_set(&ctx, "page", page_str);
context_set(&ctx, "prev_page", prev_str); context_set(&ctx, "prev_page", prev_str);
context_set(&ctx, "next_page", next_str); context_set(&ctx, "next_page", next_str);
context_set(&ctx, "two_prev_page", two_prev_str); context_set(&ctx, "two_prev_page", two_prev_str);
context_set(&ctx, "two_next_page", two_next_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;
} }
const SearchEngine *selected_engine = find_enabled_engine(selected_engine_id);
if (!selected_engine)
selected_engine_id = "all";
context_set(&ctx, "selected_engine", selected_engine_id);
char *search_href = build_search_href(raw_query, selected_engine_id, 1);
context_set(&ctx, "search_href", search_href ? search_href : "/search");
free(search_href);
int enabled_engine_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled &&
(!selected_engine || &ENGINE_REGISTRY[i] == selected_engine) &&
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref)) {
enabled_engine_count++;
}
}
pthread_t infobox_threads[HANDLER_COUNT]; pthread_t infobox_threads[HANDLER_COUNT];
InfoBoxThreadData infobox_data[HANDLER_COUNT]; InfoBoxThreadData infobox_data[HANDLER_COUNT];
@@ -273,19 +535,15 @@ int results_handler(UrlParams *params) {
} }
} }
int enabled_engine_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled) {
enabled_engine_count++;
}
}
ScrapeJob jobs[ENGINE_COUNT]; ScrapeJob jobs[ENGINE_COUNT];
SearchResult *all_results[ENGINE_COUNT]; SearchResult *all_results[ENGINE_COUNT];
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) &&
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;
@@ -303,8 +561,105 @@ int results_handler(UrlParams *params) {
} }
} }
if (enabled_engine_count > 0) { char client_key[BUFFER_SIZE_SMALL];
scrape_engines_parallel(jobs, enabled_engine_count); rate_limit_get_client_key(client_key, sizeof(client_key));
char *request_cache_key = build_search_request_cache_key(
raw_query, selected_engine_id, page, client_key);
int request_is_cached = 0;
if (request_cache_key && get_cache_ttl_search() > 0) {
char *cached_marker = NULL;
size_t cached_marker_size = 0;
if (cache_get(request_cache_key, (time_t)get_cache_ttl_search(),
&cached_marker, &cached_marker_size) == 0) {
request_is_cached = 1;
}
free(cached_marker);
}
if (engine_idx > 0 && !request_is_cached) {
RateLimitConfig rate_limit_config = {
.max_requests = global_config.rate_limit_search_requests,
.interval_seconds = global_config.rate_limit_search_interval,
};
RateLimitResult rate_limit_result =
rate_limit_check("search", &rate_limit_config);
if (rate_limit_result.limited) {
char response[256];
snprintf(response, sizeof(response), "<h1>%s</h1>", rate_limit_msg);
send_response(response);
free(request_cache_key);
if (has_user_pref) {
for (int i = 0; i < user_engine_count; i++)
free(user_engines[i]);
free(user_engines);
}
free_context(&ctx);
return -1;
}
if (request_cache_key && get_cache_ttl_search() > 0) {
cache_set(request_cache_key, "1", 1);
}
}
int filter_engine_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled &&
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref))
filter_engine_count++;
}
if (filter_engine_count > 1) {
char ***filter_matrix = NULL;
int *filter_inner_counts = NULL;
int filter_count = 0;
char *all_href = build_search_href(raw_query, "all", 1);
filter_count = add_link_to_collection(
all_href, "All",
selected_engine ? "engine-filter" : "engine-filter active",
&filter_matrix, &filter_inner_counts, filter_count);
free(all_href);
for (int i = 0; i < ENGINE_COUNT; i++) {
if (!ENGINE_REGISTRY[i].enabled ||
!engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref))
continue;
char *filter_href =
build_search_href(raw_query, ENGINE_REGISTRY[i].id, 1);
const char *filter_class =
(selected_engine && &ENGINE_REGISTRY[i] == selected_engine)
? "engine-filter active"
: "engine-filter";
filter_count = add_link_to_collection(filter_href, ENGINE_REGISTRY[i].name,
filter_class, &filter_matrix,
&filter_inner_counts, filter_count);
free(filter_href);
}
if (filter_count > 0) {
context_set_array_of_arrays(&ctx, "engine_filters", filter_matrix,
filter_count, filter_inner_counts);
for (int i = 0; i < filter_count; i++) {
for (int j = 0; j < LINK_FIELD_COUNT; j++)
free(filter_matrix[i][j]);
free(filter_matrix[i]);
}
free(filter_matrix);
free(filter_inner_counts);
}
}
if (engine_idx > 0) {
scrape_engines_parallel(jobs, engine_idx);
} }
if (page == 1) { if (page == 1) {
@@ -314,7 +669,7 @@ int results_handler(UrlParams *params) {
} }
if (btnI) { if (btnI) {
for (int i = 0; i < enabled_engine_count; i++) { for (int i = 0; i < engine_idx; i++) {
if (jobs[i].results_count > 0 && all_results[i][0].url) { if (jobs[i].results_count > 0 && all_results[i][0].url) {
char *redirect_url = strdup(all_results[i][0].url); char *redirect_url = strdup(all_results[i][0].url);
for (int j = 0; j < enabled_engine_count; j++) { for (int j = 0; j < enabled_engine_count; j++) {
@@ -332,6 +687,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);
@@ -350,8 +711,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;
} }
@@ -383,7 +752,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++;
} }
@@ -393,7 +762,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;
@@ -427,14 +796,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);
@@ -449,47 +811,42 @@ 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 *) * INFOBOX_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);
continue; continue;
} }
char *pretty_url = pretty_display_url(display_url); char *pretty_url = pretty_display_url(display_url);
char *base_url = get_base_url(display_url);
results_matrix[unique_count][0] = strdup(display_url); results_matrix[unique_count][0] = strdup(display_url);
results_matrix[unique_count][1] = strdup(pretty_url); results_matrix[unique_count][1] = strdup(pretty_url);
@@ -499,10 +856,13 @@ int results_handler(UrlParams *params) {
results_matrix[unique_count][3] = results_matrix[unique_count][3] =
all_results[i][j].snippet ? strdup(all_results[i][j].snippet) all_results[i][j].snippet ? strdup(all_results[i][j].snippet)
: strdup(""); : strdup("");
results_matrix[unique_count][4] = strdup(base_url ? base_url : "");
results_matrix[unique_count][5] = strdup("");
results_inner_counts[unique_count] = INFOBOX_FIELD_COUNT; results_inner_counts[unique_count] = RESULT_FIELD_COUNT;
free(pretty_url); free(pretty_url);
free(base_url);
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);
@@ -515,6 +875,25 @@ int results_handler(UrlParams *params) {
context_set_array_of_arrays(&ctx, "results", results_matrix, unique_count, context_set_array_of_arrays(&ctx, "results", results_matrix, unique_count,
results_inner_counts); results_inner_counts);
char ***pager_matrix = NULL;
int *pager_inner_counts = NULL;
SearchHrefData href_data = { .query = raw_query, .engine_id = selected_engine_id };
int pager_count = build_pagination(page, search_href_builder,
&href_data, &pager_matrix,
&pager_inner_counts);
if (pager_count > 0) {
context_set_array_of_arrays(&ctx, "pagination_links", pager_matrix,
pager_count, pager_inner_counts);
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);
}
char *html = render_template("results.html", &ctx); char *html = render_template("results.html", &ctx);
if (html) { if (html) {
send_response(html); send_response(html);
@@ -522,14 +901,13 @@ int results_handler(UrlParams *params) {
} }
for (int i = 0; i < unique_count; i++) { for (int i = 0; i < unique_count; i++) {
for (int j = 0; j < INFOBOX_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) {
@@ -542,6 +920,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) {
@@ -549,6 +929,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;

140
src/Routes/Settings.c Normal file
View File

@@ -0,0 +1,140 @@
#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(NULL);
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);
char **themes = NULL;
int themes_count = 0;
get_available_themes(&themes, &themes_count);
if (themes_count > 0) {
char ***theme_ptrs = malloc(sizeof(char **) * themes_count);
int *theme_inner = malloc(sizeof(int) * themes_count);
for (int i = 0; i < themes_count; i++) {
theme_ptrs[i] = malloc(sizeof(char *) * 2);
theme_ptrs[i][0] = themes[i];
theme_ptrs[i][1] = strdup(themes[i]);
if (theme_ptrs[i][1][0] >= 'a' && theme_ptrs[i][1][0] <= 'z') {
theme_ptrs[i][1][0] = theme_ptrs[i][1][0] - 'a' + 'A';
}
theme_inner[i] = 2;
}
context_set_array_of_arrays(&ctx, "themes", theme_ptrs, themes_count,
theme_inner);
for (int i = 0; i < themes_count; i++) {
free(theme_ptrs[i][1]);
free(theme_ptrs[i]);
}
free(theme_ptrs);
free(theme_inner);
}
if (enabled_count > 0) {
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

@@ -28,113 +28,82 @@ static char *build_proxy_url(const char *image_url) {
return proxy_url; return proxy_url;
} }
static int parse_image_node(xmlNodePtr node, ImageResult *result) { static char *extract_json_string(const char *json, const char *key) {
xmlNodePtr img_node = NULL; if (!json || !key)
xmlNodePtr tit_node = NULL; return NULL;
xmlNodePtr des_node = NULL;
xmlNodePtr thumb_link = NULL;
for (xmlNodePtr child = node->children; child; child = child->next) { char search_key[64];
if (child->type != XML_ELEMENT_NODE) snprintf(search_key, sizeof(search_key), "\"%s\"", key);
continue;
if (xmlStrcmp(child->name, (const xmlChar *)"a") == 0) { const char *key_pos = strstr(json, search_key);
xmlChar *class = xmlGetProp(child, (const xmlChar *)"class"); if (!key_pos)
if (class) { return NULL;
if (xmlStrstr(class, (const xmlChar *)"thumb") != NULL) {
thumb_link = child; const char *colon = strchr(key_pos + strlen(search_key), ':');
for (xmlNodePtr thumb_child = child->children; thumb_child; if (!colon)
thumb_child = thumb_child->next) { return NULL;
if (xmlStrcmp(thumb_child->name, (const xmlChar *)"div") == 0) {
xmlChar *div_class = colon++;
xmlGetProp(thumb_child, (const xmlChar *)"class"); while (*colon == ' ' || *colon == '\t' || *colon == '\n' || *colon == '\r')
if (div_class && colon++;
xmlStrcmp(div_class, (const xmlChar *)"cico") == 0) {
for (xmlNodePtr cico_child = thumb_child->children; cico_child; if (*colon != '"')
cico_child = cico_child->next) { return NULL;
if (xmlStrcmp(cico_child->name, (const xmlChar *)"img") == colon++;
0) {
img_node = cico_child; size_t len = 0;
break; const char *start = colon;
} while (*colon && *colon != '"') {
} if (*colon == '\\' && *(colon + 1))
} colon++;
if (div_class) colon++;
xmlFree(div_class); len++;
}
}
} else if (xmlStrstr(class, (const xmlChar *)"tit") != NULL) {
tit_node = child;
}
xmlFree(class);
}
} else if (xmlStrcmp(child->name, (const xmlChar *)"div") == 0) {
xmlChar *class = xmlGetProp(child, (const xmlChar *)"class");
if (class && xmlStrcmp(class, (const xmlChar *)"meta") == 0) {
for (xmlNodePtr meta_child = child->children; meta_child;
meta_child = meta_child->next) {
if (xmlStrcmp(meta_child->name, (const xmlChar *)"div") == 0) {
xmlChar *div_class =
xmlGetProp(meta_child, (const xmlChar *)"class");
if (div_class) {
if (xmlStrcmp(div_class, (const xmlChar *)"des") == 0) {
des_node = meta_child;
}
xmlFree(div_class);
}
} else if (xmlStrcmp(meta_child->name, (const xmlChar *)"a") == 0) {
xmlChar *a_class = xmlGetProp(meta_child, (const xmlChar *)"class");
if (a_class && xmlStrstr(a_class, (const xmlChar *)"tit") != NULL) {
tit_node = meta_child;
}
if (a_class)
xmlFree(a_class);
}
}
}
if (class)
xmlFree(class);
}
} }
xmlChar *iurl = char *result = malloc(len + 1);
img_node ? xmlGetProp(img_node, (const xmlChar *)"src") : NULL; if (!result)
xmlChar *full_url = return NULL;
thumb_link ? xmlGetProp(thumb_link, (const xmlChar *)"href") : NULL;
xmlChar *title = des_node ? xmlNodeGetContent(des_node)
: (tit_node ? xmlNodeGetContent(tit_node) : NULL);
xmlChar *rurl =
tit_node ? xmlGetProp(tit_node, (const xmlChar *)"href") : NULL;
if (!iurl || strlen((char *)iurl) == 0) { colon = start;
if (iurl) size_t i = 0;
xmlFree(iurl); while (*colon && *colon != '"') {
if (title) if (*colon == '\\' && *(colon + 1))
xmlFree(title); colon++;
if (rurl) result[i++] = *colon++;
xmlFree(rurl); }
if (full_url) result[i] = '\0';
xmlFree(full_url);
return result;
}
static int parse_iusc_node(xmlNodePtr node, ImageResult *result) {
xmlChar *m_attr = xmlGetProp(node, (const xmlChar *)"m");
if (!m_attr)
return 0; return 0;
char *turl = extract_json_string((const char *)m_attr, "turl");
char *murl = extract_json_string((const char *)m_attr, "murl");
char *purl = extract_json_string((const char *)m_attr, "purl");
char *title = extract_json_string((const char *)m_attr, "t");
int ok = (turl != NULL && strlen(turl) > 0);
if (ok) {
char *proxy_url = build_proxy_url(turl);
result->thumbnail_url = proxy_url ? strdup(proxy_url) : strdup(turl);
free(proxy_url);
result->title =
title && strlen(title) > 0 ? strdup(title) : strdup("Image");
result->page_url = purl && strlen(purl) > 0 ? strdup(purl) : strdup("#");
result->full_url = murl && strlen(murl) > 0 ? strdup(murl) : strdup("#");
} }
char *proxy_url = build_proxy_url((char *)iurl); free(turl);
result->thumbnail_url = proxy_url ? strdup(proxy_url) : strdup((char *)iurl); free(murl);
free(proxy_url); free(purl);
result->title = strdup(title ? (char *)title : "Image"); free(title);
result->page_url = strdup(rurl ? (char *)rurl : "#");
result->full_url = strdup(full_url ? (char *)full_url : "#");
if (iurl) xmlFree(m_attr);
xmlFree(iurl); return ok;
if (title)
xmlFree(title);
if (rurl)
xmlFree(rurl);
if (full_url)
xmlFree(full_url);
return 1;
} }
int scrape_images(const char *query, int page, ImageResult **out_results, int scrape_images(const char *query, int page, ImageResult **out_results,
@@ -157,13 +126,16 @@ int scrape_images(const char *query, int page, ImageResult **out_results,
char url[BUFFER_SIZE_LARGE]; char url[BUFFER_SIZE_LARGE];
int first = (page - 1) * IMAGE_RESULTS_PER_PAGE + 1; int first = (page - 1) * IMAGE_RESULTS_PER_PAGE + 1;
snprintf(url, sizeof(url), "%s?q=%s&first=%d", BING_IMAGE_URL, encoded_query, snprintf(
first); url, sizeof(url),
"https://www.bing.com/images/async?q=%s&async=content&first=%d&count=%d",
encoded_query, first, 35);
free(encoded_query); free(encoded_query);
HttpResponse resp = http_get( HttpResponse resp = http_get(
url, url,
"Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"); "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, "
"like Gecko) Chrome/120.0.0.0 Safari/537.36");
if (!resp.memory) { if (!resp.memory) {
return -1; return -1;
} }
@@ -183,7 +155,7 @@ int scrape_images(const char *query, int page, ImageResult **out_results,
} }
xmlXPathObjectPtr xpathObj = xmlXPathObjectPtr xpathObj =
xmlXPathEvalExpression((const xmlChar *)"//div[@class='item']", xpathCtx); xmlXPathEvalExpression((const xmlChar *)"//a[@class='iusc']", xpathCtx);
if (!xpathObj || !xpathObj->nodesetval) { if (!xpathObj || !xpathObj->nodesetval) {
if (xpathObj) if (xpathObj)
@@ -210,7 +182,7 @@ int scrape_images(const char *query, int page, ImageResult **out_results,
int count = 0; int count = 0;
for (int i = 0; i < nodes && count < IMAGE_RESULTS_PER_PAGE; i++) { for (int i = 0; i < nodes && count < IMAGE_RESULTS_PER_PAGE; i++) {
xmlNodePtr node = xpathObj->nodesetval->nodeTab[i]; xmlNodePtr node = xpathObj->nodesetval->nodeTab[i];
if (parse_image_node(node, &results[count])) { if (parse_iusc_node(node, &results[count])) {
count++; count++;
} }
} }

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

@@ -25,6 +25,12 @@ char *pretty_display_url(const char *input) {
strncpy(temp, start, sizeof(temp) - 1); strncpy(temp, start, sizeof(temp) - 1);
temp[sizeof(temp) - 1] = '\0'; temp[sizeof(temp) - 1] = '\0';
char *query = strchr(temp, '?');
if (query) {
*query = '\0';
input_len = strlen(temp);
}
if (input_len > 0 && temp[input_len - 1] == '/') { if (input_len > 0 && temp[input_len - 1] == '/') {
temp[input_len - 1] = '\0'; temp[input_len - 1] = '\0';
} }

View File

@@ -31,6 +31,17 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb,
return realsize; return realsize;
} }
static struct curl_slist *build_http_headers(void) {
struct curl_slist *headers = NULL;
headers = curl_slist_append(
headers,
"Accept: "
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
headers = curl_slist_append(headers, "Accept-Language: en-US,en;q=0.5");
headers = curl_slist_append(headers, "DNT: 1");
return headers;
}
HttpResponse http_get(const char *url, const char *user_agent) { HttpResponse http_get(const char *url, const char *user_agent) {
HttpResponse resp = {.memory = NULL, .size = 0, .capacity = 0}; HttpResponse resp = {.memory = NULL, .size = 0, .capacity = 0};
@@ -51,16 +62,24 @@ HttpResponse http_get(const char *url, const char *user_agent) {
return resp; return resp;
} }
struct curl_slist *headers = build_http_headers();
curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
curl_easy_setopt(curl, CURLOPT_USERAGENT, curl_easy_setopt(curl, CURLOPT_USERAGENT,
user_agent ? user_agent : "libcurl-agent/1.0"); user_agent ? user_agent : "libcurl-agent/1.0");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, CURL_TIMEOUT_SECS);
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, CURL_DNS_TIMEOUT_SECS);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
apply_proxy_settings(curl); apply_proxy_settings(curl);
CURLcode res = curl_easy_perform(curl); CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl); curl_easy_cleanup(curl);
if (res != CURLE_OK) { if (res != CURLE_OK) {

View File

@@ -1,4 +1,87 @@
#include "Utility.h" #include "Utility.h"
#include "../Scraping/Scraping.h"
#include <beaker.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char global_default_locale[32] = "en_gb";
static char **themes_list = NULL;
static int themes_count = 0;
static int themes_initialized = 0;
void init_themes(const char *static_path) {
if (themes_initialized)
return;
themes_initialized = 1;
char themes_dir[512];
snprintf(themes_dir, sizeof(themes_dir), "%s/themes", static_path);
DIR *dir = opendir(themes_dir);
if (!dir)
return;
struct dirent *entry;
int capacity = 4;
themes_list = malloc(sizeof(char *) * capacity);
themes_count = 0;
while ((entry = readdir(dir)) != NULL) {
size_t len = strlen(entry->d_name);
if (len > 4 && strcmp(entry->d_name + len - 4, ".css") == 0) {
if (themes_count >= capacity) {
capacity *= 2;
themes_list = realloc(themes_list, sizeof(char *) * capacity);
}
themes_list[themes_count] = strndup(entry->d_name, len - 4);
themes_count++;
}
}
closedir(dir);
for (int i = 0; i < themes_count; i++) {
for (int j = i + 1; j < themes_count; j++) {
int priority_i = 0, priority_j = 0;
if (strcmp(themes_list[i], "system") == 0)
priority_i = 0;
else if (strcmp(themes_list[i], "light") == 0)
priority_i = 1;
else if (strcmp(themes_list[i], "dark") == 0)
priority_i = 2;
else
priority_i = 3;
if (strcmp(themes_list[j], "system") == 0)
priority_j = 0;
else if (strcmp(themes_list[j], "light") == 0)
priority_j = 1;
else if (strcmp(themes_list[j], "dark") == 0)
priority_j = 2;
else
priority_j = 3;
if (priority_i > priority_j ||
(priority_i == priority_j &&
strcmp(themes_list[i], themes_list[j]) > 0)) {
char *tmp = themes_list[i];
themes_list[i] = themes_list[j];
themes_list[j] = tmp;
}
}
}
}
void get_available_themes(char ***out_themes, int *out_count) {
*out_themes = themes_list;
*out_count = themes_count;
}
void set_default_locale(const char *locale) {
if (locale && strlen(locale) > 0) {
strncpy(global_default_locale, locale, sizeof(global_default_locale) - 1);
global_default_locale[sizeof(global_default_locale) - 1] = '\0';
}
}
int hex_to_int(char c) { int hex_to_int(char c) {
if (c >= '0' && c <= '9') if (c >= '0' && c <= '9')
@@ -9,3 +92,212 @@ 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 && strlen(cookie) > 0) {
for (int i = 0; i < themes_count; i++) {
if (strcmp(cookie, themes_list[i]) == 0) {
return cookie;
}
}
}
free(cookie);
return strdup(default_theme && strlen(default_theme) > 0 ? default_theme
: "system");
}
char *get_locale(const char *default_locale) {
char *cookie = get_cookie("locale");
if (cookie && beaker_get_locale_meta(cookie) != NULL) {
return cookie;
}
free(cookie);
const char *fallback =
default_locale ? default_locale : global_default_locale;
return strdup(fallback);
}
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,34 @@
#ifndef UTILITY_H #ifndef UTILITY_H
#define UTILITY_H #define UTILITY_H
#include <beaker.h>
#ifndef VERSION
#define VERSION "unknown"
#endif
#ifndef GIT_REMOTE
#define GIT_REMOTE "https://git.bwaaa.monster/omnisearch"
#endif
#define LINK_FIELD_COUNT 3
int hex_to_int(char c); int hex_to_int(char c);
char *get_theme(const char *default_theme);
void init_themes(const char *static_path);
void get_available_themes(char ***out_themes, int *out_count);
void set_default_locale(const char *locale);
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

@@ -1,44 +1,21 @@
@import url("https://cdn.jsdelivr.net/npm/@catppuccin/palette/css/catppuccin.css"); *, *::before, *::after {
box-sizing: border-box;
:root { font-family: sans-serif;
--bg-main: var(--ctp-latte-base);
--bg-card: var(--ctp-latte-overlay0);
--border: var(--ctp-latte-overlay2);
--text-primary: var(--ctp-latte-text);
--text-secondary: var(--ctp-latte-subtext0);
--text-muted: var(--ctp-latte-overlay1);
--accent: var(--ctp-latte-mauve);
--accent-glow: rgba(0, 0, 0, 0.05);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-main: var(--ctp-mocha-base);
--bg-card: var(--ctp-mocha-surface1);
--border: var(--ctp-mocha-overlay2);
--text-primary: var(--ctp-mocha-text);
--text-secondary: var(--ctp-mocha-subtext0);
--text-muted: var(--ctp-mocha-subtext1);
--accent: var(--ctp-mocha-mauve);
--accent-glow: rgba(0, 0, 0, 0.05);
}
} }
*, html {
*::before, height:100%;
*::after {
box-sizing: border-box;
} }
body { body {
background-color: var(--bg-main); background-color:var(--bg-main);
color: var(--text-primary); background-image:radial-gradient(circle at top end, var(--bg-card) 0%, var(--bg-main) 100%);
font-family: background-attachment:fixed;
system-ui, color:var(--text-primary);
-apple-system, margin:0;
sans-serif; padding:0;
margin: 0; min-height:100%;
padding: 0; -webkit-tap-highlight-color: transparent;
-webkit-tap-highlight-color: transparent;
} }
img[src=""] { img[src=""] {
@@ -51,25 +28,20 @@ img[src=""] {
} }
.view-home { .view-home {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 100vh; min-height: 100vh;
padding: 20px; padding: 20px;
background: radial-gradient(
circle at top right,
var(--accent) 0%,
var(--bg-main) 100%
);
} }
.view-home .container { .view-home .container {
width: 100%; width: 100%;
max-width: 580px; max-width: 580px;
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.view-home .hero-logo { .view-home .hero-logo {
@@ -115,21 +87,85 @@ img[src=""] {
transform: translateY(-1px); transform: translateY(-1px);
} }
.view-home .btn-secondary { .view-home .btn-secondary {
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;
}
.home-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 24px;
font-size: 12px;
color: var(--text-muted);
text-align: center;
}
.version-link {
color: var(--text-muted);
text-decoration: underline;
}
.version-link:hover {
color: var(--text-primary);
}
.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;
border-bottom: 1px solid var(--border); padding-inline:60px;
background: var(--bg-main); border-bottom:1px solid var(--border);
background:var(--bg-main);
width:100%;
} }
.search-form { .search-form {
flex-grow: 1; flex-grow: 1;
@@ -144,6 +180,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;
@@ -162,14 +208,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;
@@ -188,6 +234,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;
} }
@@ -289,14 +338,75 @@ h1 span {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.content-layout { .content-layout {
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 {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 2px;
position: relative;
}
.result-favicon {
width: 16px;
height: 16px;
flex-shrink: 0;
background-size: cover;
background-position: center;
position: absolute;
inset-inline-start: -24px;
}
.url {
color: var(--text-secondary);
font-size: 0.85rem;
display: block;
margin-bottom: 4px;
}
@media (max-width: 768px) {
.result-favicon {
width: 14px;
height: 14px;
inset-inline-start: -20px;
}
}
@media (max-width: 480px) {
.result-favicon {
width: 12px;
height: 12px;
inset-inline-start: -16px;
}
} }
.results-container { .results-container {
grid-column: 2; grid-column: 2;
} }
.engine-filter-list {
display:flex;
flex-wrap:wrap;
gap:10px;
margin-bottom:24px;
}
.engine-filter {
background:var(--bg-card);
color:var(--text-secondary);
border:1px solid var(--border);
border-radius:999px;
padding:6px 12px;
text-decoration:none;
font-size:0.85rem;
font-weight:600;
}
.engine-filter.active {
background:var(--accent);
border-color:var(--accent);
color:var(--bg-main);
}
.engine-warning-list { .engine-warning-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -335,12 +445,6 @@ h1 span {
.result > a:hover { .result > a:hover {
text-decoration-color: var(--accent); text-decoration-color: var(--accent);
} }
.url {
color: var(--text-secondary);
font-size: 0.85rem;
display: block;
margin-bottom: 4px;
}
.desc { .desc {
color: var(--text-muted); color: var(--text-muted);
line-height: 1.6; line-height: 1.6;
@@ -435,199 +539,419 @@ h1 span {
} }
.pagination-current { .pagination-current {
background: var(--bg-card); background: var(--accent);
color: var(--text-primary); border-color: var(--accent);
border: 1px solid var(--border); color: var(--bg-main);
padding: 4px 12px;
border-radius: 8px;
text-decoration: none;
font-size: 1.2rem;
font-weight: 600;
transition: all 0.2s;
touch-action: manipulation;
} }
.pagination-current:hover { .pagination-current:hover {
background: var(--border); background: var(--accent);
border-color: var(--text-secondary); border-color: var(--accent);
} }
@media (max-width: 1200px) { [dir="rtl"] .pagination-btn.prev {
.content-layout { transform: scaleX(-1);
grid-template-columns: 1fr; }
padding: 20px 30px;
gap: 20px; [dir="rtl"] .pagination-btn.next {
} transform: scaleX(-1);
.results-container, }
.infobox-sidebar {
grid-column: 1;
max-width: 100%; @media (max-width:1200px) {
} .content-layout {
.infobox-sidebar { grid-template-columns:1fr;
order: -1; padding-block:20px;
} padding-inline:30px;
.nav-tabs, gap:20px;
.image-results-container { }
padding: 0 30px; header {
} gap:20px;
header { }
padding: 15px 30px; .results-container,.infobox-sidebar {
} grid-column:1;
max-width:100%;
}
.settings-layout {
padding-block:20px;
padding-inline:30px;
display:flex;
justify-content:center;
}
.infobox-sidebar {
order:-1;
}
.nav-tabs,.image-results-container {
padding-inline:30px;
}
header {
padding-block:15px;
padding-inline:30px;
}
}
@media (max-width:768px) {
.nav-settings-icon {
display:none;
}
.nav-settings-link {
display:inline;
}
header {
flex-direction:column;
gap:12px;
padding-block:12px;
padding-inline:16px;
text-align:center;
}
h1 {
font-size:1.3rem;
}
.search-form {
width:100%;
max-width:100%;
}
.search-form .search-box {
width:100%;
}
.nav-tabs {
overflow-x:auto;
-webkit-overflow-scrolling:touch;
padding-inline:16px;
}
.nav-container {
gap:24px;
min-width:max-content;
}
.nav-tabs a {
padding:12px 0;
font-size:0.95rem;
}
.content-layout {
padding-inline-start:40px;
padding-inline-end:16px;
padding-block:16px;
gap:16px;
}
.result {
margin-bottom:24px;
}
.engine-warning {
padding:12px 14px;
}
.result > a {
font-size:1.1rem;
word-break:break-word;
}
.url {
font-size:0.8rem;
word-break:break-all;
}
.desc {
font-size:0.9rem;
}
.cached {
font-size:0.8rem;
}
.infobox {
margin-bottom:16px;
}
.infobox-header {
padding:16px;
}
.infobox-title {
font-size:1.2rem;
}
.infobox-main {
flex-direction:column;
padding:16px;
gap:12px;
}
.infobox-image {
width:100%;
height:auto;
min-width:unset;
max-width:200px;
}
.image-results-container {
padding-inline:16px;
}
.pagination {
flex-wrap:wrap;
gap:8px;
padding:0 8px;
}
.pagination-btn {
padding:10px 14px;
font-size:0.85rem;
}
.view-home {
display: flex;
justify-content: center;
align-items: center;
padding:20px 16px;
min-height: 100vh;
}
.view-home .container {
padding:0;
width:100%;
max-width:580px;
}
.view-home .hero-logo {
font-size:3rem;
margin-bottom:24px;
}
.view-home .search-input-wrapper {
margin-bottom:16px;
}
.view-home .search-box {
width:100%;
font-size:1rem;
padding:14px 20px;
}
.view-home .buttons {
gap:10px;
}
.view-home button {
padding:12px 20px;
}
}
@media (max-width:600px) {
.content-layout {
padding-inline-start:28px;
padding-inline-end:16px;
padding-block:16px;
}
.settings-layout {
padding:0;
}
header {
padding-inline:12px;
padding-block:12px;
}
.search-box {
font-size:0.95rem;
}
.view-home .search-box {
width:100%;
}
.view-home {
padding:20px 16px;
}
.image-grid {
grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
gap:10px;
}
.image-card {
border-radius:8px;
}
.image-info {
padding:8px 10px;
}
.image-caption {
font-size:0.8rem;
}
.image-source {
font-size:0.7rem;
}
.overlay-buttons {
width:80%;
}
.overlay-btn {
padding:6px 12px;
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) { @media (max-width: 768px) {
header { [dir="rtl"] header {
flex-direction: column; flex-direction: column;
gap: 12px; }
padding: 12px 16px;
text-align: center;
}
h1 {
font-size: 1.3rem;
}
.search-form {
width: 100%;
max-width: 100%;
}
.search-form .search-box {
width: 100%;
}
.nav-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding: 0 16px;
}
.nav-container {
gap: 24px;
min-width: max-content;
}
.nav-tabs a {
padding: 12px 0;
font-size: 0.95rem;
}
.content-layout {
padding: 16px;
gap: 16px;
}
.result {
margin-bottom: 24px;
}
.engine-warning {
padding: 12px 14px;
}
.result > a {
font-size: 1.1rem;
word-break: break-word;
}
.url {
font-size: 0.8rem;
word-break: break-all;
}
.desc {
font-size: 0.9rem;
}
.cached {
font-size: 0.8rem;
}
.infobox {
margin-bottom: 16px;
}
.infobox-header {
padding: 16px;
}
.infobox-title {
font-size: 1.2rem;
}
.infobox-main {
flex-direction: column;
padding: 16px;
gap: 12px;
}
.infobox-image {
width: 100%;
height: auto;
min-width: unset;
max-width: 200px;
}
.image-results-container {
padding: 16px;
}
.pagination {
flex-wrap: wrap;
gap: 8px;
padding: 0 8px;
}
.pagination-btn {
padding: 10px 14px;
font-size: 0.85rem;
}
.view-home {
display: flex;
justify-content: center;
align-items: center;
transform: translateY(-5vh);
padding: 20px 16px;
min-height: 100vh;
}
.view-home .container {
padding: 0;
width: 100%;
max-width: 580px;
}
.view-home .hero-logo {
font-size: 3rem;
margin-bottom: 24px;
}
.view-home .search-input-wrapper {
margin-bottom: 16px;
}
.view-home .search-box {
width: 100%;
font-size: 1rem;
padding: 14px 20px;
}
.view-home .buttons {
gap: 10px;
}
.view-home button {
padding: 12px 20px;
}
} }
@media (max-width: 600px) {
header {
padding: 12px 12px;
}
.search-box {
font-size: 0.95rem;
}
.view-home .search-box {
width: 100%;
}
.view-home {
padding: 20px 16px;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.image-card {
border-radius: 8px;
}
.image-info {
padding: 8px 10px;
}
.image-caption {
font-size: 0.8rem;
}
.image-source {
font-size: 0.7rem;
}
.overlay-buttons {
width: 80%;
}
.overlay-btn {
padding: 6px 12px;
font-size: 0.75rem;
}
}

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

View File

@@ -0,0 +1,10 @@
:root {
--bg-main: #181825;
--bg-card: #1e1e2e;
--border: #313244;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--text-muted: #6c7086;
--accent: #cba6f7;
--accent-glow: rgba(255,255,255,0.1);
}

10
static/themes/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/themes/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);
}

22
static/themes/system.css Normal file
View File

@@ -0,0 +1,22 @@
:root {
--bg-main: #ffffff;
--bg-card: #f8f9fa;
--border: #e0e0e0;
--text-primary: #1a1a1a;
--text-secondary: #5f6368;
--text-muted: #757575;
--accent: #202124;
--accent-glow: rgba(0,0,0,0.05);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-main: #121212;
--bg-card: #1e1e1e;
--border: #333333;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--text-muted: #d1d1d1;
--accent: #e2e2e2;
--accent-glow: rgba(255,255,255,0.1);
}
}

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,7 +8,8 @@
OmniSearch OmniSearch
</title> </title>
<link rel="stylesheet" href="static/main.css"> <link rel="stylesheet" href="static/main.css">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> <link rel="stylesheet" href="static/themes/{{theme}}.css">
<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"
title="OmniSearch" href="/opensearch.xml"> title="OmniSearch" href="/opensearch.xml">
@@ -22,19 +23,24 @@
</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")}}
</div> </button>
</div>
</form> </form>
</div> </div>
</div> </div>
<a href="/settings" class="home-settings-btn" title="{{l("settings_tab")}}"></a>
<footer class="home-footer">
<a href="{{git_remote}}" class="version-link" target="_blank" rel="noopener noreferrer">{{version}}</a>
</footer>
</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">
@@ -7,27 +7,32 @@
<title> <title>
OmniSearch Images - {{query}} OmniSearch Images - {{query}}
</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">
<link rel="stylesheet" href="static/themes/{{theme}}.css">
</head> </head>
<body class="images-view"> <body class="images-view">
<header> <header>
<h1> <a href="/" class="logo-link"><h1>
<a id="header-icon" href="/"><span>Void</span>arc</a> 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 +45,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,49 +64,16 @@
</div> </div>
{{endfor}} {{endfor}}
</div> </div>
<nav class="pagination"> {{if exists pagination_links}}
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}"> <nav class="pagination">
&larr; {{for link in pagination_links}}
</a> <a class="{{link[2]}}" href="{{link[0]}}">
{{link[1]}}
{{if two_prev_page != 0}} </a>
<a class="pagination-btn prev" href="/images?q={{query}}&p={{two_prev_page}}"> {{endfor}}
{{two_prev_page}} </nav>
</a> {{endif}}
{{endif}}
{{if prev_page != 0}}
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}">
{{prev_page}}
</a>
{{endif}}
<a class="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>
</nav>
</main> </main>
</body> </body>
</html> </html>

View File

@@ -4,7 +4,7 @@
xmlns:moz="http://www.mozilla.org/2006/browser/search/"> xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>OmniSearch</ShortName> <ShortName>OmniSearch</ShortName>
<Description>Lightweight metasearch engine</Description> <Description>Lightweight metasearch engine</Description>
<Url type="text/html" method="get" template="{{domain}}/search?q={searchTerms}"/> <Url type="text/html" method="get" template="{{scheme}}://{{domain}}/search?q={searchTerms}"/>
<InputEncoding>UTF-8</InputEncoding> <InputEncoding>UTF-8</InputEncoding>
<OutputEncoding>UTF-8</OutputEncoding> <OutputEncoding>UTF-8</OutputEncoding>
<moz:SearchForm>{{domain}}/</moz:SearchForm> <moz:SearchForm>{{domain}}/</moz:SearchForm>

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,7 +8,8 @@
OmniSearch - {{query}} OmniSearch - {{query}}
</title> </title>
<link rel="stylesheet" href="static/main.css"> <link rel="stylesheet" href="static/main.css">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> <link rel="stylesheet" href="static/themes/{{theme}}.css">
<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"
title="OmniSearch" href="/opensearch.xml"> title="OmniSearch" href="/opensearch.xml">
@@ -16,21 +17,26 @@
<body class="results-view"> <body class="results-view">
<header> <header>
<h1> <a href="/" class="logo-link"><h1>
<a id="header-icon" href="/"><span>Void</span>arc</a> 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="q" type="text" class="search-box" autocomplete="off" placeholder="Search the web..." <input name="engine" type="hidden" value="{{selected_engine}}">
<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?q={{query}}" 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>
@@ -38,6 +44,16 @@
<aside class="sidebar-spacer"> <aside class="sidebar-spacer">
</aside> </aside>
<main class="results-container"> <main class="results-container">
{{if exists engine_filters}}
<nav class="engine-filter-list">
{{for filter in engine_filters}}
<a href="{{filter[0]}}" class="{{filter[2]}}">
{{filter[1]}}
</a>
{{endfor}}
</nav>
{{endif}}
{{if exists engine_warnings}} {{if exists engine_warnings}}
<section class="engine-warning-list"> <section class="engine-warning-list">
{{for warning in engine_warnings}} {{for warning in engine_warnings}}
@@ -55,9 +71,15 @@
{{for result in results}} {{for result in results}}
<div class="result"> <div class="result">
<span class="url"> <div class="result-header">
{{result[1]}} <div class="result-favicon"
style="background-image: url('/proxy?url=https://{{result[4]}}/favicon.ico'), url('/proxy?url=https://{{result[4]}}/favicon.png');">
</div>
<span class="url">
{{result[1]}}
</span>
</span> </span>
</div>
<a href="{{result[0]}}"> <a href="{{result[0]}}">
{{result[2]}} {{result[2]}}
</a> </a>
@@ -65,53 +87,20 @@
{{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}}
{{if exists pagination_links}}
<nav class="pagination"> <nav class="pagination">
<a class="pagination-btn prev" href="/search?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="/search?q={{query}}&p={{two_prev_page}}">
{{two_prev_page}}
</a>
{{endif}}
{{if prev_page != 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p={{prev_page}}">
{{prev_page}}
</a>
{{endif}}
<a class="pagination-current" href="/search?q={{query}}&p={{page}}">
{{page}}
</a>
<a class="pagination-btn next" href="/search?q={{query}}&p={{next_page}}">
{{next_page}}
</a>
<a class="pagination-btn next" href="/search?q={{query}}&p={{two_next_page}}">
{{two_next_page}}
</a>
{{if prev_page == 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p=4">
4
</a>
{{endif}}
{{if two_prev_page == 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p=5">
5
</a>
{{endif}}
<a class="pagination-btn next" href="/search?q={{query}}&p={{next_page}}">
&rarr;
</a> </a>
{{endfor}}
</nav> </nav>
{{endif}}
</main> </main>
<aside class="infobox-sidebar"> <aside class="infobox-sidebar">
{{if exists infoboxes}} {{if exists infoboxes}}
@@ -126,10 +115,12 @@
<img src="{{info[1]}}" alt="{{info[0]}}" class="infobox-image"> <img src="{{info[1]}}" alt="{{info[0]}}" class="infobox-image">
<div class="infobox-content"> <p class="infobox-summary"> <div class="infobox-content"> <p class="infobox-summary">
{{info[2]|safe}} {{info[2]|safe}}
</p> </p>
{{if info[3] != "#"}}
<a class="read-more" href="{{info[3]}}"> <a class="read-more" href="{{info[3]}}">
Read More {{l("read_more")}}
</a> </a>
{{endif}}
</div> </div>
</div> </div>
</section> </section>

99
templates/settings.html Normal file
View File

@@ -0,0 +1,99 @@
<!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">
<link rel="stylesheet" href="static/themes/{{theme}}.css">
<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">
{{for t in themes}}
<option value="{{t[0]}}" {{if theme == t[0]}}selected{{endif}}>{{t[1]}}</option>
{{endfor}}
</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>