<?php
/*
* Script Name: Asif
* Author: ADM, http://thousandrobots.com
* Version: 0.1 
* Version date: 2004-12-30
* Description:  This script scans a directory, identifies any JPEG files, 
*                 and outputs available EXIF data as HTML, RSS, or Atom.
*                 Generates thumbnails if desired/necessary.

* Requirements: PHP5 compiled with GD and "--enable-exif" [Can run on PHP4 if call to "scandir" is modified.]
* Output: Text stream of HTML, RSS, or Atom containing filename and exif data
* Input Parameters: 
*     -Passed on the URL: 
*         $directory = the directory to scan. Defaults to the directory in which the script resides.
*         $thumbnails = toggles whether to produce and display thumbnails in the HTML feed.
*                 [options: yes; defaults to no]
*         $format = format in which the page should be presented.
*                 [options: rss, atom, html; defaults to html]
*         $alldata = whether to show all available exif data. [options: yes/no; defaults to no]

* Usage: 
* Drop this file into any directory containing photos. Access the page. Your EXIF data
* will be displayed as HTML. Append "?thumbnails=yes" to the URL generate and display thumbnails for the images.
* Append "?format=rss" or "?format=atom" to render the page as RSS or Atom. 

* To display pictures in another directory, append the name of the directory to the url. Omit slashes
* from the end of the path name. So "http://mysite.com/pictures/asif.php?directory=2004/nyc" will display
* the EXIF data for files in pictures/2004/nyc.

* Any or all of these parameters can be combined,
* so "http://mysite.com/pictures/asif.php?format=rss&thumbnails=yes&alldata=yes&directory=travel" will generate an 
* RSS feed containing thumbnails and *all* EXIF data associated with each file in your "travel" directory.

* It may be useful to rename asif.php to "index.php" and drop it into a directory full of JPEGs.
* Then, any user accessing that folder will be presented with the EXIF data for those files.

* Note on directory paths: 
* Directory paths are *relative* to the path in which the script resides. For security purposes,
* you cannot access files further in a "higher" directory than where the script resides. For instance,
* if asif.php resides in mydomain.com/photos/galleries, the script will not be able to process files
* in mydomain.com/photos, unless these photos are in the "galleries" directory. If a user 
* attempts to do something like "mydomain.com/photos/asif?directory=/secure", the script will fail. This behavior is configurable
* by commenting out one line of code below (around line 492).

* Note on formatting HTML output: 
* All formatting is controlled via asif.css. Change it as desired.
* *  
* Security Considerations:
*     - You should make sure your php.ini's open_basedir restriction is set to your web directory or similar. If it isn't,
*     users can scan directories outside of your web path! In other words, even if your website
*     is in /home/username/yourdomain.com, users could theoretically scan /home/username/private.
*     This script contains a very rudimentary function to prevent this, but it should not be relied upon.
*    

* Version notes:
* - 0.1 (2004-12-30): Script created. 

* License:
* This free software is the copyright of thousandrobots.com and released under the terms of the GNU General Public License. 
* See thousandrobots.com/gpl for more information.

* Warranty:
* This software is offered without warranty of any kind, and without assumption of liability. 
* Use at your own risk.

*/

define("ASIF_SUFFIX"".tn.jpg"); //appended to filenames of generated thumbnails
define("ASIF_MAX_EXIF_LENGTH"25); //max length of each string in EXIF data. useful for cutting out oddly encoded garbage.
define("ASIF_TIMEZONE_ENGLISH""EST"); //timezone appended to dates
define("ASIF_TIMEZONE_NUMERIC""-00:00"); //timezone appended to Atom dates
define("ASIF_PATH_LENGTH_LIMIT"100); //maximum length of directory path appended to URL


class Picture
{
    var 
$filename;
    var 
$filepath;
    var 
$filedatetime;
    var 
$size;
    var 
$exif//all the available data
}

class 
XMLItem
{
/* 
* this class contains all the elements necessary
* for an RSS or basic Atom item.

*/

    
var $title;
    var 
$description;
    var 
$url;
    var 
$category;
    var 
$authoremail;
    var 
$authorname;
    var 
$pubdate;
    var 
$datemodified;
    var 
$guid;
    var 
$timestamp;
    
    
//atom only
    
var $relatedlink;
    var 
$relatedlinktitle;
    var 
$via;
    var 
$viaurl;
    
    function 
update($title,
                    
$description,
                    
$url,
                    
$category,
                    
$authoremail,
                    
$authorname,
                    
$pubdate,
                    
$datemodified,
                    
$guid,
                    
$timestamp,
                    
$relatedlink,
                    
$relatedlinktitle,
                    
$via,
                    
$viaurl)
    {
        
$this->title $title;
        
$this->description $description;
        
$this->url $url;
        
$this->category $category;
        
$this->authoremail $authoremail;
        
$this->authorname $authorname;
        
$this->pubdate $pubdate;
        
$this->datemodified $datemodified;
        
$this->guid $guid;
        
$this->timestamp $timestamp;
        
        
//atom-only
        
$this->relatedlink $this->relatedlink;
        
$this->relatedlinktitle $this->relatedlinktitle;
        
$this->via $this->via;
        
$this->viaurl $this->viaurl;
        
    }
//end update
}

