Using PHP to Extract Image Exif Data

Posted by Nessa | Posted in uncategorized | Posted on 02-08-2010

8

Those of us fluent in digital photography have come across the term “Exif data” numerous times when it comes to software we use to digitally manipulate photographs. Exif (Exchangeable image file format) data is generally used to identify the properties of the camera that snapped a picture, and usually the software that altered it afterwards. It can tell you when a picture was taken, what kind of camera took it, as well as the camera’s model, shutter speed, focal length, and even provide a thumbnail of the image on the camera’s LCD screen.

Why would you need to extract this information?  If you’re ever uploaded images to stock photography sites and wonder how they know so much about your pictures, it’s because they extract the Exif data from your pictures to provide more information on how they were taken. This quick tutorial will demonstrate how to extract Exif data from an image using PHP.

Enabling the Exif Extension

The Exif functions for PHP may not be native to your installation, so you can check by viewing your phpinfo file or running “php -m” via command line to see a list of modules compiled in. If you don’t see Exif listed, there are three ways you can enable it depending on how you installed PHP:

  • If you compiled PHP manually, you can re-compile while adding –enable-exif to the configure line
  • If PHP is installed via package (rpm/deb), it should already have Exif enabled. If not, you can install an RPM for the extension manually
  • If you use cPanel, run EasyApache and select the Exif extension from the PHP module list, and recompile

Determining the Image Type

The exif_imagetype function identifies the format of an image, but returns the result as a code.  The PHP function reference provides a full list of these return codes, but 1-8 are the most common out of the 16:

1: GIF
2: JPEG
3: PNG
4: SWF
5: PSD
6: BMP
7/8: TIFF

Here’s a code example that lists all the desired valid image types in an array and detects the type of image from the specified file, returning the result in “human readable” format:

<?php
$image = "/path/to/myimage";

$types = array(
1 => "GIF",
2 => "JPEG",
3 => "PNG",
4 => "SWF",
5 => "PSD",
6 => "BMP",
7 => "TIFF",
8 => "TIFF"
);

$imagetype = exif_imagetype($image);

if (array_key_exists($imagetype, $types)) {
echo "Image type is: " . $types[$imagetype];
} else {
echo "Not a valid image type";
}
?>

Reading Exif Header Data

The exif_read_data function can be used to extract header data from JPEG and TIFF files:

<?php
$image = "/path/to/myimage";
$exif = exif_read_data($image, 0, true);
foreach ($exif as $key => $section) {
foreach ($section as $name => $val) {
echo "$key.$name: $val\n";
}
}
?>

This will return the elements of the array from exif_read_data, which can be very long depending on what information is available for the image. There are seven sections (arrays) of data types:

  • FILE:  Contains the file’s name, size, timestamp, and what other sections were found (as listed below)
  • COMPUTED: Contains the actual attributes of the image
  • ANY_TAG:  Any information that is tagged
  • IFD0:  Mostly contains information about the camera itself, the software used to edit the image, when it was last modified, etc
  • THUMBNAIL: Information about the embedded thumbnail for the image
  • COMMENT: Comment headers for JPEG images
  • EXIF: Contains more information supplementary to what is in IFD0, mostly related to the camera (includes focal length, zoom ratio, etc)

Depending on the information available for the image, you’ll actually see a lot of data in the output. Say, for instance, you want to only output the IFD0 data to see the information of the camera that took the image:

<?php
$image = "image.jpg";
$exif = exif_read_data($image, 0, true);

foreach ($exif as $key => $section) {
foreach ($section as $name => $val) {
if($key == "IFD0"){
echo "$key.$name: $val\n";
}
}
}
?>

This will output:

IFD0.ImageWidth: 2592
IFD0.ImageLength: 3872
IFD0.BitsPerSample: Array
IFD0.Compression: 1
IFD0.PhotometricInterpretation: 2
IFD0.Make: NIKON CORPORATION
IFD0.Model: NIKON D80
IFD0.Orientation: 1
IFD0.UndefinedTag:0x0000:
IFD0.XResolution: 72/10000
IFD0.YResolution: 72/1
IFD0.PlanarConfiguration: 1
IFD0.ResolutionUnit: 2
IFD0.Software: Adobe Photoshop CS4 Windows
IFD0.DateTime: 2010:02:06 22:24:09
IFD0.Exif_IFD_Pointer: 304

