620 Commits

Author SHA1 Message Date
pax
6801a0b45e security: fix #4 — chmod data_dir to 0o700 on POSIX
The data directory holds the SQLite database whose `sites` table
stores api_key and api_user in plaintext. Previous behavior used
the inherited umask (typically 0o755), which leaves the dir
world-traversable on shared workstations and on networked home
dirs whose home is 0o755. Tighten to 0o700 unconditionally on
every data_dir() call so the fix is applied even when an older
version (or external tooling) left the directory loose.

Failures from filesystems that don't support chmod (some FUSE
mounts) are swallowed — better to keep working than refuse to
start. Windows: no-op, NTFS ACLs handle this separately.

behavior change from v0.2.5: ~/.local/share/booru-viewer is now
0o700 even if it was previously 0o755.

Audit-Ref: SECURITY_AUDIT.md finding #4
Severity: Medium
2026-04-11 16:14:30 -05:00
pax
19a22be59c security: fix #3 — redact params in GelbooruClient debug log
Same fix as danbooru.py and e621.py — Gelbooru's params dict
carries api_key + user_id when configured. Route through
redact_params() before the debug log emits them.

Audit-Ref: SECURITY_AUDIT.md finding #3
Severity: Medium
2026-04-11 16:13:25 -05:00
pax
49fa2c5b7a security: fix #3 — redact params in E621Client debug log
Same fix as danbooru.py — the search() log.debug params line
previously emitted login + api_key. Route through redact_params().

Audit-Ref: SECURITY_AUDIT.md finding #3
Severity: Medium
2026-04-11 16:13:06 -05:00
pax
c0c8fdadbf drop unused httpx[http2] extra
http2 was declared in the dependency spec but no httpx client
actually passes http2=True, so the extra (and its h2 pull-in) was
dead weight.
2026-04-11 16:12:50 -05:00
pax
9a3bb697ec security: fix #3 — redact params in DanbooruClient debug log
The log.debug(f"  params: {params}") line in search() previously
dumped login + api_key to the booru logger at DEBUG level. Route
the params dict through redact_params() so the keys are replaced
with *** before formatting.

Audit-Ref: SECURITY_AUDIT.md finding #3
Severity: Medium
2026-04-11 16:12:47 -05:00
pax
d6909bf4d7 security: fix #3 — redact URL in BooruClient._log_request
The httpx request event hook converts request.url to a str so
log_connection can parse it — at that point the credential query
params (login, api_key, etc.) are in scope and could be captured
by any traceback, debug hook, or monitoring agent observing the
hook call. Pipe through redact_url() first so the rendered string
never carries the secrets, even transiently.

Audit-Ref: SECURITY_AUDIT.md finding #3
Severity: Medium
2026-04-11 16:12:28 -05:00
pax
c735db0c68 security: fix #1 — wire SSRF hook into detect_site_type client
detect_site_type constructs a fresh BooruClient._shared_client
directly (bypassing the BooruClient.client property) for the
/posts.json, /index.php, and /post.json probes. The hooks set
here are the ones installed on that initial construction — if
detection runs before any BooruClient instance's .client is
accessed, the shared singleton must still have SSRF validation
and connection logging.

This additionally closes finding #16 for the detect client — site
detection requests now appear in the connection log instead of
being invisible.

behavior change from v0.2.5: Test Connection from the site dialog
now rejects private-IP targets. Adding a local/RFC1918 booru via
the "auto-detect type" dialog will fail with "blocked request
target ..." instead of probing it. Explicit api_type selection
still goes through the BooruClient.client path, which is also
now protected.

Audit-Ref: SECURITY_AUDIT.md finding #1
Also-Closes: SECURITY_AUDIT.md finding #16 (detect half)
Severity: High
2026-04-11 16:11:37 -05:00
pax
ef95509551 security: fix #1 — wire SSRF hook into E621Client custom client
E621 maintains its own httpx.AsyncClient because their TOS requires
a per-user User-Agent string that BooruClient's shared client can't
carry. The client is rebuilt on User-Agent change, so the hook must
be installed in the same construction path.

