Sync() treats an empty list on either side as authoritative:
* a local contact holding an etag but missing from the remote listing is
deleted locally, so an empty or unparsed listing removes every
previously synced contact;
* a local tombstone is deleted remotely, so a local store that is empty
apart from tombstones prunes the server.
An empty list is far more often a fault than a real "delete everything":
a fresh or rebuilt local store, a listing that failed to parse, a stale
server-side index. On a server whose DAV index had lost its records the
listing came back empty and the first sync removed 7 local contacts that
existed on both sides moments earlier.
Skip the deletions in whichever direction the source list is empty and
let the existing import/export reconcile instead. Both guards log a
warning with the counts, so a skipped pass is visible rather than silent.
When both sides hold data the behaviour is unchanged.
The "Check Proxy" option inspected the forwarded client IP returned
by Http::GetClientIp(true), which resolves to HTTP_X_FORWARDED_FOR
first and only falls back to REMOTE_ADDR. In any typical reverse-proxy
deployment that header carries the end-user's IP, not the proxy's, so
the option never actually validated that the request came from the
trusted proxy. It only constrained the set of end-user IPs allowed
to use SSO, which is not a meaningful guarantee on its own.
Drop the option, the paired proxy_ip subnet field, and the
ip_in_range helper. Document that access must be gated at the
network layer (firewall / docker network / bound interface) and
that the upstream proxy must strip any client-supplied value of
the configured remote-user header.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add README note explaining that AUTHENTICATIONFAILED after a
container rebuild or data reset is typically caused by an
APP_SALT change invalidating the encrypted master credentials,
and that re-entering them in the admin UI restores access.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The proxy IP check was never enforced: ServiceProxyAuth read config
key 'proxy_check' but the property is registered as 'check_proxy',
so the lookup always returned the default empty string and the
else-branch set $sProxyRequest = true unconditionally.
The BOOL property also had SetEncrypted(), which caused getDecrypted()
to call DecryptFromJSON() on a literal bool and fail with
"DecryptFromJSON() invalid $data", so the admin UI never reflected
the saved value. BOOLs are stored as plain bools in JSON; switch to
Get() and drop SetEncrypted() on the property.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
With many folders, the line-height is too large.
It is reduced to make the view more compact.
Also, the top margin for the number of unread-folder will
be adapted.
The official nginx images do not listen on ipv6, very likely to avoid nginx crashing on startup in ipv4-only environments:
```
docker run --rm -it nginx:alpine cat /etc/nginx/conf.d/default.conf | grep -i listen
```
Hence, we simply need to detect whether ipv6 is disabled, and not listen on ipv6 if ipv6 is disabled in the kernel or in the container.