Commit graph

1355 commits

Author SHA1 Message Date
Michael Mayer
0cecb5287f Search: Match folder album_path case-insensitively #5724
Folder search lowercases the query term, but album_path is VARBINARY and
compared byte-exact (case-sensitive) on MySQL/MariaDB, so a lowercased
term no longer matched uppercase folder paths. A parent folder still
appeared via its case-insensitive title, while child folders - matchable
only by path - dropped out of the results.

Add a dialect-aware PathLike helper: MySQL converts album_path to a
case-insensitive collation for the LIKE, SQLite and other dialects fall
back to a plain LIKE (SQLite is already ASCII case-insensitive). Only the
folder search filter is affected; byte-exact album_path identity lookups
are unchanged, so emoji and case-distinct folders stay distinct.
2026-07-16 16:53:28 +00:00
Michael Mayer
3f039866ff Sharing: Fix single-item access for shared smart albums #5727
Pictures shared only through a folder, moment, calendar, or region link
have no photos_albums row, so the by-UID and by-hash visibility checks
reported them out of scope and returned "Entity not found" - for example
when downloading a single image from a shared folder. The photo and file
visibility helpers now fall back to the shared album's filter, matching
what browsing and full-album downloads already allow. The fallback runs
only after the cheaper personal-scope check misses and skips regular
albums, so full-access and regular-share sessions add no queries.
2026-07-16 12:10:44 +00:00
Keith Martin
b6687b6c29
Tests: Improve isolation and add fixes #5263
* Tests: fix random selection of deleted record causing test failure
* Tests: fix test folders being left after test
* Tests: ensure that required records are available
* Tests: make each package execute against it's own testdata folder to ensure no interpackage test failures
* Tests: add unit tests of CleanupTestFolder
* Tests:  Allow defer in TestMain
* Tests: rename testMain to runTestMain
2026-07-12 05:47:21 +02:00
Cathie Integra
fb6e17ee69 Index: Resolve S2 cell in XMP branch, downgrade geocoding warn
When an XMP sidecar provides GPS coordinates, call UpdateLocation()
immediately so the S2 cell is resolved and cached in the local DB.
Without this, subsequent re-indexes always triggered a live geocoding
request (even though the coordinates were already known), producing a
spurious error on every re-index pass.

Downgrade the geocoding failure log in photo_location.go and
places/cell.go from Errorf to Warnf: a transient network failure to
the places service is non-fatal (the photo is indexed successfully,
just without a resolved place name) and must not surface as an
indexing error in the test suite.
2026-07-10 10:14:02 +02:00
Ömer Duran
6dc0acac13
Settings: Reorganize UI and add an Accessibility section #848 #799 #5429
* Frontend: Add settings for albums, favorites, folders, and media features

* Frontend: Enhance download settings for files and albums #848

* Frontend: Enhance download settings for files and albums #848

* Frontend: Update album download settings to restrict access to super admins

* Frontend: Add album sorting options to settings for super admins

* Frontend: Update download settings visibility

* Frontend: Refine download settings access control for user roles

* Tests: Add album, folder, moment, state, and month order selection options to page-model

* Frontend: Update access control for super admins in settings to include scope check

* Tests: Add new tests for download name and album order settings persistence

* Tests: Implement new tests for default album sort order persistence in settings

* Frontend: Move default album order settings

* Frontend: Add zoom accessibility option in settings #799

* Frontend: Update download name from "Share Identifier" to "Share Friendly" and refine hints for download options in settings

* Tests: Add new acceptance test for disabling album downloads while allowing file downloads in settings

* Frontend: Improve download options layout and hints

* Frontend: Refactor download options condition in settings page

* Frontend: Implement native zoom control in lightbox component

* Settings: Add Collections tab and move Services to its own page #848 #5429

- Add a super-admin Collections tab after Content holding the album
  Download and Sort Order settings; remove those cards from Content.
- Move Services from a settings tab to a dedicated page
  (page/services.vue) with a search toolbar and a Reload / Learn More
  action menu; link it from the navigation settings menu.
- Implement server-side services search (acc_name filter) with tests.
- De-duplicate the picture sort-order options into a shared
  SortOrderOptions builder in options.js.