Also installs BooruClient._log_request as a second hook (this
additionally closes finding #16 for the e621 client — e621 requests
previously bypassed the connection log entirely, and this wires
them in consistently with the base client).

Audit-Ref: SECURITY_AUDIT.md finding #1
Also-Closes: SECURITY_AUDIT.md finding #16 (e621 half)
Severity: High
2026-04-11 16:11:12 -05:00
pax
ec79be9c83 security: fix #1 — wire SSRF hook into cache download client
Adds validate_public_request to the cache module's shared httpx
client event_hooks. Covers image/video/thumbnail downloads, which
are the most likely exfil path — file_url comes straight from the
booru JSON response and previously followed any 3xx that landed,
so a hostile booru could point downloads at a private IP. Every
redirect hop is now rejected if the target is non-public.

The import is lazy inside _get_shared_client because
core.api.base imports log_connection from this module; a top-level
`from .api._safety import ...` would circular-import through
api/__init__.py during cache.py load. By the time
_get_shared_client is called the api package is fully loaded.

Audit-Ref: SECURITY_AUDIT.md finding #1
Severity: High
2026-04-11 16:10:50 -05:00
pax
6eebb77ae5 security: fix #1 — wire SSRF hook into BooruClient shared client
Adds validate_public_request to the BooruClient event_hooks list so
every request (and every redirect hop) is checked against the block
list from _safety.py. Danbooru, Gelbooru, and Moebooru subclasses
all go through BooruClient.client and inherit the protection.

Preserves the existing _log_request hook by listing both hooks in
order: validate first (so blocked hops never reach the log), then
log.

Audit-Ref: SECURITY_AUDIT.md finding #1
Severity: High
2026-04-11 16:10:12 -05:00
pax
013fe43f95 security: fix #1 — add public-host validator helper
Introduces core/api/_safety.py containing check_public_host and the
validate_public_request async request-hook. The hook rejects any URL
whose host is (or resolves to) loopback, RFC1918, link-local
(including 169.254.169.254 cloud metadata), CGNAT, unique-local v6,
or multicast. Called on every request hop so it covers both the
initial URL and every redirect target that httpx would otherwise
follow blindly.

Also exports redact_url / redact_params for finding #3 — the
secret-key set lives in the same module since both #1 and #3 work
is wired through httpx client event_hooks. Helper is stdlib-only
(ipaddress, socket, urllib.parse) plus httpx; no new deps.

Not yet wired into any httpx client; per-file wiring commits follow.

Audit-Ref: SECURITY_AUDIT.md finding #1
Severity: High
2026-04-11 16:09:53 -05:00
pax
72803f0b14 security: fix #2 — wire hardened mpv options into _MpvGLWidget
Replaces the inline mpv.MPV(...) literal kwargs with a call through
build_mpv_kwargs(), which adds ytdl=no, load_scripts=no, a lavf
protocol whitelist (file,http,https,tls,tcp), and POSIX input_conf
lockdown. Closes the yt-dlp delegation surface (CVE-prone extractors
invoked on attacker-supplied URLs) and the concat:/subfile: local-
file-read gadget via ffmpeg's lavf demuxer.

behavior change from v0.2.5: any file_url whose host is only
handled by yt-dlp (youtube.com, reddit.com, etc.) will no longer
play. Boorus do not legitimately return such URLs, so in practice
this only affects hostile responses. Cached local files and direct
https .mp4/.webm/.mkv continue to work.

Manually smoke tested: played a cached local .mp4 from the library
(file: protocol) and a fresh network .webm from a danbooru search
(https: protocol) — both work.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:07:33 -05:00
pax
22744c48af security: fix #2 — add pure mpv options builder helper
Extracts the mpv.MPV() kwargs into a Qt-free pure function so the
security-relevant options can be unit-tested on CI (which lacks
PySide6 and libmpv). The builder embeds the audit #2 hardening —
ytdl="no", load_scripts="no", and a lavf protocol whitelist of
file,http,https,tls,tcp — alongside the existing playback tuning.
Not yet wired into _MpvGLWidget; that lands in the next commit.