Or, you can further narrow down the output by specifying specific values in the $exif multi-dimensional array:

<?php
$image = "image.jpg";
$exif = exif_read_data($image, 0, true);
echo "Software: " . $exif['IFD0']['Software'] . "\n";
?>

This will return:

Software: Adobe Photoshop CS4 Windows

Using Exif to generate a thumbnail

As touched on previously, many cameras and image manipulation software will include an embedded thumbnail for an image. You can extract this thumbnail using the exif_thumbnail function:

<?php
$image = "image.jpg";
$thumbnail = exif_thumbnail($image, $width, $height, $type);
echo "<img  width='$width' height='$height' src='data:image;base64,".base64_encode($thumbnail)."'>";
?>

Keep in mind that the thumbnail generated here is from the Exif data – there are other ways to create a thumbnails using many of the PHP image functions.

Simple PHP Script for RBL Checking

Posted by Nessa | Posted in uncategorized | Posted on 16-07-2010

0

It’s useful for ISP’s and email service providers to run occasional RBL checks against their IPs to know when they are being blacklisted by populate CBL services. I’ve written a simple script that utilizes the DNSBL pear library to check against common blacklists, when given a list of IPs in a file.

First, you need to download or install the NET_DNSBL pear module. (Command: pear install NET_DNSBL)

<?php
require_once('Net/DNSBL.php');

$iplist = file("/path/to/iplist");

foreach ($iplist as $ip){

$dnsbl = new Net_DNSBL();

$dnsbl->setBlacklists(array(
'sbl-xbl.spamhaus.org',
'dnsbl.sorbs.net',
'bl.spamcop.net',
'dnsbl-1.uceprotect.net',
'dnsbl-2.uceprotect.net',
'dnsbl-3.uceprotect.net',
'isps.spamblocked.com',
'zen.spamhaus.org'
));

if ($dnsbl->isListed($ip)) {

echo "IP $ip is blacklisted!\n";

}

else {
echo "IP $ip not listed\n";
}
}

?>

Of course, this script can be very easily modified to pull IPs from a database, or assign the $ip variable from a GET or POST request (like from a form or API).  Here’s an exclusive list of some RBL’s you can check against using this script, by adding them to the array shown:

asiaspam.spamblocked.com
bl.deadbeef.com
bl.emailbasura.org
bl.spamcop.net
blackholes.five-ten-sg.com
blacklist.woody.ch
bogons.cymru.com
cbl.abuseat.org    cdl.anti-spam.org.cn
combined.abuse.ch
combined.rbl.msrbl.net
db.wpbl.info
dnsbl-1.uceprotect.net
dnsbl-2.uceprotect.net
dnsbl-3.uceprotect.net
dnsbl.abuse.ch
dnsbl.ahbl.org
dnsbl.cyberlogic.net
dnsbl.inps.de
dnsbl.njabl.org
dnsbl.sorbs.net
drone.abuse.ch
duinv.aupads.org
dul.dnsbl.sorbs.net
dul.ru
dyna.spamrats.com
dynip.rothen.com
eurospam.spamblocked.com
fl.chickenboner.biz
http.dnsbl.sorbs.net
images.rbl.msrbl.net
ips.backscatterer.org
isps.spamblocked.com
ix.dnsbl.manitu.net
korea.services.net
lacnic.spamblocked.com
misc.dnsbl.sorbs.net
noptr.spamrats.com
ohps.dnsbl.net.au
omrs.dnsbl.net.au
orvedb.aupads.org
osps.dnsbl.net.au
osrs.dnsbl.net.au
owfs.dnsbl.net.au
owps.dnsbl.net.au
pbl.spamhaus.org
phishing.rbl.msrbl.net
probes.dnsbl.net.au
proxy.bl.gweep.ca
proxy.block.transip.nl
psbl.surriel.com
rbl.interserver.net
rdts.dnsbl.net.au
relays.bl.gweep.ca
relays.bl.kundenserver.de
relays.nether.net
residential.block.transip.nl
ricn.dnsbl.net.au
rmst.dnsbl.net.au
sbl.spamhaus.org
short.rbl.jp
smtp.dnsbl.sorbs.net
socks.dnsbl.sorbs.net
spam.dnsbl.sorbs.net
spam.rbl.msrbl.net
spam.spamrats.com
spamlist.or.kr
spamrbl.imp.ch
t3direct.dnsbl.net.au
tor.ahbl.org
tor.dnsbl.sectoor.de
torserver.tor.dnsbl.sectoor.de
ubl.lashback.com
ubl.unsubscore.com
virbl.bit.nl
virus.rbl.jp
virus.rbl.msrbl.net
web.dnsbl.sorbs.net
wormrbl.imp.ch
xbl.spamhaus.org
zen.spamhaus.org

