Category: "PHP"

PHP Abstract Base Class - Including validation

An example of a base class.

Sample extension and call for instantiation:


<?php
class Course extends Base {

        public function __construct($properties) {
                $this->_properties = array(
                        'id'=>array(
                                'required' => false,
                                'default' => null,
                                'filter' => FILTER_VALIDATE_INT,
                                'options' => array('min_range' => 0, 'max_range' => PHP_INT_MAX)),
                        'name' => array(
                                'required' => true,
                                'default' => null,
                                'filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^[\w\,\.\'\-\(\)\$ ]{3,}$/')),
                        'isCore' => array(
                                'required' => true,
                                'default' => true,
                                'filter' => FILTER_VALIDATE_BOOLEAN));

                return parent::__construct($properties);
        }
}

<?php
Abstract Class Base {

        protected $_properties = null;
        protected $_data = array();
        protected $_valid = false;
        protected $_error_message = array();

        function __construct($properties = null) {
                if (is_array($properties)) {
                        foreach ($this->_properties as $k => $v) {
                                if (isset($properties[$k])) {
                                        $p = $properties[$k];
                                        if (is_string($p)) {
                                                $p = trim($p);
                                                if ($p === "") {
                                                        if (empty($p) && $v['required']) {  // this should never happen
                                                                return null;
                                                        }
                                                }
                                        }
                                } else {
                                        $p = $v['default'];
                                }
                                $this->_data[$k] = $p;
                        }
                        $filter = filter_var_array($this->_data,$this->_properties);
                        if ($filter === false || in_array(false,$filter,true)) {
                                foreach ($filter as $k => $v) {
                                        if (($v === false) &&
                                                ($this->_properties[$k]['filter'] !== FILTER_VALIDATE_BOOLEAN)) {
                                                // This is an important test, a boolean false, 
                                                // validated as such, doesn't mean the filter
                                                // failed.  
                                                trigger_error('Invalid data');
                                        }
                                }
                        }
                }
                return $this->_data;
        }

        function __get($property) {
                return isset($this->_data[$property]) ? $this->_data[$property] : null;
        }

        function __set($property, $value) {
                $this->_data[$property] = $value;
        }

        function toJson() {
                return json_encode($this->_data);
        }

        function isValid($property = null) {
                if ($property === null)
                        return $this->_valid || (count($this->_error_message) == 0);
                else
                        return !isset($this->_error_message[$property]);
        }

        function error($property) {
                if (isset($this->_error_message[$property])) {
                        return $this->_error_message[$property];
                } else {
                        return '';
                }
        }
}

PHP filter_var_array Example

This is a very simple PHP contact form validation script.

It fails silently, based on the expectation the client is validating the data prior to submitting it. In this case, if the server receives invalid inputs, inputs with invalid data, or a data set missing required inputs, it is assumed the data is either not being submitted by the expected client code, or it has been tampered with en route.

The silent fail is a die with no output. This provides no information for potentially malicious visitors.

If the data is valid, it is echoed back to the server JSON encoded.

if (isset($_POST) && !empty($_POST)) {
        $required = array('name', 'email', 'interest', 'relationship', 'message');
        $optional = array('phone', 'subscribe');
        $inputs = array_merge($required, $optional);
        foreach ($_POST as $k => $v) {
                if (!in_array($k, $inputs)) {
                        die;
                }
                $v = trim($v);
                if (!empty($v)) {
                        $data[$k] = $v;
                } else {
                        if (in_array($k, $required)) {
                                die;
                        } else {
                                $data[$k] = '';
                        }
                }
        }

        $filter = filter_var_array($data, array(
                'name' => array('filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^[ \w\,\.\'\-]{5,}$/')),
                'email' => FILTER_VALIDATE_EMAIL,
                'interest' => array('filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^(Sales|Support)$/')),
                'relationship' => array('filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^(Client|Partner|Vendor)$/')),
                'subscribe' => array('filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^(on)?$/')),
                'message' => array('filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^[\w\,\.\'\-\(\)\$ ]{5,}$/')),
                'phone' => array('filter' => FILTER_VALIDATE_REGEXP,
                                'options' => array('regexp' => '/^(1[ \-\.])?(\d{3})?[ \-\.]?\d{3}[ \-\.]?\d{4}$/'))));

        if ($filter === false || in_array(false,$filter)) {
                die;
        }
        echo json_encode($filter);
}

