FILES_WEB_SERVER_READABLE asks for 0755 on the directories a download has
to be traversed through, and asks for it from a key that is never
consulted.
FilesystemManager::createLocalDriver passes
`directory_visibility ?? visibility ?? private` to
PortableVisibilityConverter::fromArray() as the default visibility for
directories. This disk sets `visibility` to public two lines above, and no
`directory_visibility`, so directories are public and the converter reads
`dir.public`. The configuration names only `dir.private`.
The mode is 0755 regardless, because 0755 is Flysystem's default for a
public directory -- the right answer from the wrong place. Adding
`directory_visibility` to this disk, or a change to that default, is all
it would take for the flag to stop doing what it says. Measured on main,
with the flag on:
dir.private 0755 → 0750 directory stays 0755 (nothing reads it)
dir.public 0755 → 0750 directory becomes 0750 (this is the key)
Both are named now, so the intent survives either way round.
FilePermissionsTest could not have caught this, because it was not testing
this configuration. filesDiskWith() restated the shipped branch inline,
verbatim down to the 0755, so it went on passing against its own copy
however the real one changed. It now requires config/filesystems.php and
replaces only the root, which is what makes the mutation above visible to
it.
Two more things in the same helper, both about the suite rather than the
subject: the scratch root is per worker now (Tests\TestCase does the same
for upload parts, and eight workers sharing one directory means one
worker's afterEach deletes another's tree mid-test), and it is cleared
before each test as well as after, so a killed run does not poison the
next one.
A download is not served by PHP. PHP authorizes it and hands the web
server the path with X-Accel-Redirect, so the web server has to open a
file PHP wrote. Where those are different users — cPanel and Plesk
commonly arrange it that way — it cannot: uploads land 0600 inside a 0700
directory, and traversing 0700 means being its owner. Nothing else on the
site shows a symptom. Uploading works, the library lists everything, and
only downloads fail, as ERR_INVALID_RESPONSE in the browser and
`open() ... failed (13: Permission denied)` in the web server's log.
FILES_WEB_SERVER_READABLE writes uploads 0644/0755 instead. Opt-in and
spread into the disk configuration rather than switched by a ternary, so
an install that does not set it keeps byte-for-byte the configuration it
had: the relaxed modes are readable by every account on the machine,
which is the wrong trade wherever the web server and PHP are one user, as
in the image and on most self-administered servers.
The two halves are not enforced alike, which is the part worth knowing.
`visibility` has Flysystem chmod each file after writing it, so 0644
holds under any umask. A directory is created by mkdir(), which masks its
mode argument, so 0755 is a ceiling: a pool at umask 0077 still produces
0700 and still cannot be traversed. That cannot be fixed from config, so
INSTALL.md carries it — how to tell the two users apart, the one-time
chmod for files already on disk, and the pool setting for the umask.
FilePermissionsTest asserts all three modes, umask cases included, since
the asymmetry is invisible from the configuration.
Reported by @denkfabrik-li (#1668), who diagnosed it and verified the
remedy on the affected host.
Four unrelated one-liners, each already written down and none of them
worth a branch of its own.
The lock was still pinned to the community package's previous commit,
which is the one before it started shipping its own sixteen catalogues.
The mechanism that carries a package's translations to the browser landed
here last week; without this bump the release would have shipped that
mechanism with nothing to carry, and the Custom Assets screen would have
stayed half-English in every language.
The stock `local` disk had `serve` left on. Nothing in this application
writes to it, so the framework's /storage route was a door with nothing
behind it — but it was still a door, and closing it costs one word.
nginx evaluated `\.php$` before `/protected-files/`, so a protected path
ending in .php would have reached the PHP handler instead of streaming
under the sandbox headers that block sets. Not reachable on a default
install — the upload allowlist refuses php and X-Accel paths are UUIDs —
but the guarantee read stronger than it was. `^~` makes it true.
And `.release-build` is now ignored by eslint, so linting after building a
zip stops walking the vendored minified JS inside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Client file sharing, rebuilt from the ground up: a private area per
client, resumable uploads, folders, groups and categories, sharing with
expiry dates and download limits, comments, file versions, an activity
log, a REST API, and sixteen languages.
This repository begins here. ProjectSend 2 was developed privately, and
that development history is not published — the previous generation
remains available, with its own history, at projectsend/legacy.
Free software under the GNU General Public License v2, or (at your
option) any later version.