PHP Proxy to hotlink images

I couldn't make this work but took the idea and had my first play with PHP to make a version that uses the API as intended rather than accesses the database. I'm pleased with the results; I hosted simple PHP files on a LAMP stack, just like your IT specialist did, but these ones pass all required details in the URL to get the token and image via the API. I can now use just about anything to get images from ODK. At long last!

Obviously it's important never to expose these files to the web; they're just hosted locally on my network. I'm also not a programmer; it took me countless hours to get this working and it doesn't do what yours does i.e. compress the images or rotate them. I was sort of hoping someone would take my code and improve it to add the features as it's a little beyond my skill set!

Code as follows.

First tile, token.php:

$username = $_GET["userID"];
$pass = $_GET["pass"];


$url = 'https://odk-url.com/v1/sessions';
$data = ['email' => $username, 'password' => $pass];

$options = [
    'http' => [
        'header' => "Content-type: application/json",
        'method' => 'POST',
        'content' => json_encode($data),
    ],
];

$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === false) {
    /* Handle error */
}

extract(json_decode($result, true));

echo "$token @ $expiresAt";
?>

This allows you to get a token by going to http://my-internal-lamp-server/token.php?userID=email@address.com&pass=odkpassword

It returns a string with the token on the left (first 64 characters) and the expiry date & time, with spaces and an @ between them.

You can use any reporting software to then use the token with the next file to hotlink an image from the ODK API.

image.php:

<?php
$token = $_GET["sessiontoken"];
$projectID = $_GET["pid"];
$xmlfID = $_GET["xmlfid"];
$parentkey = $_GET["parentkey"];
$filenm = $_GET["filenm"];

$url = 'https://odk-url.com/v1/projects/'.$projectID.'/forms/'.$xmlfID.'/submissions/'.$parentkey.'/attachments/'.$filenm;

$options = [
    'http' => [
        'header' => "Authorization: Bearer $token",
        'method' => 'GET',

    ],
];

$context = stream_context_create($options);
$imagedata = file_get_contents($url, false, $context);

header('Content-type: image/jpeg');
echo $imagedata;
?>

For this one you just pass it a URL like the following:

http://my-internal-lamp-server/token.php?sessiontoken=token-goes-here&pid=project-id-goes-here&xmlfid=xml-form-id-here&parentkey=instance-id-here&filenm=filename.jpg

So far it's working great and it's fast, with no need to use a second database. As stated before I'd love it if someone was able to tidy up the code/add the auto rotate & image compression options as it's beyond my ability - I've never coded PHP!

1 Like

Thanks. This was really helpful.

Well, this is a case of StormBound + Inquisitive = Solution (of sorts)

I have taken the concept and used some php to produce something that can (probably) be used on a production environment - I have used it on a standard webserver (not the same as my ODK Central instance) and it works... I'm sure it could be streamilined further, and I haven't managed to get the auto-rotate or other php functions to work yet (it throws an error when I use exif_read_data claiming that the data contains null bytes). Anyway, that gives a coder the chance to improve this...

I wanted to be able to access the images without manually needing to insert the token into a url, or passing sensitive data in a URL. This is what I came up with. There may be better ways to do it.

As usual, please use with care and you take full responsibility for anything that might go awry in using this code.

There are 5 files and I placed them in a folder called odkimage, so that I can visit the page [mydomain]/odkimages/index.php?[my-parameters]. I have 'hidden' the login details and generate the token using POST, which I think is relatively low risk. No sensitive data is/are stored in cache or browser history.

It feels a bit convoluted as my method requires the parameters to be decoded and re-encoded, and I constructed it so that you only need to adapt secrets.php (I hope) so it looks like there is duplication.

The folder "odkimages" looks like this:

/index.php
/image.php
/includes/token.php
/includes/secrets.php
/includes/.htaccess [on linux server]
Code

Here is the code for each file:
index.php

<!doctype html>
<html>
<?php
/* Get images from ODK Central
 * adapted from code by MrRodge
 * written by Chris York (seewhy)
 * license     GNU/GPL http://www.gnu.org/copyleft/gpl.html
 *
 * Pull an image from the ODK Central server based on projectid, formid, instanceid and filename (these variables need to be compiled from the submission data, so will need to be known!)
 * This file asks to create a token (includes/token.php) and then displays an image <image src=...> with an authenticated token
 */

//collect the parameters to pass on to image.php
$projectID = $_GET["pid"];
$xmlfID = $_GET["xmlfid"];
$parentkey = $_GET["parentkey"];
$filenm = $_GET["filenm"];

//prepare the token (has a dependent requirement for secrets.php)
require __DIR__.'/includes/token.php';

//create the address to call for the image - includes the token and API address (ODK server)
$imageconf = "image.php?pid=".$projectID."&xmlfid=".$xmlfID."&parentkey=".$parentkey."&filenm=".$filenm."&token=".$token."&baseurl=".$baseurl;