Using PHP to Perform DNS Lookups

Posted by Nessa | Posted in uncategorized | Posted on 30-06-2010

4

PHP has a couple DNS functions you can use to perform record lookups.

Most of us are familiar with the two basic ones – gethostbyname() and gethostbyaddr(), both of which perform a single function – returning a hostname or IP address. Here’s an example of both:

<?php

$ip = gethostbyname("v-nessa.net");
$host = gethostbyaddr("69.174.114.71");

echo "v-nessa.net has the IP $ip, which reverses to $host";
?>

The above will return:

v-nessa.net has the IP 69.174.114.71, which has a PTR of server.v-nessa.net

Similarly to gethostbyname, there’s gethostbynamel which is useful for hostnames with multiple A records:

$ips = gethostbynamel("test.v-nessa.net");
foreach ($ips as $ip => $value){
echo $value . "\n";
}

Will return:

69.174.114.71
69.174.115.243

A more advanced function is dns_get_record, which can pull any valid record for a hostname or IP.  Think about the dig command you use in Unix to find DNS records:

nessa@nessa-desktop:~$ dig +short v-nessa.net A
69.174.114.71

The dns_get_record function works in a similar way, and can obtain the following DNS record types:

DNS_A, DNS_CNAME, DNS_HINFO, DNS_MX, DNS_NS, DNS_PTR, DNS_SOA, DNS_TXT, DNS_AAAA, DNS_SRV, DNS_NAPTR, DNS_A6, DNS_ALL or DNS_ANY.

The following will give you a similar result:

$recs = dns_get_record("v-nessa.net", DNS_A);
print_r($recs);

Will return:

Array
(
[0] => Array
(
[host] => v-nessa.net
[type] => A
[ip] => 69.174.114.71
[class] => IN
[ttl] => 13728
)
)

If you want to obtain multiple DNS types, you can do so by separating the record types with a plus sign:

$recs = dns_get_record("v-nessa.net", DNS_A + DNS_MX);

Will return:

Array
(
[0] => Array
(
[host] => v-nessa.net
[type] => A
[ip] => 69.174.114.71
[class] => IN
[ttl] => 13736
)

[1] => Array
(
[host] => v-nessa.net
[type] => MX
[pri] => 0
[target] => v-nessa.net
[class] => IN
[ttl] => 14145
)

)

You’ll notice that the output is a double array, so to call individual values you can do either of the following:

// will return the IP for array 0 ( A record)

echo $recs[0]['ip'];

// will return results for common records

foreach ($recs as $type => $value){
echo $value[ip] . "\n";
}

Similar to the example above, you can use the DNS_ALL type to show any records available for the hostname, and use a minus sign to exclude certain record types. For example, the below code will return all DNS results for v-nessa.net, but exclude NS records:

$recs = dns_get_record("v-nessa.net", DNS_ALL - DNS_NS );

foreach ($recs as $type => $value){
echo $value[type] . "\n";
}

The following records were returned:

A
SOA
MX
TXT

Here’s a more practical example – a simple PHP DNS lookup script. Say you have a form on your site that allows a user to type in a hostname and select a type of record: A, MX, NS, and TXT. The passed form values are $host and $type, and the below script will accept the variables and output the results according to the record type:

HTML Form:

