Compare commits

...

63 Commits

Author SHA1 Message Date
e48c6366d0 theming 2026-06-03 18:42:08 +01:00
2bd19a8b7b Merge branch 'indev' of https://git.bwaaa.monster/omnisearch 2026-06-03 18:02:39 +01:00
indium114
b8fd03344d feat(themes): add catppuccin mocha theme 2026-06-03 12:59:14 -04:00
frosty
c26a08c6a2 fix: some attempts to resolve some issues with images 2026-06-02 18:18:45 -04:00
indium114
5808459c75 feat(locale): add afrikaans (south africa) locale 2026-05-11 20:34:54 -04:00
frosty
3c856f93ed feat(wip): load themes dynamically from static/themes/*.css 2026-05-10 00:00:08 -04:00
frosty
ba6dae676a feat: remove domain field from config and derive domain from headers 2026-05-04 20:43:27 -04:00
m
a9db276fd8 feat(locale): add dutch (netherlands) locale 2026-04-29 20:17:36 -04:00
Lignum Crucis
92dd87eb57 feat(locale): add portuguese (brazil) locale 2026-04-15 01:11:11 -04:00
frosty
7665efca73 feat: hide 'view more' button when there is no link 2026-04-14 19:27:57 -04:00
frosty
6738ab0eac feat: added colour code preview infobox 2026-04-14 18:13:55 -04:00
claymorwan
3a99a37b0c feat(locale): add french locale 2026-04-13 22:44:27 -04:00
frosty
4368e16368 fix: fixed weird font issue 2026-04-11 01:23:37 -04:00
Else
3570cb5fb5 Handle empty image proxy responses 2026-04-10 16:53:03 -04:00
lehuy
896b6cd266 Vietnamese language support 2026-04-09 12:24:55 -04:00
claymorwan
eb2773f91c feat(nix): locale support 2026-04-09 08:45:27 -04:00
frosty
f6c8242e72 feat: setting default locale for instance 2026-04-06 01:56:11 -04:00
cybardev
e0c209c974 fix: match container port binding to example config 2026-04-05 11:24:58 -04:00
cybardev
deb3c308b8 locale: add bn_bd (Bengali) 2026-04-04 15:29:01 -04:00
cybardev
7c5062dd07 fix: add locales and config to container 2026-04-04 15:28:50 -04:00
frosty
8cd861fd86 locale: renamed en_gb to 'English', en_us to 'English (US)' 2026-04-03 16:56:50 -04:00
frosty
a675d19d4f test: remove testing locale 2026-04-03 16:54:22 -04:00
RDJX3
ccbb5d71de updated latvian and russian translations :D 2026-04-03 16:50:40 -04:00
frosty
627a219bea feat: made version on homepage link to repo 2026-04-03 15:26:01 -04:00
frosty
db02c4cc80 feat: added version to homepage 2026-04-03 15:15:41 -04:00
frosty
c9709029ca fix: replaced en_uk with en_gb as default locale 2026-04-03 14:09:06 -04:00
crumpetalpaca
0eff62bf68 fix: Use the correct contry code for English (Traditional) 2026-04-03 14:09:06 -04:00
frosty
96b57648dd fix(locale): changed wording on en_uk, en_us to be more accurate 2026-04-02 19:19:37 -04:00
rdjx3
37f18d148f Added Latvian and Russian translations 2026-04-03 02:10:57 +03:00
frosty
62e6d0c3d6 fix: fixed issue with last commit 2026-04-02 09:59:44 +03:00
frosty
08c1aa8abe fix: prioritise theme files 2026-04-02 09:55:12 +03:00
frosty
b9f775fc2d fix: prevent SIGSEGV on NULL extract in infobox handler 2026-04-02 07:39:11 +03:00
frosty
821e670ddd fix: copy locale folder in install process 2026-04-02 07:28:23 +03:00
frosty
2fb5f975de optimise: improve duplicate URL detection 2026-04-02 07:23:23 +03:00
frosty
d2e0c7f481 fix: improved speed in ImagesProxy.c 2026-04-02 06:44:40 +03:00
violet
f66686a959 fix: startpage captcha detection 2026-04-02 01:25:00 +03:00
frosty
f29fa38398 design: changed appearance of arrows on pagination 2026-04-02 01:16:39 +03:00
frosty
478aafcf87 fix: fixed unused 'locale' parameter on pagination 2026-04-02 01:06:31 +03:00
frosty
1382d73d53 feat: configure search engines in user settings p2 2026-04-01 22:40:10 +03:00
frosty
8176078105 feat: configure search engines in user settings 2026-04-01 22:39:22 +03:00
frosty
614bd26cb3 refactor: internationalise pagination and clean up related code 2026-04-01 05:49:18 +03:00
frosty
c6bdeecb2a test: made ca_ca locale rtl for testing purposes 2026-04-01 04:05:22 +03:00
frosty
116069c8e9 feat: add more locale keys 2026-04-01 04:01:07 +03:00
frosty
c41ab84738 feat: begin working on localisation 2026-04-01 00:37:15 +03:00
frosty
335b6f4683 feat: make clicking on logo in header bring you back to homepage 2026-03-31 06:03:57 +03:00
frosty
71d3d0dcb0 feat: improve navigation behaviour for settings 2026-03-31 06:03:50 +03:00
frosty
0ea4bc726c fix: make check for X-Forwarded-For case insensitive in RateLimit.c 2026-03-31 05:22:42 +03:00
stab
f38fe3c42e Added rate limiting and settings fixes. 2026-03-31 05:10:22 +03:00
frosty
c3ed901738 feat: begin adding settings menu, move theme to settings 2026-03-30 10:37:46 +03:00
Ansari
9e6e763064 docker: use multi-stage build to reduce image size 2026-03-30 03:01:16 +03:00
frosty
6b8a278b4f docs(fix): update README to include additional deps for debian 2026-03-28 23:37:49 -04:00
crumpetalpaca
252ed2fcfb fix: stupid nix highlighting stuff 2026-03-28 19:40:19 -04:00
crumpetalpaca
ebdbbc2ff0 fix: Make it so runit users specify services directory 2026-03-28 19:36:36 -04:00
frosty
82075a664e fix: pagination on images page looked improper 2026-03-28 15:20:56 -04:00
frosty
1b9187b153 feat: changes to image proxy, proxy favicons 2026-03-28 15:01:13 -04:00
DinoShrimp
86a9ebb90a Add docker compose 2026-03-27 00:20:52 -04:00
frosty
783a58d954 feat: ignore query parameters in formatted URLs for readability 2026-03-24 16:03:31 -04:00
frosty
e9b01902d9 removed search engine indicator from results 2026-03-24 15:37:13 -04:00
frosty
8b7b8de06c fix: fixed favicons from merge conflict 2026-03-23 10:46:17 -04:00
frosty
5a4af40b74 fix: fix leaks in add_link_to_collection on fail 2026-03-23 10:32:44 -04:00
Else
4ed9ec9fc5 Add engine filters and result source labels 2026-03-23 10:29:36 -04:00
frosty
660a4918b8 style: changed how favicons appear on the result page 2026-03-23 03:09:00 -04:00
stab
51e7fcaad2 Added favicon to search results 2026-03-23 02:11:26 -04:00
52 changed files with 3127 additions and 406 deletions

46
Dockerfile Normal file
View File

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

View File

@@ -3,13 +3,22 @@ UNAME_S := $(shell uname -s)
PKG_CONFIG ?= pkg-config
PKG_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"

View File

@@ -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:

10
docker-compose.yml Normal file
View File

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

View File

@@ -1,7 +1,9 @@
[server]
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

14
flake.lock generated
View File

@@ -3,11 +3,11 @@
"beaker-src": {
"flake": false,
"locked": {
"lastModified": 1773884524,
"narHash": "sha256-1dnlofWaxI/YRID+WPz2jHZNDyloBubDt/bAQk9ePLU=",
"lastModified": 1780436704,
"narHash": "sha256-3aMH1YblnpiXKdkQVMRLmMZ5/8G7vB4HAfLg7+izyHg=",
"ref": "refs/heads/master",
"rev": "abc598baf15d6f8a4de395a27ba34b1e769558e1",
"revCount": 21,
"rev": "360d6271e1a20d128430e52637d5d35f4c706ca5",
"revCount": 34,
"shallow": false,
"type": "git",
"url": "https://git.bwaaa.monster/beaker"
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1773734432,
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
"lastModified": 1780243769,
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
"type": "github"
},
"original": {

View File

@@ -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 = {

34
locales/af_za.ini Normal file
View File

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

34
locales/bn_bd.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "bn_bd"
Name = "বাংলা"
Direction = "ltr"
[Keys]
search_placeholder = "ওয়েব অনুসন্ধান করুন..."
search_button = "অনুসন্ধান"
surprise_me_button = "চমক প্রদর্শন"
all_tab = "যাবতীয়"
images_tab = "ছবি"
settings_tab = "সেটিংস"
settings_title = "সেটিংস"
theme_label = "বাহ্যরূপ"
theme_desc = "আপনার পছন্দের বর্ণবিন্যাস নির্ধারণ করুন।"
theme_system = "সিস্টেম"
theme_light = "আলো"
theme_dark = "আঁধার"
language_label = "ভাষা"
display_language_label = "প্রদর্শিত ভাষা"
language_desc = "আপনার পছন্দের ভাষা নির্ধারণ করুন।"
engines_label = "সার্চ ইঞ্জিন"
engines_desc = "ব্যবহৃত সার্চ ইঞ্জিন নির্ধারণ করুন। শুধু সার্ভারে চলনকৃত সার্চ ইঞ্জিন দেখানো।"
save_settings_button = "সেটিংস সংরক্ষণ করুন"
no_results = "কোনো ফলাফল পাওয়া যায়নি"
error_images = "ছবি অনুসন্ধান করায় অক্ষম"
rate_limit = "আস্তে-ধীরে! বহুত অনুসন্ধান করছেন দেখি!"
warning_fetch_error = "অমনিসার্চ ফলাফল বোঝার আগেই আবেদন ব্যার্থ হয়েছে।"
warning_parse_mismatch = "অমনিসার্চ বুঝতে অক্ষম এমনপ্রকার ফলাফল ফিরেছে।"
warning_blocked = "অনুসন্ধানের ফলাফলের বদলে ক্যাপ্চা অথবা অন্য ব্লককারী পেজ ফিরেছে।"
read_more = "পড়তে থাকুন"
view_cached = "ক্যাশকৃত"
view_image = "ছবি"
visit_site = "সাইট"

34
locales/en_gb.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "en_gb"
Name = "English"
Direction = "ltr"
[Keys]
search_placeholder = "Search the web..."
search_button = "Search"
surprise_me_button = "Surprise me"
all_tab = "All"
images_tab = "Images"
settings_tab = "Settings"
settings_title = "Settings"
theme_label = "Appearance"
theme_desc = "Choose your preferred colour scheme."
theme_system = "System"
theme_light = "Light"
theme_dark = "Dark"
language_label = "Language"
display_language_label = "Display Language"
language_desc = "Choose your preferred language."
engines_label = "Search Engines"
engines_desc = "Choose which search engines to use. Only engines enabled on the server are shown."
save_settings_button = "Save Settings"
no_results = "No results found"
error_images = "Error fetching images"
rate_limit = "Slow down! Too many searches from you!"
warning_fetch_error = "request failed before OmniSearch could read search results."
warning_parse_mismatch = "returned search results in a format OmniSearch could not parse."
warning_blocked = "returned a captcha or another blocking page instead of search results."
read_more = "Read More"
view_cached = "Cached"
view_image = "Image"
visit_site = "Site"

34
locales/en_us.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "en_us"
Name = "English (US)"
Direction = "ltr"
[Keys]
search_placeholder = "Search the web..."
search_button = "Search"
surprise_me_button = "Surprise me"
all_tab = "All"
images_tab = "Images"
settings_tab = "Settings"
settings_title = "Settings"
theme_label = "Appearance"
theme_desc = "Choose your preferred color scheme."
theme_system = "System"
theme_light = "Light"
theme_dark = "Dark"
language_label = "Language"
display_language_label = "Display Language"
language_desc = "Choose your preferred language."
engines_label = "Search Engines"
engines_desc = "Choose which search engines to use. Only engines enabled on the server are shown."
save_settings_button = "Save Settings"
no_results = "No results found"
error_images = "Error fetching images"
rate_limit = "Slow down! Too many searches from you!"
warning_fetch_error = "request failed before OmniSearch could read search results."
warning_parse_mismatch = "returned search results in a format OmniSearch could not parse."
warning_blocked = "returned a captcha or another blocking page instead of search results."
read_more = "Read More"
view_cached = "Cached"
view_image = "Image"
visit_site = "Site"

34
locales/fr_fr.ini Normal file
View File

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

34
locales/lv_lv.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "lv_lv"
Name = "Latviešu"
Direction = "ltr"
[Keys]
search_placeholder = "Meklēt tīmeklī..."
search_button = "Meklēt"
surprise_me_button = "Pārsteidz mani"
all_tab = "Viss"
images_tab = "Attēli"
settings_tab = "Iestatījumi"
settings_title = "Iestatījumi"
theme_label = "Izskats"
theme_desc = "Izvēlieties vēlamo krāsu shēmu."
theme_system = "Sistēma"
theme_light = "Gaišs"
theme_dark = "Tumšs"
language_label = "Valoda"
display_language_label = "Saskarnes valoda"
language_desc = "Izvēlieties vēlamo valodu."
save_settings_button = "Saglabāt iestatījumus"
no_results = "Rezultāti nav atrasti"
error_images = "Kļūda, ielādējot attēlus"
rate_limit = "Lēnāk! Pārāk daudz jūsu meklēšanas vaicājumu!"
warning_fetch_error = "Neizdevās izpildīt pieprasījumu, pirms OmniSearch varēja nolasīt meklēšanas rezultātus."
warning_parse_mismatch = "Meklēšanas rezultāti tika atgriezti formātā, kuru OmniSearch nevarēja apstrādāt."
warning_blocked = "Tika atgriezta captcha vai cita bloķējoša lapa meklēšanas rezultātu vietā."
read_more = "Lasīt vairāk"
view_cached = "Saglabātā kopija"
view_image = "Skatīt attēlu"
visit_site = "Apmeklēt vietni"
engines_label = "Meklēšanas dzinēji"
engines_desc = "Izvēlieties, kurus meklēšanas dzinējus izmantot. Tiek rādīti tikai serverī iespējotie."

34
locales/nl_nl.ini Normal file
View File

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

34
locales/pt_br.ini Normal file
View File

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

34
locales/ru_ru.ini Normal file
View File

@@ -0,0 +1,34 @@
[Meta]
Id = "ru_ru"
Name = "Русский"
Direction = "ltr"
[Keys]
search_placeholder = "Поиск в интернете..."
search_button = "Найти"
surprise_me_button = "Удиви меня"
all_tab = "Все"
images_tab = "Изображения"
settings_tab = "Настройки"
settings_title = "Настройки"
theme_label = "Внешний вид"
theme_desc = "Выберите предпочитаемую цветовую схему."
theme_system = "Системная"
theme_light = "Светлая"
theme_dark = "Тёмная"
language_label = "Язык"
display_language_label = "Язык интерфейса"
language_desc = "Выберите предпочитаемый язык."
save_settings_button = "Сохранить настройки"
no_results = "Ничего не найдено"
error_images = "Ошибка загрузки изображений"
rate_limit = "Слишком много поисковых запросов. Пожалуйста, попробуйте позже."
warning_fetch_error = "Не удалось выполнить запрос до того, как OmniSearch смог прочитать результаты поиска."
warning_parse_mismatch = "Результаты поиска возвращены в формате, который OmniSearch не смог обработать."
warning_blocked = "Вместо результатов поиска возвращена капча или страница блокировки."
read_more = "Подробнее"
view_cached = "Сохранённая копия"
view_image = "Просмотр изображения"
visit_site = "Перейти на сайт"
engines_label = "Поисковые системы"
engines_desc = "Выберите, какие поисковые системы использовать. Отображаются только включённые на сервере."

34
locales/vi_vn.ini Normal file
View File

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

View File

@@ -20,6 +20,7 @@ let
host = ${cfg.settings.server.host}
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"
];

View File

@@ -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;

View File

@@ -17,7 +17,9 @@ char *cache_compute_key(const char *query, int page, const char *engine_name);
void set_cache_ttl_search(int ttl);
void set_cache_ttl_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

View File

@@ -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);
}
}
}
}

View File

@@ -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);

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

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

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

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

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

@@ -0,0 +1,210 @@
#include "RateLimit.h"
#include <arpa/inet.h>
#include <beaker.h>
#include <ctype.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
typedef struct RateLimitEntry {
char client_key[64];
char scope[32];
time_t window_start;
time_t last_seen;
int count;
struct RateLimitEntry *next;
} RateLimitEntry;
extern __thread int current_client_socket;
extern __thread char current_request_buffer[];
static pthread_mutex_t rate_limit_mutex = PTHREAD_MUTEX_INITIALIZER;
static RateLimitEntry *rate_limit_entries = NULL;
static int is_blank_char(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
static const char *str_case_str(const char *haystack, const char *needle) {
size_t nlen = strlen(needle);
for (; *haystack; haystack++) {
if (tolower((unsigned char)*haystack) == tolower((unsigned char)*needle)) {
size_t i;
for (i = 1; i < nlen; i++) {
if (tolower((unsigned char)haystack[i]) !=
tolower((unsigned char)needle[i]))
break;
}
if (i == nlen)
return haystack;
}
}
return NULL;
}
static void trim_copy(char *dest, size_t dest_size, const char *src,
size_t src_len) {
while (src_len > 0 && is_blank_char(*src)) {
src++;
src_len--;
}
while (src_len > 0 && is_blank_char(src[src_len - 1])) {
src_len--;
}
if (dest_size == 0)
return;
if (src_len >= dest_size)
src_len = dest_size - 1;
memcpy(dest, src, src_len);
dest[src_len] = '\0';
}
static void get_client_key(char *client_key, size_t client_key_size) {
const char *header = str_case_str(current_request_buffer, "x-forwarded-for:");
if (!header)
return;
header += strlen("X-Forwarded-For:");
const char *line_end = strpbrk(header, "\r\n");
size_t line_len = line_end ? (size_t)(line_end - header) : strlen(header);
const char *comma = memchr(header, ',', line_len);
size_t value_len = comma ? (size_t)(comma - header) : line_len;
trim_copy(client_key, client_key_size, header, value_len);
}
static void get_client_key_from_socket(char *client_key,
size_t client_key_size) {
struct sockaddr_storage addr;
socklen_t addr_len = sizeof(addr);
if (getpeername(current_client_socket, (struct sockaddr *)&addr, &addr_len) !=
0) {
return;
}
if (addr.ss_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)&addr;
inet_ntop(AF_INET, &ipv4->sin_addr, client_key, client_key_size);
} else if (addr.ss_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)&addr;
inet_ntop(AF_INET6, &ipv6->sin6_addr, client_key, client_key_size);
} else if (addr.ss_family == AF_UNIX) {
snprintf(client_key, client_key_size, "unix:%d", current_client_socket);
}
}
void rate_limit_get_client_key(char *client_key, size_t client_key_size) {
if (!client_key || client_key_size == 0)
return;
client_key[0] = '\0';
get_client_key(client_key, client_key_size);
if (client_key[0] == '\0') {
get_client_key_from_socket(client_key, client_key_size);
}
if (client_key[0] == '\0') {
snprintf(client_key, client_key_size, "nun");
}
}
static void prune_stale_entries(time_t now) {
RateLimitEntry **cursor = &rate_limit_entries;
while (*cursor) {
RateLimitEntry *entry = *cursor;
if (now - entry->last_seen > 9999) {
*cursor = entry->next;
free(entry);
continue;
}
cursor = &entry->next;
}
}
static RateLimitEntry *find_entry(const char *client_key, const char *scope) {
for (RateLimitEntry *entry = rate_limit_entries; entry; entry = entry->next) {
if (strcmp(entry->client_key, client_key) == 0 &&
strcmp(entry->scope, scope) == 0) {
return entry;
}
}
return NULL;
}
static RateLimitEntry *create_entry(const char *client_key, const char *scope,
time_t now) {
RateLimitEntry *entry = (RateLimitEntry *)calloc(1, sizeof(RateLimitEntry));
if (!entry)
return NULL;
snprintf(entry->client_key, sizeof(entry->client_key), "%s", client_key);
snprintf(entry->scope, sizeof(entry->scope), "%s", scope);
entry->window_start = now;
entry->last_seen = now;
entry->next = rate_limit_entries;
rate_limit_entries = entry;
return entry;
}
RateLimitResult rate_limit_check(const char *scope,
const RateLimitConfig *config) {
RateLimitResult result = {.limited = 0, .retry_after_seconds = 0};
if (!scope || !config || config->max_requests <= 0 ||
config->interval_seconds <= 0) {
return result;
}
char client_key[64];
time_t now = time(NULL);
rate_limit_get_client_key(client_key, sizeof(client_key));
pthread_mutex_lock(&rate_limit_mutex);
prune_stale_entries(now);
RateLimitEntry *entry = find_entry(client_key, scope);
if (!entry) {
entry = create_entry(client_key, scope, now);
if (!entry) {
pthread_mutex_unlock(&rate_limit_mutex);
return result;
}
}
entry->last_seen = now;
if (now - entry->window_start >= config->interval_seconds) {
entry->window_start = now;
entry->count = 0;
}
if (entry->count >= config->max_requests) {
result.limited = 1;
result.retry_after_seconds =
config->interval_seconds - (int)(now - entry->window_start);
if (result.retry_after_seconds < 1) {
result.retry_after_seconds = 1;
}
pthread_mutex_unlock(&rate_limit_mutex);
return result;
}
entry->count++;
pthread_mutex_unlock(&rate_limit_mutex);
return result;
}

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

@@ -0,0 +1,20 @@
#ifndef RATE_LIMIT_H
#define RATE_LIMIT_H
#include <stddef.h>
typedef struct {
int max_requests;
int interval_seconds;
} RateLimitConfig;
typedef struct {
int limited;
int retry_after_seconds;
} RateLimitResult;
void rate_limit_get_client_key(char *client_key, size_t client_key_size);
RateLimitResult rate_limit_check(const char *scope,
const RateLimitConfig *config);
#endif

View File

@@ -13,20 +13,44 @@
#include "Routes/ImageProxy.h"
#include "Routes/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;

View File

@@ -1,14 +1,30 @@
#include "Home.h"
#include "../Utility/Utility.h"
#include <beaker.h>
#include <stdlib.h>
#include <string.h>
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;
}

View File

@@ -1,11 +1,31 @@
#include "ImageProxy.h"
#include "../Cache/Cache.h"
#include "../Proxy/Proxy.h"
#include <arpa/inet.h>
#include <curl/curl.h>
#include <curl/urlapi.h>
#include <netdb.h>
#include <netinet/in.h>
#include <openssl/evp.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#define MAX_IMAGE_SIZE (10 * 1024 * 1024)
#define DNS_CACHE_TTL 300
typedef struct DnsCacheEntry {
char hostname[256];
char ip_str[INET_ADDRSTRLEN];
time_t resolved_at;
struct DnsCacheEntry *next;
} DnsCacheEntry;
static DnsCacheEntry *dns_cache = NULL;
static pthread_mutex_t dns_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
typedef struct {
char *data;
@@ -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;
}

View File

@@ -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 <beaker.h>
#include <stdlib.h>
#include <string.h>
static char *build_images_request_cache_key(const char *query, int page,
const char *client_key) {
char scope_key[BUFFER_SIZE_MEDIUM];
snprintf(scope_key, sizeof(scope_key), "images_request:%s",
client_key ? client_key : "unknown");
return cache_compute_key(query, page, scope_key);
}
static char *build_images_href(const char *query, int page) {
const char *safe_query = query ? query : "";
size_t needed = strlen("/images?q=") + strlen(safe_query) + 1;
if (page > 1)
needed += strlen("&p=") + 16;
char *href = (char *)malloc(needed);
if (!href)
return NULL;
snprintf(href, needed, "/images?q=%s", safe_query);
if (page > 1) {
char page_buf[16];
snprintf(page_buf, sizeof(page_buf), "%d", page);
strcat(href, "&p=");
strcat(href, page_buf);
}
return href;
}
static char *images_href_builder(int page, void *data) {
return build_images_href((const char *)data, page);
}
int images_handler(UrlParams *params) {
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("<h1>No query provided</h1>");
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), "<h1>%s</h1>", rate_limit_msg);
send_response(response);
free(request_cache_key);
free(display_query);
free_context(&ctx);
return -1;
}
if (request_cache_key && get_cache_ttl_image() > 0) {
cache_set(request_cache_key, "1", 1);
}
}
ImageResult *results = NULL;
int result_count = 0;
if (scrape_images(raw_query, page, &results, &result_count) != 0 ||
!results) {
send_response("<h1>Error fetching images</h1>");
char error_html[128];
snprintf(error_html, sizeof(error_html), "<h1>%s</h1>", error_images_msg);
send_response(error_html);
free(request_cache_key);
free(display_query);
free_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);

View File

@@ -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 <ctype.h>
#include <openssl/evp.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define URL_HASH_TABLE_SIZE 64
typedef struct UrlHashEntry {
char *url;
struct UrlHashEntry *next;
} UrlHashEntry;
typedef struct {
UrlHashEntry *buckets[URL_HASH_TABLE_SIZE];
} UrlHashTable;
static void url_hash_init(UrlHashTable *ht) {
for (int i = 0; i < URL_HASH_TABLE_SIZE; i++) {
ht->buckets[i] = NULL;
}
}
static unsigned int url_hash(const char *url) {
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len;
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx)
return 0;
EVP_DigestInit_ex(ctx, EVP_md5(), NULL);
EVP_DigestUpdate(ctx, url, strlen(url));
EVP_DigestFinal_ex(ctx, hash, &hash_len);
EVP_MD_CTX_free(ctx);
unsigned int h = 0;
for (unsigned int i = 0; i < hash_len; i++) {
h = h * 31 + hash[i];
}
return h % URL_HASH_TABLE_SIZE;
}
static int url_hash_contains(UrlHashTable *ht, const char *url) {
unsigned int idx = url_hash(url);
for (UrlHashEntry *e = ht->buckets[idx]; e; e = e->next) {
if (strcmp(e->url, url) == 0) {
return 1;
}
}
return 0;
}
static int url_hash_insert(UrlHashTable *ht, const char *url) {
unsigned int idx = url_hash(url);
for (UrlHashEntry *e = ht->buckets[idx]; e; e = e->next) {
if (strcmp(e->url, url) == 0) {
return 0;
}
}
UrlHashEntry *new_entry = malloc(sizeof(UrlHashEntry));
if (!new_entry)
return -1;
new_entry->url = strdup(url);
if (!new_entry->url) {
free(new_entry);
return -1;
}
new_entry->next = ht->buckets[idx];
ht->buckets[idx] = new_entry;
return 0;
}
static void url_hash_free(UrlHashTable *ht) {
for (int i = 0; i < URL_HASH_TABLE_SIZE; i++) {
UrlHashEntry *e = ht->buckets[i];
while (e) {
UrlHashEntry *next = e->next;
free(e->url);
free(e);
e = next;
}
ht->buckets[i] = NULL;
}
}
typedef struct {
const char *query;
InfoBox result;
@@ -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("<h1>No query provided</h1>");
send_redirect("/");
if (has_user_pref) {
for (int i = 0; i < user_engine_count; i++)
free(user_engines[i]);
free(user_engines);
}
free_context(&ctx);
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), "<h1>%s</h1>", rate_limit_msg);
send_response(response);
free(request_cache_key);
if (has_user_pref) {
for (int i = 0; i < user_engine_count; i++)
free(user_engines[i]);
free(user_engines);
}
free_context(&ctx);
return -1;
}
if (request_cache_key && get_cache_ttl_search() > 0) {
cache_set(request_cache_key, "1", 1);
}
}
int filter_engine_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled &&
engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref))
filter_engine_count++;
}
if (filter_engine_count > 1) {
char ***filter_matrix = NULL;
int *filter_inner_counts = NULL;
int filter_count = 0;
char *all_href = build_search_href(raw_query, "all", 1);
filter_count = add_link_to_collection(
all_href, "All",
selected_engine ? "engine-filter" : "engine-filter active",
&filter_matrix, &filter_inner_counts, filter_count);
free(all_href);
for (int i = 0; i < ENGINE_COUNT; i++) {
if (!ENGINE_REGISTRY[i].enabled ||
!engine_allowed_for_user(&ENGINE_REGISTRY[i], user_engines,
user_engine_count, has_user_pref))
continue;
char *filter_href =
build_search_href(raw_query, ENGINE_REGISTRY[i].id, 1);
const char *filter_class =
(selected_engine && &ENGINE_REGISTRY[i] == selected_engine)
? "engine-filter active"
: "engine-filter";
filter_count = add_link_to_collection(filter_href, ENGINE_REGISTRY[i].name,
filter_class, &filter_matrix,
&filter_inner_counts, filter_count);
free(filter_href);
}
if (filter_count > 0) {
context_set_array_of_arrays(&ctx, "engine_filters", filter_matrix,
filter_count, filter_inner_counts);
for (int i = 0; i < filter_count; i++) {
for (int j = 0; j < LINK_FIELD_COUNT; j++)
free(filter_matrix[i][j]);
free(filter_matrix[i]);
}
free(filter_matrix);
free(filter_inner_counts);
}
}
if (engine_idx > 0) {
scrape_engines_parallel(jobs, engine_idx);
}
if (page == 1) {
@@ -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("<h1>No results found</h1>");
char no_results_html[128];
snprintf(no_results_html, sizeof(no_results_html), "<h1>%s</h1>", no_results_msg);
send_response(no_results_html);
return 0;
}
@@ -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;

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

@@ -0,0 +1,140 @@
#include "Settings.h"
#include "../Scraping/Scraping.h"
#include "../Utility/Utility.h"
#include <beaker.h>
#include <stdlib.h>
#include <string.h>
int settings_handler(UrlParams *params) {
const char *query = "";
if (params) {
for (int i = 0; i < params->count; i++) {
if (strcmp(params->params[i].key, "q") == 0) {
query = params->params[i].value;
}
}
}
char *theme = get_theme("system");
char *locale = get_locale(NULL);
LocaleInfo locales[32];
int locale_count = beaker_get_all_locales(locales, 32);
char **locale_data[32];
int inner_counts[32];
for (int i = 0; i < locale_count; i++) {
locale_data[i] = malloc(sizeof(char *) * 2);
locale_data[i][0] = locales[i].meta.id;
locale_data[i][1] = locales[i].meta.name;
inner_counts[i] = 2;
}
char **user_engines = NULL;
int user_engine_count = 0;
int has_user_pref =
(get_user_engines(&user_engines, &user_engine_count) == 0);
int enabled_count = 0;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (ENGINE_REGISTRY[i].enabled)
enabled_count++;
}
char ***engine_data = NULL;
int *engine_inner = NULL;
if (enabled_count > 0) {
engine_data = malloc(sizeof(char **) * enabled_count);
engine_inner = malloc(sizeof(int) * enabled_count);
int idx = 0;
for (int i = 0; i < ENGINE_COUNT; i++) {
if (!ENGINE_REGISTRY[i].enabled)
continue;
int is_selected = !has_user_pref;
if (has_user_pref) {
for (int j = 0; j < user_engine_count; j++) {
if (strcmp(user_engines[j], ENGINE_REGISTRY[i].id) == 0) {
is_selected = 1;
break;
}
}
}
engine_data[idx] = malloc(sizeof(char *) * 3);
engine_data[idx][0] = (char *)ENGINE_REGISTRY[i].id;
engine_data[idx][1] = (char *)ENGINE_REGISTRY[i].name;
engine_data[idx][2] = is_selected ? "checked" : "";
engine_inner[idx] = 3;
idx++;
}
}
TemplateContext ctx = new_context();
beaker_set_locale(&ctx, locale);
context_set(&ctx, "query", query);
context_set(&ctx, "theme", theme);
context_set(&ctx, "locale", locale);
context_set_array_of_arrays(&ctx, "locales", locale_data, locale_count,
inner_counts);
char **themes = NULL;
int themes_count = 0;
get_available_themes(&themes, &themes_count);
if (themes_count > 0) {
char ***theme_ptrs = malloc(sizeof(char **) * themes_count);
int *theme_inner = malloc(sizeof(int) * themes_count);
for (int i = 0; i < themes_count; i++) {
theme_ptrs[i] = malloc(sizeof(char *) * 2);
theme_ptrs[i][0] = themes[i];
theme_ptrs[i][1] = strdup(themes[i]);
if (theme_ptrs[i][1][0] >= 'a' && theme_ptrs[i][1][0] <= 'z') {
theme_ptrs[i][1][0] = theme_ptrs[i][1][0] - 'a' + 'A';
}
theme_inner[i] = 2;
}
context_set_array_of_arrays(&ctx, "themes", theme_ptrs, themes_count,
theme_inner);
for (int i = 0; i < themes_count; i++) {
free(theme_ptrs[i][1]);
free(theme_ptrs[i]);
}
free(theme_ptrs);
free(theme_inner);
}
if (enabled_count > 0) {
context_set_array_of_arrays(&ctx, "enabled_engines", engine_data,
enabled_count, engine_inner);
context_set(&ctx, "has_enabled_engines", "1");
}
for (int i = 0; i < locale_count; i++) {
free(locale_data[i]);
}
if (engine_data) {
for (int i = 0; i < enabled_count; i++)
free(engine_data[i]);
free(engine_data);
}
free(engine_inner);
if (has_user_pref) {
for (int i = 0; i < user_engine_count; i++)
free(user_engines[i]);
free(user_engines);
}
char *rendered_html = render_template("settings.html", &ctx);
send_response(rendered_html);
free(rendered_html);
free(theme);
free(locale);
free_context(&ctx);
return 0;
}

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

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

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

@@ -0,0 +1,66 @@
#include "SettingsSave.h"
#include "../Scraping/Scraping.h"
#include "../Utility/Utility.h"
#include <stdlib.h>
#include <string.h>
#define MAX_ENGINE_IDS ENGINE_COUNT
int settings_save_handler(UrlParams *params) {
const char *theme = "";
const char *locale = "";
const char *query = "";
int engines_present = 0;
char selected_ids[ENGINE_COUNT][32];
int selected_count = 0;
if (params) {
for (int i = 0; i < params->count; i++) {
if (strcmp(params->params[i].key, "theme") == 0) {
theme = params->params[i].value;
} else if (strcmp(params->params[i].key, "locale") == 0) {
locale = params->params[i].value;
} else if (strcmp(params->params[i].key, "q") == 0) {
query = params->params[i].value;
} else if (strcmp(params->params[i].key, "engines_present") == 0) {
engines_present = 1;
} else if (strncmp(params->params[i].key, "engine_", 7) == 0 &&
strcmp(params->params[i].value, "1") == 0) {
const char *engine_id = params->params[i].key + 7;
if (engine_id[0] != '\0' && is_engine_id_enabled(engine_id) &&
selected_count < ENGINE_COUNT) {
strncpy(selected_ids[selected_count], engine_id,
sizeof(selected_ids[selected_count]) - 1);
selected_ids[selected_count][sizeof(selected_ids[selected_count]) - 1] =
'\0';
selected_count++;
}
}
}
}
if (strlen(theme) > 0) {
set_cookie("theme", theme, "Fri, 31 Dec 2038 23:59:59 GMT", "/", false, false);
}
if (strlen(locale) > 0) {
set_cookie("locale", locale, "Fri, 31 Dec 2038 23:59:59 GMT", "/", false, false);
}
if (engines_present) {
char cookie_value[512];
cookie_value[0] = '\0';
for (int i = 0; i < selected_count; i++) {
if (i > 0)
strcat(cookie_value, ",");
strcat(cookie_value, selected_ids[i]);
}
set_cookie("engines", cookie_value, "Fri, 31 Dec 2038 23:59:59 GMT", "/",
false, false);
}
char redirect_url[512];
snprintf(redirect_url, sizeof(redirect_url), "/settings?q=%s", query);
send_redirect(redirect_url);
return 0;
}

View File

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

View File

@@ -28,113 +28,82 @@ static char *build_proxy_url(const char *image_url) {
return proxy_url;
}
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++;
}
}

View File

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

View File

@@ -25,6 +25,12 @@ char *pretty_display_url(const char *input) {
strncpy(temp, start, sizeof(temp) - 1);
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';
}

View File

@@ -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) {

View File

@@ -1,4 +1,87 @@
#include "Utility.h"
#include "../Scraping/Scraping.h"
#include <beaker.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char global_default_locale[32] = "en_gb";
static char **themes_list = NULL;
static int themes_count = 0;
static int themes_initialized = 0;
void init_themes(const char *static_path) {
if (themes_initialized)
return;
themes_initialized = 1;
char themes_dir[512];
snprintf(themes_dir, sizeof(themes_dir), "%s/themes", static_path);
DIR *dir = opendir(themes_dir);
if (!dir)
return;
struct dirent *entry;
int capacity = 4;
themes_list = malloc(sizeof(char *) * capacity);
themes_count = 0;
while ((entry = readdir(dir)) != NULL) {
size_t len = strlen(entry->d_name);
if (len > 4 && strcmp(entry->d_name + len - 4, ".css") == 0) {
if (themes_count >= capacity) {
capacity *= 2;
themes_list = realloc(themes_list, sizeof(char *) * capacity);
}
themes_list[themes_count] = strndup(entry->d_name, len - 4);
themes_count++;
}
}
closedir(dir);
for (int i = 0; i < themes_count; i++) {
for (int j = i + 1; j < themes_count; j++) {
int priority_i = 0, priority_j = 0;
if (strcmp(themes_list[i], "system") == 0)
priority_i = 0;
else if (strcmp(themes_list[i], "light") == 0)
priority_i = 1;
else if (strcmp(themes_list[i], "dark") == 0)
priority_i = 2;
else
priority_i = 3;
if (strcmp(themes_list[j], "system") == 0)
priority_j = 0;
else if (strcmp(themes_list[j], "light") == 0)
priority_j = 1;
else if (strcmp(themes_list[j], "dark") == 0)
priority_j = 2;
else
priority_j = 3;
if (priority_i > priority_j ||
(priority_i == priority_j &&
strcmp(themes_list[i], themes_list[j]) > 0)) {
char *tmp = themes_list[i];
themes_list[i] = themes_list[j];
themes_list[j] = tmp;
}
}
}
}
void get_available_themes(char ***out_themes, int *out_count) {
*out_themes = themes_list;
*out_count = themes_count;
}
void set_default_locale(const char *locale) {
if (locale && strlen(locale) > 0) {
strncpy(global_default_locale, locale, sizeof(global_default_locale) - 1);
global_default_locale[sizeof(global_default_locale) - 1] = '\0';
}
}
int hex_to_int(char c) {
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;
}

View File

@@ -1,6 +1,34 @@
#ifndef UTILITY_H
#define UTILITY_H
#include <beaker.h>
#ifndef VERSION
#define VERSION "unknown"
#endif
#ifndef GIT_REMOTE
#define GIT_REMOTE "https://git.bwaaa.monster/omnisearch"
#endif
#define LINK_FIELD_COUNT 3
int hex_to_int(char c);
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

View File

@@ -1,43 +1,26 @@
@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: system-ui;
}
html {
height: 100%;
}
body {
background-color: var(--bg-main);
background-image: radial-gradient(
circle at top end,
var(--bg-card) 0%,
var(--bg-main) 100%
);
background-attachment: fixed;
color: var(--text-primary);
font-family:
system-ui,
-apple-system,
sans-serif;
margin: 0;
padding: 0;
min-height: 100%;
-webkit-tap-highlight-color: transparent;
}
@@ -56,11 +39,6 @@ img[src=""] {
align-items: center;
min-height: 100vh;
padding: 20px;
background: radial-gradient(
circle at top right,
var(--accent) 0%,
var(--bg-main) 100%
);
}
.view-home .container {
@@ -118,18 +96,82 @@ img[src=""] {
background: var(--bg-card);
color: var(--text-primary);
border-color: var(--border);
text-decoration: none;
display: inline-flex;
align-items: center;
padding: 10px 24px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border);
}
.view-home .btn-secondary:hover {
background: var(--border);
border-color: var(--text-secondary);
}
.home-settings-btn {
position: fixed;
top: 27px;
inset-inline-end: 60px;
width: 24px;
height: 24px;
background-color: var(--text-primary);
-webkit-mask-image: url("/static/settings.svg");
mask-image: url("/static/settings.svg");
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
text-decoration: none;
}
.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;
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 +186,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 +214,14 @@ h1 span {
box-shadow: 0 0 0 4px var(--accent-glow);
}
.nav-tabs {
padding: 0 60px;
padding-inline: 60px;
border-bottom: 1px solid var(--border);
background: var(--bg-main);
width: 100%;
}
.nav-container {
display: flex;
gap: 30px;
max-width: 1200px;
}
.nav-tabs a {
padding: 14px 0;
@@ -188,6 +240,9 @@ h1 span {
color: var(--accent);
border-bottom-color: var(--accent);
}
.nav-right {
margin-inline-start: auto;
}
.image-results-container {
padding: 30px 60px;
}
@@ -292,11 +347,71 @@ h1 span {
display: grid;
grid-template-columns: 140px minmax(0, 700px) 450px;
gap: 60px;
padding: 30px 60px;
padding-block: 30px;
padding-inline: 60px;
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 2px;
position: relative;
}
.result-favicon {
width: 16px;
height: 16px;
flex-shrink: 0;
background-size: cover;
background-position: center;
position: absolute;
inset-inline-start: -24px;
}
.url {
color: var(--text-secondary);
font-size: 0.85rem;
display: block;
margin-bottom: 4px;
}
@media (max-width: 768px) {
.result-favicon {
width: 14px;
height: 14px;
inset-inline-start: -20px;
}
}
@media (max-width: 480px) {
.result-favicon {
width: 12px;
height: 12px;
inset-inline-start: -16px;
}
}
.results-container {
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 +450,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,27 +544,32 @@ 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);
}
[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: 20px 30px;
padding-block: 20px;
padding-inline: 30px;
gap: 20px;
}
header {
gap: 20px;
}
.results-container,
@@ -463,23 +577,37 @@ h1 span {
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: 0 30px;
padding-inline: 30px;
}
header {
padding: 15px 30px;
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: 12px 16px;
padding-block: 12px;
padding-inline: 16px;
text-align: center;
}
h1 {
@@ -495,7 +623,7 @@ h1 span {
.nav-tabs {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding: 0 16px;
padding-inline: 16px;
}
.nav-container {
gap: 24px;
@@ -506,7 +634,9 @@ h1 span {
font-size: 0.95rem;
}
.content-layout {
padding: 16px;
padding-inline-start: 40px;
padding-inline-end: 16px;
padding-block: 16px;
gap: 16px;
}
.result {
@@ -550,7 +680,7 @@ h1 span {
max-width: 200px;
}
.image-results-container {
padding: 16px;
padding-inline: 16px;
}
.pagination {
flex-wrap: wrap;
@@ -565,7 +695,6 @@ h1 span {
display: flex;
justify-content: center;
align-items: center;
transform: translateY(-5vh);
padding: 20px 16px;
min-height: 100vh;
}
@@ -595,8 +724,17 @@ h1 span {
}
@media (max-width: 600px) {
.content-layout {
padding-inline-start: 28px;
padding-inline-end: 16px;
padding-block: 16px;
}
.settings-layout {
padding: 0;
}
header {
padding: 12px 12px;
padding-inline: 12px;
padding-block: 12px;
}
.search-box {
font-size: 0.95rem;
@@ -631,3 +769,193 @@ h1 span {
font-size: 0.75rem;
}
}
.settings-layout {
padding-block: 30px;
padding-inline-start: 260px;
padding-inline-end: 60px;
}
.settings-container {
max-width: 700px;
}
.settings-title {
font-size: 1.8rem;
font-weight: 700;
margin: 0 0 32px 0;
letter-spacing: -0.5px;
}
.settings-section {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
}
.settings-section-title {
font-size: 1.1rem;
font-weight: 700;
margin: 0 0 4px 0;
}
.settings-section-desc {
color: var(--text-secondary);
font-size: 0.9rem;
margin: 0 0 20px 0;
line-height: 1.4;
}
.settings-field {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
}
.settings-field + .settings-field {
border-top: 1px solid var(--border);
}
.settings-label {
font-size: 0.95rem;
color: var(--text-primary);
}
.settings-select {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg-main);
color: var(--text-primary);
font-size: 0.9rem;
outline: none;
cursor: pointer;
transition: border-color 0.2s;
}
.settings-select:focus {
border-color: var(--accent);
}
.settings-checkbox {
width: 18px;
height: 18px;
accent-color: var(--accent);
cursor: pointer;
}
.settings-actions {
display: flex;
gap: 12px;
margin-top: 8px;
padding-bottom: 40px;
justify-content: flex-start;
}
.settings-actions .btn-primary {
background: var(--accent);
color: var(--bg-main);
border: 1px solid transparent;
padding: 10px 24px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
touch-action: manipulation;
}
.settings-actions .btn-primary:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.settings-actions .btn-secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border);
padding: 10px 24px;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
touch-action: manipulation;
}
.settings-actions .btn-secondary:hover {
background: var(--border);
border-color: var(--text-secondary);
}
@media (max-width: 768px) {
.settings-layout {
padding: 12px;
display: block;
}
.settings-container {
max-width: 100%;
}
.settings-title {
font-size: 1.4rem;
margin-bottom: 24px;
}
.settings-section {
padding: 16px;
}
.settings-field {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.settings-actions {
flex-direction: column;
}
.settings-actions .btn-primary,
.settings-actions .btn-secondary {
width: 100%;
text-align: center;
}
}
[dir="rtl"] {
direction: rtl;
unicode-bidi: embed;
}
[dir="rtl"] header {
flex-direction: row-reverse;
direction: ltr;
}
[dir="rtl"] .nav-container {
flex-direction: row-reverse;
direction: ltr;
}
[dir="rtl"] .search-box {
text-align: right;
direction: rtl;
}
[dir="rtl"] .url {
text-align: end;
}
[dir="rtl"] .nav-settings-icon {
margin-inline-start: unset;
margin-inline-end: auto;
}
[dir="rtl"] .settings-actions .btn-primary {
margin-inline-end: auto;
}
@media (max-width: 768px) {
[dir="rtl"] header {
flex-direction: column;
}
}

4
static/settings.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 829 B

View File

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

10
static/themes/dark.css Normal file
View File

@@ -0,0 +1,10 @@
:root {
--bg-main: #121212;
--bg-card: #1e1e1e;
--border: #333333;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--text-muted: #d1d1d1;
--accent: #e2e2e2;
--accent-glow: rgba(255,255,255,0.1);
}

10
static/themes/light.css Normal file
View File

@@ -0,0 +1,10 @@
:root {
--bg-main: #ffffff;
--bg-card: #f8f9fa;
--border: #e0e0e0;
--text-primary: #1a1a1a;
--text-secondary: #5f6368;
--text-muted: #757575;
--accent: #202124;
--accent-glow: rgba(0,0,0,0.05);
}

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

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

View File

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

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
<head>
<meta charset="UTF-8">
@@ -7,27 +7,32 @@
<title>
OmniSearch Images - {{query}}
</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="static/main.css">
<link rel="stylesheet" href="static/themes/{{theme}}.css">
</head>
<body class="images-view">
<header>
<h1>
<a id="header-icon" href="/"><span>Void</span>arc</a>
</h1>
<a href="/" class="logo-link"><h1>
Omni<span>Search</span>
</h1></a>
<form action="/images" method="GET" class="search-form">
<input name="q" autocomplete="off"="text" class="search-box" placeholder="Search for images..."
<input name="q" autocomplete="off"="text" class="search-box" placeholder="{{l("search_placeholder")}}"
value="{{query}}">
</form>
<a href="/settings?q={{query}}" class="nav-settings-icon" title="{{l("settings_tab")}}"></a>
</header>
<nav class="nav-tabs">
<div class="nav-container">
<a href="/search?q={{query}}">
All
{{l("all_tab")}}
</a>
<a href="/images?q={{query}}" class="active">
Images
{{l("images_tab")}}
</a>
<a href="/settings?q={{query}}" class="nav-settings-link">
{{l("settings_tab")}}
</a>
</div>
</nav>
@@ -40,10 +45,10 @@
<div class="image-overlay">
<div class="overlay-buttons">
<a href="{{img[3]}}" target="_blank" class="overlay-btn primary">
View Image
{{l("view_image")}}
</a>
<a href="{{img[2]}}" target="_blank" class="overlay-btn secondary">
Visit Site
{{l("visit_site")}}
</a>
</div>
</div>
@@ -59,49 +64,16 @@
</div>
{{endfor}}
</div>
<nav class="pagination">
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}">
&larr;
</a>
{{if two_prev_page != 0}}
<a class="pagination-btn prev" href="/images?q={{query}}&p={{two_prev_page}}">
{{two_prev_page}}
</a>
{{endif}}
{{if prev_page != 0}}
<a class="pagination-btn prev" href="/images?q={{query}}&p={{prev_page}}">
{{prev_page}}
</a>
{{endif}}
<a class="pagination-current" href="/images?q={{query}}&p={{page}}">
{{page}}
</a>
<a class="pagination-btn next" href="/images?q={{query}}&p={{next_page}}">
{{next_page}}
</a>
<a class="pagination-btn next" href="/images?q={{query}}&p={{two_next_page}}">
{{two_next_page}}
</a>
{{if prev_page == 0}}
<a class="pagination-btn prev" href="/images?q={{query}}&p=4">
4
</a>
{{endif}}
{{if two_prev_page == 0}}
<a class="pagination-btn prev" href="/images?q={{query}}&p=5">
5
</a>
{{endif}}
<a class="pagination-btn next" href="/images?q={{query}}&p={{next_page}}">
&rarr;
</a>
</nav>
{{if exists pagination_links}}
<nav class="pagination">
{{for link in pagination_links}}
<a class="{{link[2]}}" href="{{link[0]}}">
{{link[1]}}
</a>
{{endfor}}
</nav>
{{endif}}
</main>
</body>
</html>
</html>

View File

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

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
<head>
<meta charset="UTF-8">
@@ -8,7 +8,8 @@
OmniSearch - {{query}}
</title>
<link rel="stylesheet" href="static/main.css">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="static/themes/{{theme}}.css">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="search"
type="application/opensearchdescription+xml"
title="OmniSearch" href="/opensearch.xml">
@@ -16,21 +17,26 @@
<body class="results-view">
<header>
<h1>
<a id="header-icon" href="/"><span>Void</span>arc</a>
</h1>
<a href="/" class="logo-link"><h1>
VOID<span>ARC</span>
</h1></a>
<form action="/search" method="GET" class="search-form">
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="Search the web..."
<input name="engine" type="hidden" value="{{selected_engine}}">
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="{{l("search_placeholder")}}"
value="{{query}}">
</form>
<a href="/settings?q={{query}}" class="nav-settings-icon" title="{{l("settings_tab")}}"></a>
</header>
<nav class="nav-tabs">
<div class="nav-container">
<a href="/search?q={{query}}" class="active">
All
<a href="{{search_href}}" class="active">
{{l("all_tab")}}
</a>
<a href="/images?q={{query}}">
Images
{{l("images_tab")}}
</a>
<a href="/settings?q={{query}}" class="nav-settings-link">
{{l("settings_tab")}}
</a>
</div>
</nav>
@@ -38,6 +44,16 @@
<aside class="sidebar-spacer">
</aside>
<main class="results-container">
{{if exists engine_filters}}
<nav class="engine-filter-list">
{{for filter in engine_filters}}
<a href="{{filter[0]}}" class="{{filter[2]}}">
{{filter[1]}}
</a>
{{endfor}}
</nav>
{{endif}}
{{if exists engine_warnings}}
<section class="engine-warning-list">
{{for warning in engine_warnings}}
@@ -55,9 +71,15 @@
{{for result in results}}
<div class="result">
<span class="url">
{{result[1]}}
<div class="result-header">
<div class="result-favicon"
style="background-image: url('/proxy?url=https://{{result[4]}}/favicon.ico'), url('/proxy?url=https://{{result[4]}}/favicon.png');">
</div>
<span class="url">
{{result[1]}}
</span>
</span>
</div>
<a href="{{result[0]}}">
{{result[2]}}
</a>
@@ -65,53 +87,20 @@
{{result[3]}}
</p>
<span>
<a class="cached" href="https://web.archive.org/web/{{result[0]|safe}}">View Cached</a>
<a class="cached" href="https://web.archive.org/web/{{result[0]|safe}}">{{l("view_cached")}}</a>
</span>
</div>
{{endfor}}
{{if exists pagination_links}}
<nav class="pagination">
<a class="pagination-btn prev" href="/search?q={{query}}&p={{prev_page}}">
&larr;
</a>
{{if two_prev_page != 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p={{two_prev_page}}">
{{two_prev_page}}
</a>
{{endif}}
{{if prev_page != 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p={{prev_page}}">
{{prev_page}}
</a>
{{endif}}
<a class="pagination-current" href="/search?q={{query}}&p={{page}}">
{{page}}
</a>
<a class="pagination-btn next" href="/search?q={{query}}&p={{next_page}}">
{{next_page}}
</a>
<a class="pagination-btn next" href="/search?q={{query}}&p={{two_next_page}}">
{{two_next_page}}
</a>
{{if prev_page == 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p=4">
4
</a>
{{endif}}
{{if two_prev_page == 0}}
<a class="pagination-btn prev" href="/search?q={{query}}&p=5">
5
</a>
{{endif}}
<a class="pagination-btn next" href="/search?q={{query}}&p={{next_page}}">
&rarr;
{{for link in pagination_links}}
<a class="{{link[2]}}" href="{{link[0]}}">
{{link[1]}}
</a>
{{endfor}}
</nav>
{{endif}}
</main>
<aside class="infobox-sidebar">
{{if exists infoboxes}}
@@ -126,10 +115,12 @@
<img src="{{info[1]}}" alt="{{info[0]}}" class="infobox-image">
<div class="infobox-content"> <p class="infobox-summary">
{{info[2]|safe}}
</p>
</p>
{{if info[3] != "#"}}
<a class="read-more" href="{{info[3]}}">
Read More
{{l("read_more")}}
</a>
{{endif}}
</div>
</div>
</section>

99
templates/settings.html Normal file
View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="{{__locale_id}}" dir="{{__locale_direction}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>
OmniSearch - {{l("settings_title")}}
</title>
<link rel="stylesheet" href="static/main.css">
<link rel="stylesheet" href="static/themes/{{theme}}.css">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="search"
type="application/opensearchdescription+xml"
title="OmniSearch" href="/opensearch.xml">
</head>
<body class="settings-view">
<header>
<a href="/" class="logo-link"><h1>
Omni<span>Search</span>
</h1></a>
{{if query != ""}}
<form action="/search" method="GET" class="search-form">
<input name="q" type="text" class="search-box" autocomplete="off" placeholder="{{l("search_placeholder")}}"
value="{{query}}">
</form>
{{endif}}
{{if query != ""}}
<a href="/search?q={{query}}" class="nav-settings-icon active" title="{{l("settings_tab")}}"></a>
{{else}}
<a href="/" class="nav-settings-icon active" title="{{l("settings_tab")}}"></a>
{{endif}}
</header>
{{if query != ""}}
<nav class="nav-tabs">
<div class="nav-container">
<a href="/search?q={{query}}">
{{l("all_tab")}}
</a>
<a href="/images?q={{query}}">
{{l("images_tab")}}
</a>
<a href="/settings" class="active nav-settings-link">
{{l("settings_tab")}}
</a>
</div>
</nav>
{{endif}}
<div class="settings-layout">
<main class="settings-container">
<form action="/save_settings" method="GET">
<input type="hidden" name="q" value="{{query}}">
<section class="settings-section">
<h3 class="settings-section-title">{{l("theme_label")}}</h3>
<p class="settings-section-desc">{{l("theme_desc")}}</p>
<div class="settings-field">
<label class="settings-label" for="theme">{{l("theme_label")}}</label>
<select id="theme" name="theme" class="settings-select">
{{for t in themes}}
<option value="{{t[0]}}" {{if theme == t[0]}}selected{{endif}}>{{t[1]}}</option>
{{endfor}}
</select>
</div>
</section>
<section class="settings-section">
<h3 class="settings-section-title">{{l("language_label")}}</h3>
<p class="settings-section-desc">{{l("language_desc")}}</p>
<div class="settings-field">
<label class="settings-label" for="locale">{{l("display_language_label")}}</label>
<select id="locale" name="locale" class="settings-select">
{{for loc in locales}}
<option value="{{loc[0]}}" {{if __locale_id == loc[0]}}selected{{endif}}>{{loc[1]}}</option>
{{endfor}}
</select>
</div>
</section>
{{if has_enabled_engines}}
<section class="settings-section">
<h3 class="settings-section-title">{{l("engines_label")}}</h3>
<p class="settings-section-desc">{{l("engines_desc")}}</p>
<input type="hidden" name="engines_present" value="1">
{{for eng in enabled_engines}}
<div class="settings-field">
<label class="settings-label" for="engine_{{eng[0]}}">{{eng[1]}}</label>
<input type="checkbox" id="engine_{{eng[0]}}" name="engine_{{eng[0]}}" value="1" class="settings-checkbox" {{eng[2]}}>
</div>
{{endfor}}
</section>
{{endif}}
<div class="settings-actions">
<button type="submit" class="btn-primary">{{l("save_settings_button")}}</button>
</div>
</form>
</main>
</div>
</body>
</html>