Skip to content

Commit

Permalink
feature(embed): adds serve-icon page handler
Browse files Browse the repository at this point in the history
Adds a handler to serve embedded icons. Adds elgg_get_embed_url() to
generate embed URLs.

Adds a trait to inject the time in components for testing.

Fixes Elgg#9582
  • Loading branch information
hypeJunction authored and mrclay committed Apr 20, 2016
1 parent 958b8b7 commit f8459bc
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 35 deletions.
9 changes: 9 additions & 0 deletions docs/guides/file-system.rst
Expand Up @@ -97,3 +97,12 @@ that access settings are respected and users do not share download URLs with som

You can also invalidated all previously generated URLs by updating file's modified time, e.g.
by using ``touch()``.


Embedding Files
---------------
Please note that due to their nature inline and download URLs are not suitable for embedding.
Embed URLs must be permanent, whereas inline and download URLs are volatile (bound to user session
and file modification time).

To embed an entity icon, use ``elgg_get_embed_url()``.
2 changes: 2 additions & 0 deletions docs/guides/upgrading.rst
Expand Up @@ -49,6 +49,8 @@ New API for handling entity icons
* ``ElggEntity::deleteIcon()`` - deletes entity icons
* ``ElggEntity::getIconLastChange()`` - return modified time of the icon file
* ``ElggEntity::hasIcon()`` - checks if an icon with given size has been created
* ``elgg_get_embed_url()`` - can be used to return an embed URL for an entity's icon (served via `/serve-icon` handler)


From 2.0 to 2.1
===============
Expand Down
2 changes: 1 addition & 1 deletion engine/classes/Elgg/Di/ServiceProvider.php
Expand Up @@ -192,7 +192,7 @@ public function __construct(\Elgg\Config $config) {
});

$this->setFactory('iconService', function(ServiceProvider $c) {
return new \Elgg\EntityIconService($c->config, $c->hooks, $c->request, $c->logger);
return new \Elgg\EntityIconService($c->config, $c->hooks, $c->request, $c->logger, $c->entityTable);
});

$this->setClassName('input', \Elgg\Http\Input::class);
Expand Down
101 changes: 91 additions & 10 deletions engine/classes/Elgg/EntityIconService.php
Expand Up @@ -2,13 +2,17 @@

namespace Elgg;