- Update acceptance/unit tests and page models to match.

* Frontend: Regenerate translation catalogs #848 #5429

Ran `make gettext-extract` to pick up the new settings strings
(Collections, Features, Disable Downloads, and related messages) so
Weblate can translate them. Existing translations are preserved.

* Frontend: Move lightbox viewport-zoom handling into the view helper

- Move the viewport pinch-zoom lock/restore (disableNativeZoom /
  restoreNativeZoom + savedViewportContent) from lightbox.vue onto the
  View singleton in common/view.js.
- Drive it passively from View.apply() via a disableViewportZoom flag in
  the PLightbox case, alongside hideScrollbar / disableScrolling /
  disableNavigationGestures; lightbox.vue no longer references zoom.
- Make save/restore idempotent and extract the viewport constants.
- Cover the helpers and the apply() wiring in view.test.js.

* Settings: Add Accessibility section to General with Reduce Motion #848 #5429

- Move the page-zoom checkbox out of the feature-flags grid into a
  dedicated Accessibility card (super-admin only) with four toggles:
  Open on Hover, Reduce Motion, Hide Scrollbar, and Allow Page Zoom.
- Add a UI.ReduceMotion flag (defaults false) that toggles an
  html.reduce-motion class via config.setReduceMotion and suppresses map
  fly-to animations non-destructively (util.mapAnimateDuration).
- Make Open on Hover apply live by reading shouldOpenOnHover() through a
  reactive computed in the action/auth/lightbox menus; Zoom and Hide
  Scrollbar reload since they are baked into the server-rendered HTML.
- Reword two feature-grid hints to the "pictures" convention and cover
  the new helpers with Go and Vitest tests.

* Frontend: Regenerate translation catalogs #848 #5429

Ran `make gettext-extract` to pick up the new Accessibility settings
strings (Accessibility, Allow Page Zoom, Open on Hover, Hide Scrollbar,
Reduce Motion, and their hints) and the reworded feature-grid hints.
Existing translations are preserved.

---------

Co-authored-by: Michael Mayer <michael@photoprism.app>
2026-07-08 14:11:36 +02:00
Michael Mayer
1b572c0da7 Auth: Store session login_at as NULL when no client IP is set #5707 2026-06-29 23:28:12 +00:00
Michael Mayer
d330f0e05c Users: Apply 2FA changes for the cluster service principal 2026-06-29 21:59:48 +00:00
Ömer Duran
b2b1785126
Tests: Add unit tests for recent metadata, auth, and config changes #5697 2026-06-26 14:12:59 +02:00
Cathie Integra
3f096a592b Links: Use canonical trailing-slash form for website URLs 2026-06-24 17:20:30 +02:00
Ömer Duran
3ea547a564
Lightbox: Add support for 360° photos and videos #352 #4718 #5623
* Lightbox: Add 360° photo and video sphere viewer support

Wires Photo Sphere Viewer v5 into the lightbox for equirectangular
media. Backend detects UsePanoramaViewer and IsPhotosphere markers
alongside ProjectionType, and exposes Panorama + Projection in the
viewer / search result DTOs so the frontend can dispatch on the
projection type. The renderer (ThreeJS + PSV core + video plugin +
equirectangular video adapter) is lazy-loaded into a dedicated
webpack chunk so the base bundle is unchanged for users with no
360° media. PhotoPrism's existing lightbox controls drive the
underlying HTMLVideoElement that the PSV adapter creates for 360°
videos. Includes Vitest coverage for the sphere helper and the
lightbox dispatch, plus a TestCafe smoke spec and three new
ExifTool metadata fixtures.

* Lightbox: Fix 360° controls leak, hide zoom button, add sidebar icon

* Lightbox: Fix 360° touch panning and navigation arrows

* Lightbox: Fix fast swipe switching photos on 360° slides

Suppress PhotoSwipe swipe/drag navigation at its source via the dispatched pointer hook while a sphere slide is active, since the per-element gesture trap is outrun by a fast swipe whose pointer leaves the sphere container (touch-capable Windows). UI controls stay excluded so buttons and arrows still navigate.

* Lightbox: Route only equirectangular media to the 360° viewer