In an environment where security is important (security is important in all environments), this code should be extended to include a unique token validation, where a token is sent to the client on the initial request and the next request must have the identical token or it will be considered invalid.

PHP - ImageMagick command line Pie Chart

This code was converted from a bash version to create pie charts. It is much faster and does not require bc on the server.

PHP is used to create the ImageMagick command line.

This code creates pie chart and legend images which can then be placed on a web page or added in to a document.

To call the code, create an array of values, instantiate the GFX objects, establish the label_color_map as an associative array where the label text is the key and the value is a hex color code, and finally create the chart, sending a name and the data array, like so:


                        $data = array(50, 70, 100, 3, 49);
                        $chart = new GFX();
                        $chart->label_color_map(array('one' => '#1c28a1', 
                          'two' => '#600087',
                          'three' => '#107a3f',
                          'four' => '#bf0000',
                          'five' => '#ff6d00'));
                        $chart->piechart('project',$data);

<?php 
Class GFX extends Base {

        public function __construct($parms = null) {
                $this->_properties = array('centerx','centery','width','height',
'radius');
                if (!is_array($parms)) {
                        $parms['width'] = $parms ['height'] = 330;
                        $parms['centerx'] = $parms['centery'] = 160;
                        $parms['radius'] = 135;
                }
                parent::__construct($parms);
        }

        public function label_color_map ($map = array()) {
                $this->_data['labels'] = array();
                foreach ($map as $k => $v) {
                        $this->_data['labels'][$k] = $v;
                }
        }
        public function piechart($name = null, $data = array()) {
                // Thanks to: http://jbkflex.wordpress.com/2011/07/28/creating-a-svg-pie-chart-html5/
                if ($name === null) return;

                $total = array_sum($data);

                $max=0;$kmax='None';
                foreach ($data as $k => $v) {
                        $value[$k] = (int)(($v/$total) * 100);
                        if ($value[$k] > $max) {
                                $kmax=$k;
                                $max=$value[$k];
                        }
                }
                while (array_sum($value) < 100) {
                        $value[$kmax]++;
                }

                foreach ($data as $k => $v) {
                        $value[$k] = (int)(($v/$total) * 100);
                        if ($value[$k] > $max) {
                                $kmax=$k;
                                $max=$value[$k];
                        }
                }
                while (array_sum($value) < 100) {
                        $value[$kmax]++;
                }
                $centerx = $this->_data['centerx'];
                $centery = $this->_data['centery'];
                $radius= $this->_data['radius'];
                $count=0;
                $startAngle=0;
                $endAngle=0;
                $arc=0;
                $x1=0;
                $x2=0;
                $y1=0;
                $y2=0;
                $pi=pi();
                $cmd='convert -size '.$this->_data['width'].'x'.$this->_data['height'].' xc:white -stroke white -strokewidth 5 ';

                foreach ($value as $k => $v) {
                        $startAngle=$endAngle;
                        $endAngle=$startAngle+(360*$v/100);
                        $theta = $pi*$startAngle/180;
                        $x1=$centerx+$radius*cos($theta);
                        $y1=$centery+$radius*sin($theta);
                        $theta = $pi*$endAngle/180;
                        $x2=$centerx+$radius*cos($theta);
                        $y2=$centery+$radius*sin($theta);
                        $arc = ($v >= 50) ? '1' : '0';
                        $cmd.=' -fill "'.$this->_data['labels'][$k].'" -draw "path \'M '.$centerx.','.$centery.' L '.$x1.','.$y1.
                                ' A '.$radius.','.$radius.' 0 '.$arc.',1 '.$x2.','.$y2.' Z"';
                        $count++;
                }
                $cmd.=' '.escapeshellarg(_GFX_DIR_.'/'.$name.'_chart.jpg');
                `$cmd`;

                if (!is_array($this->_data['labels'])) return;

                $KEY_SIZE=20;
                $MARGIN=5;
                $TEXT_X=$KEY_SIZE+$MARGIN;

                $height =  $TEXT_X * count($value);

                $cmd = 'convert -size 135x'.$height.' xc:white -fill white ';
                $label = " -font 'Nimbus-Sans-Bold' -stroke none -pointsize 12 ";
                $count=0;
                $y1=5;
                foreach ($value as $k => $v) {
                        $y2=$y1+$KEY_SIZE;
                        $y3=$y2-$MARGIN;
                        $label.=' -fill "'.$this->_data['labels'][$k].'" -draw "rectangle 0,'.$y1.' '.$KEY_SIZE.','.$y2.'"'.
                                ' -draw "text '.$TEXT_X.','.$y3.' '.escapeshellarg(/*$data[$k].' '.*/$v.'% '.$k).'"';
                        $count++;
                        $y1=$y1+$KEY_SIZE+$MARGIN;
                }
                $cmd.= $label.' '.escapeshellarg(_GFX_DIR_.'/'.$name.'_legend.jpg');
                `$cmd`;
        }
}

