Developers
Server
Hubzero\Content\Server
sends a file off disk to the browser. It handles the headers a download
needs — content type, length, disposition, byte ranges — and streams the
body, so a controller that serves an attachment does not have to.
You need it whenever a file must not sit under the document root: a
calibration report attached to an instrument booking, a dataset only the
lab's members may read. The file lives somewhere the web server will not
serve, your controller decides whether this member may have it, and Server
does the rest. Byte ranges are why you should not roll your own — without
them a member cannot resume an interrupted download, and audio and video
will not seek.
There is no facade for it. Construct one where you need it:
use Hubzero\Content\Server;
Serving a file
// Serve up the image
$server = new Server();
$server->filename($file_path);
$server->disposition('inline');
$server->acceptranges(false); // @TODO fix byte range support
// Serve up file
if (!$server->serve())
{
// Should only get here on error
throw new RuntimeException(Lang::txt('An error occurred while trying to output the file'), 500);
}
exit;
That is the whole pattern: set the file, say how it should be presented,
call serve(), and stop. serve() returns false if it could not send
the file, and by the time it returns true the response body is already
written, so nothing may be echoed afterwards.
The setters
Each setter is also its own getter: called with a value it sets and returns it, called with none it returns what is set.
| Method | Notes |
|---|---|
filename($filename = null) |
Absolute path of the file to send |
saveas($saveas = null) |
Name the browser should save it under. Run through basename() |
disposition($disposition = null) |
attachment or inline. Anything else becomes inline |
acceptranges($acceptranges = null) |
Whether to advertise byte-range support |
setContentType($contentType = null) |
Override the type instead of detecting it |
allowXsendFile() |
Hand the transfer to Apache when possible |
The constructor sets the disposition to inline, so a viewer only has to
set filename(). attachment asks the browser to save rather than
display, and is what saveas() names:
$server = new Server();
$server->filename(PATH_APP . DS . 'site' . DS . 'bookings' . DS . $booking->get('id') . DS . $file);
$server->disposition('attachment');
$server->saveas('calibration.pdf');
$server->serve();
Two convenience methods skip the setters:
| Method | Equivalent to |
|---|---|
serve_attachment($filename, $saveas = null, $acceptranges = true) |
disposition attachment, then serve() |
serve_inline($filename, $acceptranges = true) |
disposition inline, no save-as, then serve() |
X-Sendfile
allowXsendFile() turns on delegation to mod_xsendfile: instead of PHP
reading the file and echoing it, the server sends an X-Sendfile header
and Apache streams it. It takes effect only when apache_get_modules()
reports the module and allow_xsendfile is set to 1 in the global
configuration, so calling it on a hub without either is harmless.
The file service provider — the path that serves everything under the hub's file area — turns it on:
// Initiate a new content server
$server = new Server();
$server->disposition('inline');
$server->acceptranges(true);
$server->allowXsendFile();
$server->filename($filename);
// Serve up the file
$result = $server->serve();
Checking a path
Server::valid($filename) is static and answers whether a path is safe to
serve. It rejects anything that looks like an absolute URL, a call back
into index.php, a Windows drive letter, a backslash, or a path containing
... It is a filter on user-supplied names, not a check that the file
exists or that the caller may read it.
if (!Server::valid($file))
{
App::abort(404, Lang::txt('File not found'));
}
Confirm the file exists and that the current user is entitled to it before
serving. Server does neither; give it a path you have already resolved
against a directory you control, never one assembled straight from the
request.
The order to write it in:
- Load the record the file belongs to, by id, from your own table.
- Check the current user may see that record.
- Build the path from the record — the directory from
PATH_APPand the record's id, the name from the record's own column. Filesystem::exists().- Serve.
Taking the filename from the request and validating it with
Server::valid() is not step 3. valid() stops a path escaping upwards;
it has nothing to say about whether this member may read this file, so a
member who swaps one booking id for another in the URL gets somebody else's
report and no error.
Rewritten and checked against 2.4-main @ 348f0057c2 on 2026-09-10.