Routing was based on the loose photo_panorama flag for videos, which is also
true for cubemap and merely-wide (aspect > 1.9) videos, so non-equirectangular
media wrongly opened, distorted, in the sphere viewer. Route strictly on the
equirectangular projection for both photos and videos. Surface the video file's
projection in the viewer DTO via Photo.MediaProjection() (the primary search row
is a poster JPEG with no projection). Add an MP4 metadata-detection test.

* Lightbox: Open tagless 2:1 360° videos in the sphere viewer

Many 360° videos carry no projection tag PhotoPrism can read, so strict
equirectangular-projection routing left them in the flat player. Fall back to
equirectangular's defining 2:1 frame for panorama-flagged videos without a
projection, via is360Equirectangular() in common/sphere.js, while cubemap and
ultrawide (~2.35:1) clips stay flat.

* Tests: Make the 360° sphere video test assert instead of skipping
2026-06-17 03:26:25 +02:00
Michael Mayer
a2e708e7ab Meta: Update go-mp4, regenerate NOTICE, fix XMP doc spelling #2260 #5563
Bump abema/go-mp4 to v1.7.1 (only outdated direct dependency) and tidy
go.sum, pruning stale checksums left by the XMP sidecar merge. Regenerate
the license NOTICE for the updated dependency.

Use US-English spelling in the new XMP sidecar comments and tests, and
drop a specs/ reference from internal/meta/README.md so the public package
doc has no link into the private subrepo. Regenerate dialect_mysql.go from
SQL.
2026-06-17 00:14:33 +00:00
Ömer Duran
86efc1c982
Index: Improve support for metadata in XMP sidecar files #2260 #2828 #5563
* Meta: Add XMP sidecar fixture corpus for reader rewrite #2260
* Meta: Replace XMP sidecar reader with antchfx/xmlquery #2260
* Meta: Add XMP sidecar reader test suite #2260
* Meta: Assert XMP GPS extraction on Apple/Adobe sidecars #2828
* Meta: Apply XMP sidecar metadata to photo and primary file #2260
* Meta: Resolve XMP sidecar time zone via shared helper #2260
* Meta: Derive panorama & caption keywords on XMP sidecar reads #2260
* Meta: Assert XMP sidecar GPS override and malformed-file handling #2260
* Meta: Tidy XMP sidecar reader comments and reuse logName local #2260
* Meta: Fix XMP sidecar capture-time priority OffsetTime handling #2260
* Meta: Address XMP sidecar review feedback #2260
2026-06-17 01:53:28 +02:00
Michael Mayer
ee0b1a616a Search: Sort geo "near" results by distance to the photo #5643
A "near=<uid>" geo search set the S2 cell but left the form lat/lng at
zero, so results sorted by distance from the (0,0) origin instead of
the referenced picture. Use the picture's own position in the ORDER BY,
falling back to time order when it has no usable coordinates.
2026-06-16 23:19:46 +00:00
Michael Mayer
c45ee6e54f Auth: Revoke derived sessions & gate app passwords by login state #5647
- Add scoped session revocation (RevokeSessions / RevokeDerivedSessions)
  shared by the user-update API and the CLI: a privilege-level change now
  revokes logins and app-password-derived sessions while keeping the app
  passwords themselves, so configured devices keep working.
- Identify app-password-derived sessions by auth_method "session".
- Deny app passwords on the REST API when the account cannot log in
  (DenyLogIn); WebDAV access stays governed by CanUseWebDAV.
- Reduce the WebDAV auth cache expiration to one minute.
- Add IsSystemOrInvalid; allow deleting the initial admin account and skip
  re-initializing a deactivated or deleted admin in InitAccount.
- Move session-revocation scopes to pkg/authn and extend tests.
2026-06-15 23:41:01 +00:00
Michael Mayer
2fd0ed8631 Events: UID-only camera/lens/country payloads & people refetch #5663 #1307
Make the cameras/lenses/countries content-channel events publish only a
stable identity ([]string of slug or ISO code) instead of full entity
models, matching the UID-only invariant of every other content channel.
The update path no longer republishes count.* (an edit does not change
the count).