function 
clean($input$maxlength)
{
    
//makes user input safe for processing
    
$input substr($input0$maxlength);
    
$input EscapeShellCmd($input);
    return (
$input);
}

function 
ifd02unixdate($timestamp)
{
// translates a ifd0 timestamp into a unix timestamp
// e.g. 2004:12:30 15:41:23 becomes like 1104446760

// replace the space so it can implode into an array
    
$timestamp str_replace(" ",":",$timestamp);
    
$time explode(":",$timestamp);
    
$unixtime mktime($time[3],$time[4],$time[5],$time[1],$time[2],$time[0]);
    return 
$unixtime;

}

function 
MakeRSSDate($date,$timezone)
{
/*
 translates date from unix timestamp to valid rss format
 "1098861960" becomes like "Wed, 27 Oct 2004 00:26:00 PST"
*/

//date comes in as unix time stamp
$rssdate date('D, d M Y H:i:s'trim($date));
$rssdate trim($rssdate " " $timezone);

return 
$rssdate;
}

function 
MakeAtomDate($date,$timezone)
{
/*
 translates date from unix timestamp to valid Atom date format
 "1098861960" becomes like "2004-05-27T03:48:48-05:00"
 * 
 * $date is unix timestamp
 * $timezone is like "-5:00"
 * output must be like "2002-10-02T10:00:00" or "2002-10-02T10:00:00-05:00"
*/

//date comes in as unix time stamp
$atomdate date('Y-m-d\TH:i:s',$date);
$atomdate trim($atomdate strval($timezone));

return 
$atomdate;
}

function 
ResizeImage($file)
{
// adapted from http://www.jetevents.pl's script
// posted to http://us3.php.net/manual/en/function.imagecreatefromjpeg.php
// input:
// str $file = full system path to file (e.g., /home/username/htdocs/some/subfolder/mypic.jpg)
// returns: str path to resized thumbnail pic

  
$maxx=100;    // maximum width
  
$maxy=75;    // maximum height

     // name of file - must be jpg
    
$size getimagesize ($file);        // params of image

   
if ($size[0]>$size[1]) {$sizemin[0]=$maxx;$sizemin[1]=$maxy;};
   if (
$size[1]>$size[0]) {$sizemin[0]=$maxy;$sizemin[1]=$maxx;};
  
   
//ADM additions
   
$newpath $file ASIF_SUFFIX;
   
//
   
   // create the thumbnail, unless it already exists.
   
if (!is_file($newpath))    
   {
   
$im=@imagecreatefromjpeg($file); // incoming image
   
$small imagecreatetruecolor($sizemin[0], $sizemin[1]);    // new image
   
ImageCopyResampled($small$im0000$sizemin[0], $sizemin[1], $size[0], $size[1]);
   
// below is main function resampling image

   
ImageDestroy($im);                        // free memory

   
if (ImageJPEG($small,$newpath,100))                // try to save image
       
{
             
//echo "File $newpath has been written<br>\n";            // success
       
}
     else
       {
          
// failed to write file
       
echo "Error ! File has not been written.";
       };
  }
//end if tn exists
  
   
return $newpath;

}


