horst

Protect original Images in site/assets/files/

Recommended Posts

Hi, on a site I want to disable access to original images and only allow to access thumbnails and watermarked image variations.

To create the watermarked images I use the PiM, what creates filenames with prefix "pim_". For individual thumbnails I use the Thumbnails-Module and define the prefix like "thumb_".

A simple way would be to use a htaccess file in top of the assets folder with something like this for example:

# deny access to all jpegs and pngs
<FilesMatch "^.*\.(jpg|png)$">
    Deny from all
</FilesMatch>

# allow access to pim_ - variations (my watermarked images)
<FilesMatch "^pim_.*\.(jpg|png)$">
    Allow from all
</FilesMatch>

# allow access to thumb_ - prefixed files (my individual thumbnails)
<FilesMatch "^thumb_.*\.(jpg|png)$">
    Allow from all
</FilesMatch>

# allow access to 100px thumbnails as used by default in PW-Admin when images-field is configured to display thumbnails
<FilesMatch "^.*\.0x100\.(jpg|png)$">
    Allow from all
</FilesMatch>
<FilesMatch "^.*\.100x0\.(jpg|png)$">
    Allow from all
</FilesMatch>

The only downside is that I cannot access the original images from the admin backend. While it isn't an option to use a proxy-script from the front-end, it could be an option for the admin.

Any ideas how I could access the originals when in admin backend?

  • Like 2

Share this post


Link to post
Share on other sites

Hi Horst, 

maybe you could set up a rule which takes care of the referrer, something like this?

RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?yourdomain.com [NC]

Only if the referrer equals your domain, allow access to the originals. Problem could be that your server does not use http referrers...

Share this post


Link to post
Share on other sites

 Only if the referrer equals your domain, allow access to the originals. Problem could be that your server does not use http referrers...

Maybe something like domain/processwire, but a referrer header could be set to anything one want, I guess.

Better could be to use the REMOTE_ADDR. Does someone know how a variable for apache could be set by script? If this could be done everytime a superuser log in I could set this IP to a apache / environment variable for which access is allowed.

Share this post


Link to post
Share on other sites

Private file handling is your solution. You would abstract all files from public access by doing something like... 

/files.php?file=myfile.jpg

vs.

/myfile.jpg

Then in your private file handler you can check permissions and show the users the correct image according to their access level. I'm not sure if idea is native to PW.

  • Like 1

Share this post


Link to post
Share on other sites

@sshaw: what you are describing is what I have called proxy-script in my opening post. I don't want that for frontend output, because it is not needed and I want avoid overhead. All images that should have public access can be accessed directly. Others (the originals) are blocked to the public. Thats fine.

The only downside is that an admin in backend also cannot access the originals.

What I'm looking for is how I can define this in htaccess file:

all images that are now blocked for direct access should be not served but handled by a proxy script.

Share this post


Link to post
Share on other sites
To do it your way, maybe use RedirectMatch in place of your first FilesMatch
RedirectMatch \.(png|gif|jpg|jpeg)$ http://example.com/image_handler.php
I think mod rewrite will be your ideal solution, several examples below that will get you close. Personally I'd rewrite all image requests to my proxy script, then store my allowed prefixes in an array and simply loop through the array. It's not the best solution, but it's simple and allows for some scalability. 
 
in .htaccess 
 
Redirect all images to proxy script
RewriteEngine On
RewriteBase /
RewriteRule ^(.*)\.(jpg|png|jpeg|gif)$ watermark.php?image=$1.$2 [NC,L]
in your proxy script watermark.php
$prefixes[] = "thumb_";
$prefixes[] = "100x0";

foreach ($prefixes[] as $prefix) {
    if((substr(0,strlen($prefix)) == $prefix) xor ($user->isSuperuser())) {
        //return the image
    } else {
        //return a different image (maybe one that says access to this file is denied)
    }
}
Some rewrite examples that may help:
 
Redirect requests that don't originate from the host www.example.com (stop hotlinking) 
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !www.example.com [NC]
RewriteRule \.(gif|jpg|png)$ /images/go-away.png [R,NC]
Check for a login cookie and redirect if not logged in
RewriteEngine On
RewriteBase /
RewriteCond %{HTTP_COOKIE} !CookieName= [NC]
RewriteRule .* http://www.example.com/members/login.php [L]

Share this post


Link to post
Share on other sites

coming back to this: I have made good progress!
 
I do not use the htaccess file in site/assets/files/ anymore but have edited the htaccess file in pw root folder. Somewhere at top of the mod_rewrite directives I have added my lines that should redirect requests to original images to a proxy-script and let others pass through:
 

htaccess with Pim1 and PW < 2.5.11

Spoiler