Restore surgical people updates: onPeople refetches only the affected
people by UID via Subject.search (refetchPeople) and upserts them in
place instead of forcing a full client-config reload (a #1307 regression).

Document the dormant folders.*/faces.* subscribers (no publisher, not
forwarded) and fix the misleading payload example in the event README.
2026-06-15 11:37:26 +00:00
Michael Mayer
bc327016ad Metadata: Add Camera Make and Model updates via CLI & API #5663 #5656
Mirror the lens make/model editing surface for cameras: entity UpdateMakeModel/SaveForm, form validation, query, search, the GET/PUT /api/v1/cameras endpoints, and the cameras CLI command, plus a cameras ACL resource and scope.

Also tidy the lens surface for parity: self-validating SaveForm, empty make/model guard, X-Count search header, service-role grant, the empty-id/slug docs, and order cameras before lenses everywhere.
2026-06-15 09:25:44 +00:00
Keith Martin
c3eda657fd
Metadata: Add Lens Make and Model updates via CLI & API #5644 #5656
Adds the ability to override a lens's Make/Model (e.g. fixing Pentax lenses that ExifTool decodes as `4 38`) via a new photoprism lenses update CLI command and a `PUT /api/v1/lenses/:id endpoint`, plus a `GET /api/v1/lenses` search endpoint, a new lenses ACL resource, and an lenses ls list command.
2026-06-15 09:37:32 +02:00
Michael Mayer
d594edd5de Cluster: Declare per-node group config via instance env/flags 2026-06-12 18:52:54 +00:00
Michael Mayer
d091958a60 Cluster: Collect OIDC groups at login for group-based admission 2026-06-12 16:08:38 +00:00
Michael Mayer
d26412bcca Auth: Normalize a super admin's role to the running edition 2026-06-11 15:49:30 +00:00
Michael Mayer
7486982342 Events: Refactor entity change notifications for sharing features #1307 2026-06-11 15:40:57 +00:00
Michael Mayer
71a82a9ac9 Auth: Refine passcode recovery code comparison 2026-06-11 09:37:40 +00:00
Michael Mayer
8b09cfe76e Photos: Limit by-UID and batch photo edits to the session scope
Single-photo update/approve/set-primary and batch archive/restore/approve/private/delete now apply the same shared-scope visibility predicate as photo search, so by-UID and bulk edits stay consistent with album updates and never act on content outside the session's shared scope. Visibility is evaluated against both the client and user role for client sessions. Full-access sessions skip the check with no database query via PhotoSessionSeesEverything / SelectedPhotoUIDsForSession.
2026-06-10 19:49:27 +02:00
Michael Mayer
0379692a46 OIDC: Surface account-collision reconcile hint and add --auth-issuer
When an OIDC login matches a pre-existing non-OIDC account, log a
specific operator remedy (the exact 'users mod --auth oidc --auth-id'
command) to the audit and login-error logs instead of a bare provider
mismatch; the user-facing page stays a generic credentials error so the
account is not disclosed. Add a 'users mod --auth-issuer' flag so the
documented manual reconcile can pin the OIDC issuer, scoping the link to
that provider instead of matching any issuer with the same subject.
2026-06-10 15:31:34 +00:00
Michael Mayer
71aeb26137 OIDC: Store email only when the token marks it verified 2026-06-10 00:04:26 +00:00
Michael Mayer
cb48c209b6 Auth: Gate app passwords behind a settings feature flag #5647
Adds an AppPasswords feature flag (default on) so app passwords can be disabled, closing the bypass where an app password minted by an OIDC-provisioned user keeps working after the account expires. When off, minting (oauth/token and auth add) is rejected and existing app passwords are no longer accepted on the REST API (403) or WebDAV (401). App passwords are identified by their session auth provider (IsApplication), covering every grant type. The Apps and Devices affordance is hidden for sessions that may not change a password; operators can force it off via PHOTOPRISM_DISABLE_FEATURES.
2026-06-09 17:36:24 +00:00
Michael Mayer
680469e781 Cluster: Add a per-instance node DisplayName field 2026-06-09 07:23:05 +00:00
Michael Mayer
680f1fef51 Auth: Drop the hard seed-admin lock in User.SaveForm 2026-06-09 04:18:36 +00:00
Ömer Duran
cdc671677d
Auth: Let the cluster JWT sync privilege-level user fields
Thread admin authorization into SaveForm via byAdmin so the user-less cluster service principal can persist login/role/WebDAV instead of silently dropping them.
2026-06-05 21:03:38 +02:00
Michael Mayer
fbe8a6889b Auth: Add cluster_admin role with admin-tier and federation helpers
Adds the Portal-only cluster_admin operator role (declared in CE, registered
only on Portal builds), the acl.AdminRoles/IsAdminRole admin-tier set, and the
acl.IsFederatedRole/FederatedRoleUpdate helpers that keep cluster_admin and
visitor out of externally mapped account roles. SuperAdmin resolves to
cluster_admin on registered builds, SaveForm keeps an admin-level role on a
self-save, and UpdateUser rejects self-role changes. RoleStrings.Strings hides
visitor and none from the CLI role help.
2026-06-03 17:12:59 +02:00
Michael Mayer
717b21c5ec Backend: Call clip.Bytes directly and drop the txt.ClipBytes facade #5638 2026-06-03 09:50:56 +00:00
Michael Mayer
d33bac96cb Entity: Rename Sanitize* to Clip* and string.go to clip.go 2026-06-03 09:50:56 +00:00
Michael Mayer
f70d6c113b Entity: Clip album, photo, and folder paths via shared ClipPath #5615 2026-06-03 09:34:40 +00:00
Michael Mayer
2d3aa02dac Backend: Clip VARBINARY text columns by bytes to avoid write errors #5638 2026-06-03 08:04:40 +00:00
Michael Mayer
deb3723fd6 Albums: Clip album_path to the column byte budget on write #5615 2026-06-03 07:53:16 +00:00
Michael Mayer
186a11b007 Entity: Document VARBINARY index-prefix limit and byte-exact paths #5614 #5615
Record that album_path is now a byte-exact VARBINARY column (alongside
album_slug, album_filter, photo_path) and that folder albums dedupe by
album_filter, in internal/entity/README.md. Add an Index Prefix Limits
note to the migrate README and AGENTS: InnoDB caps key prefixes at 767
bytes on COMPACT/REDUNDANT row formats, so VARBINARY prefix indexes use
512 by convention.
2026-06-01 11:42:59 +00:00
Michael Mayer
128bd2d8be Albums: Store album_path as VARBINARY for byte-exact paths #5614 #5615
album_path mirrors photos.photo_path (already VARBINARY(1024)) and is
compared against it directly, but was VARCHAR with the utf8mb4_unicode_ci
collation, which collapses case and most emoji. That made album_path = ?
lookups collation-fuzzy and forced byte-exact re-checks in Go.

Store it as VARBINARY(1024) so folder album path comparisons are
byte-exact at the database, consistent with album_slug, album_filter,
and photo_path. The MySQL migration preserves the existing 768-byte
prefix index. SQLite compares TEXT byte-exact by default, so no SQLite
migration is needed.
2026-06-01 11:34:41 +00:00
Michael Mayer
d6249107a0 Moments: Dedup folder albums by filter, not slug #5615 #5614
Folder album slugs are not unique identities: long nested paths are
truncated to ClipSlug runes and slug.Make drops emoji, so distinct
sibling folders can share one album_slug. Deduplicate folder albums by
album_filter (the serialized path) instead, and keep slug matching only
for non-folder album types.

This stops RemoveDuplicateMoments from deleting legacy folder rows with
an empty album_path that merely share a truncated slug, which the
indexer then recreated on every run. The empty/NULL album_path escape
hatch in the previous byte-exact path guard no longer applies.
2026-06-01 11:18:30 +00:00
Michael Mayer
8bf671b0b8 Entity: Document timestamps and database test specifics
Add internal/entity/README.md covering the second-precision timestamp behavior,
the time helpers, MariaDB strict-mode test gotchas, and the utf8mb4 emoji
collation pitfalls so they don't have to be rediscovered.
2026-05-31 01:13:31 +00:00
Michael Mayer
41ecbf9095 Entity: Store created and updated timestamps with second precision
The schema stores created_at/updated_at as DATETIME without fractional seconds,
so set gorm.NowFunc to entity.Now() (UTC truncated to the second). This keeps
in-memory and persisted times in sync and makes timestamp comparisons behave the
same on SQLite and MariaDB. Adapt the affected tests to compare with Sub() in a
sane range or a clearly past base instead of strict sub-second ordering, and give
the duplicate-client registry test distinct update times.
2026-05-31 01:13:27 +00:00
Michael Mayer
3c00d6fea9 Tests: Use valid fixture IDs so entity tests pass on MariaDB
MariaDB strict mode rejects empty primary keys and out-of-range or oversized
values that SQLite accepts. Give the affected entity tests valid IDs: existing
fixtures where the row is additive, and throwaway in-range IDs where a real
reference would overwrite seeded data.
2026-05-31 00:23:23 +00:00
Michael Mayer
61a89f9ae0 Users: Set the user UID when creating user settings
CreateUserSettings inserted an empty UserSettings, leaving the user_uid
primary key blank. Build it with NewUserSettings so the row carries the UID;
MariaDB strict mode rejected the missing value.
2026-05-31 00:23:18 +00:00
Michael Mayer
c569c02e9e Moments: Guard folder album dedup against slug collisions #5615 #5614
Legacy folder slugs drop emoji (slug.Make), so two siblings collide on the
VARBINARY album_slug and RemoveDuplicateMoments deleted one each pass while
the indexer recreated it. Skip folder albums whose album_path differs
byte-exact via HEX(), which compares the same on MariaDB and SQLite.
2026-05-31 00:23:12 +00:00
Michael Mayer
0d1b45e00a Albums: Keep emoji sibling folders distinct on MariaDB #5366
MariaDB's utf8mb4_unicode_ci collation collapses most emoji, so album_path
lookups matched a sibling folder and merged the two albums. Re-check the
path byte-exact in Go in FindFolderAlbum, the UpdateFolder dedup, and the
slug-collision scope check.
2026-05-31 00:23:05 +00:00
Michael Mayer
2a944d931d Search: Exclude deleted files and document per-session scope checks #1307
FileVisibleToSession excludes soft-deleted file rows; PhotoVisibleToSession's UnscopedDb use is documented (archived handling deferred to ScopeVisiblePhotos, full-access short-circuits). Adds TestUserPhotos_ScopeAuthorization pinning that a restricted session may scope only to albums it owns or has shared.
2026-05-29 08:20:26 +00:00
Michael Mayer
68716b5c2b Photos: Reduce identifying metadata for shared-only sessions #1307
RedactForSession also clears the camera serial, XMP DocumentID, and per-file InstanceID for shared-only/unregistered sessions, matching common EXIF/XMP privacy practice; FileName, PhotoUID, and FileUID are kept.
2026-05-29 08:20:26 +00:00
Michael Mayer
5f8ea3aba0 API: Reduce photo detail in responses for shared-only sessions
GetPhoto returns a reduced view for sessions limited to shared content: album membership is restricted to the albums shared with the session, and labels, people, and owner/storage metadata are omitted. Full-access sessions are unaffected. Adds Photo.RedactForSession, in preparation for upcoming sharing features.
2026-05-29 05:26:56 +00:00
Michael Mayer
763757cc4b API: Scope file lookup and zip selection to the session
GetFile and the zip selection now limit results to the session's shared scope via the shared search helpers, consistent with photo search and in preparation for upcoming sharing features. No change for full-access sessions.
2026-05-29 07:04:47 +02:00
Michael Mayer
e6aa402d9c Search: Add reusable per-session photo and file scope helpers
Introduces ScopePhotosForSession, ScopeVisiblePhotos, PhotoVisibleToSession, and FileVisibleToSession, and refactors searchPhotos to reuse the shared predicate, in preparation for upcoming sharing features. Full-access sessions short-circuit without an extra query.
2026-05-29 07:04:47 +02:00
Michael Mayer
e39f9f20a6 Index: Reassign primary when current primary is deleted #5625
FixPrimaries counted soft-deleted files as primaries, so a photo whose
primary was deleted kept photo_quality = -1 and stayed in Hidden even
with a present, valid preview file. Also clear the primary flag from
soft-deleted files so a present file is promoted and quality recovers.
2026-05-29 02:51:31 +00:00