diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11aac36 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile index 0353e95..e57c991 100644 --- a/Makefile +++ b/Makefile @@ -3,13 +3,22 @@ UNAME_S := $(shell uname -s) PKG_CONFIG ?= pkg-config 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) DEP_CFLAGS := $(shell $(PKG_CONFIG) --cflags $(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 else -CFLAGS := -Wall -Wextra -O2 -Isrc -I/usr/include/libxml2 +CFLAGS += -I/usr/include/libxml2 LIBS := -lbeaker -lcurl -lxml2 -lpthread -lm -lssl -lcrypto endif @@ -76,6 +85,7 @@ USER := omnisearch GROUP := omnisearch SYSTEMD_DIR := /etc/systemd/system +RUNIT_DIR ?= $(error Please set RUNIT_DIR to your services directory) OPENRC_DIR := /etc/init.d DINIT_DIR := /etc/dinit.d LAUNCHD_DIR ?= /Library/LaunchDaemons @@ -94,9 +104,10 @@ install: @echo "Example: doas/sudo make install-openrc" 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 static/* $(DATA_DIR)/static/ + @cp -rf locales/* $(DATA_DIR)/locales/ @cp -n example-config.ini $(DATA_DIR)/config.ini || true install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch @mkdir -p $(LAUNCHD_DIR) @@ -115,9 +126,10 @@ install-launchd: $(TARGET) @echo "Start with: sudo launchctl kickstart -k system/$(LAUNCHD_LABEL)" 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 static/* $(DATA_DIR)/static/ + @cp -rf locales/* $(DATA_DIR)/locales/ @cp -n example-config.ini $(DATA_DIR)/config.ini || true install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch @echo "Setting up user '$(USER)'..." @@ -133,9 +145,10 @@ install-systemd: $(TARGET) @echo "Run 'systemctl enable --now omnisearch' to start" 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 static/* $(DATA_DIR)/static/ + @cp -rf locales/* $(DATA_DIR)/locales/ @cp -n example-config.ini $(DATA_DIR)/config.ini || true install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch @echo "Setting up user '$(USER)'..." @@ -151,9 +164,10 @@ install-openrc: $(TARGET) @echo "Run 'rc-update add omnisearch default' to enable" 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 static/* $(DATA_DIR)/static/ + @cp -rf locales/* $(DATA_DIR)/locales/ @cp -n example-config.ini $(DATA_DIR)/config.ini || true install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch @echo "Setting up user '$(USER)'..." @@ -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) @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 - install -m 755 init/runit/run /etc/service/omnisearch/run - install -m 755 init/runit/log/run /etc/service/omnisearch/log/run + install -m 755 init/runit/run $(RUNIT_DIR)/omnisearch/run + install -m 755 init/runit/log/run $(RUNIT_DIR)/omnisearch/log/run @echo "" @echo "Config: $(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 "Void: ln -s /etc/service/omnisearch/ /var/service" - @echo "Artix: ln -s /etc/service/omnisearch/ /run/runit/" + @echo "Void: ln -s $(RUNIT_DIR)/omnisearch/ /var/service/" + @echo "Artix: ln -s $(RUNIT_DIR)/omnisearch/ /run/runit/" 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 static/* $(DATA_DIR)/static/ + @cp -rf locales/* $(DATA_DIR)/locales/ @cp -n example-config.ini $(DATA_DIR)/config.ini || true install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch @echo "Setting up user '$(USER)'..." @@ -193,9 +208,10 @@ install-s6: $(TARGET) @echo "Service will start automatically" 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 static/* $(DATA_DIR)/static/ + @cp -rf locales/* $(DATA_DIR)/locales/ @cp -n example-config.ini $(DATA_DIR)/config.ini || true install -m 755 $(TARGET) $(INSTALL_BIN_DIR)/omnisearch @echo "Setting up user '$(USER)'..." @@ -218,6 +234,7 @@ uninstall: rm -f $(DINIT_DIR)/omnisearch rm -rf /etc/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 @grep -q '^$(GROUP):' /etc/group 2>/dev/null && groupdel $(GROUP) 2>/dev/null || true @echo "Uninstalled omnisearch" diff --git a/README.md b/README.md index 6e7a176..00f2173 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Depending on your system, you may first need to install libcurl and libxml2. ### Debian/Ubuntu ``` -# apt install libxml2-dev libcurl4-openssl-dev +# apt install build-essential libssl-dev libxml2-dev libcurl4-openssl-dev ``` ### Fedora @@ -50,7 +50,7 @@ On Alpine, `shadow` is needed for the user creation process during the install. ### NixOS 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: -``` +```nix # flake.nix { inputs = { @@ -108,6 +108,20 @@ On macOS, use `install-launchd`. ## Hosting 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 To make your own changes while still being able to receive upstream updates: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..249ae6a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + omnisearch: + build: + context: . + dockerfile: Dockerfile + ports: + - "8087:8087" + volumes: + - ./locales:/app/locales + - ./example-config.ini:/etc/omnisearch/config.ini diff --git a/example-config.ini b/example-config.ini index fc6ea8d..2760c53 100644 --- a/example-config.ini +++ b/example-config.ini @@ -1,7 +1,9 @@ [server] host = 0.0.0.0 port = 8087 -domain = https://search.example.com + +# Default locale (default: en_gb) +#locale = en_gb [proxy] # 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) # Available engines: ddg, startpage, yahoo, mojeek engines="*" + +[rate_limit] +# Rate limit searches per interval + +# /search +#search_requests = 10 +#search_interval = 60 + +# /images +#images_requests = 20 +#images_interval = 60 diff --git a/flake.lock b/flake.lock index 468ecff..330fc96 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "beaker-src": { "flake": false, "locked": { - "lastModified": 1773884524, - "narHash": "sha256-1dnlofWaxI/YRID+WPz2jHZNDyloBubDt/bAQk9ePLU=", + "lastModified": 1775244490, + "narHash": "sha256-4TJv7X6D0l4rEbTRKf47gU43L8G5uJgxxtsqMkVixQY=", "ref": "refs/heads/master", - "rev": "abc598baf15d6f8a4de395a27ba34b1e769558e1", - "revCount": 21, + "rev": "3fab89ecf8f4c664477a82add660d28db87357b4", + "revCount": 27, "shallow": false, "type": "git", "url": "https://git.bwaaa.monster/beaker" diff --git a/flake.nix b/flake.nix index eaf5253..1a7140e 100644 --- a/flake.nix +++ b/flake.nix @@ -61,7 +61,7 @@ installPhase = '' mkdir -p $out/bin $out/share/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 = { diff --git a/locales/af_za.ini b/locales/af_za.ini new file mode 100644 index 0000000..a21ad65 --- /dev/null +++ b/locales/af_za.ini @@ -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" diff --git a/locales/bn_bd.ini b/locales/bn_bd.ini new file mode 100644 index 0000000..9d3bcf8 --- /dev/null +++ b/locales/bn_bd.ini @@ -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 = "সাইট" diff --git a/locales/en_gb.ini b/locales/en_gb.ini new file mode 100644 index 0000000..db919a3 --- /dev/null +++ b/locales/en_gb.ini @@ -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" diff --git a/locales/en_us.ini b/locales/en_us.ini new file mode 100644 index 0000000..422f220 --- /dev/null +++ b/locales/en_us.ini @@ -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" diff --git a/locales/fr_fr.ini b/locales/fr_fr.ini new file mode 100644 index 0000000..5864778 --- /dev/null +++ b/locales/fr_fr.ini @@ -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" diff --git a/locales/lv_lv.ini b/locales/lv_lv.ini new file mode 100644 index 0000000..9130e3f --- /dev/null +++ b/locales/lv_lv.ini @@ -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." \ No newline at end of file diff --git a/locales/nl_nl.ini b/locales/nl_nl.ini new file mode 100644 index 0000000..4ff712f --- /dev/null +++ b/locales/nl_nl.ini @@ -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" diff --git a/locales/pt_br.ini b/locales/pt_br.ini new file mode 100644 index 0000000..355dd1b --- /dev/null +++ b/locales/pt_br.ini @@ -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" diff --git a/locales/ru_ru.ini b/locales/ru_ru.ini new file mode 100644 index 0000000..448f70d --- /dev/null +++ b/locales/ru_ru.ini @@ -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 = "Выберите, какие поисковые системы использовать. Отображаются только включённые на сервере." \ No newline at end of file diff --git a/locales/vi_vn.ini b/locales/vi_vn.ini new file mode 100644 index 0000000..8a7a1c7 --- /dev/null +++ b/locales/vi_vn.ini @@ -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" diff --git a/module.nix b/module.nix index 40415b3..c0c7825 100644 --- a/module.nix +++ b/module.nix @@ -20,6 +20,7 @@ let host = ${cfg.settings.server.host} port = ${toString cfg.settings.server.port} domain = ${cfg.settings.server.domain} + ${lib.optionalString (cfg.settings.server.locale != null) "locale = ${cfg.settings.server.locale}"} [proxy] ${lib.optionalString (cfg.settings.proxy.proxy != null) "proxy = \"${cfg.settings.proxy.proxy}\""} @@ -64,7 +65,11 @@ in }; domain = lib.mkOption { 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 = { @@ -122,6 +127,7 @@ in BindReadOnlyPaths = [ "${pkg}/share/omnisearch/templates:/var/lib/omnisearch/templates" "${pkg}/share/omnisearch/static:/var/lib/omnisearch/static" + "${pkg}/share/omnisearch/locales:/var/lib/omnisearch/locales" "${finalConfigFile}:/var/lib/omnisearch/config.ini" ]; diff --git a/src/Cache/Cache.c b/src/Cache/Cache.c index d739277..22f2553 100644 --- a/src/Cache/Cache.c +++ b/src/Cache/Cache.c @@ -11,15 +11,20 @@ static char cache_dir[BUFFER_SIZE_MEDIUM] = {0}; static int cache_ttl_search_val = DEFAULT_CACHE_TTL_SEARCH; 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_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_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) { unsigned char hash[EVP_MAX_MD_SIZE]; unsigned int hash_len; diff --git a/src/Cache/Cache.h b/src/Cache/Cache.h index 0a84406..9989b43 100644 --- a/src/Cache/Cache.h +++ b/src/Cache/Cache.h @@ -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_infobox(int ttl); +void set_cache_ttl_image(int ttl); int get_cache_ttl_search(void); int get_cache_ttl_infobox(void); +int get_cache_ttl_image(void); #endif diff --git a/src/Config.c b/src/Config.c index 0c243bd..9883d45 100644 --- a/src/Config.c +++ b/src/Config.c @@ -65,9 +65,10 @@ int load_config(const char *filename, Config *config) { config->host[sizeof(config->host) - 1] = '\0'; } else if (strcmp(key, "port") == 0) { config->port = atoi(value); - } else if (strcmp(key, "domain") == 0) { - strncpy(config->domain, value, sizeof(config->domain) - 1); - config->domain[sizeof(config->domain) - 1] = '\0'; + } else if (strcmp(key, "locale") == 0) { + strncpy(config->default_locale, value, + sizeof(config->default_locale) - 1); + config->default_locale[sizeof(config->default_locale) - 1] = '\0'; } } else if (strcmp(section, "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); } else if (strcmp(key, "ttl_infobox") == 0) { 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) { if (strcmp(key, "engines") == 0) { strncpy(config->engines, value, sizeof(config->engines) - 1); config->engines[sizeof(config->engines) - 1] = '\0'; } + } else if (strcmp(section, "rate_limit") == 0) { + if (strcmp(key, "search_requests") == 0) { + config->rate_limit_search_requests = atoi(value); + } else if (strcmp(key, "search_interval") == 0) { + config->rate_limit_search_interval = atoi(value); + } else if (strcmp(key, "images_requests") == 0) { + config->rate_limit_images_requests = atoi(value); + } else if (strcmp(key, "images_interval") == 0) { + config->rate_limit_images_interval = atoi(value); + } } } } diff --git a/src/Config.h b/src/Config.h index 4143bbd..25bd978 100644 --- a/src/Config.h +++ b/src/Config.h @@ -6,6 +6,7 @@ #define DEFAULT_CACHE_DIR "/tmp/omnisearch_cache" #define DEFAULT_CACHE_TTL_SEARCH 3600 #define DEFAULT_CACHE_TTL_INFOBOX 86400 +#define DEFAULT_CACHE_TTL_IMAGE 604800 #define DEFAULT_MAX_PROXY_RETRIES 3 #define BUFFER_SIZE_SMALL 256 @@ -20,7 +21,7 @@ #define MD5_HASH_LEN 32 #define HEX_CHARS "0123456789abcdef" -#define INFOBOX_FIELD_COUNT 4 +#define INFOBOX_FIELD_COUNT 5 #define MAX_RESULTS_PER_ENGINE 10 #define CURL_TIMEOUT_SECS 15L @@ -34,6 +35,7 @@ typedef struct { char host[256]; int port; char domain[256]; + char default_locale[32]; char proxy[256]; char proxy_list_file[256]; int max_proxy_retries; @@ -42,7 +44,12 @@ typedef struct { char cache_dir[512]; int cache_ttl_search; int cache_ttl_infobox; + int cache_ttl_image; char engines[512]; + int rate_limit_search_requests; + int rate_limit_search_interval; + int rate_limit_images_requests; + int rate_limit_images_interval; } Config; int load_config(const char *filename, Config *config); diff --git a/src/Infobox/ColourCode.c b/src/Infobox/ColourCode.c new file mode 100644 index 0000000..7b4d807 --- /dev/null +++ b/src/Infobox/ColourCode.c @@ -0,0 +1,166 @@ +#include "ColourCode.h" +#include +#include +#include +#include + +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), + "
" + "
" + "
" + "
" + "
" + "#%.*s
" + "
RGB(%d, %d, %d)
" + "
HSL(%d, %d%%, %d%%)
" + "
HSV(%d, %d%%, %d%%)
" + "
CMYK(%d%%, %d%%, %d%%, %d%%)
" + "
" + "
" + "
", + 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; +} diff --git a/src/Infobox/ColourCode.h b/src/Infobox/ColourCode.h new file mode 100644 index 0000000..4fcfabf --- /dev/null +++ b/src/Infobox/ColourCode.h @@ -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 diff --git a/src/Limiter/RateLimit.c b/src/Limiter/RateLimit.c new file mode 100644 index 0000000..d6a59c9 --- /dev/null +++ b/src/Limiter/RateLimit.c @@ -0,0 +1,210 @@ +#include "RateLimit.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/src/Limiter/RateLimit.h b/src/Limiter/RateLimit.h new file mode 100644 index 0000000..fabd05d --- /dev/null +++ b/src/Limiter/RateLimit.h @@ -0,0 +1,20 @@ +#ifndef RATE_LIMIT_H +#define RATE_LIMIT_H + +#include + +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 diff --git a/src/Main.c b/src/Main.c index 8aa161d..d7ff185 100644 --- a/src/Main.c +++ b/src/Main.c @@ -13,20 +13,44 @@ #include "Routes/ImageProxy.h" #include "Routes/Images.h" #include "Routes/Search.h" +#include "Routes/Settings.h" +#include "Routes/SettingsSave.h" #include "Scraping/Scraping.h" +#include "Utility/Utility.h" Config global_config; - + int handle_opensearch(UrlParams *params) { (void)params; - extern Config global_config; 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); - serve_data(rendered, strlen(rendered), "application/opensearchdescription+xml"); + serve_data(rendered, strlen(rendered), + "application/opensearchdescription+xml"); free(rendered); free_context(&ctx); + return 0; } @@ -43,7 +67,7 @@ int main() { Config cfg = {.host = DEFAULT_HOST, .port = DEFAULT_PORT, - .domain = "", + .default_locale = "en_gb", .proxy = "", .proxy_list_file = "", .max_proxy_retries = DEFAULT_MAX_PROXY_RETRIES, @@ -52,14 +76,30 @@ int main() { .cache_dir = DEFAULT_CACHE_DIR, .cache_ttl_search = DEFAULT_CACHE_TTL_SEARCH, .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) { fprintf(stderr, "[WARN] Could not load config file, using defaults\n"); } + set_default_locale(cfg.default_locale); + init_themes("static"); + global_config = cfg; + int loaded = beaker_load_locales(); + if (loaded > 0) { + fprintf(stderr, "[INFO] Loaded %d locales\n", loaded); + } else { + fprintf(stderr, "[WARN] No locales loaded (make sure to run from " + "omnisearch directory)\n"); + } + apply_engines_config(cfg.engines); if (cache_init(cfg.cache_dir) != 0) { @@ -72,6 +112,7 @@ int main() { set_cache_ttl_search(cfg.cache_ttl_search); set_cache_ttl_infobox(cfg.cache_ttl_infobox); + set_cache_ttl_image(cfg.cache_ttl_image); if (cfg.proxy_list_file[0] != '\0') { if (load_proxy_list(cfg.proxy_list_file) < 0) { @@ -95,6 +136,8 @@ int main() { set_handler("/search", results_handler); set_handler("/images", images_handler); set_handler("/proxy", image_proxy_handler); + set_handler("/settings", settings_handler); + set_handler("/save_settings", settings_save_handler); fprintf(stderr, "[INFO] Starting Omnisearch on %s:%d\n", cfg.host, cfg.port); @@ -109,6 +152,7 @@ int main() { curl_global_cleanup(); xmlCleanupParser(); + beaker_free_locales(); free_proxy_list(); cache_shutdown(); return EXIT_SUCCESS; diff --git a/src/Routes/Home.c b/src/Routes/Home.c index 4526a9d..bf85fe1 100644 --- a/src/Routes/Home.c +++ b/src/Routes/Home.c @@ -1,14 +1,30 @@ #include "Home.h" +#include "../Utility/Utility.h" +#include #include +#include int home_handler(UrlParams *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(); + 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); send_response(rendered_html); free(rendered_html); free_context(&ctx); + free(theme); + free(locale); return 0; } diff --git a/src/Routes/ImageProxy.c b/src/Routes/ImageProxy.c index c2d1a9a..2d6d3a9 100644 --- a/src/Routes/ImageProxy.c +++ b/src/Routes/ImageProxy.c @@ -1,11 +1,31 @@ #include "ImageProxy.h" +#include "../Cache/Cache.h" #include "../Proxy/Proxy.h" +#include #include +#include +#include +#include +#include +#include #include #include #include +#include +#include #define MAX_IMAGE_SIZE (10 * 1024 * 1024) +#define DNS_CACHE_TTL 300 + +typedef struct DnsCacheEntry { + char hostname[256]; + char ip_str[INET_ADDRSTRLEN]; + time_t resolved_at; + struct DnsCacheEntry *next; +} DnsCacheEntry; + +static DnsCacheEntry *dns_cache = NULL; +static pthread_mutex_t dns_cache_mutex = PTHREAD_MUTEX_INITIALIZER; typedef struct { char *data; @@ -13,7 +33,157 @@ typedef struct { size_t capacity; } 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, "://"); if (!protocol) { protocol = url; @@ -30,21 +200,18 @@ static int is_allowed_domain(const char *url) { } strncpy(host, protocol, host_len); - const char *allowed_domains[] = {"mm.bing.net", "th.bing.com", NULL}; - - for (int i = 0; allowed_domains[i] != NULL; i++) { - 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; - } - } + char *colon = strchr(host, ':'); + if (colon) { + *colon = '\0'; } - 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, @@ -73,6 +240,31 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb, 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) { const char *url = NULL; for (int i = 0; i < params->count; i++) { @@ -87,13 +279,67 @@ int image_proxy_handler(UrlParams *params) { return 0; } - if (!is_allowed_domain(url)) { - send_response("Domain not allowed"); + char resolved_ip[INET_ADDRSTRLEN] = {0}; + 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; } CURL *curl = curl_easy_init(); if (!curl) { + free(cache_key); send_response("Failed to initialize curl"); return 0; } @@ -102,6 +348,7 @@ int image_proxy_handler(UrlParams *params) { if (!buf.data) { curl_easy_cleanup(curl); + free(cache_key); send_response("Memory allocation failed"); return 0; } @@ -111,8 +358,37 @@ int image_proxy_handler(UrlParams *params) { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 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); + struct curl_slist *resolves = NULL; + if (resolved_ip[0] != '\0') { + CURLU *u = curl_url(); + if (u) { + curl_url_set(u, CURLUPART_URL, url, 0); + char *rhost = NULL; + curl_url_get(u, CURLUPART_HOST, &rhost, 0); + if (rhost) { + char *rscheme = NULL; + curl_url_get(u, CURLUPART_SCHEME, &rscheme, 0); + int port = (rscheme && strcasecmp(rscheme, "https") == 0) ? 443 : 80; + if (rscheme) + curl_free(rscheme); + + char resolve_str[512]; + snprintf(resolve_str, sizeof(resolve_str), "%s:%d:%s", rhost, port, + resolved_ip); + resolves = curl_slist_append(NULL, resolve_str); + curl_easy_setopt(curl, CURLOPT_RESOLVE, resolves); + curl_free(rhost); + } + curl_url_cleanup(u); + } + } + CURLcode res = curl_easy_perform(curl); long response_code; @@ -126,18 +402,40 @@ int image_proxy_handler(UrlParams *params) { strncpy(content_type, content_type_ptr, sizeof(content_type) - 1); } + if (resolves) + curl_slist_free_all(resolves); curl_easy_cleanup(curl); if (res != CURLE_OK || response_code != 200) { free(buf.data); + free(cache_key); send_response("Failed to fetch image"); 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 = strlen(content_type) > 0 ? content_type : "image/jpeg"; + + cache_set(cache_key, buf.data, buf.size); + serve_data(buf.data, buf.size, mime_type); free(buf.data); + free(cache_key); return 0; } diff --git a/src/Routes/Images.c b/src/Routes/Images.c index 0f8ff1e..40ab88f 100644 --- a/src/Routes/Images.c +++ b/src/Routes/Images.c @@ -1,9 +1,48 @@ #include "Images.h" +#include "../Cache/Cache.h" +#include "../Limiter/RateLimit.h" #include "../Scraping/ImageScraping.h" #include "../Utility/Unescape.h" +#include "../Utility/Utility.h" #include "Config.h" +#include +#include +#include + +static char *build_images_request_cache_key(const char *query, int page, + const char *client_key) { + char scope_key[BUFFER_SIZE_MEDIUM]; + snprintf(scope_key, sizeof(scope_key), "images_request:%s", + client_key ? client_key : "unknown"); + return cache_compute_key(query, page, scope_key); +} + +static char *build_images_href(const char *query, int page) { + const char *safe_query = query ? query : ""; + size_t needed = strlen("/images?q=") + strlen(safe_query) + 1; + if (page > 1) + needed += strlen("&p=") + 16; + + char *href = (char *)malloc(needed); + if (!href) + return NULL; + + snprintf(href, needed, "/images?q=%s", safe_query); + if (page > 1) { + char page_buf[16]; + snprintf(page_buf, sizeof(page_buf), "%d", page); + strcat(href, "&p="); + strcat(href, page_buf); + } + return href; +} + +static char *images_href_builder(int page, void *data) { + return build_images_href((const char *)data, page); +} int images_handler(UrlParams *params) { + extern Config global_config; TemplateContext ctx = new_context(); char *raw_query = ""; int page = 1; @@ -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, "page", page_str); - context_set(&ctx, "prev_page", prev_str); - context_set(&ctx, "next_page", next_str); - context_set(&ctx, "two_prev_page", two_prev_str); - context_set(&ctx, "two_next_page", two_next_str); + + char *theme = get_theme(""); + 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 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); context_set(&ctx, "query", display_query); if (!raw_query || strlen(raw_query) == 0) { - send_response("

No query provided

"); + send_redirect("/"); if (display_query) free(display_query); free_context(&ctx); return -1; } + char client_key[BUFFER_SIZE_SMALL]; + rate_limit_get_client_key(client_key, sizeof(client_key)); + + char *request_cache_key = + build_images_request_cache_key(raw_query, page, client_key); + int request_is_cached = 0; + + if (request_cache_key && get_cache_ttl_image() > 0) { + char *cached_marker = NULL; + size_t cached_marker_size = 0; + + if (cache_get(request_cache_key, (time_t)get_cache_ttl_image(), + &cached_marker, &cached_marker_size) == 0) { + request_is_cached = 1; + } + + free(cached_marker); + } + + if (!request_is_cached) { + RateLimitConfig rate_limit_config = { + .max_requests = global_config.rate_limit_images_requests, + .interval_seconds = global_config.rate_limit_images_interval, + }; + RateLimitResult rate_limit_result = + rate_limit_check("images", &rate_limit_config); + if (rate_limit_result.limited) { + char response[256]; + snprintf(response, sizeof(response), "

%s

", rate_limit_msg); + send_response(response); + free(request_cache_key); + free(display_query); + free_context(&ctx); + return -1; + } + + if (request_cache_key && get_cache_ttl_image() > 0) { + cache_set(request_cache_key, "1", 1); + } + } + ImageResult *results = NULL; int result_count = 0; if (scrape_images(raw_query, page, &results, &result_count) != 0 || !results) { - send_response("

Error fetching images

"); + char error_html[128]; + snprintf(error_html, sizeof(error_html), "

%s

", error_images_msg); + send_response(error_html); + free(request_cache_key); free(display_query); free_context(&ctx); return -1; @@ -66,6 +161,7 @@ int images_handler(UrlParams *params) { if (inner_counts) free(inner_counts); free_image_results(results, result_count); + free(request_cache_key); free(display_query); free_context(&ctx); return -1; @@ -99,7 +195,18 @@ int images_handler(UrlParams *params) { free(image_matrix); free(inner_counts); + if (pager_count > 0) { + for (int i = 0; i < pager_count; i++) { + for (int j = 0; j < LINK_FIELD_COUNT; j++) + free(pager_matrix[i][j]); + free(pager_matrix[i]); + } + free(pager_matrix); + free(pager_inner_counts); + } + free_image_results(results, result_count); + free(request_cache_key); free(display_query); free_context(&ctx); diff --git a/src/Routes/Search.c b/src/Routes/Search.c index bc35fb6..b580c10 100644 --- a/src/Routes/Search.c +++ b/src/Routes/Search.c @@ -1,20 +1,102 @@ #include "Search.h" +#include "../Cache/Cache.h" #include "../Infobox/Calculator.h" +#include "../Infobox/ColourCode.h" #include "../Infobox/CurrencyConversion.h" #include "../Infobox/Dictionary.h" #include "../Infobox/UnitConversion.h" #include "../Infobox/Wikipedia.h" +#include "../Limiter/RateLimit.h" #include "../Scraping/Scraping.h" #include "../Utility/Display.h" #include "../Utility/Unescape.h" +#include "../Utility/Utility.h" #include "Config.h" #include +#include #include #include #include #include #include +#define URL_HASH_TABLE_SIZE 64 + +typedef struct UrlHashEntry { + char *url; + struct UrlHashEntry *next; +} UrlHashEntry; + +typedef struct { + UrlHashEntry *buckets[URL_HASH_TABLE_SIZE]; +} UrlHashTable; + +static void url_hash_init(UrlHashTable *ht) { + for (int i = 0; i < URL_HASH_TABLE_SIZE; i++) { + ht->buckets[i] = NULL; + } +} + +static unsigned int url_hash(const char *url) { + unsigned char hash[EVP_MAX_MD_SIZE]; + unsigned int hash_len; + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (!ctx) + return 0; + EVP_DigestInit_ex(ctx, EVP_md5(), NULL); + EVP_DigestUpdate(ctx, url, strlen(url)); + EVP_DigestFinal_ex(ctx, hash, &hash_len); + EVP_MD_CTX_free(ctx); + unsigned int h = 0; + for (unsigned int i = 0; i < hash_len; i++) { + h = h * 31 + hash[i]; + } + return h % URL_HASH_TABLE_SIZE; +} + +static int url_hash_contains(UrlHashTable *ht, const char *url) { + unsigned int idx = url_hash(url); + for (UrlHashEntry *e = ht->buckets[idx]; e; e = e->next) { + if (strcmp(e->url, url) == 0) { + return 1; + } + } + return 0; +} + +static int url_hash_insert(UrlHashTable *ht, const char *url) { + unsigned int idx = url_hash(url); + for (UrlHashEntry *e = ht->buckets[idx]; e; e = e->next) { + if (strcmp(e->url, url) == 0) { + return 0; + } + } + UrlHashEntry *new_entry = malloc(sizeof(UrlHashEntry)); + if (!new_entry) + return -1; + new_entry->url = strdup(url); + if (!new_entry->url) { + free(new_entry); + return -1; + } + new_entry->next = ht->buckets[idx]; + ht->buckets[idx] = new_entry; + return 0; +} + +static void url_hash_free(UrlHashTable *ht) { + for (int i = 0; i < URL_HASH_TABLE_SIZE; i++) { + UrlHashEntry *e = ht->buckets[i]; + while (e) { + UrlHashEntry *next = e->next; + free(e->url); + free(e); + e = next; + } + ht->buckets[i] = NULL; + } +} + typedef struct { const char *query; InfoBox result; @@ -27,6 +109,10 @@ typedef struct { char *(*url_construct_fn)(const char *query); } InfoBoxHandler; +enum { + RESULT_FIELD_COUNT = 6, +}; + static InfoBox fetch_wiki_wrapper(char *query) { char *url = construct_wiki_url(query); if (!url) @@ -53,7 +139,31 @@ static InfoBox fetch_unit_wrapper(char *query) { static InfoBox fetch_currency_wrapper(char *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) { if (!query) return 0; @@ -107,11 +217,16 @@ static int is_calculator_query(const char *query) { return 0; } +static InfoBox fetch_colour_wrapper(char *query) { + return fetch_colour_data(query); +} + static InfoBoxHandler handlers[] = { {is_dictionary_query, fetch_dict_wrapper, NULL}, {is_calculator_query, fetch_calc_wrapper, NULL}, {is_unit_conv_query, fetch_unit_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}, }; 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->success = (data->result.title != NULL && data->result.extract != NULL && - strlen(data->result.extract) > 10); + data->result.extract[0] != '\0'); return NULL; } @@ -150,6 +265,7 @@ static int add_infobox_to_collection(InfoBox *infobox, char ****collection, (*collection)[current_count][2] = infobox->extract ? strdup(infobox->extract) : 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; return current_count + 1; @@ -202,26 +318,133 @@ static int add_warning_to_collection(const char *engine_name, 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) { - case SCRAPE_STATUS_FETCH_ERROR: - return "request failed before OmniSearch could read search results."; - case SCRAPE_STATUS_PARSE_MISMATCH: - return "returned search results in a format OmniSearch could not parse."; - case SCRAPE_STATUS_BLOCKED: - return "returned a captcha or another blocking page instead of search " - "results."; + case SCRAPE_STATUS_FETCH_ERROR: { + const char *msg = beaker_get_locale_value(locale, "warning_fetch_error"); + return msg ? msg : "request failed before OmniSearch could read search results."; + } + case SCRAPE_STATUS_PARSE_MISMATCH: { + const char *msg = beaker_get_locale_value(locale, "warning_parse_mismatch"); + return msg ? msg : "returned search results in a format OmniSearch could not parse."; + } + case SCRAPE_STATUS_BLOCKED: { + const char *msg = beaker_get_locale_value(locale, "warning_blocked"); + return msg ? msg : "returned a captcha or another blocking page instead of search results."; + } default: 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) { + extern Config global_config; TemplateContext ctx = new_context(); char *raw_query = ""; + const char *selected_engine_id = "all"; int page = 1; int btnI = 0; + char **user_engines = NULL; + int user_engine_count = 0; + int has_user_pref = (get_user_engines(&user_engines, &user_engine_count) == 0); + if (params) { for (int i = 0; i < params->count; i++) { if (strcmp(params->params[i].key, "q") == 0) { @@ -230,6 +453,8 @@ int results_handler(UrlParams *params) { int parsed = atoi(params->params[i].value); if (parsed > 1) 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) { btnI = atoi(params->params[i].value); } @@ -238,25 +463,62 @@ int results_handler(UrlParams *params) { context_set(&ctx, "query", raw_query); - char page_str[16], prev_str[16], next_str[16], two_prev_str[16], - two_next_str[16]; + char *theme = get_theme(""); + 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); + context_set(&ctx, "page", page_str); + + char prev_str[16], next_str[16], two_prev_str[16], two_next_str[16]; snprintf(prev_str, sizeof(prev_str), "%d", page > 1 ? page - 1 : 0); snprintf(next_str, sizeof(next_str), "%d", page + 1); snprintf(two_prev_str, sizeof(two_prev_str), "%d", page > 2 ? page - 2 : 0); snprintf(two_next_str, sizeof(two_next_str), "%d", page + 2); - context_set(&ctx, "page", page_str); context_set(&ctx, "prev_page", prev_str); context_set(&ctx, "next_page", next_str); context_set(&ctx, "two_prev_page", two_prev_str); context_set(&ctx, "two_next_page", two_next_str); if (!raw_query || strlen(raw_query) == 0) { - send_response("

No query provided

"); + 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); 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]; 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]; SearchResult *all_results[ENGINE_COUNT]; int engine_idx = 0; 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; jobs[engine_idx].engine = &ENGINE_REGISTRY[i]; jobs[engine_idx].query = raw_query; @@ -303,8 +561,105 @@ int results_handler(UrlParams *params) { } } - if (enabled_engine_count > 0) { - scrape_engines_parallel(jobs, enabled_engine_count); + char client_key[BUFFER_SIZE_SMALL]; + rate_limit_get_client_key(client_key, sizeof(client_key)); + + char *request_cache_key = build_search_request_cache_key( + raw_query, selected_engine_id, page, client_key); + int request_is_cached = 0; + + if (request_cache_key && get_cache_ttl_search() > 0) { + char *cached_marker = NULL; + size_t cached_marker_size = 0; + + if (cache_get(request_cache_key, (time_t)get_cache_ttl_search(), + &cached_marker, &cached_marker_size) == 0) { + request_is_cached = 1; + } + + free(cached_marker); + } + + if (engine_idx > 0 && !request_is_cached) { + RateLimitConfig rate_limit_config = { + .max_requests = global_config.rate_limit_search_requests, + .interval_seconds = global_config.rate_limit_search_interval, + }; + RateLimitResult rate_limit_result = + rate_limit_check("search", &rate_limit_config); + if (rate_limit_result.limited) { + char response[256]; + snprintf(response, sizeof(response), "

%s

", 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) { @@ -314,7 +669,7 @@ int results_handler(UrlParams *params) { } 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) { char *redirect_url = strdup(all_results[i][0].url); 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); if (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); - send_response("

No results found

"); + char no_results_html[128]; + snprintf(no_results_html, sizeof(no_results_html), "

%s

", no_results_msg); + send_response(no_results_html); return 0; } @@ -383,7 +752,7 @@ int results_handler(UrlParams *params) { int warning_count = 0; for (int i = 0; i < enabled_engine_count; i++) { - if (warning_message_for_job(&jobs[i])) + if (warning_message_for_job(&jobs[i], locale)) warning_count++; } @@ -393,7 +762,7 @@ int results_handler(UrlParams *params) { int warning_index = 0; for (int i = 0; i < enabled_engine_count; i++) { - const char *warning_message = warning_message_for_job(&jobs[i]); + const char *warning_message = warning_message_for_job(&jobs[i], locale); if (!warning_message) continue; @@ -427,14 +796,7 @@ int results_handler(UrlParams *params) { if (total_results > 0) { char ***results_matrix = (char ***)malloc(sizeof(char **) * total_results); int *results_inner_counts = (int *)malloc(sizeof(int) * total_results); - char **seen_urls = (char **)malloc(sizeof(char *) * total_results); - if (!results_matrix || !results_inner_counts || !seen_urls) { - if (results_matrix) - free(results_matrix); - if (results_inner_counts) - free(results_inner_counts); - if (seen_urls) - free(seen_urls); + if (!results_matrix || !results_inner_counts) { char *html = render_template("results.html", &ctx); if (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); return 0; } int unique_count = 0; + UrlHashTable url_table; + url_hash_init(&url_table); for (int i = 0; i < enabled_engine_count; i++) { for (int j = 0; j < jobs[i].results_count; j++) { char *display_url = all_results[i][j].url; - int is_duplicate = 0; - for (int k = 0; k < unique_count; k++) { - if (strcmp(seen_urls[k], display_url) == 0) { - is_duplicate = 1; - break; - } - } - - if (is_duplicate) { + if (url_hash_contains(&url_table, display_url)) { free(all_results[i][j].url); free(all_results[i][j].title); free(all_results[i][j].snippet); continue; } - seen_urls[unique_count] = strdup(display_url); - if (!seen_urls[unique_count]) { - free(all_results[i][j].url); - free(all_results[i][j].title); - free(all_results[i][j].snippet); - continue; - } + url_hash_insert(&url_table, display_url); + results_matrix[unique_count] = - (char **)malloc(sizeof(char *) * INFOBOX_FIELD_COUNT); + (char **)malloc(sizeof(char *) * RESULT_FIELD_COUNT); if (!results_matrix[unique_count]) { - free(seen_urls[unique_count]); free(all_results[i][j].url); free(all_results[i][j].title); free(all_results[i][j].snippet); continue; } 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][1] = strdup(pretty_url); @@ -499,10 +856,13 @@ int results_handler(UrlParams *params) { results_matrix[unique_count][3] = all_results[i][j].snippet ? strdup(all_results[i][j].snippet) : 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(base_url); free(all_results[i][j].url); free(all_results[i][j].title); 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, 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); if (html) { send_response(html); @@ -522,14 +901,13 @@ int results_handler(UrlParams *params) { } 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]); - free(seen_urls[i]); } - free(seen_urls); free(results_matrix); free(results_inner_counts); + url_hash_free(&url_table); } else { char *html = render_template("results.html", &ctx); if (html) { @@ -542,6 +920,8 @@ int results_handler(UrlParams *params) { } } + free(request_cache_key); + if (page == 1) { for (int i = 0; i < HANDLER_COUNT; i++) { if (infobox_data[i].success) { @@ -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); return 0; diff --git a/src/Routes/Settings.c b/src/Routes/Settings.c new file mode 100644 index 0000000..eb3072b --- /dev/null +++ b/src/Routes/Settings.c @@ -0,0 +1,140 @@ +#include "Settings.h" +#include "../Scraping/Scraping.h" +#include "../Utility/Utility.h" +#include +#include +#include + +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; +} diff --git a/src/Routes/Settings.h b/src/Routes/Settings.h new file mode 100644 index 0000000..269c085 --- /dev/null +++ b/src/Routes/Settings.h @@ -0,0 +1,8 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include + +int settings_handler(UrlParams *params); + +#endif diff --git a/src/Routes/SettingsSave.c b/src/Routes/SettingsSave.c new file mode 100644 index 0000000..cacff50 --- /dev/null +++ b/src/Routes/SettingsSave.c @@ -0,0 +1,66 @@ +#include "SettingsSave.h" +#include "../Scraping/Scraping.h" +#include "../Utility/Utility.h" +#include +#include + +#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; +} diff --git a/src/Routes/SettingsSave.h b/src/Routes/SettingsSave.h new file mode 100644 index 0000000..e10cc2f --- /dev/null +++ b/src/Routes/SettingsSave.h @@ -0,0 +1,8 @@ +#ifndef SETTINGS_SAVE_H +#define SETTINGS_SAVE_H + +#include + +int settings_save_handler(UrlParams *params); + +#endif diff --git a/src/Scraping/ImageScraping.c b/src/Scraping/ImageScraping.c index 33f710a..2341244 100644 --- a/src/Scraping/ImageScraping.c +++ b/src/Scraping/ImageScraping.c @@ -28,113 +28,82 @@ static char *build_proxy_url(const char *image_url) { return proxy_url; } -static int parse_image_node(xmlNodePtr node, ImageResult *result) { - xmlNodePtr img_node = NULL; - xmlNodePtr tit_node = NULL; - xmlNodePtr des_node = NULL; - xmlNodePtr thumb_link = NULL; +static char *extract_json_string(const char *json, const char *key) { + if (!json || !key) + return NULL; - for (xmlNodePtr child = node->children; child; child = child->next) { - if (child->type != XML_ELEMENT_NODE) - continue; + char search_key[64]; + snprintf(search_key, sizeof(search_key), "\"%s\"", key); - if (xmlStrcmp(child->name, (const xmlChar *)"a") == 0) { - xmlChar *class = xmlGetProp(child, (const xmlChar *)"class"); - if (class) { - if (xmlStrstr(class, (const xmlChar *)"thumb") != NULL) { - thumb_link = child; - for (xmlNodePtr thumb_child = child->children; thumb_child; - thumb_child = thumb_child->next) { - if (xmlStrcmp(thumb_child->name, (const xmlChar *)"div") == 0) { - xmlChar *div_class = - xmlGetProp(thumb_child, (const xmlChar *)"class"); - if (div_class && - xmlStrcmp(div_class, (const xmlChar *)"cico") == 0) { - for (xmlNodePtr cico_child = thumb_child->children; cico_child; - cico_child = cico_child->next) { - if (xmlStrcmp(cico_child->name, (const xmlChar *)"img") == - 0) { - img_node = cico_child; - break; - } - } - } - if (div_class) - xmlFree(div_class); - } - } - } 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); - } + const char *key_pos = strstr(json, search_key); + if (!key_pos) + return NULL; + + const char *colon = strchr(key_pos + strlen(search_key), ':'); + if (!colon) + return NULL; + + colon++; + while (*colon == ' ' || *colon == '\t' || *colon == '\n' || *colon == '\r') + colon++; + + if (*colon != '"') + return NULL; + colon++; + + size_t len = 0; + const char *start = colon; + while (*colon && *colon != '"') { + if (*colon == '\\' && *(colon + 1)) + colon++; + colon++; + len++; } - xmlChar *iurl = - img_node ? xmlGetProp(img_node, (const xmlChar *)"src") : NULL; - xmlChar *full_url = - 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; + char *result = malloc(len + 1); + if (!result) + return NULL; - if (!iurl || strlen((char *)iurl) == 0) { - if (iurl) - xmlFree(iurl); - if (title) - xmlFree(title); - if (rurl) - xmlFree(rurl); - if (full_url) - xmlFree(full_url); + colon = start; + size_t i = 0; + while (*colon && *colon != '"') { + if (*colon == '\\' && *(colon + 1)) + colon++; + result[i++] = *colon++; + } + result[i] = '\0'; + + return result; +} + +static int parse_iusc_node(xmlNodePtr node, ImageResult *result) { + xmlChar *m_attr = xmlGetProp(node, (const xmlChar *)"m"); + if (!m_attr) 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); - result->thumbnail_url = proxy_url ? strdup(proxy_url) : strdup((char *)iurl); - free(proxy_url); - result->title = strdup(title ? (char *)title : "Image"); - result->page_url = strdup(rurl ? (char *)rurl : "#"); - result->full_url = strdup(full_url ? (char *)full_url : "#"); + free(turl); + free(murl); + free(purl); + free(title); - if (iurl) - xmlFree(iurl); - if (title) - xmlFree(title); - if (rurl) - xmlFree(rurl); - if (full_url) - xmlFree(full_url); - - return 1; + xmlFree(m_attr); + return ok; } 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]; int first = (page - 1) * IMAGE_RESULTS_PER_PAGE + 1; - snprintf(url, sizeof(url), "%s?q=%s&first=%d", BING_IMAGE_URL, encoded_query, - first); + snprintf( + url, sizeof(url), + "https://www.bing.com/images/async?q=%s&async=content&first=%d&count=%d", + encoded_query, first, 35); free(encoded_query); HttpResponse resp = http_get( 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) { return -1; } @@ -183,7 +155,7 @@ int scrape_images(const char *query, int page, ImageResult **out_results, } xmlXPathObjectPtr xpathObj = - xmlXPathEvalExpression((const xmlChar *)"//div[@class='item']", xpathCtx); + xmlXPathEvalExpression((const xmlChar *)"//a[@class='iusc']", xpathCtx); if (!xpathObj || !xpathObj->nodesetval) { if (xpathObj) @@ -210,7 +182,7 @@ int scrape_images(const char *query, int page, ImageResult **out_results, int count = 0; for (int i = 0; i < nodes && count < IMAGE_RESULTS_PER_PAGE; i++) { xmlNodePtr node = xpathObj->nodesetval->nodeTab[i]; - if (parse_image_node(node, &results[count])) { + if (parse_iusc_node(node, &results[count])) { count++; } } diff --git a/src/Scraping/Scraping.c b/src/Scraping/Scraping.c index 91200f2..b81c216 100644 --- a/src/Scraping/Scraping.c +++ b/src/Scraping/Scraping.c @@ -24,7 +24,8 @@ static int response_is_startpage_captcha(const ScrapeJob *job, return 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"); } static int response_looks_like_results_page(const ScrapeJob *job, diff --git a/src/Utility/Display.c b/src/Utility/Display.c index 1322391..aee6929 100644 --- a/src/Utility/Display.c +++ b/src/Utility/Display.c @@ -25,6 +25,12 @@ char *pretty_display_url(const char *input) { strncpy(temp, start, sizeof(temp) - 1); 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] == '/') { temp[input_len - 1] = '\0'; } diff --git a/src/Utility/HttpClient.c b/src/Utility/HttpClient.c index bdd2f4d..0ffb9ff 100644 --- a/src/Utility/HttpClient.c +++ b/src/Utility/HttpClient.c @@ -31,6 +31,17 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb, 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 resp = {.memory = NULL, .size = 0, .capacity = 0}; @@ -51,16 +62,24 @@ HttpResponse http_get(const char *url, const char *user_agent) { return resp; } + struct curl_slist *headers = build_http_headers(); + 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_WRITEDATA, &resp); curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent ? user_agent : "libcurl-agent/1.0"); 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); CURLcode res = curl_easy_perform(curl); + curl_slist_free_all(headers); curl_easy_cleanup(curl); if (res != CURLE_OK) { diff --git a/src/Utility/Utility.c b/src/Utility/Utility.c index 3be4ef4..1428722 100644 --- a/src/Utility/Utility.c +++ b/src/Utility/Utility.c @@ -1,4 +1,87 @@ #include "Utility.h" +#include "../Scraping/Scraping.h" +#include +#include +#include +#include +#include + +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) { if (c >= '0' && c <= '9') @@ -9,3 +92,212 @@ int hex_to_int(char c) { return c - 'A' + 10; return -1; } + +char *get_theme(const char *default_theme) { + char *cookie = get_cookie("theme"); + if (cookie && 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; +} diff --git a/src/Utility/Utility.h b/src/Utility/Utility.h index 3b0181c..1e1de09 100644 --- a/src/Utility/Utility.h +++ b/src/Utility/Utility.h @@ -1,6 +1,34 @@ #ifndef UTILITY_H #define UTILITY_H +#include + +#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); +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 diff --git a/static/main.css b/static/main.css index 69daad7..adea1c5 100644 --- a/static/main.css +++ b/static/main.css @@ -1,44 +1,21 @@ -@import url("https://cdn.jsdelivr.net/npm/@catppuccin/palette/css/catppuccin.css"); - -:root { - --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); - } +*, *::before, *::after { + box-sizing: border-box; + font-family: sans-serif; } -*, -*::before, -*::after { - box-sizing: border-box; +html { + height:100%; } body { - background-color: var(--bg-main); - color: var(--text-primary); - font-family: - system-ui, - -apple-system, - sans-serif; - margin: 0; - padding: 0; - -webkit-tap-highlight-color: transparent; + background-color:var(--bg-main); + background-image:radial-gradient(circle at top end, var(--bg-card) 0%, var(--bg-main) 100%); + background-attachment:fixed; + color:var(--text-primary); + margin:0; + padding:0; + min-height:100%; + -webkit-tap-highlight-color: transparent; } img[src=""] { @@ -51,25 +28,20 @@ img[src=""] { } .view-home { - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - padding: 20px; - background: radial-gradient( - circle at top right, - var(--accent) 0%, - var(--bg-main) 100% - ); + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 20px; } .view-home .container { - width: 100%; - max-width: 580px; - margin: 0 auto; - text-align: center; - display: flex; - flex-direction: column; + width: 100%; + max-width: 580px; + margin: 0 auto; + text-align: center; + display: flex; + flex-direction: column; } .view-home .hero-logo { @@ -115,21 +87,85 @@ img[src=""] { transform: translateY(-1px); } .view-home .btn-secondary { - background: var(--bg-card); - color: var(--text-primary); - border-color: var(--border); + background:var(--bg-card); + color:var(--text-primary); + border-color:var(--border); + text-decoration:none; + display:inline-flex; + align-items:center; + padding:10px 24px; + border-radius:8px; + font-weight:600; + font-size:0.9rem; + cursor:pointer; + transition:all 0.2s; + border:1px solid var(--border); } .view-home .btn-secondary:hover { background: var(--border); border-color: var(--text-secondary); } +.home-settings-btn { + position:fixed; + top:27px; + inset-inline-end:60px; + width:24px; + height:24px; + background-color:var(--text-primary); + -webkit-mask-image:url('/static/settings.svg'); + mask-image:url('/static/settings.svg'); + mask-size:contain; + mask-repeat:no-repeat; + mask-position:center; + text-decoration:none; +} +.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 { - display: flex; - align-items: center; - gap: 20px; - padding: 15px 60px; - border-bottom: 1px solid var(--border); - background: var(--bg-main); + display:flex; + align-items:center; + gap:20px; + padding-block:15px; + padding-inline:60px; + border-bottom:1px solid var(--border); + background:var(--bg-main); + width:100%; } .search-form { flex-grow: 1; @@ -144,6 +180,16 @@ h1 { h1 span { color: var(--accent); } +.logo-link { + text-decoration:none; + color:inherit; +} +header .logo-link { + transition:transform 0.2s; +} +header .logo-link:hover { + transform:scale(1.03); +} .search-box { width: 100%; padding: 12px 24px; @@ -162,14 +208,14 @@ h1 span { box-shadow: 0 0 0 4px var(--accent-glow); } .nav-tabs { - padding: 0 60px; - border-bottom: 1px solid var(--border); - background: var(--bg-main); + padding-inline:60px; + border-bottom:1px solid var(--border); + background:var(--bg-main); + width:100%; } .nav-container { - display: flex; - gap: 30px; - max-width: 1200px; + display:flex; + gap:30px; } .nav-tabs a { padding: 14px 0; @@ -188,6 +234,9 @@ h1 span { color: var(--accent); border-bottom-color: var(--accent); } +.nav-right { + margin-inline-start:auto; +} .image-results-container { padding: 30px 60px; } @@ -289,14 +338,75 @@ h1 span { text-overflow: ellipsis; } .content-layout { - display: grid; - grid-template-columns: 140px minmax(0, 700px) 450px; - gap: 60px; - padding: 30px 60px; + display:grid; + grid-template-columns:140px minmax(0,700px) 450px; + gap: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 { 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 { display: flex; flex-direction: column; @@ -335,12 +445,6 @@ h1 span { .result > a:hover { text-decoration-color: var(--accent); } -.url { - color: var(--text-secondary); - font-size: 0.85rem; - display: block; - margin-bottom: 4px; -} .desc { color: var(--text-muted); line-height: 1.6; @@ -435,199 +539,419 @@ h1 span { } .pagination-current { - background: var(--bg-card); - color: var(--text-primary); - border: 1px solid var(--border); - padding: 4px 12px; - border-radius: 8px; - text-decoration: none; - font-size: 1.2rem; - font-weight: 600; - transition: all 0.2s; - touch-action: manipulation; + background: var(--accent); + border-color: var(--accent); + color: var(--bg-main); } .pagination-current:hover { - background: var(--border); - border-color: var(--text-secondary); + background: var(--accent); + border-color: var(--accent); } -@media (max-width: 1200px) { - .content-layout { - grid-template-columns: 1fr; - padding: 20px 30px; - gap: 20px; - } - .results-container, - .infobox-sidebar { - grid-column: 1; - max-width: 100%; - } - .infobox-sidebar { - order: -1; - } - .nav-tabs, - .image-results-container { - padding: 0 30px; - } - header { - padding: 15px 30px; - } +[dir="rtl"] .pagination-btn.prev { + transform: scaleX(-1); +} + +[dir="rtl"] .pagination-btn.next { + transform: scaleX(-1); +} + + +@media (max-width:1200px) { + .content-layout { + grid-template-columns:1fr; + padding-block:20px; + padding-inline:30px; + gap:20px; + } + header { + gap:20px; + } + .results-container,.infobox-sidebar { + grid-column:1; + max-width:100%; + } + .settings-layout { + padding-block:20px; + padding-inline:30px; + display:flex; + justify-content:center; + } + .infobox-sidebar { + order:-1; + } + .nav-tabs,.image-results-container { + padding-inline:30px; + } + 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) { - header { - 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; - } + [dir="rtl"] header { + flex-direction: column; + } } -@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; - } -} + diff --git a/static/settings.svg b/static/settings.svg new file mode 100644 index 0000000..4aebf8f --- /dev/null +++ b/static/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/themes/catppuccin mocha.css b/static/themes/catppuccin mocha.css new file mode 100644 index 0000000..c2d5ea0 --- /dev/null +++ b/static/themes/catppuccin mocha.css @@ -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); +} diff --git a/static/themes/dark.css b/static/themes/dark.css new file mode 100644 index 0000000..246aabe --- /dev/null +++ b/static/themes/dark.css @@ -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); +} diff --git a/static/themes/light.css b/static/themes/light.css new file mode 100644 index 0000000..1c69377 --- /dev/null +++ b/static/themes/light.css @@ -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); +} diff --git a/static/themes/system.css b/static/themes/system.css new file mode 100644 index 0000000..f9c52cf --- /dev/null +++ b/static/themes/system.css @@ -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); + } +} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index e49e1e9..6f7142a 100644 --- a/templates/home.html +++ b/templates/home.html @@ -1,5 +1,5 @@ - + @@ -8,7 +8,8 @@ OmniSearch - + + @@ -22,19 +23,24 @@
-
+ {{l("surprise_me_button")}} + +
+ + diff --git a/templates/images.html b/templates/images.html index 16e1de7..4674bee 100644 --- a/templates/images.html +++ b/templates/images.html @@ -1,5 +1,5 @@ - + @@ -7,27 +7,32 @@ OmniSearch Images - {{query}} - + +
-

- Voidarc -

+

+ OmniSearch +

-
+
@@ -40,10 +45,10 @@ @@ -59,49 +64,16 @@ {{endfor}} - + {{if exists pagination_links}} + + {{endif}} - + \ No newline at end of file diff --git a/templates/opensearch.xml b/templates/opensearch.xml index 8544b09..14d3760 100644 --- a/templates/opensearch.xml +++ b/templates/opensearch.xml @@ -4,7 +4,7 @@ xmlns:moz="http://www.mozilla.org/2006/browser/search/"> OmniSearch Lightweight metasearch engine - + UTF-8 UTF-8 {{domain}}/ diff --git a/templates/results.html b/templates/results.html index a608090..bea337f 100644 --- a/templates/results.html +++ b/templates/results.html @@ -1,5 +1,5 @@ - + @@ -8,7 +8,8 @@ OmniSearch - {{query}} - + + @@ -16,21 +17,26 @@
-

- Voidarc -

+

+ OmniSearch +

- +
+
@@ -38,6 +44,16 @@
+ {{if exists engine_filters}} + + {{endif}} + {{if exists engine_warnings}}
{{for warning in engine_warnings}} @@ -55,9 +71,15 @@ {{for result in results}}
- - {{result[1]}} +
+
+
+ + {{result[1]}} + +
{{result[2]}} @@ -65,53 +87,20 @@ {{result[3]}}

- View Cached + {{l("view_cached")}}
{{endfor}} + {{if exists pagination_links}} + {{endif}}