...

  # -----------------------------------------------------------------------------------------------
  # Optional: Set a rewrite base if rewrites aren't working properly on your server.
  # And if your site directory starts with a "~" you will most likely have to use this.
  # -----------------------------------------------------------------------------------------------

  RewriteBase /
  # RewriteBase /pw/
  # RewriteBase /~user/

    # -----------------------------------------------------------------------------------------------
    # CUSTOMSETTING : redirect original images to proxy-script - /pwimg.php?fn=...
    # -----------------------------------------------------------------------------------------------

    RewriteCond %{REQUEST_FILENAME} -f
    RewriteCond %{REQUEST_FILENAME} (^|/)site/assets/files/(.*?)/
    RewriteCond %{REQUEST_FILENAME} \.(jpg|jpeg|gif|png)$ [NC]
    RewriteCond %{REQUEST_FILENAME} !/pim_
   #RewriteCond %{REQUEST_FILENAME} !/(thumb_|thumb2_)
    RewriteCond %{REQUEST_FILENAME} !.*/.*?\.([0-9]+)x([0-9]+)\.(jpg|png|jpeg|gif)$ [NC]

    RewriteRule ^(.*)$ pwimg.php?fn=$1 [L,QSA]

  # -----------------------------------------------------------------------------------------------
  # Access Restrictions: Keep web users out of dirs that begin with a period
  # -----------------------------------------------------------------------------------------------

...

 

 htaccess with Pim2 and PW 2.5.11+ / PW 3+

Spoiler

        # -----------------------------------------------------------------------------------------------
        # CUSTOMSETTING : redirect original images to proxy-script - /pwimg.php?fn=...
        # -----------------------------------------------------------------------------------------------

        RewriteCond %{REQUEST_FILENAME} -f
        RewriteCond %{REQUEST_FILENAME} (^|/)site/assets/files/(.*?)/

        RewriteCond %{REQUEST_FILENAME} \.(jpg|jpeg|gif|png)$ [NC]
        RewriteCond %{REQUEST_FILENAME} !-piacrop
        RewriteCond %{REQUEST_FILENAME} !-piacontain
        RewriteCond %{REQUEST_FILENAME} !-pim2-full
        RewriteCond %{REQUEST_FILENAME} !-blogthumb
        RewriteCond %{REQUEST_FILENAME} !.*/.*?\.([0-9]+)x([0-9]+)\.(jpg|png|jpeg|gif)$ [NC]
        RewriteRule ^(.*)$ pwimg.php?fn=$1 [L]

 


Now from all existing images the originals get redirected to the proxy-script and the others will delivered directly by apache. Requests to none existing imagefiles get answered by a 404.
 
So as everything seems to work fine, the RewriteConditions could be optimized a bit.
 
 
---- pwimg.php ----

<?php

// check filename
$imgFilename = isset($_GET['fn']) ? preg_replace('/[^a-zA-Z0-9_\-\/\.@]/', '', $_GET['fn']) : false;
$imgFilename = is_file(dirname(__FILE__) . "/$imgFilename") && is_readable(dirname(__FILE__) . "/$imgFilename") ? dirname(__FILE__) . "/$imgFilename" : false;
if (false == $imgFilename) {
    header('HTTP/1.1 404 Not Found');
    exit(2);
}

// check imagetype
$imgType = getImageType($imgFilename);
if (false == $imgType) {
    header('HTTP/1.1 403 Forbidden');
    header('Content-type: image/jpeg');
    exit(1);
}

// bootstrap PW
require_once(dirname(__FILE__) . '/index.php');

// check user-account
if (! wire('user')->hasRole('superuser|editor')) {
    header('HTTP/1.1 403 Forbidden');
    header('Content-type: ' . $imgType);
    exit(1);
}

// collect infos
$maxAge = (60 * 60 * 2); // 2 hours
$imgTimestamp = filemtime($imgFilename);
$imgExpiration = intval(time() + $maxAge);

// create headers
$imgHeaders = array();
$imgHeaders[] = 'Content-type: ' . $imgType;
$imgHeaders[] = 'Content-Length: ' . filesize($imgFilename);
$imgHeaders[] = 'Date: ' . gmdate('D, d M Y H:i:s',time()) . ' GMT';
$imgHeaders[] = 'Last-Modified: ' . gmdate('D, d M Y H:i:s',$imgTimestamp) . ' GMT';
$imgHeaders[] = 'Expires: ' . gmdate('D, d M Y H:i:s', $imgExpiration) . ' GMT';
$imgHeaders[] = 'pragma: cache';
$imgHeaders[] = "Cache-Control: no-transform, private, s-maxage={$maxAge}, max-age={$maxAge}";

// send headers
foreach($imgHeaders as $imgHeader) header($imgHeader);

// send file
$errorCode = @readfile($imgFilename) === FALSE ? 1 : 0;

// and exit
exit($errorCode);