Audit-Ref: SECURITY_AUDIT.md finding #2
Severity: High
2026-04-11 16:06:33 -05:00
pax
0aa3d8113d README: add AUR install instructions
booru-viewer-git is now on the AUR — lead the Linux install section
with it for Arch-family distros, keep the source-build path for other
distros and dev use.
2026-04-11 16:00:42 -05:00
pax
75bbcc5d76 strip 'v' prefix from version strings
pyproject.toml and installer.iss both used 'v0.2.5' — not PEP 440
compliant, so hatchling silently normalized it to '0.2.5' in wheel
builds. Align the source strings with what actually gets shipped.
2026-04-11 15:59:57 -05:00
pax
c91326bf4b fix issue template field: about -> description
GitHub's YAML issue forms require `description:`, not `about:` (which
is for the legacy markdown templates). GitHub silently ignores forms
with invalid top-level fields, so only the config.yml contact links
were showing in the new-issue picker.
2026-04-10 22:58:35 -05:00
pax
b1e4efdd0b add GitHub issue templates 2026-04-10 22:54:04 -05:00
pax
836e2a97e3 update HYPRLAND.md to reflect anchor point setting 2026-04-10 22:41:21 -05:00
pax
4bc7037222 point README Hyprland section to HYPRLAND.md 2026-04-10 22:35:55 -05:00
pax
cb4d0ac851 add HYPRLAND.md with integration reference and ricer examples 2026-04-10 22:35:51 -05:00
pax
10c2dcb8aa fix popout menu flash on wrong monitor and preview unsave button
- preview_pane: unsave button now checks self._is_saved instead of
  self._save_btn.text() == "Unsave", which stopped matching after the
  button text became a Unicode icon (✕ / ⤓)
- popout: new _exec_menu_at_button helper uses menu.popup() +
  QEventLoop blocked on aboutToHide instead of menu.exec(globalPos).
  On Hyprland the popout gets moved via hyprctl after Qt maps it and
  Qt's window-position tracking stays stale, so exec(btn.mapToGlobal)
  resolved to a global point on the wrong monitor, flashing the menu
  there before the compositor corrected it. popup() routes through a
  different positioning path that anchors correctly.
2026-04-10 22:10:27 -05:00
pax
a90aa2dc77 rebuild CHANGELOG.md from Gitea release bodies 2026-04-10 21:53:16 -05:00
pax
5bf85f223b add v prefix to version strings v0.2.5 2026-04-10 21:25:27 -05:00
pax
5e6361c31b release 0.2.5 2026-04-10 21:17:10 -05:00
pax
35135c9a5b video controls: 1x icon, responsive layout, EOF replay, autoplay icon fix
- Render "Once" loop icon as bold "1×" text via QPainter drawText
  instead of the hand-drawn line art
- Responsive controls bar: hide volume slider below 320px, duration
  label below 240px, current time label below 200px
- _toggle_play seeks to 0 if paused at EOF so pressing play replays
  the video in Once mode instead of doing nothing
- Fix stray "Auto" text leaking through the autoplay icon — the
  autoplay property setter was still calling setText
2026-04-10 21:09:49 -05:00
pax
fa9fcc3db0 rubber band from cell padding with 30px drag threshold
- ThumbnailWidget detects clicks outside the pixmap and calls
  grid.on_padding_click() via parent walk (signals + event filters
  both failed on Wayland/QScrollArea)
- Grid tracks a pending rubber band origin; only activates past 30px
  manhattan distance so small clicks deselect cleanly
- Move/release events forwarded from ThumbnailWidget to grid for both
  the pending-drag check and the active rubber band drag
