I\'m looking for a method that encodes an string to shortest possible length and lets it be decodable (pure PHP, no SQL). I have working sc
From the discussion in the comments section it looks like what you really want is to protect your original hi-res images.
Having that in mind I'd suggest to actually do that first using your web server configuration (e.g. Apache mod_authz_core or Nginx ngx_http_access_module) to deny access from the web to the directory where your original images are stored.
Note that we server will only deny access to your images from the web but you will still be able to access them directly from your php scripts. Since you already are displaying images using some "resizer" script I'd suggest putting some hard limit there and refuse to resize images to anything bigger then that (e.g. something like this $width = min(1000, $_GET['w'])
).
I know this does not answer your original question but I think this would the right solution to protect your images. And if you still want to obfuscate the original name and resizing parameters you can do that however you see fit without worrying that someone might figure out whats behind it.
You say that you want the size there so that if you decide some day that the preview images are too small, you want to increase the size - the solution here is to hard code the image size into the php script and eliminate it from the url. If you want to change the size in the future, change the hardcoded values in the php script (or in a config.php that you include into the script).
You've also said that you are already using files to store image data as a JSON object, like: name
, title
, description
. Exploiting this, you don't need a database and can use the JSON file name as the key for looking up the image data.
When the user visits a url like this:
www.mysite.com/share/index.php?ax9v
You load ax9v.json
from the location you are already storing the json files, and within that json file the image's real path is stored. Then load the image, resize it according to the hardcoded size in your script and send it to the user.
Drawing from the conclusions in https://blog.codinghorror.com/url-shortening-hashes-in-practice/ , to get the smallest search string part of the url you would need to iterate valid character combinations as new files are uploaded (eg. the first one is "AAA" then "AAB", "AAC", etc.) instead of using a hashing algorithm. Your solution would then have only 3 characters in the string for the first 238,328 photos you upload.
I had started to prototype a php solution on phpfiddle but the code disappeared (don't use phpfiddle).
EDIT
Reading from the above and below comments, you need a solution to hide the real path of your image parser, giving it a fixed image width.
http://www.example.com/tn/full/animals/images/lion.jpg
You can achieve a basic "thumbnailer" by taking profit of .htaccess
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule tn/(full|small)/(.*) index.php?size=$1&img=$2 [QSA,L]
Your PHP file:
$basedir="/public/content/";
$filename=realpath($basedir.$_GET["img"]);
## check that file is in $basedir
if ((!strncmp($filename, $basedir, strlen($basedir))
||(!file_exists($filename)) die("Bad file path");
switch ($_GET["size"]) {
case "full":
$width=700;
$height=500;
## you can also use getimagesize() to test if the image is landscape or portrait
break;
default:
$width=350;
$height=250;
break;
}
## here is your old code for resizing images
## Note that the "tn" directory can exist and store the actual reduced images
This lets you using the url www.example.com/tn/full/animals/images/lion.jpg
to view your reduced in size image.
This has the advantage for SEO to preserve the original file name.
http://www.example.com/tn/full/lion.jpg
If you want a shorter url, if the number of images you have is not too much, you can use the basename of the file (eg. "lion.jpg") and recursively search. When collision use an index to identify which one you want (eg. "1--lion.jpg")
function matching_files($filename, $base) {
$directory_iterator = new RecursiveDirectoryIterator($base);
$iterator = new RecursiveIteratorIterator($directory_iterator);
$regex_iterator = new RegexIterator($iterator, "#$filename\$#");
$regex_iterator->setFlags(RegexIterator::USE_KEY);
return array_map(create_function('$a', 'return $a->getpathName();'), iterator_to_array($regex_iterator, false));
}
function encode_name($filename) {
$files=matching_files(basename($filename), realpath('public/content'));
$tot=count($files);
if (!$tot) return NULL;
if ($tot==1) return $filename;
return "/tn/full/".array_search(realpath($filename), $files)."--".basename($filename);
}
function decode_name($filename) {
$i=0;
if (preg_match("#^([0-9]+)--(.*)#", $filename, $out)) {
$i=$out[1];
$filename=$out[2];
}
$files=matching_files($filename, realpath('public/content'));
return $files ? $files[$i] : NULL;
}
echo $name=encode_name("gallery/animals/images/lion.jpg").PHP_EOL;
## --> returns lion.jpg
## You can use with the above solution the url http://www.example.com/tn/lion.jpg
echo decode_name(basename($name)).PHP_EOL;
## -> returns the full path opn disk to the image "lion.jpg"
Original post:
Basically, if you add some formatting in your example your shorten url is in fact longer:
img=/dir/dir/hi-res-img.jpg&w=700&h=500 // 39 chars
y8xNt9VPySwC44xM3aLUYt3M3HS9rIJ0tXJbcwMDtQxbUwMDAA // 50 chars
Using base64_encode
will always result in longer strings. And gzcompress
will require at less to store one occurence of the different chars; this is not a good solution for small strings.
So doing nothing (or a simple str_rot13
) is clearly the first option to consider if you want to shorten the result you had previously.
You can also use a simple character replacement method of your choice:
$raw_query_string = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';
$from="0123456789abcdefghijklmnopqrstuvwxyz&=/ABCDEFGHIJKLMNOPQRSTUVWXYZ";
// the following line if the result of str_shuffle($from)
$to="0IQFwAKU1JT8BM5npNEdi/DvZmXuflPVYChyrL4R7xc&SoG3Hq6ks=e9jW2abtOzg";
echo strtr($raw_query_string, $from, $to)."\n";
// Result: EDpL4MEu4MEu4NE-u5f-EDp.dmprYLU00rNLA00 // 39 chars
Reading from your comment, what you really want is "to prevent anyone to gets a hi-res image".
The best way to achieve that is to generate a checksum with a private key.
Encode:
$secret="ujoo4Dae";
$raw_query_string = 'img=/dir/dir/hi-res-img.jpg&w=700&h=500';
$encoded_query_string = $raw_query_string."&k=".hash("crc32", $raw_query_string.$secret);
Result: img=/dir/dir/hi-res-img.jpg&w=700&h=500&k=2ae31804
Decode:
if (preg_match("#(.*)&k=([^=]*)$#", $encoded_query_string, $out)
&& (hash("crc32", $out[1].$secret) == $out[2])) {
$decoded_query_string=$out[1];
}
This does not hide the original path but this path has no reason to be public, your "index.php" can output your image from the local directory once the key has been checked.
If you really want to shorten your original URL, you have to consider the acceptable characters in the original url to be restricted. Many compression methods are based on the fact that you can use a full byte to store more than a character.
A lot has been said about how encoding doesn't help security so I am just concentrating on the shortening and aesthetics.
Rather than thinking of it as a string, you could consider it as 3 individual components. Then if you limit your code space for each component, you can pack things together a lot smaller.
E.g.
I'm limiting path to only consist of a maximum 31 characters so we can use 5 bit groupings.
Pack your fixed length dimensions first, and append each path character as 5 bits. It might also be necessary to add a special null character to fill up the end byte. Obviously you need to use the same dictionary string for encoding and decoding.
See code below.
This shows that by limiting what you encode and how much you can encode, you can get a shorter string. You could make it even shorter by using only 12 bit dimension integers (max 2048), or even removing parts of the path if they are known such as base path or file extension (see last example).
<?php
function encodeImageAndDimensions($path, $width, $height) {
$dictionary = str_split("abcdefghijklmnopqrstuvwxyz/-."); //Max 31 chars please
if ($width >= pow(2,16)) {
throw new Exception("Width value is too high to encode with 16 bits");
}
if ($height >= pow(2,16)) {
throw new Exception("Height value is too high to encode with 16 bits");
}
//Pack width, then height first
$packed = pack("nn", $width, $height);
$path_bits = "";
foreach (str_split($path) as $ch) {
$index = array_search($ch, $dictionary, true);
if ($index === false) {
throw new Exception("Cannot encode character outside of the allowed dictionary");
}
$index++; //Add 1 due to index 0 meaning NULL rather than a.
//Work with a bit string here rather than using complicated binary bit shift operators.
$path_bits .= str_pad(base_convert($index, 10, 2), 5, "0", STR_PAD_LEFT);
}
//Remaining space left?
$modulo = (8 - (strlen($path_bits) % 8)) %8;
if ($modulo >=5) {
//There is space for a null character to fill up to the next byte
$path_bits .= "00000";
$modulo -= 5;
}
//Pad with zeros
$path_bits .= str_repeat("0", $modulo);
//Split in to nibbles and pack as a hex string
$path_bits = str_split($path_bits, 4);
$hex_string = implode("", array_map(function($bit_string) {
return base_convert($bit_string, 2, 16);
}, $path_bits));
$packed .= pack('H*', $hex_string);
return base64_url_encode($packed);
}
function decodeImageAndDimensions($str) {
$dictionary = str_split("abcdefghijklmnopqrstuvwxyz/-.");
$data = base64_url_decode($str);
$decoded = unpack("nwidth/nheight/H*path", $data);
$path_bit_stream = implode("", array_map(function($nibble) {
return str_pad(base_convert($nibble, 16, 2), 4, "0", STR_PAD_LEFT);
}, str_split($decoded['path'])));
$five_pieces = str_split($path_bit_stream, 5);
$real_path_indexes = array_map(function($code) {
return base_convert($code, 2, 10) - 1;
}, $five_pieces);
$real_path = "";
foreach ($real_path_indexes as $index) {
if ($index == -1) {
break;
}
$real_path .= $dictionary[$index];
}
$decoded['path'] = $real_path;
return $decoded;
}
//These do a bit of magic to get rid of the double equals sign and obfuscate a bit. It could save an extra byte.
function base64_url_encode($input) {
$trans = array('+' => '-', '/' => ':', '*' => '$', '=' => 'B', 'B' => '!');
return strtr(str_replace('==', '*', base64_encode($input)), $trans);
}
function base64_url_decode($input) {
$trans = array('-' => '+', ':' => '/', '$' => '*', 'B' => '=', '!' => 'B');
return base64_decode(str_replace('*', '==',strtr($input,$trans)));
}
//Example usage
$encoded = encodeImageAndDimensions("/dir/dir/hi-res-img.jpg", 700, 500);
var_dump($encoded); // string(27) "Arw!9NkTLZEy2hPJFnxLT9VA4A$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"]=> int(700) ["height"]=> int(500) ["path"]=> string(23) "/dir/dir/hi-res-img.jpg" }
$encoded = encodeImageAndDimensions("/another/example/image.png", 4500, 2500);
var_dump($encoded); // string(28) "EZQJxNhc-iCy2XAWwYXaWhOXsHHA"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"]=> int(4500) ["height"]=> int(2500) ["path"]=> string(26) "/another/example/image.png" }
$encoded = encodeImageAndDimensions("/short/eg.png", 300, 200);
var_dump($encoded); // string(19) "ASwAyNzQ-VNlP2DjgA$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"]=> int(300) ["height"]=> int(200) ["path"]=> string(13) "/short/eg.png" }
$encoded = encodeImageAndDimensions("/very/very/very/very/very-hyper/long/example.png", 300, 200);
var_dump($encoded); // string(47) "ASwAyN2LLO7FlndiyzuxZZ3Yss8Rm!ZbY9x9lwFsGF7!xw$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"]=> int(300) ["height"]=> int(200) ["path"]=> string(48) "/very/very/very/very/very-hyper/long/example.png" }
$encoded = encodeImageAndDimensions("only-file-name", 300, 200);
var_dump($encoded); //string(19) "ASwAyHuZnhksLxwWlA$"
$decoded = decodeImageAndDimensions($encoded);
var_dump($decoded); // array(3) { ["width"]=> int(300) ["height"]=> int(200) ["path"]=> string(14) "only-file-name" }
I think this would be better done by not obscuring at all. You could quite simply cache returned images and use a handler to provide them. This requires the image sizes to be hardcoded into the php script. When you get new sizes you can just delete everything in the cache as it is 'lazy loaded'.
1. Get the image from the request
This could be this: /thumbnail.php?image=img.jpg&album=myalbum
. It could even be made to be anything using rewrite and have a URL like: /gallery/images/myalbum/img.jpg
.
2. Check to see if a temp version does not exist
You can do this using is_file()
.
3. Create it if it does not exist
Use your current resizing logic to do it, but don't output the image. Save it to the temp location.
4. Read the temp file contents to the stream
Pretty much just output it.
Here is an untested code example...
<?php
// assuming we have a request /thumbnail.php?image=img.jpg&album=myalbum
// these are temporary filenames places. you need to do this yourself on your system.
$image = $_GET['image']; // the file name
$album = $_GET['album']; // the album
$temp_folder = sys_get_temp_dir(); // temp dir to store images
// (this should really be a specific cache path)
$image_gallery = "images"; // root path to the image gallery
$width = 700;
$height = 500;
$real_path = "$image_gallery/$album/$image";
$temp_path = "$temp_folder/$album/$image";
if(!is_file($temp_path))
{
// read in the image
$contents = file_get_contents($real_path);
// resize however you are doing it now.
$thumb_contents = resizeImage($contents, $width, $height);
// write to temp
file_put_contents($temp_path, $thumb_contents);
}
$type = 'image/jpeg';
header('Content-Type:'.$type);
header('Content-Length: ' . filesize($temp_path));
readfile($temp_path);
?>
Instead of encoding the url, how about outputting a thumbnail copy of the original image? Here's what I'm thinking:
1) Create a "map" for php by naming your pictures (the actual file names) using random characters. Random_bytes is a great place to start.
2) Embed the desired resolution within the randomized url string from #1.
3) Use the imagecopyresampled function to copy the original image into the resolution you would like to output before outputting it out to the client's device.
So for example:
1 - Filename example (from bin2hex(random_bytes(6))
): a1492fdbdcf2.jpg
2 - Resolution desired: 800x600. My new link could look like:
http://myserver.com/?800a1492fdbdcf2600
or maybe http://myserfer.com/?a1492800fdbdc600f2
or maybe even http://myserver.com/?800a1492fdbdcf2=600
depending on where I choose to embed the resolution within the link
3 - PHP would know that the file name is a1492fdbdcf2.jpg, grab it, use the imagecopyresampled to copy to the resolution you want, and output it.