<?php
Abstract Class Base {

        protected $_properties;
        protected $_data;
        protected $_valid = false;
        protected $_error_message = array();

        function __construct($props) {
                $this->_data = array_fill_keys($this->_properties, null);
                $this->_data = array_merge($this->_data, $props);
        }

        function __get($property) {
                return isset($this->_data[$property]) ? $this->_data[$property] : null;
        }

        function __set($property, $value) {
                $this->_data[$property] = $value;
        }

        function isValid($property = null) {
                if ($property === null)
                        return $this->_valid || (count($this->_error_message) == 0);
                else
                        return !isset($this->_error_message[$property]);
        }

        function error($property) {
                if (isset($this->_error_message[$property])) {
                        return $this->_error_message[$property];
                } else {
                        return '';
                }
        }
}

Many thanks to the link above for the chart algorithm.

This post courtesy of Worktrainer.

eZ Publish Workflow - electronic media delivery

I’m helping on a site that sells music.

I wanted a way to bundle a person’s purchases into a single file that could be downloaded once. There are two types of electronic media, mp3_product and album. Each may have an image associated with it. An mp3_product is an .mp3 files, an album is a .zip file, containing .mp3s.

The link above is a great resource. Using it, I created a custom workflow:

<?php
 
class CreateDownloadFileType extends eZWorkflowEventType
{
    const WORKFLOW_TYPE_STRING = "createdownloadfile";
    public function __construct()
    {
        parent::__construct( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'Create Download File');
    }
 
    public function execute( $process, $event )
    {
        $parameters = $process->attribute( 'parameter_list' );

	$order_id = $parameters['order_id'];
	$user_id = $parameters['user_id'];

	$order = eZShopFunctionCollection::fetchOrder ($order_id);

	$items = eZProductCollectionItem::fetchList( array( 'productcollection_id' => $order['result']->ProductCollectionID ) );

	$order_number = $order['result']->OrderNr;

	$zip_file='/home/account/www/download/'.md5($order_number.$user_id).'.zip';
	$zip = new ZipArchive();
	if ($zip->open($zip_file,ZIPARCHIVE::CREATE)!==true) {
		eZDebug::writeError('Order: '.$order_id.' - could not create zip file '.$zip_file);
	}

	$storageDir = eZSys::storageDirectory();
	$attributes_to_include = array('file','image');

	foreach ($items as $item)
	{
		$product = eZContentObject::fetch($item->ContentObjectID);

		if (in_array($product->ClassIdentifier,array('mp3_product','album')))
		{
			$product_data = $product->dataMap();
			$name = preg_replace('/[^\w]/','_',trim($product_data['name']->DataText));

			foreach ($attributes_to_include as $v)
			{
				if ($product_data[$v]!==null)
				{
					$attribute_data = $product_data[$v]->Content();
					$file = $attribute_data->Filename;
					$mime_type = $attribute_data->MimeType;
				        list( $group, $type ) = explode( '/', $mime_type );
					$filePath = $storageDir . '/original/' . $group . '/' . $file;
					$pathInfo = pathinfo($filePath);
					if ($zip->addFile($filePath,$name.'.'.$pathInfo['extension'])!==true) {
						eZDebug::writeError('Order: '.$order_id.' - failed to add '.$filePath.' to '.$zip_file);
					}
				}
			}
		}
		$zip->close();
	}

        return eZWorkflowType::STATUS_ACCEPTED;
    }
}
eZWorkflowEventType::registerEventType( CreateDownloadFileType::WORKFLOW_TYPE_STRING, 'createdownloadfiletype' );
?>