// --- functions ---

function getImageType($fn, $returnAsInteger = false) {

    $types1 = array(1 => 'gif', 2 => 'jpg', 3 => 'png');
    $types2 = array('gif' => 1, 'jpg' => 2, 'jpeg' => 2, 'png' => 3);

    if (function_exists('exif_imagetype') && isset($types1[@exif_imagetype($fn)])) {
        $success = $types1[exif_imagetype($fn)];
    }
    if (!isset($success) && function_exists('getimagesize')) {
        $info = @getimagesize($fn);
        if (isset($info[2]) && isset($types1[$info[2]])) {
            $success = $types1[$info[2]];
        }
    }
    if (!isset($success)) {
        $extension = strtolower(pathinfo($fn, PATHINFO_EXTENSION));
        if (isset($types2[$extension])) {
            $success = $types1[$types2[$extension]];
        }
    }

    if (!isset($success)) return false;

    return true === $returnAsInteger ? $types2[$success] : $success;
}
Edited by horst
updated with newer settings for PW 2.5.11+ and Pim2
  • Like 5

Share this post


Link to post
Share on other sites

Hi @horst,

Im looking for an exact same solution, website is showing images, but can not be accessed through going directly to the URL.

I tried your solution, but also the page where the image is displayed is getting a 404 response for this image.

I also have de pwimg.php as a template and page /pwimg/ instead of a 'normal' php file, is that maybe a problem?

I hope you can help me with this!
Thnx.

Share this post


Link to post
Share on other sites

Please read & follow my solution exactly. The way you described doesn't work & it isn't meant to do so.
It describes a way to only protect the original images. Therefor you need to deny all access to them via htaccess file. In a second step, you enable access for superusers (or who ever you like) via a proxy (php) script. Nothing (!) is embedded into your PW-installation, only the htaccess file.

Share this post


Link to post
Share on other sites