- Fixed mapFrom/mapTo direction (mapFrom's first arg must be a parent)
2026-04-10 20:54:37 -05:00
pax
c440065513 install event filter on each ThumbnailWidget for reliable padding detection 2026-04-10 20:36:54 -05:00
pax
00b8e352ea use viewport event filter for cell padding detection instead of signals 2026-04-10 20:34:36 -05:00
pax
c8b21305ba fix padding click: pass no args through signal, just deselect 2026-04-10 20:31:56 -05:00
pax
9081208170 cell padding clicks deselect via signal instead of broken event propagation 2026-04-10 20:27:54 -05:00
pax
b541f64374 fix cell padding hit-test: use mapFrom instead of broken mapToGlobal on Wayland 2026-04-10 20:25:00 -05:00
pax
9c42b4fdd7 fix coordinate mapping for cell padding hit-test in grid 2026-04-10 20:23:36 -05:00
pax
a1ea2b8727 remove dead enterEvent, reset cursor in leaveEvent 2026-04-10 20:22:17 -05:00
pax
4ba9990f3a pixmap-aware double-click and dynamic cursor on hover 2026-04-10 20:21:58 -05:00
pax
868b1a7708 cell padding starts rubber band and deselects, not just flow gaps 2026-04-10 20:20:23 -05:00
pax
09fadcf3c2 hover only when cursor is over the pixmap, not cell padding 2026-04-10 20:18:49 -05:00
pax
88a3fe9528 fix stuck hover state when mouse exits grid on Wayland 2026-04-10 20:16:49 -05:00
pax
e28ae6f4af Reapply "only select cell when clicking the pixmap, not the surrounding padding"
This reverts commit 6aa8677a2d28af2eb00961fb16169128df72d2fc.
2026-04-10 20:15:50 -05:00
pax
6aa8677a2d Revert "only select cell when clicking the pixmap, not the surrounding padding"
This reverts commit cc616d1cf4ab460f204095af44607b7fce5a2dad.
2026-04-10 20:15:24 -05:00
pax
cc616d1cf4 only select cell when clicking the pixmap, not the surrounding padding 2026-04-10 20:14:49 -05:00
pax
42e7f2b529 add Escape to deselect in grid 2026-04-10 20:13:54 -05:00
pax
0b4fc9fa49 click empty grid space to deselect, reset stuck drag cursor on release 2026-04-10 20:12:08 -05:00
pax
0f2e800481 skip media reload when clicking already-selected post 2026-04-10 20:10:04 -05:00
pax
15870daae5 fix stuck forbidden cursor after drag-and-drop 2026-04-10 20:07:52 -05:00
pax
27c53cb237 prevent info panel from pushing splitter on long source URLs 2026-04-10 20:05:57 -05:00
pax
b1139cbea6 update README settings list with new options 2026-04-10 19:58:26 -05:00
pax
93459dfff6 UI overhaul: icon buttons, video controls, popout anchor, layout flip, compact top bar
- Preview/popout toolbar: icon buttons (☆/★, ↓/✕, ⊘, ⊗, ⧉) with QSS
  object names (#_tb_bookmark, #_tb_save, etc.) for theme targeting
- Video controls: QPainter-drawn icons for play/pause, volume/mute;
  text labels for loop/once/next and autoplay
- Popout anchor setting: resize pivot (center/tl/tr/bl/br) controls
  which corner stays fixed on aspect change, works on all platforms
- Hyprland monitor reserved areas: reads waybar exclusive zones from
  hyprctl monitors -j for correct edge positioning
- Layout flip setting: swap grid and preview sides
- Compact top bar: AdjustToContents combos, tighter spacing, named
  containers (#_top_bar, #_nav_bar) for QSS targeting
- Reduced main window minimum size from 900x600 to 740x400
- Trimmed bundled QSS: removed 12 unused widget selectors, added
  popout overlay font-weight/size, regenerated all 12 theme files
- Updated themes/README.md with icon button reference
2026-04-10 19:58:11 -05:00
pax
d7b3c304d7 add B/S keybinds to popout, refactor toggle_save 2026-04-10 18:32:57 -05:00
pax
28c40bc1f5 document B/F and S keybinds in KEYBINDS.md 2026-04-10 18:30:39 -05:00
pax
094a22db25 add B and S keyboard shortcuts for bookmark and save 2026-04-10 18:29:58 -05:00