?>

<body>
<img src ="<?php echo $imageconf; ?>" />
</body>
</html>

image.php

<?php
/* Get images from ODK Central
 * adapted from code by MrRodge
 * written by Chris York (seewhy)
 * license     GNU/GPL http://www.gnu.org/copyleft/gpl.html
 *
 * Pull an image from the ODK Central server based on projectid, formid, instanceid and filename (these variables need to be compiled from the submission data, so will need to be known!)
 */

//Get the variables from the URL parameters passed from index.php
$projectID = $_GET["pid"];
$xmlfID = $_GET["xmlfid"];
$parentkey = $_GET["parentkey"];
$filenm = $_GET["filenm"];
$token = $_GET["token"];
$baseurl = $_GET["baseurl"];

//create the correct URL to send to ODK Central API
$imageurl = $baseurl.'projects/'.$projectID.'/forms/'.$xmlfID.'/submissions/'.$parentkey.'/attachments/'.$filenm;

// use cURL to stream the images - note that this will work for jpegs only
$ch2 = curl_init();
$timeout = 5;
$authorization = "Authorization: Bearer ".$token; // Prepare the authorisation token
curl_setopt($ch2, CURLOPT_HTTPHEADER, array('Content-Type: image/jpeg' , $authorization )); // Inject the token into the header
curl_setopt($ch2,CURLOPT_URL,$imageurl);
curl_setopt($ch2,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch2,CURLOPT_CONNECTTIMEOUT,$timeout);
$imagestream = curl_exec($ch2);

//output the image - sent back to index.php at <img src= ...
echo $imagestream; 
?>

token.php

<?php

/* Get images from ODK Central
 * adapted from code by MrRodge
 * written by Chris York (seewhy)
 * license     GNU/GPL http://www.gnu.org/copyleft/gpl.html
 *
 * Generate the token to access ODK Central server 
 */


$conf = require'secrets.php'; // this gets the sensitive data without exposing it

$username = $conf->username;
$password = $conf->password;
$baseurl = $conf->url;

$tokenurl = $baseurl.'sessions/';
$secretdata = ['email' => $username, 'password' => $password];

$ch1 = curl_init(); // Initialise cURL
$post = json_encode($secretdata); // Encode the data array into a JSON string
curl_setopt($ch1, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));// Specify the type of data
curl_setopt($ch1,CURLOPT_URL,$tokenurl); // Specify the API address
curl_setopt($ch1, CURLOPT_POST, 1); // Specify the request method as POST
curl_setopt($ch1, CURLOPT_POSTFIELDS, $post); // Set the posted fields
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, 1); // return as data rather than output to browser (stumped me for a while!)
$result = curl_exec($ch1); // Execute the cURL statement
$decode = json_decode($result, true); // decode the json string into an array
extract ($decode,EXTR_OVERWRITE); //write the array to parameters $token, $createdAt etc

?>

secrets.php (change the details!)

<?php

/* Get images from ODK Central
 * adapted from code  MrRodge
 * written by Chris York (seewhy)
 * license     GNU/GPL http://www.gnu.org/copyleft/gpl.html
 *
 * configuration data - protect this by placing it in a relative folder "/includes" with a .htaccess file with 'deny from all'
 */
 
return (object) array (
 'username' => 'odk.user@your.email.domain', // probably good to create a separate user with project viewer role
 'password' => 'FillThisInWithYourPassword',
 'url' => 'http(s)://your.central.server.path/v1/' // this is the base url of your server API
);

?>

.htaccess

deny from all

This allows you to construct a URL similar to the original idea, but these php files collect the token, add it to the image call and output as an html image in a single pass. It took me a bit of figuring as my server does not allow fopen. Hence the use of cURL, and then needing to output the image as html in the form <img src='image.php?parameters] / > I think that's because the json token can't be mixed with the jpeg stream in a single page? I relied heavily on StackExchange and the php manual to come to this answer.

Once you've got the files in place (and updated secrets.php) you can visit:

[yourdomain]/odkimages/index.php?pid=[fProjectid]&xmlfid=[formid]&parentkey=[uuid]&filenm=[imagename.jpg]

and it should present you with an image. Obviously you'll get the parameters from your ODK Central dataset! My set up is a self-hosted ODK Central server on the cloud and a separate shared web server running linux (for my php files).

I think this renews the token each time, so there might be a way to use cookies instead, but that's beyond me. I created a specific web user for the credentials and assigned it as a 'Project viewer' - so hopefully that should prevent any nefarious editing of data if someone did manage to get hold of the token and then work out how to manipulate the API to access other parts of Central.

Feedback welcome if you can improve the code please share, and adding more functionality (e.g. image size, quality, auto-rotate) would be good, and should be feasible with php.

Now I'm going to see how many ways I can use this! Please let me know if I've done anything daft or dangerous with the API

Enjoy.

2 Likes