The zip file created is named with an md5 of the order number and user id, and placed in a download directory. It’s probably not the most secure approach, but I think it is okay for this application, and it can be changed later.

This triggers on shop/checkout/after, which means the person has paid.

The download link for the zip file is placed in both the order view and order email templates.

    {def $current_user=fetch( 'user', 'current_user' )}
    {if $current_user.is_logged_in}
    <h2>{"Download"|i18n("design/base/shop")}:</h2>
    <p><a href="/download/{concat($order.order_nr,$current_user.contentobject_id)|md5()}.zip">{"Click to download"|i18n("design/base/shop")}</
a></p>
    {/if}
    {undef $current_user}

You may need an Alias and a RewriteRule:


Alias download /home/account/www/download
RewriteRule ^download/.*\.zip - [L]

The final step was to create a mechanism to delete the files after they are downloaded. There are at least two ways to do this. The first approach is to use an Apache filter, with a bash script:

Apache configuration code:

ExtFilterDefine delete mode=output
cmd="/home/account/webfilter.sh"

<Directory /home/account/www/download>
SetOutputFilter delete
</Directory>


FILENAME="/home/account/www$DOCUMENT_URI";
cat -
if [ -e $FILENAME ]; then
        rm $FILENAME;
fi

The Apache filter code is courtesy of Lyrix, Inc..

If you can’t use a filter - which is likely if you are using shared hosting, you can also use a PHP script. This uses a RewriteRule to route the request through PHP.

Options -Indexes
<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteRule ^(.*\.zip)$ zip.php?path=$1 [L]
</IfModule>

Finally, this is the PHP code that will deliver the file, then delete it.

<?php
// downloading a file and then deleting it
$filename = $_GET['path'];
if (is_file($filename))
{
        // fix for IE catching or PHP bug issue
        header("Pragma: public");
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        header("Cache-Control: public");
        header("Content-Description: File Transfer");
        // browser must download file from server instead of cache

        // force download dialog
        header("Content-Type: application/zip");

        // use the Content-Disposition header to supply a recommended filename and
        // force the browser to display the save dialog.
        header("Content-Disposition: attachment; filename=".basename($filename).";");

        /*
        The Content-transfer-encoding header should be binary, since the file will be read
        directly from the disk and the raw bytes passed to the downloading computer.
        The Content-length header is useful to set for downloads. The browser will be able to
        show a progress meter as a file downloads. The content-lenght can be determines by
        filesize function returns the size of a file.
        */
        header("Content-Transfer-Encoding: binary");
        header("Content-Length: ".filesize($filename));

        @readfile($filename);
        unlink($filename);
        exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Requested Content Not Available</title>
</head>
<body>
<h1>Requested Content Not Available</h1>
<p>The content you requested is not available.</p>
<p>If you were unable to download your purchases, please contact us for assistance.</p>
</body>
</html>

Installing ffmpeg-php on CentOS 5.8

Follow the instructions in the link above, they’re excellent.

If you get a few compile errors related to missing files, add these RPMs (excerpt from /var/log/yum.log):

faad2-devel-2.7-1.el5.rf.i386
x264-devel-0.0.0-0.4.20101111.el5.rf.i386
lame-devel-3.99.5-1.el5.rf.i386
a52dec-devel-0.7.4-8.el5.rf.i386
dirac-devel-1.0.2-1.el5.rf.i386
faac-devel-1.26-1.el5.rf.i386
alsa-lib-devel-1.0.17-1.el5.i386
SDL-devel-1.2.10-9.el5.i386
gsm-devel-1.0.13-1.el5.rf.i386
opencore-amr-devel-0.1.2-1.el5.rf.i386
imlib2-1.4.4-1.el5.rf.i386
imlib2-devel-1.4.4-1.el5.rf.i386
ffmpeg-devel-0.6.5-1.el5.rf.i386
libXpm-devel-3.5.5-3.i386
gd-devel-2.0.33-9.4.el5_4.2.i386

I recommend using the package manager, it will save you a lot of time.

If you get an error referencing PIX_FMT_RGBA32, follow the instructions at: http://blog.hostonnet.com/ffmpeg-php-error-pix_fmt_rgba32