Thanks for your reply. Indeed there was some misunderstanding. I got your solution working now and should be fine for now. Thanks @horst :).

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By Peter Knight
      Is there a way to make JPGs progressive by default via the API?
      I've added the following to my site/config.php file but user-uploaded images are often displayed as non progressive.
      $config->imageSizerOptions = array( 'upscaling' => true, // upscale if necessary to reach target size? 'cropping' => true, // crop if necessary to reach target size? 'autoRotation' => true, // automatically correct orientation? 'interlace' => true, // use interlaced JPEGs by default? (recommended) 'sharpening' => 'soft', // sharpening: none | soft | medium | strong 'quality' => 95, // quality: 1-100 where higher is better but bigger 'hidpiQuality' => 60, // Same as above quality setting, but specific to hidpi images 'defaultGamma' => 0.5, // defaultGamma: 0.5 to 4.0 or -1 to disable gamma correction (default=2.0) ); Thanks
    • By cosmicsafari
      Hi all,
      Just wondering if someone with a bit more PW knowledge than me could give a run down of what this method actually does and how its achieved.
      I get that it rebuilds image variations but based on what settings?
      If I wanted to rebuild all the websites image variations but at say a reduced image quality can this be set somewhere globally that this method would take into account?
      For some context I have built a fairly simple module to delete all the image variations connected to any FieldtypeImage which is being used on the website, for the most part this works quite well. As I was quite happy with how that turned out I figured I would give the module another option to rebuild the images also. So there would be a 'Remove' and 'Rebuild' button on the modules config page.
      The idea being that I could use this tool to delete all the image variations, update some global settings then regenerate them all but currently it doesn't appear to do that.
      I assume this is either because my codes borked or im misunderstanding something fundamental about how rebuildVariations() works.
    • By Sergio
      Hi everyone,
      Yesterday I began working on a module to create a filesystem abstraction for files and images using the Flysytem library (http://flysystem.thephpleague.com), out of the necessity of having the images copied (and probably linked) on Amazon S3 and other places like Dropbox. There two reasons why I decided to tackle this:
      1 - When I work on the project in my machine, I need a way to automatically sync the images from the server and/or the external storage.
      When an image is added on the server and the database is imported on my local env, PW shows a blank thumbnail of it. The idea for the module if to check if the page image has a width == 0 and if it exists on the server, add it to the local filesystem.
      2 - In the past, I had to move a large website to a different server in a hurry and a lot of images were left behind (it was a mess). So I'm planning for a possible future worst-case scenario of the server exploding 🙂 
      The code I quickly wrote is below (please bear with me that it's pretty raw at the moment). One thing I had to figure it out is why PW fires the Pageimage::size hook wherever a page is loaded in the admin, even though the thumbnails are already created. I was planning to save the image variations on S3 as well. Can someone clarify?
      I know that @LostKobrakai was working on a similar project, and so I would like to ask him and everyone else if you think I (and who may help) should evolve this idea into a full-featured module where the user can select which server (adapter) to use (AWS, Digital Ocean spaces, Dropbox etc.)
      <?php namespace ProcessWire; use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; use League\Flysystem\Filesystem; use League\Flysystem\Adapter\Local; use Spatie\Dropbox\Client; use Spatie\FlysystemDropbox\DropboxAdapter; class ProcessFly extends WireData implements Module, ConfigurableModule { public static function getModuleInfo() { return array( 'title' => 'ProcessWire Flysystem Integration', 'version' => 001, 'summary' => 'Synchronize all the page assets uploaded through PW to a specified bucket in Amazon S3 and...', 'author' => 'Sérgio Jardim', 'singular' => true, 'autoload' => true, 'icon' => 'image' ); } public function init() { $this->client = S3Client::factory([ 'credentials' => [ 'key' => '', 'secret' => '', ], 'region' => 'sa-east-1', 'version' => 'latest', ]); $this->bucket_name = ""; $this->dir_name = "images"; $this->s3_adapter = new AwsS3Adapter($this->client, $this->bucket_name, $this->dir_name); $this->s3_filesystem = new Filesystem($this->s3_adapter); // DROPBOX $this->authorizationToken = ""; $this->dropbox_client = new Client($this->authorizationToken); $this->adapter_dropbox = new DropboxAdapter($this->dropbox_client); $this->dropbox_filesystem = new Filesystem($this->adapter_dropbox); $this->addHookAfter('Pages::saved', $this, 'checkImageS3'); // Download images that are not in local filesystem for whatever reason but are available in the remove one. $this->addHookAfter('InputfieldFile::fileAdded', $this, 'uploadImageS3'); // Fired when a file/image is added to a page in the admin // $this->addHookAfter('Pageimage::size', $this, 'uploadImageS3'); // Fired when a file is resized via API. Note: this hook is also called when the page is loaded in Admin. Need to know why. $this->addHookAfter('Pageimage::url', $this, 'redirectImageURL'); //Replace image URL for the S3 path } public function redirectImageURL($event){ if($event->page->template == 'admin') return; else $event->return = "https://s3-sa-east-1.amazonaws.com/[bucket name]/images/" . $event->object->page . "/" . $event->object->name; } // UPLOAD public function uploadImageS3($event){ if(count($event->return)) { //if it is a image resize event get image variation data $file = $event->return; } else { $file = $event->arguments(0); } $filename = $file->name; $filename_original = $file->url; $pathToFile = $file->page . "/" . $filename; $system_path = $file->filename(); try{ $file_on_s3 = $this->s3_filesystem->has($pathToFile); //check if file exists on AWS if(!$file_on_s3) { $contents = file_get_contents($system_path); $this->s3_filesystem->put($pathToFile, $contents, array( 'visibility' => 'public', )); //upload file with the same folder structure as in assets/files: page_id/file_name //Also add a copy to Dropbox (OPTIONAL) $this->dropbox_filesystem->put($pathToFile, $contents); } } catch (Exception $e){ throw new WireException("Error: Image not Added to S3: {$e->getMessage()}"); } } public function checkImageS3($event){ $page = $event->arguments(0); if(count($page->images)) { foreach($page->images as $file) { if($file->width == 0) { return $this->downloadImageS3($page, $file); }; } } } public function downloadImageS3($page, $file){ $pathToFile = $page->id . "/" . $file->name; $file_on_s3 = $this->s3_filesystem->has($pathToFile); //check if file exists on AWS if($file_on_s3) { $image = "https://s3-sa-east-1.amazonaws.com/[bucket name]/images/" . $pathToFile; // $page->images->remove($image); $page->images->add($image); $page->save(); } else { throw new WireException("Error: Image not found on S3: {$file->name}"); } } }  
    • By neophron
      Hi there,
      a few weeks ago I completed a 3.098 installation (server php 7.2). Yesterday I wanted to login but failed. I tried different browsers, meanwhile on two different PCs, but no login.
      Then I realized, that after hitting the »Login« button, there was no message (admin name – Login failed). I also tried all procedures (https://processwire-recipes.com/recipes/resetting-admin-password-via-api/). Then I checked the errors.txt (site/assets/logs) – nothing.
      The frontend works fine. Is there a way to check this Processwire installation? Could be something missing?
       
    • By humanafterall
      Hi,
      I'm using the on the fly image resizing in Processwire to create suitable image sizes for different viewports (using Foundation's Image Interchange).
      All seemed to be working fine but I'm having real trouble with the quality of the outputted image.
      This is the orginal image:

      This is an example of a resized image:

      It looks like it's losing colour information in the process of being resized.
      I've tried setting the default quality to 100% (in the wire/core/ImageSizer.php) but to no avail.
      Could anyone share any light on what might be happening here?