Permalink
Browse files

feature(files): adds a service for serving files from filestore

Adds API and handlers for serving files from the filestore
  • Loading branch information...
hypeJunction authored and mrclay committed Nov 25, 2015
1 parent 1a31497 commit 1d6b23c704495603f862d4189fd135992cc71f32
@@ -391,6 +391,11 @@ public function run() {
return true;
}
if (0 === strpos($path, '/serve-file/')) {
(new Application\ServeFileHandler($this))->getResponse($this->services->request)->send();
return true;
}
if ($path === '/rewrite.php') {
require Directory\Local::root()->getPath("install.php");
return true;
@@ -0,0 +1,108 @@
<?php
namespace Elgg\Application;
use DateTime;
use Elgg\Application;
use Elgg\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* File server handler
*
* @access private
*
* @package Elgg.Core
*/
class ServeFileHandler {
/** @var Application */
private $application;
/**
* Constructor
*
* @param Application $app Elgg Application
*/
public function __construct(Application $app) {
$this->application = $app;
}
/**
* Handle a request for a file
*
* @param Request $request HTTP request
* @return Response
*/
public function getResponse($request) {
$response = new Response();
$response->prepare($request);
$path = implode('/', $request->getUrlSegments());
if (!preg_match('~serve-file/e(\d+)/l(\d+)/d([ia])/c([01])/([a-zA-Z0-9\-_]+)/(.*)$~', $path, $m)) {
return $response->setStatusCode(400)->setContent('Malformatted request URL');
}
list(, $expires, $last_updated, $disposition, $use_cookie, $mac, $path_from_dataroot) = $m;
if ($expires && $expires < time()) {
return $response->setStatusCode(403)->setContent('URL has expired');
}
$etag = '"' . $last_updated . '"';
$response->setPublic()->setEtag($etag);
if ($response->isNotModified($request)) {
return $response;
}
// @todo: change to minimal boot without plugins
$this->application->bootCore();
$hmac_data = array(
'expires' => (int) $expires,
'last_updated' => (int) $last_updated,
'disposition' => $disposition,
'path' => $path_from_dataroot,
'use_cookie' => (int) $use_cookie,
);
if ((bool) $use_cookie) {
$hmac_data['cookie'] = _elgg_services()->session->getId();
}
ksort($hmac_data);
$hmac = elgg_build_hmac($hmac_data);
if (!$hmac->matchesToken($mac)) {
return $response->setStatusCode(403)->setContent('HMAC mistmatch');
}
$dataroot = _elgg_services()->config->getDataPath();
$filenameonfilestore = "{$dataroot}{$path_from_dataroot}";
if (!is_readable($filenameonfilestore)) {
return $response->setStatusCode(404)->setContent('File not found');
}
$actual_last_updated = filemtime($filenameonfilestore);
if ($actual_last_updated != $last_updated) {
return $response->setStatusCode(403)->setContent('URL has expired');
}
$public = $use_cookie ? false : true;
$content_disposition = $disposition == 'i' ? 'inline' : 'attachment';
$response = new BinaryFileResponse($filenameonfilestore, 200, array(), $public, $content_disposition);
$response->prepare($request);
if (empty($expires)) {
$expires = strtotime('+1 year');
}
$expires_dt = (new DateTime())->setTimestamp($expires);
$response->setExpires($expires_dt);
$response->setEtag($etag);
return $response;
}
}
@@ -0,0 +1,134 @@
<?php
namespace Elgg\FileService;
/**
* File service
*
* @access private
*/
class File {
const INLINE = 'inline';
const ATTACHMENT = 'attachment';
/**
* @var \ElggFile
*/
private $file;
/**
* @var int
*/
private $expires;
/**
* @var string
*/
private $disposition;
/**
* @var bool
*/
private $use_cookie = true;
/**
* Set file object
*
* @param \ElggFile $file File object
* @return void
*/
public function setFile(\ElggFile $file) {
$this->file = $file;
}
/**
* Sets URL expiration
*
* @param int $expires String suitable for strtotime()
* @return void
*/
public function setExpires($expires = '+2 hours') {
$this->expires = strtotime($expires);
}
/**
* Sets content disposition
*
* @param string $disposition Content disposition ('inline' or 'attachment')
* @return void
*/
public function setDisposition($disposition = self::ATTACHMENT) {
if (!in_array($disposition, array(self::ATTACHMENT, self::INLINE))) {
throw new \InvalidArgumentException("Disposition $disposition is not supported in " . __CLASS__);
}
$this->disposition = $disposition;
}
/**
* Bind URL to current user session
*
* @param bool $use_cookie Use cookie
* @return void
*/
public function bindSession($use_cookie = true) {
$this->use_cookie = $use_cookie;
}
/**
* Returns publically accessible URL
* @return string|false
*/
public function getURL() {
if (!$this->file instanceof \ElggFile || !$this->file->exists()) {
elgg_log("Unable to resolve resource URL for a file that does not exist on filestore");
return false;
}
$relative_path = '';
$root_prefix = _elgg_services()->config->get('dataroot');
$path = $this->file->getFilenameOnFilestore();
if (substr($path, 0, strlen($root_prefix)) == $root_prefix) {
$relative_path = substr($path, strlen($root_prefix));
}
if (!$relative_path) {
elgg_log("Unable to resolve relative path of the file on the filestore");
return false;
}
$data = array(
'expires' => isset($this->expires) ? $this->expires : 0,
'last_updated' => filemtime($this->file->getFilenameOnFilestore()),
'disposition' => $this->disposition == self::INLINE ? 'i' : 'a',
'path' => $relative_path,
);
if ($this->use_cookie) {
$data['cookie'] = _elgg_services()->session->getId();
if (empty($data['cookie'])) {
return false;
}
$data['use_cookie'] = 1;
} else {
$data['use_cookie'] = 0;
}
ksort($data);
$mac = elgg_build_hmac($data)->getToken();
$url_segments = array(
'serve-file',
"e{$data['expires']}",
"l{$data['last_updated']}",
"d{$data['disposition']}",
"c{$data['use_cookie']}",
$mac,
$relative_path,
);
return elgg_normalize_url(implode('/', $url_segments));
}
}
View
@@ -581,6 +581,45 @@ function _elgg_filestore_test($hook, $type, $value) {
return $value;
}
/**
* Returns file's download URL
*
* @param \ElggFile $file File object or entity
* @param bool $use_cookie Limit URL validity to current session only
* @param string $expires URL expiration, as a string suitable for strtotime()
* @return string
*/
function elgg_get_download_url(\ElggFile $file, $use_cookie = true, $expires = '+2 hours') {
$file_svc = new Elgg\FileService\File();
$file_svc->setFile($file);
$file_svc->setExpires($expires);
$file_svc->setDisposition('attachment');
$file_svc->bindSession($use_cookie);
return $file_svc->getURL();
}
/**
* Returns file's URL for inline display
* Suitable for displaying cacheable resources, such as user avatars
*
* @param \ElggFile $file File object or entity
* @param bool $use_cookie Limit URL validity to current session only
* @param string $expires URL expiration, as a string suitable for strtotime()
* @return string
*/
function elgg_get_inline_url(\ElggFile $file, $use_cookie = false, $expires = false) {
$file_svc = new Elgg\FileService\File();
$file_svc->setFile($file);
if ($expires) {
$file_svc->setExpires($expires);
}
$file_svc->setDisposition('inline');
$file_svc->bindSession($use_cookie);
return $file_svc->getURL();
}
return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) {
$events->registerHandler('init', 'system', '_elgg_filestore_init', 100);
};
Oops, something went wrong.

0 comments on commit 1d6b23c

Please sign in to comment.