Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for limiting the number of crop/resize versions which can be created from one image #202

Closed
wants to merge 6 commits into from
4 changes: 4 additions & 0 deletions inc/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ function exportlink($id = '', $format = 'raw', $more = '', $abs = false, $sep =
function ml($id = '', $more = '', $direct = true, $sep = '&', $abs = false) {
global $conf;
if(is_array($more)) {
// add token for resized images
if($more['w'] || $more['h']){
$more['tok'] = media_get_token($id,$more['w'],$more['h']);
}
// strip defaults for shorter URLs
if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']);
if(!$more['w']) unset($more['w']);
Expand Down
149 changes: 147 additions & 2 deletions inc/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -1796,9 +1796,15 @@ function media_resize_image($file, $ext, $w, $h=0){
if($w > 2000 || $h > 2000) return $file;

//cache
$local = getCacheName($file,'.media.'.$w.'x'.$h.'.'.$ext);
$basename = getCacheName($file);
$local = $basename.'.media.'.$w.'x'.$h.'.'.$ext;
$mtime = @filemtime($local); // 0 if not exists

if (!$mtime && !media_reserve_version($basename,$local)){
// unable to reserve a file for a new version, return the original image
return $file;
}

if( $mtime > filemtime($file) ||
media_resize_imageIM($ext,$file,$info[0],$info[1],$local,$w,$h) ||
media_resize_imageGD($ext,$file,$info[0],$info[1],$local,$w,$h) ){
Expand Down Expand Up @@ -1828,6 +1834,13 @@ function media_crop_image($file, $ext, $w, $h=0){
// calculate crop size
$fr = $info[0]/$info[1];
$tr = $w/$h;

// check if the crop can be handled completely by resize,
// i.e. the specified width & height match the aspect ratio of the source image
if ($w == round($h*$fr)) {
return media_resize_image($file, $ext, $w);
}

if($tr >= 1){
if($tr > $fr){
$cw = $info[0];
Expand All @@ -1850,9 +1863,15 @@ function media_crop_image($file, $ext, $w, $h=0){
$cy = (int) (($info[1]-$ch)/3);

//cache
$local = getCacheName($file,'.media.'.$cw.'x'.$ch.'.crop.'.$ext);
$basename = getCacheName($file);
$local = $basename.'.media.'.$cw.'x'.$ch.'.crop.'.$ext;
$mtime = @filemtime($local); // 0 if not exists

if (!$mtime && !media_reserve_version($basename,$local)){
// unable to reserve a file for a new version, return the original image
return $file;
}

if( $mtime > @filemtime($file) ||
media_crop_imageIM($ext,$file,$info[0],$info[1],$local,$cw,$ch,$cx,$cy) ||
media_resize_imageGD($ext,$file,$cw,$ch,$local,$cw,$ch,$cx,$cy) ){
Expand All @@ -1864,6 +1883,132 @@ function media_crop_image($file, $ext, $w, $h=0){
return media_resize_image($file,$ext, $w, $h);
}

/**
* Reserve a cache file for a new version of an image
*
* its necessary to reserve, using touch(), a placeholder for the new
* resize/crop version before the resize/crop operation to ensure
* consistency with any other nearly simultaneous fetch requests for
* resizes/crops of the same source image.
*
* @param string $basename base name of the cache file used for the image versions (a hash on the source image)
* @param string $version complete path to the cachefile to be used for this version
* @return bool true if the cachefile could be reserved
*
* @author Christopher Smith <chris@jalakai.co.uk>
*/
define(MEDIA_VERSION_LIMIT, 20);
define(MEDIA_VERSION_LIST_EXT, '.versions');

function media_reserve_version($basename,$version){
global $conf;

$version_list = $basename.MEDIA_VERSION_LIST_EXT; // name of the file containing the current list of versions of the image
$this_version = $version.' '.time()."\n"; // version list line format: {versionfilepath} {timestamp}
$can_reserve = false;

io_lock($version_list);
$version_list_changed = false;

$versions = @file($version_list);
if (!is_array($versions)) $versions = array();
$version_count = count($versions);

if ($version_count >= MEDIA_VERSION_LIMIT){
$stale = time() - max($conf['cachetime'],3600);

foreach ($versions as $i => $line){
list($cachefile, $timestamp) = preg_split('/ (?=[^ ]*$)/',trim($line),2); // split at last space

// test stale - assume non-stale files exist to avoid lots of unnecessary file accesses
// (the version list is in the cache, so emptying the cache will remove it, and
// presumably anything more specific will only remove cachefiles older than cachetime)
if ($stale < $timestamp) continue;

// version file says "stale", re-check against the actual file
$mtime = @filemtime($cachefile);
if ($stale < $mtime) continue;

// remove the cachefile, if it exists (mtime not false)
if ($mtime === false || @media_unlink_version($cachefile)){
unset($versions[$i]);
$version_list_changed = true;
--$version_count;
}

// only do the minimum necessary, stale files could still be valid and useful
if ($version_count < MEDIA_VERSION_LIMIT) break;
}
}

if ($version_count < MEDIA_VERSION_LIMIT) {
$can_reserve = true;
touch($version);

$versions[] = $this_version;
$version_list_changed = true;
}

if ($version_list_changed) {
file_put_contents($version_list, join('',$versions));
}

io_unlock($version_list);
return $can_reserve;
}

/**
* delete a resized/cropped image version
* and for crops, look for and delete any derived resize versions and their version list
*
* @param string $version path to version file to be deleted
* @return bool success deleting $version
*
* @author Christopher Smith <chris@jalakai.co.uk>
*/
function media_unlink_version($version){
if (strpos($version,'.crop.') !== false){
$basename = getCacheName($version);
$crop_version_list = $basename.MEDIA_VERSION_LIST_EXT;
$crop_versions = @file($crop_version_list);

// if $crop_version_list exists, $crop_versions will be an array
if (is_array($crop_versions)) {
foreach ($crop_versions as $line) {
list($cachefile, $timestamp) = preg_split('/ (?=[^ ]*$)/',trim($line),2); // split at last space
@unlink($cachefile);
}
@unlink($crop_version_list);
}
}

return @unlink($version);
}

/**
* Calculate a token to be used to verify fetch requests for resized or
* cropped images have been internally generated - and prevent external
* DDOS attacks via fetch
*
* @param string $id id of the image
* @param int $w resize/crop width
* @param int $h resize/crop height
*
* @author Christopher Smith <chris@jalakai.co.uk>
*/
function media_get_token($id,$w,$h){
// token is only required for modified images
if ($w || $h) {
$token = auth_cookiesalt().$id;
if ($w) $token .= '.'.$w;
if ($h) $token .= '.'.$h;

return substr(md5($token),0,6);
}

return '';
}

/**
* Download a remote file and return local filename
*
Expand Down
8 changes: 6 additions & 2 deletions lib/exe/fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
}

// check for permissions, preconditions and cache external files
list($STATUS, $STATUSMESSAGE) = checkFileStatus($MEDIA, $FILE, $REV);
list($STATUS, $STATUSMESSAGE) = checkFileStatus($MEDIA, $FILE, $REV, $WIDTH, $HEIGHT);

// prepare data for plugin events
$data = array(
Expand Down Expand Up @@ -180,7 +180,7 @@ function sendFile($file, $mime, $dl, $cache, $public = false) {
* @param $file reference to the file variable
* @returns array(STATUS, STATUSMESSAGE)
*/
function checkFileStatus(&$media, &$file, $rev = '') {
function checkFileStatus(&$media, &$file, $rev = '', $width=0, $height=0) {
global $MIME, $EXT, $CACHE, $INPUT;

//media to local file
Expand All @@ -200,6 +200,10 @@ function checkFileStatus(&$media, &$file, $rev = '') {
if(empty($media)) {
return array(400, 'Bad request');
}
// check token for resized images
if (($width || $height) && media_get_token($media, $width, $height) !== $INPUT->str('tok')) {
return array(412, 'Precondition Failed');
}

//check permissions (namespace only)
if(auth_quickaclcheck(getNS($media).':X') < AUTH_READ) {
Expand Down