function 
PrepareItems($pictures,$thumbnails,$alldata)
{
/*
* arr $pictures: an array of Picture objects
* str $alldata: whether to include all available exif data.
* str $thumbnails: whether to include links to thumbnail images in output

* Function translates Picture objects into XMLItem objects.

*/

    
foreach ($pictures as $pic)
    {
    
        
$item = new XMLItem;
        
$title $pic->filename;
         
        
//if "taken" date is available, use it.
        //otherwise, use file creation date.
        
if ($pic->exif["IFD0"]["DateTime"])
        {
            
$timestamp ifd02unixdate($pic->exif["IFD0"]["DateTime"]);
        }
        else
        {
            
$timestamp $pic->exif["FILE"]["FileDateTime"];
        }
                    
        
$pubdate MakeRSSDate($timestamp,ASIF_TIMEZONE_ENGLISH);
        
$url $pic->filepath;
        
        
//change these two values to suit your needs.
        
$category "photos";
        
$authoremail "spam@" $_ENV['HTTP_HOST'];
        
        
$authorname "Asif, the thousand robots exif processor";
        
$guid $pic->filepath "#" $timestamp;
                        
        if (
$alldata=="yes")
        {
            foreach (
$pic->exif as $key => $section
            {
                foreach (
$section as $name => $val
                {
                   
$allexif .= "$name: " substr($val,0,ASIF_MAX_EXIF_LENGTH) . " \n | ";
                   }
            }
            
/*
            * if you have "$alldata" set to yes, you may get
            * crazily encoded stuff back. this attempts to deal with some of it.
            * need a better way to escape unknown characters, or maybe one of the
            * settings at http://tinyurl.com/4ooch would work.
            */
            
$description utf8_encode($allexif);
            unset(
$allexif);
        } 
//end if all data
        
else
        {
        
// better way to do this?
        
$description "Taken: " $pic->exif["IFD0"]["DateTime"] . " | \n"
            
"Comment: " $pic->exif["COMPUTED"]["UserComment"] . " | \n"
            
"Filesize: " round(intval($pic->exif["FILE"]["FileSize"])/1024,0) . "kb" " | \n"
            
"Height: " $pic->exif["COMPUTED"]["Height"] . " | \n"
            
"Width: " $pic->exif["COMPUTED"]["Width"] . " | \n"
            
"F-stop: " $pic->exif["COMPUTED"]["ApertureFNumber"] . " | \n"
            
"Camera: " $pic->exif["IFD0"]["Make"] . " " $pic->exif["IFD0"]["Model"] . " | \n"
            
"ISO: " $pic->exif["EXIF"]["ISOSpeedRatings"]. " | \n"
            
"Flash: " $pic->exif["EXIF"]["Flash"]. " | \n"
            
"Focal Length: " $pic->exif["EXIF"]["FocalLength"];
        }
        
        
//add img tag and link to thumbnail image if necessary
        
if ($thumbnails=="yes")
            
$description "<a href=\"$pic->filepath\"><img src=\"" $pic->filepath ASIF_SUFFIX 
            
"\" alt=\"$title\" class=\"asifphoto\" /></a>\n | " $description;
                
        
$item->update($title,$description,$url,$category,$authoremail,$authorname,$pubdate,$datemodified,$guid,$timestamp,$relatedlink,$relatedlinktitle,$via,$viaurl);
                      
        
//add to the array of items
        
$items[]=$item;        
        } 
//end foreach
        
    
return $items;

}

function 
PrepareMetaData($format)
{
    
$metadata["commenttext"] = "Produced by Asif, the Thousand Robots EXIF processor. See 
thousandrobots.com/projects/asif/ for more info. 
                        \n This document is rendered in $format and is available in HTML, RSS, or Atom. For info on RSS and Atom, see bloglines.com. \n \n-ADM"
;
    
$metadata["feedtitle"] = "Photos and EXIF data from " $_ENV['HTTP_HOST'];
    
$metadata["feedurl"] = $_SERVER["SCRIPT_URI"];
    
$metadata["feedtagline"] =  "EXIF data (like image size, date, and user comments) can be stored along with digital photos. This document contains that data.";
    
$metadata["feedauthorname"] = "spam@" $_ENV['SERVER_NAME'];
    
$metadata["feedid"] = "tag:" $_ENV['HTTP_HOST'] . ",2004-12-29:/exif";
    
$metadata["timezone_numeric"] = ASIF_TIMEZONE_NUMERIC;
    
$metadata["timezone_english"] = ASIF_TIMEZONE_ENGLISH;

return 
$metadata;
}

function 
BuildRSSFeed($commenttext,$channeltitle,$channelurl,$channeldescription,$editor,$timezone,$arrItems)
{
/*
* This function is a generic RSS feed assembler. Pass in the values for the feed
* and an array filled with Item objects, and it will return a valid RSS 2.0 feed.

* it sets the channel buildtime to whatever the current time is. timezone is passed
* into this function by whatever function calls it.

* note the construction of the <author> element. it adds an author email and name.
* Email addresses are necessary for valid RSS 2.0.

*  
*/

// contains the text used in the first part of the rss output
$buildtime MakeRSSDate(time(),$timezone);
$header ="";
$header .= "<!-- \n $commenttext \n --> \n 

<rss version=\"2.0\">

<channel>
<title>$channeltitle</title>
<link>$channelurl</link>
<description>$channeldescription</description>
<language>en-us</language>
<copyright>see " 
$_ENV['HTTP_HOST'] . " for copyright info</copyright>
<docs>http://backend.userland.com/rss</docs>
<generator>thousandrobots.com rss generator</generator>
<managingEditor>$editor</managingEditor>
<webMaster>$editor</webMaster>
<lastBuildDate>$buildtime</lastBuildDate>
"
;

//build the item string
 
if (!empty($arrItems))
  {
    foreach (
$arrItems as $item)
    {
    
$data .= "\n<item>\n" .
     
"<title>$item->title</title>\n" .
     
"<description><![CDATA[$item->description]]></description>\n" .
     
"<link>$item->url</link>\n" .
     
"<author>$item->authoremail ($item->authorname)</author>\n" .
     
"<pubDate>$item->pubdate</pubDate>\n" .
     
"<category>$item->category</category>\n" .
     
"<guid>$item->guid</guid>\n" .
     
"</item>\n";
    }
  }
//end if not empty

$footer "</channel>\n</rss>";

$rss trim($header $data $footer);

return 
$rss;

}

function 
BuildAtomFeed($commenttext,$feedtitle,$alturl,$feedtagline,$feedid,$feedauthorname,$timezone,$arrEntries)
{

// Generic Atom feed builder. Re-use for anything. 
// Just pass it an array full of objects ($arrEntries)to build into the feed.
// Feed will validate as long as the stuff you pass into it is valid.

$buildtime MakeAtomDate(time(),$timezone);

$header ="";
$header .= "<!-- \n$commenttext \n-->" .
"
<feed version=\"0.3\" xmlns=\"http://purl.org/atom/ns#\" xml:lang=\"en\">
<title>$feedtitle</title>
<tagline>$feedtagline</tagline>
<link rel=\"alternate\" type=\"text/html\" href=\"$alturl\"/>
<id>$feedid</id>
<copyright>see " 
$_ENV['HTTP_HOST'] . " for copyright info</copyright>
<modified>$buildtime</modified>
<author>
    <name>$feedauthorname</name>
</author>"
;

//build the item string
if (!empty($arrEntries))
{
  foreach (
$arrEntries as $entry)
  {
  
  
$pubdate MakeAtomDate($entry->timestamp,$timezone);
      
$data .= "\n<entry>\n" .
     
"<title>$entry->title</title>\n" .
     
"<link rel=\"alternate\" type=\"text/html\" href=\"$entry->url\" />\n" .
     
"<issued>$pubdate</issued>\n" .
     
//should have a separate modified date, but whatever.
     
"<modified>$pubdate</modified>\n" .
     
"<id>$entry->guid</id>\n" .
     
"<content type=\"text/html\" mode=\"escaped\"><![CDATA[$entry->description]]></content>\n";
    if (
$entry->relatedlink)
        
$data .= "<link rel=\"related\" type=\"text/html\" href=\"$entry->relatedlink\" title=\"$entry->relatedlinktitle\" />\n";
    
    if (
$entry->via)
        
$data .= "<link rel=\"via\" type=\"text/html\" href=\"$entry->viaurl\" title=\"$entry->via\" />\n";
    
    
$data .= "</entry>\n";
  }
//end foreach
}//end if not empty
$footer "</feed>";

$atom trim($header $data $footer);

return 
$atom;

}

function 
BuildHTML($commenttext,$feedtitle,$channelurl,$feeddescription,$editor,$timezone,$items,$thumbnails)    
{

//outputs the items as html.
//note the link to the stylesheet, and the classes on all the divs, etc.

$html '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">' 

"\n<head>\n<!-- $commenttext --> \n
<link rel=\"stylesheet\" href=\"asif.css\" type=\"text/css\" />
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n
<title>$feedtitle</title>\n
</head>\n
<body>\n
    <div class=\"asifheader\">\n<h2>$feedtitle</h2>\n
        <p>
        $feeddescription
        </p></div>\n<div class=\"exifdata\">"
;
        
        foreach (
$items as $item)
        {
        
            
//turn the description back into an array so it can be output on separate lines.
            
$description explode(" | ",$item->description);
            
$itemlist .= "<div class=\"asifitem\">\n<h3>$item->title</h3>\n";
                    
            
$itemlist.=    "<p><a href=\"$item->url\">view</a><br />";
                        
            
//split the description into separate lines
            
foreach ($description as $field)
            {
                
$itemlist .= $field "<br />\n";
            }
        
$itemlist .= "</p>\n</div>\n";
        }
//end foreach
        
$html .= $itemlist "</div>\n</body>\n</html>";
        
return 
$html;
}
//end function

function ProcessFiles($directory,$thumbnails)
{

/*
* str $directory: the directory to process
* bool $thumbnails: whether thumbnails should be generated
*/

// comment out the next two statements if you want to permit access to
// files "above" the directory where the script resides. This is NOT recommended.
// You should adjust your openbasedir setting in php.ini instead so you aren't vulnerable.
if (strpos($directory,"/")===0)
    die (
"Access to that directory not allowed");
    
if (
strpos($directory,".")===0)
    die (
"Access to that directory not allowed");

//if no dir specified, set to dir where script resides
if (!$directory)
    
$directory ".";

$realpath realpath($directory);

//make sure the directory exists
if (!is_dir($directory))
    die(
"No directory at $directory.");

//the next statement requires PHP5. 
//See below for PHP4 equivalent.
$files scandir($realpath);

/*
PHP4 rough equivalent to scandir. (Untested)
code from: http://tinyurl.com/5ovps

$dh  = opendir($realpath);
while (false !== ($filename = readdir($dh))) {
   $files[] = $filename;
}
*/
    
if (!is_array($files))
    {
        
$error "No files to process in this directory.";
        die (
$error);
    }
    
 foreach (
$files as $file)
 {
    
//check if it's a file and a jpeg
    
$realfile $realpath "/" $file;
    
    if (
is_file($realfile) && (exif_imagetype($realfile)==2) && (strpos($realfile,ASIF_SUFFIX)===false))
    {        
        
$exif exif_read_data($realfile0true);
        
        
$pic = new Picture();
        
        
//edit the path so it renders into a proper url when appended to the domain name.
        //yuck -- got to be a better way to do this
        
if ($directory==="."
            { 
$htmldirectory "/"; }
            else
            { 
$htmldirectory "/" $directory "/"; }
                        
        
$pic->filename $file;
        
$pic->filepath dirname($_SERVER["SCRIPT_URI"]) . $htmldirectory $file;
        
$pic->exif $exif;
                        
        
$pictures[] = $pic;
                
        
//create thumbnails if necessary
        
if ($thumbnails == "yes")
            
ResizeImage($realfile);
                
     } 
//end if jpegs

    
}//end foreach file

    
if (is_array($pictures))
        {return 
$pictures;}
    else
        {
        
$error "No images to process in this directory.";
        die (
$error);
        }
        
}
//end function

$photos ProcessFiles(clean($_GET["directory"],ASIF_PATH_LENGTH_LIMIT),clean($_GET["thumbnails"],3));

    switch (
clean($_GET["format"],4))
    {
    case 
"rss":
        
$feeditems PrepareItems($photos,clean($_GET["thumbnails"],3),clean($_GET["alldata"],3));
        
$feeddata PrepareMetaData("RSS");
        
$output BuildRSSFeed
            
(
            
$feeddata["commenttext"],
            
$feeddata["feedtitle"],
            
$feeddata["feedurl"],
            
$feeddata["feedtagline"],
            
$feeddata["feedauthorname"],
            
ASIF_TIMEZONE_ENGLISH,
            
$feeditems
            
);
        
header('Content-Type: text/xml');
        echo 
$output;
        break;
    case 
"atom":
        
$feeditems PrepareItems($photos,clean($_GET["thumbnails"],3),clean($_GET["alldata"],3));
        
$feeddata PrepareMetaData("Atom");
        
$output BuildAtomFeed
            
(
            
$feeddata["commenttext"],
            
$feeddata["feedtitle"],
            
$feeddata["feedurl"],
            
$feeddata["feedtagline"],
            
$feeddata["feedid"],
            
$feeddata["feedauthorname"],
            
ASIF_TIMEZONE_NUMERIC,
            
$feeditems 
            
);
        
header('Content-Type: text/xml');
        echo 
$output;
        break;
    default:
        
//output as HTML
        
$feeditems PrepareItems($photos,clean($_GET["thumbnails"],3),clean($_GET["alldata"],3));
        
$feeddata PrepareMetaData("html");
        
$output BuildHTML
            
(
            
$feeddata["commenttext"],
            
$feeddata["feedtitle"],
            
$feeddata["feedurl"],
            
$feeddata["feedtagline"],
            
$feeddata["feedauthorname"],
            
ASIF_TIMEZONE_ENGLISH,
            
$feeditems,
            
clean($_GET["thumbnails"],3)
            );
        echo 
$output;
    }
?>