use DateTime;
use Elgg\Database\EntityTable;
use Elgg\Filesystem\MimeTypeDetector;
use Elgg\Http\Request;
use ElggEntity;
use ElggFile;
use ElggIcon;
use InvalidParameterException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
Expand All @@ -19,6 +23,7 @@
* @since 2.2
*/
class EntityIconService {
use TimeUsing;

/**
* @var Config
Expand All @@ -40,19 +45,26 @@ class EntityIconService {
*/
private $logger;

/**
* @var EntityTable
*/
private $entities;

/**
* Constructor
*
* @param Config $config Config
* @param PluginHooksService $hooks Hook registration service
* @param Request $request Http request
* @param Logger $logger Logger
* @param Config $config Config
* @param PluginHooksService $hooks Hook registration service
* @param Request $request Http request
* @param Logger $logger Logger
* @param EntityTable $entities Entity table
*/
public function __construct(Config $config, PluginHooksService $hooks, Request $request, Logger $logger) {
public function __construct(Config $config, PluginHooksService $hooks, Request $request, Logger $logger, EntityTable $entities) {
$this->config = $config;
$this->hooks = $hooks;
$this->request = $request;
$this->logger = $logger;
$this->entities = $entities;
}

/**
Expand Down Expand Up @@ -132,7 +144,7 @@ public function saveIconFromLocalFile(ElggEntity $entity, $filename, $type = 'ic
* Saves icons using a file located in the data store as the source.
*
* @param ElggEntity $entity Entity to own the icons
* @param string $file An ElggFile instance
* @param ElggFile $file An ElggFile instance
* @param string $type The name of the icon. e.g., 'icon', 'cover_photo'
* @param array $coords An array of cropping coordinates x1, y1, x2, y2
* @return bool
Expand Down Expand Up @@ -228,15 +240,15 @@ public function saveIcon(ElggEntity $entity, ElggFile $file, $type = 'icon', arr
'x2' => $x2,
'y2' => $y2,
], false);

if ($created === true) {
return $success();
}

$sizes = $this->getSizes($entity_type, $entity_subtype, $type);

foreach ($sizes as $size => $opts) {

$width = (int) elgg_extract('w', $opts);
$height = (int) elgg_extract('h', $opts);
$square = (bool) elgg_extract('square', $opts);
Expand Down Expand Up @@ -316,13 +328,13 @@ public function deleteIcon(ElggEntity $entity, $type = 'icon') {
if ($delete === false) {
return;
}

$sizes = array_keys($this->getSizes($entity->getType(), $entity->getSubtype(), $type));
foreach ($sizes as $size) {
$icon = $this->getIcon($entity, $size, $type);
$icon->delete();
}

if ($type == 'icon') {
unset($entity->icontime);
unset($entity->x1);
Expand Down Expand Up @@ -426,4 +438,73 @@ public function getSizes($entity_type = null, $entity_subtype = null, $type = 'i
return $sizes;
}

/**
* Handle request to /serve-icon handler
*
* @param bool $allow_removing_headers Alter PHP's global headers to allow caching
* @return BinaryFileResponse
*/
public function handleServeIconRequest($allow_removing_headers = true) {

$response = new Response();
$response->setExpires($this->getCurrentTime('-1 day'));
$response->prepare($this->request);

if ($allow_removing_headers) {
// clear cache-boosting headers set by PHP session
header_remove('Cache-Control');
header_remove('Pragma');
header_remove('Expires');
}

$path = implode('/', $this->request->getUrlSegments());
if (!preg_match('~serve-icon/(\d+)/(.*+)$~', $path, $m)) {
return $response->setStatusCode(400)->setContent('Malformatted request URL');
}

list(, $guid, $size) = $m;

$entity = $this->entities->get($guid);
if (!$entity instanceof \ElggEntity) {
return $response->setStatusCode(404)->setContent('Item does not exist');
}

$thumbnail = $entity->getIcon($size);
if (!$thumbnail->exists()) {
return $response->setStatusCode(404)->setContent('Icon does not exist');
}

$if_none_match = $this->request->headers->get('if_none_match');
if (!empty($if_none_match)) {
// strip mod_deflate suffixes
$this->request->headers->set('if_none_match', str_replace('-gzip', '', $if_none_match));
}

$filenameonfilestore = $thumbnail->getFilenameOnFilestore();
$last_updated = filemtime($filenameonfilestore);
$etag = '"' . $last_updated . '"';

$response->setPrivate()
->setEtag($etag)
->setExpires($this->getCurrentTime('+1 day'))
->setMaxAge(86400);

if ($response->isNotModified($this->request)) {
return $response;
}

$headers = [
'Content-Type' => (new MimeTypeDetector())->getType($filenameonfilestore),
];
$response = new BinaryFileResponse($filenameonfilestore, 200, $headers, false, 'inline');
$response->prepare($this->request);

$response->setPrivate()
->setEtag($etag)
->setExpires($this->getCurrentTime('+1 day'))
->setMaxAge(86400);

return $response;
}

}
47 changes: 47 additions & 0 deletions engine/classes/Elgg/TimeUsing.php
@@ -0,0 +1,47 @@
<?php
namespace Elgg;

use DateTime;

/**
* Adds methods for fixing the current time (for testing)
*
* @access private
*/
trait TimeUsing {

/**
* @var DateTime
*/
private $time;

/**
* Get a (cloned) time or the preset time if set
*
* @see DateTime::modify
*
* @param string $modifier Time modifier
* @return DateTime
*/
public function getCurrentTime($modifier = '') {
$time = $this->time ? $this->time : new DateTime();
$time = clone $time;
if ($modifier) {
$time->modify($modifier);
}
return $time;
}

/**
* Fix the current time (and return it)
*
* @param DateTime $time Current time (empty for now)
* @return void
*/
public function presetCurrentTime(DateTime $time = null) {
if (!$time) {
$time = new DateTime();
}
$this->time = clone $time;
}
}
35 changes: 34 additions & 1 deletion engine/lib/filestore.php
Expand Up @@ -488,6 +488,9 @@ function _elgg_filestore_init() {
// Unit testing
elgg_register_plugin_hook_handler('unit_test', 'system', '_elgg_filestore_test');

// Handler for serving embedded icons
elgg_register_page_handler('serve-icon', '_elgg_filestore_serve_icon_handler');

// Touch entity icons if entity access id has changed
elgg_register_event_handler('update:after', 'object', '_elgg_filestore_touch_icons');
elgg_register_event_handler('update:after', 'group', '_elgg_filestore_touch_icons');
Expand Down Expand Up @@ -607,8 +610,38 @@ function elgg_get_inline_url(\ElggFile $file, $use_cookie = false, $expires = ''
}

/**
* Reset icon URLs if access_id has changed
* Returns a URL suitable for embedding entity's icon in a text editor.
* We can not use elgg_get_inline_url() for these purposes due to a URL structure
* bound to user session and file modification time.
* This function returns a generic (permanent) URL that will then be resolved to
* an inline URL whenever requested.
*
* @param \ElggEntity $entity Entity
* @param string $size Size
* @return string
* @since 2.2
*/
function elgg_get_embed_url(\ElggEntity $entity, $size) {
return elgg_normalize_url("serve-icon/$entity->guid/$size");
}

/**
* Handler for /serve-icon resources
* /serve-icon/<entity_guid>/<size>
*
* @return void
* @access private
* @since 2.2
*/
function _elgg_filestore_serve_icon_handler() {
$response = _elgg_services()->iconService->handleServeIconRequest();
$response->send();
exit;
}

/**
* Reset icon URLs if access_id has changed
*
* @param string $event "update:after"
* @param string $type "object"|"group"
* @param ElggObject $entity Entity
Expand Down

0 comments on commit f8459bc

Please sign in to comment.