<form action="dns.php" method="POST">
Hostname: <input type="text" name="host" />
Type: <select name="type">
<option value="a">A</option>
<option value="mx">MX</option>
<option value="ns">NS</option>
<option value="txt">TXT</option> </select>
</form>

PHP Script (dns.php):

<?php

if(!empty($_POST['host']) && !empty($_POST['type'])){

    // Grab variable from form and define valid types

    $host = $_POST['host'];
    $type = strtoupper($_POST['type');
    $validtypes=array("A","MX","NS","TXT");

    // Check that dns type is defined or allowed

    if(!defined("DNS_" . $type) or !in_array($type,$validtypes)){
       echo "Invalid DNS Type!";
    }else{

       $type = constant("DNS_" . $type);
       $rec = dns_get_record($host, $type);

       // Set result types - can be modified by using available elements from $rec array

       switch($type){
             case DNS_A:
                    $recvals=array("Hostname" => "host","Type" => "type", "IP" => "ip");
                    break;
             case DNS_MX:
                    $recvals=array("Hostname" => "host","Type" => "type", "Target" => "target", "Priority" => "pri");
                    break;
             case DNS_NS:
                    $recvals=array("Hostname" => "host","Type" => "type", "Target" => "target");
                    break;
             case DNS_TXT:
                    $recvals=array("Hostname" => "host","Type" => "type", "Record" => "txt");
                    break;
        }

      // Output results

      foreach ($rec as $arr => $num){
             foreach ($recvals as $title => $value){
                    echo $title . " : " . $num[$value] . "\n";
             }
      }

    }
} else {

     echo "Either hostname or record type is missing";
}

It’s of course easy to modify the above code accordingly, and I’m sure there may be better ways to do this. Feel free to post your own code snippets or comments.

PHP 5.3: Why We’re All Late to the Party

Posted by Nessa | Posted in uncategorized | Posted on 27-05-2010

10

It’s been almost a year since the PHP 5.3 branch was released to the PHP community, and yet we’re all still in the shadow of PHP 5.2.  If you’re just a faithful customer wondering why your host isn’t getting with the times, I’ll tell you exactly why: We don’t stock enough diapers to keep up with that ish.

Let me talk about you, the common website owner, that runs a simple Drupal or WordPress site.  You didn’t sign up with your host to go around in circles over compatibility problems that could have been avoided by your host doing a little research. As a programmer, I would hold it to any site owner to check their site’s requirements and the offerings of their host before they unnecessarily waste a lot of time and money, but as a system administrator I frown upon shared hosting providers offering software with known compatibility issues just to be able to advertise as the “latest and greatest”. The latest isn’t always the greatest, and it won’t be until the community catches up with what the greatest has to offer.

I’ve had numerous discussions with my superiors about whether to upgrade to PHP 5.3, and the end result is pretty much the same – we’re just not ready. And neither are our customers, or the developers of the applications they use.  And trust me – this is normal. We all went through the same thing when PHP 4.2 came out, and again with PHP 5, and again with PHP 5.2.  That being said, this is why all the good hosts are now also holding off jumping on the PHP 5.3 bandwagon:

  • Lack of compatibility:  Many open source applications and frameworks (such as Drupal, Joomla, Magento, and even parts of WordPress) are not fully compatible with PHP 5.3 yet
  • No Zend Optimizer or Zend Guard support
  • It’s not required for PCI compliance yet, which is one of the main reasons why hosts upgrade PHP on their servers

PHP 5.3 does have a lot of new features and functions to offer, and it will eventually wiggle its way into mainstream hosting -  it’s just not ready for the shared hosting population yet until everyone else catches up.  Most hosts (including IMH) will still offer PHP 5.3 as an optional upgrade for customers on VPS or Dedicated servers, or as an optional move to a server that has that version. In the meantime, start upgrading your applications and keep tabs on your softwares’ developers to find out when php 5.3 support will be available.  PHP 5.2 will likely become obscure near the end of 2010 or early 2011 in favor of 5.3, and you should be ready when it is.

Command Line PHP: Part 3

Posted by Nessa | Posted in uncategorized | Posted on 25-05-2010

4

This is part third and final part in my PHP command line tutorial series. If you didn’t see parts 1 and 2:

Command Line PHP: Part 1

Command Line PHP: Part 2

Command Line PHP: Part 2

Posted by Nessa | Posted in uncategorized | Posted on 21-05-2010

3

This post is continuing on my three-part series on command line PHP programming. Missed part one? It’s right behind you. This part will go over command execution and processes.

Command Line PHP – Part 1

Posted by Nessa | Posted in uncategorized | Posted on 18-05-2010

13

PHP isn’t just for websites anymore. In fact, almost every script I’ve written to perform server-side functions is either written in bash or PHP, rather than Perl or Python as preferred by my colleagues. It’s a common belief that PHP isn’t suited for CLI programming since it’s mainly used in web applications, but PHP has over a hundred functions specifically intended for system management.

These kinds of posts can be rather lengthy, so I’m making this into a series with three parts.  Part 1 will go over the basic filesystem functions. You can find a complete listing here, but I’ll just go over a few of the more important and common ones.

Simple API Writing, Part I

Posted by Nessa | Posted in uncategorized | Posted on 09-02-2009

6

A lot of people don’t realize how easy it is to write an API with PHP. It really is as easy as having a simple PHP script accepting GET variables, and when you add in some security, you can pretty much do anything you want with a single script that accepts variables from any authenticated source.  So a little while ago I posted about the new cPanel XML API and how to integrate that with your own scripts – well now, I’ll use that as an example to show you how to write an API for their API, a.k.a, an API connector.   Though in real applications you wouldn’t need an API, all you’d need is a PHP script that accepts GET or POST input to perform some kind of action. In this example, we’ll have a script that automatically adds DNS zones to a nameserver that runs cPanel as well.

Most APIs do the same thing – you have a script, then that script accepts post/get variables, then does something.

The Interface (addzone.php):

You guy remember the one I posted a while back – well, we’ll use the same one only a tad different.  This is the script that runs statically on the server, which accepts the variables passed through the URL:


<?php
// API for adding a DNS zone to ns cluster
$isinclude = “1″; // specifies $isinclude for xmlapi.php
// GET & POST definitions

$key = “098f6bcd4621d373cade4e832627b4f6″;
$domain = $_GET['domain'];
$ip = $_GET['ip'];

// Validation – make sure that we have the right information

if($_POST['key'] != $key){ echo “Invalid key!!”; die(); }
if(empty($domain)){ echo “Domain value missing!!”; die(); }
if(empty($ip)){ echo “IP Value missing!!”; die(); }

$theServer = “ns1.v-nessa.net”; // the server to connect to
$apiPath = “/xml-api/adddns?domain=$domain&ip=$ip”; // the xml api path

$user = “root”; // use to connect to whm as

// ns1 hash (whm > remote access)

$rhash = “e9917f16b3fda69137192725a06b68e7
230e99fd445473807e33d637878641a5
–edited out for sake of length–
f673567ab443acedc77f9aec62ff953f”;

// Include the API connector
include(“xmlapi.php”);

// Output XML Result
$xmlObject=simplexml_load_string($xmlresult);

echo $xmlObject->result->statusmsg . “\n”;

?>

The file that is called via include() is the basic xml function file which constructs all of the variables from the outser script (shown above).  You can get a copy of of xmlapi.php from here, but for this example you need to comment out the output.

Now all we need to do is pass the variables that the script needs in order to know what information to process, which is $ip and $domain.  Therefore, in order to successfully call this API, you would enter the following in a browser:

http://v-nessa.net/api/addzone.php?domain=test.v-nessa.net&ip=205.134.252.71

This will pass the ‘domain’ and ‘ip’ variables to addzone.php, which uses the XML API to connect to WHM and add a dns zone on the nameserver ns1.v-nessa.net.  This is a problem though – what’s to keep outsiders from finding this and abusing it?  Well, there are several forms of non-interactive authentication you can use, such as:

  • Have an allow list of ips that can access the script (look up environmental variables at php.net)
  • Requiring a key or token

I generally use a key, though there are better ways to do this.  The way I’m about to show you is simple and secure, but slightly limits the way your API can be called.

First, I generated an md5 hash and defined it in the scipt (remember $key = “098f6bcd4621d373cade4e832627b4f6″; ?).  Then all I need to do is make sure that key is used whenever I call the API. Notice that in addzone.php it’s defined as a POST variable?  That is mainly for preference but you can just as easily make it a GET variable and just add it to your URL line.  Here I want it to be posted, so I can call the API through cURL as follows:

curl -k http://v-nessa.net/api/addzone.php?domain=test.v-nessa.net&ip=205.134.252.71 -d key = “098f6bcd4621d373cade4e832627b4f6

And there you have it! A very simple way to write an API using POST and GET.


Alternative PHP Caching FTW

Posted by Nessa | Posted in uncategorized | Posted on 27-06-2008

4

.!.

We get a TON of requests for the PHP APC pecl module because after having adopted suPHP into our configuration, eAccelerator is worthless. It’s quick to install, and especially if you’re running suPHP or phpsuexec, each user can maintain their own settings within their local php.ini without me having to do anything — basically the best thing that a lazy system admin can ask for.

Soooo, here’s how you install it:


wget http://pecl.php.net/get/APC-3.0.17.tgz
tar -xvzf APC-3.0.17.tgz

phpize
./configure && make && make install

Then just add “extension=/apc.so” to your php.ini and you’re done. With PHP under Apache this will load the APC module for everyone, but for suPHP users you’ll need to add it to their php.ini which will also allow them to modify their own APC settings. These are the ones I recommend using:

apc.enabled = 1
apc.shm_segments = 1
apc.shm_size = 30
apc.optimization = 0
apc.ttl = 7200
apc.user_ttl = 7200
apc.num_files_hint = 1000
apc.mmap_file_mask = /tmp/apc.XXXXXX

Now, if you want to get even sexier with it I came across this little tool that monitors the performance of APC on your server.

Sexifying WHM with XML API

Posted by Nessa | Posted in uncategorized | Posted on 07-06-2008

5

.!.

I don’t know about you other cPanel system admins out there, but I find WHM to be very useful for the more advanced and time-consuming tasks, such as installing SSL certificates. However, the easy stuff like changing an account’s package and resetting passwords is a royal pain in the ass as far as convenience is concerned when you have to log into WHM, list accounts, and make whatever change.

I recently became favorable towards the WHM XML API functionality which will let me do a majority of the everyday account-related tasks from command line without ever opening my browser, which is a lot easier when managing thousands of users across multiple servers. Below are a couple scripts I’ve put together using the XML API from a base script in the cPanel forums:

Change account password

Change account package

Both are run via command line, and the arguments passed to the PHP script as variables. For example, to change an account’s password:

./chacctpass myuser mypass1234

Customizing these scripts to perform different functions is easy via the following steps:

- change if ($argc != 3) to the number of command line arguments you wish to pass to the script plus one. In the above example there are two arguments and since the script name counts, add one and that makes 3.

- in the section where the arguments are assigned to variables (like $cpuser, etc), name your variables. The first one should have an array value of 0, then 1, 2, etc.

- edit the usage example, which will come up if the required number of arguments is not provided…you can add any text you like

- if you’re using a hash (which is more secure than user/pass authentication), go fetch your remote access key from WHM and put it in the $hash value within quotes, format intact. Otherwise, put in your WHM user’s username and password

- change the $server variable to your server’s hostname

- change $apipath to the WHM path for the function you are using. You can find a whole list of them here, and most will give you the path to use in the examples sections. In the API path, insert your variable names where the values are suppose to be. For instance:

$apiPath = “/xml-api/passwd?user=myuser&pass=mypass1234″;

Would be:

$apiPath = “/xml-api/passwd?user=$cpuser&pass=$newpass”;

In the header section, uncomment whichever $header .= “Authorization: line that matches your authentication method (user/pass or hash)

Once you’ve configured your API script, chmod to 700 and run from the command line as show in my example. It’s better to lock down the script by changing its ownership only to the user that will be using it, and not giving read, write, or execute permissions to anyone else.

Note: for these scripts to work you have to have PHP compiled with OpenSSL support, otherwise change the socket variables to http over port 2086.