Stripping HTML automatically from your data

About a week ago I talked about automatically sanitizing your data before its saved. Now I want to talk about automatically stripping HTML from your data before its saved, which is good practice. Personally, I hate saving any type of HTML to a database, thats why I prefer a BB code type system for this website. To strip all tags from your data, add this method to your AppModel.

/**
 * Strip all html tags from an array
 *
 * @param array $data
 * @return array
 */
public function cleanHtml($data) {
	if (is_array($data)) {
		foreach ($data as $key => $var) {
			$data[$key] = $this->cleanHtml($var);
		}
	} else {
		$data = Sanitize::html($data, true);
	}
	return $data;
}

Pretty simple right? The next and final step is to add it to AppModel::beforeSave(). In the next example, I will use the code snippet from my previous related article. Once you have done this your are finished, now go give it a test drive.

function beforeSave() {
	if (!empty($this->data) && $this->cleanData === true) {
		$connection = (!empty($this->useDbConfig)) ? $this->useDbConfig : 'default';
		$this->data = Sanitize::clean($this->data, array('connection' => $connection, 'escape' => false));
		$this->data = $this->cleanHtml($this->data);
	}
	return true;
}

Feed Aggregator Component officially released!

I recently needed the ability to display a list of feed items, but the catch was I needed them to be combined from multiple feeds, a feed aggregator. At first I was expecting to find this in the bakery, but was saddened when there was none. So I sat down and began coding. I've never created a feed aggregator before, but it was relatively easy.

Its really simple, the script is a CakePHP Component that will take a list of feeds and aggregate them into a single array based on their timestamp. It works with RSS 1.0 (RDF), RSS 2.0 and Atom 1.0 and has the ability to cache its results.

View full documentation and download version 1.1

Enjoy! Let me know if there are any bugs.

Automatically sanitizing data with beforeSave()

So if you are like me and hate having to sanitize or clean your data manually within each action, and was hoping there was an easier way, there is. Simple combine the magic of Model::beforeSave() and the powerful strength of Sanitize::clean().

function beforeSave() {
	if (!empty($this->data) && $this->cleanData === true) {
		$connection = (!empty($this->useDbConfig)) ? $this->useDbConfig : 'default';
		$this->data = Sanitize::clean($this->data, array('connection' => $connection, 'escape' => false));
	}
	return true;
}

The previous code will attempt to clean all data before it is saved. Secondly it will convert HTML, it will not strip tags completely. So if you do not want HTML in your database, you will need to add some extra functionality and set encode to false in the clean() options.

But that's not it, were not finished just yet. You may have noticed a $cleanData variable and are probably wondering what it does. This is a custom property that should be placed in your AppModel and IS NOT a CakePHP property. By placing it in the AppModel we will receive no error notices and all data will be cleaned, additionally you can disable cleaning in certain models by setting the property to false in the respective model.

public $cleanData = true;
Known Errors

So far this has worked smoothly, except for the following exception:

- Serialized arrays will be escaped incorrectly and will break when trying to unserialize(), simply set $cleanData to false to not escape the serialized arrays.

- When escape is set to true, all data will have slashes added on top of the slashes already added with the Model class, so its best to turn escaping off.

Uploader Plugin officially released!

For the past month I have been talking about CakePHPs lack of a built in uploader component or mechanism, and how I wanted to build one. Well wouldn't it be great if I actually did build it? Oh wait, what is this? I did build one? Yes. Is it released? Yes. May I use it? Of course! The Uploader is primarily used as an all purpose file uploader and was built to not interact with a model/database. Here is a quick rundown of the plugin.

The plugin comes bundled with an uploader component and a validation behavior. The component is used to upload the files to a destination, resize or generate images, validate against mime types, log errors, scan for viruses and more. The behavior is used to add validation rules to a model to check against image uploads.

A big thank you to http://mark-story.com/ and Matt Curry for beta testing and giving input and ideas.

- Download Uploader v1.3 and view full documentation

Validating an images dimensions through Model validation

Since Cake's default model validation really doesn't support files that well, we have to build the methods our self. Right now I will be showing you how to validate an images dimensions. The method will either check the width, the height or both width and height. Simply place the following code in your AppModel.

/**
 * Checks an image dimensions
 *
 * @param array $data
 * @param int $width
 * @param int $height
 * @return boolean
 */
public function dimension($data, $width = 100, $height = null) {
	$data = array_values($data);
	$field = $data[0];
	if (empty($field['tmp_name'])) {
		return false;
	} else {
		$file = getimagesize($field['tmp_name']);
		if (!$file) {
			return false;
		}
		$w = $file[0];
		$h = $file[1];
		$width = intval($width);
		$height = intval($height);
		if ($width > 0 && $height > 0) {
			return ($w > $width || $h > $height) ? false : true;
		} else if ($width > 0 && !$height) {
			return ($w > $width) ? false : true;
		} else if ($height > 0 && !$width) {
			return ($h > $height) ? false : true;
		} else {
			return false;
		}
	}
	return true;
}

To use this validation, you would write it like any other custom validation rule. Lets set up our example view and model validation.

// View
echo $form->create('TestModel', array('type' => 'file'));
echo $form->input('image', array('type' => 'file'));
echo $form->end('Upload');
// Model
class TestModel extends AppModel {
    public $validate = array(
        'image' => array(
            'rule' => array('dimensions', 500, 500),
            'message' => 'Your image dimensions are incorrect: 500x500'
        )
    );
}

Now all you have to do is validate the data using your Models save() or validates() method. If the image fails the dimensions, the error should appear next to the field.

Problems with allowEmpty on files

Since the file support in Cake is lacking, you cannot use allowEmpty equals true. So this means that your image validation fields will always be required. There is currently a Trac bug for this with a quick fix.

Protecting your forms with the Security Component

Many users are unaware of this feature as it is not stated within the Cookbook, but the SecurityComponent by default will secure and protect your forms (if you have added Security to the $components array). What does that mean you ask, well it's simple. The Security will add hidden token fields within all your forms that will protect you against many types of robots. An example of a token field can be seen below.

Array
(
    [_Token] => Array
        (
            [key] => 40bbf3ac6cb4cd9bfaa617c088aa938bb398e80f
            [fields] => b5a93a2492bd2e6016856828d8046ba1f6f6200b%3An%3A0%3A%7B%7D
        )
)

These token fields are a dynamically generated hash, based on all the fields currently available in a specific form. On top of this, the Token will only last for a limited duration, so if your session takes forever on a certain form, you will be blackholed. Now on to the term blackhole, this basically means your form will post to a blank/white page and will fail.

Using Javascript to change input values

If you use Javascript to change a hidden inputs value, the Security will blackhole the form because it checks to see if any hidden fields have changed values (if it has, its usually a bot). To bypass this check, you would add this code to your controllers beforeFilter(). The array should consist of the field names you DO NOT want the Security to validate, so in this case it would be the name of our hidden field.

$this->Security->disabledFields = array('hiddenfield1', 'hiddenfield2', 'randomfield');
Not validating a form at all

If you have Security enabled, but do not want it to validate a certain form, you would set validatePost to false in your beforeFilter(). This is MANDATORY if you are doing any type of Ajax requests.

if ($this->params['action'] == 'actionName') {
	$this->Security->validatePost = false;
}
// Ajax requests
$this->Security->validatePost = false; 
if (!$this->RequestHandler->isAjax()) {
	$this->Security->blackHole($this, 'You are not authorized to process this request!');
}

And that is all, simple isn't it? All it requires is you adding the SecurityComponent to your controllers $components and a bit of magic on your end.

Official release of the AutoLogin Component

Many of you have heard me mention a component that keeps your Auth session active, even after the browser is closed. Well I think its time to officially announce it, my AutoLogin component. This component ties into the Auth component and allows a user to "remember me" to keep them constantly logged in. All data is saved to cookies and encrypted and hashed to no hijacking can occur. The best part though, is that the component automatically and magically saves and deletes cookies without you having to configure it (although you can configure it, check out the docs).

Download the latest version of AutoLogin

In other script related news...
Commentia has been updated to v1.3
Resession has been updated to v1.8 (now with more Security!)

Two new scripts, Resession and Decoda

It has been a while since I released some of my scripts, but now I have two to reveal! Well actually the Resession script has been up for nearly 2 months now, but I'm finally getting around to announcing. And without further ado, I give you Resession (Session Manager) and Decoda (A BBcode style parser).

Decoda

Decoda is a lightweight class that extracts and parses a custom markup language; based on the concept of BB code. Decoda supports all the basic HTML tags and manages special features for making links and emails auto-clickable, using shorthand emails and links, and finally allowing the user to add their own code tags.

Download the latest versions of Decoda

Resession

A small lightweight script that can manage and manipulate Session data. Calls session_start() in memory so that no header errors are thrown, as well as stores the session id in the object.

Download the latest version of Resession

If you have any suggestions for either of these classes, please feel free to comment this post or send me an email!

Caching each query individually

So lately I have been delving into the caching capabilities of CakePHP. Most, if not all of its capabilities work wonderfully; although I personally can't get into $cacheAction (within the controllers). The $cacheAction property only works for static and non-user generated pages, in other terms, any content that changes depending on a logged in user wont work correctly with $cacheAction (unless you want thousands and thousands of cache files). So I stopped using $cacheAction all together in my latest application, and instead built a method that caches individual queries, instead of the whole page. All the modifications have been applied to the models find() method. To use this, place the following code within your app/app_model.php.

/**
 * Wrapper find to cache sql queries
 * @param array $conditions
 * @param array $fields
 * @param string $order
 * @param string $recursive
 * @return array
 */
public function find($conditions = null, $fields = array(), $order = null, $recursive = null) {
	if (Configure::read('Cache.disable') === false && Configure::read('Cache.check') === true && isset($fields['cache']) && $fields['cache'] !== false) {
		$key = $fields['cache'];
		$expires = '+1 hour';
		if (is_array($fields['cache'])) {
			$key = $fields['cache'][0];
			if (isset($fields['cache'][1])) {
				$expires = $fields['cache'][1];
			}
		}
		// Set cache settings
		Cache::config('sql_cache', array(
			'prefix' 	=> strtolower($this->name) .'-',
			'duration'	=> $expires
		));
		// Load from cache
		$results = Cache::read($key, 'sql_cache');
		if (!is_array($results)) {
			$results = parent::find($conditions, $fields, $order, $recursive);
			Cache::write($key, $results, 'sql_cache');
		}
		return $results;
	}
	// Not cacheing
	return parent::find($conditions, $fields, $order, $recursive);
}

In the next step, you would create a folder called sql within your tmp/cache/ and chmod the permissions to 777. Once you have created the folder, open up your app/config/core.php file and place the following code at the bottom (near the default cache settings).

Cache::config('sql_cache', array(
    'engine'		=> 'File',
    'path'		=> CACHE .'sql'. DS,
    'serialize'	=> true,
));

By default, caching will not work on your applications queries, you would need to set an additional "cache" option within your find(). Each SQL cache should have its own unique identifier so that it does not conflict with other queries. Also by default, queries will be cached for one hour and will be saved as a serialized array. The following examples explain how the cache option works.

// Cache query to /tmp/cache/sql/model-test_sql_query
$results = $this->Model->find('all', array(
	'cache' => 'test_sql_query'
));
// Cache query to /tmp/cache/sql/model-another_query that expires in 24 hours
$results = $this->Model->find('all', array(
	'cache' => array('another_query', '+24 hours')
));

What if I have a query that's used multiple times but each has its own limit (custom method), but uses the same cache slug? Simply give the cache slug a dynamic name like so:

// Cache query to /tmp/cache/sql/model-dynamic_query-15 
$results = $this->Model->find('all', array(
	'limit' => $limit, // 20, 30, etc
	'cache' => 'dynamic_query-'. $limit
));

I personally have found an increase in load times up to 150-200% faster using this method. This should only be applied to queries that are used on landing pages, and queries that do not change according to which user is logged in. Have fun.

CSS/JS/Asset Compression in CakePHP

This code was developed for CakePHP 1.2 and will no longer work on the most recent versions. You should be using an asset handler plugin in 1.3+.

The other day I spent way too many hours trying to figure out CakePHPs built in CSS and JS compression system. To say the least, its not really built in, you have to configure and do a lot of it yourself. I did much searching and found tutorials that didn't work, or using custom built helpers that were of no use, and in the end I wrote my own script (based off webroot/css.php written by gwoo). My script does the following:

  • Compresses both CSS and JS, using one file
  • Caches the assets, and recaches if a change is made to the original
  • Comes prebuilt with CSS compression
  • Must download and install JSMin
  • Extremely easy to install and configure

To use my asset compression script, create a file called assets.php within webroot, and then copy and paste the code found at the end of this post. Once you have done this, download and place the jsmin.php file within /vendors/ or /app/vendors/ (the JSMin file should be named jsmin.php to work). The next step is to create a folder called assets within your /app/tmp/cache/ directory, and chmod the permissions to 777. The final step is to uncomment the filtering in core.php and set the path to assets.php.

Configure::write('Asset.filter.css', 'assets.php');
Configure::write('Asset.filter.js', 'assets.php');

Now to take it for a test spin! Once you have uncommented the configuration, direct your browser to the actual JS and CSS paths to see if it works (http://www.domain.com/ccss/style.css), and that's it! Hope this has been helpful and easy, like it should have been!

<?php 
// No cake installation
if (!defined('CAKE_CORE_INCLUDE_PATH')) { 
	header('HTTP/1.1 404 Not Found');
	exit('File Not Found'); 
} 
// Get asset type
$ext = trim(strrchr($url, '.'), '.');
$assetType = ($ext === 'css') ? 'css' : 'js';
// Wrong file
if (preg_match('|\.\.|', $url) || !preg_match('|^c'. $assetType .'/(.+)$|i', $url, $regs)) {
	die('Wrong File Name');
}
$cachePath = CACHE .'assets'. DS . str_replace(array('/','\\'), '-', $regs[1]);
$fileName = $assetType .'/'. $regs[1];
if ($assetType == 'css') {
	$filePath = CSS . $regs[1];
	$fileType = 'text/css';
} else {
	$filePath = JS . $regs[1];
	$fileType = 'text/javascript';
}
if (!file_exists($filePath)) {
	die('Asset Not Found');
}
/**
 * Compress the asset
 * @param string $path
 * @param string $name
 * @return string
 */
public function compress($path, $name, $type) {
	$input = file_get_contents($path);
	if ($type == 'css') {
		$stylesheet = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $input);
		$stylesheet = str_replace(array("\r\n", "\r", "\n", "\t", '/\s\s+/', '  ', '   '), '', $stylesheet);
		$stylesheet = str_replace(array(' {', '{ '), '{', $stylesheet);
		$stylesheet = str_replace(array(' }', '} '), '}', $stylesheet);
		$output = $stylesheet;
	} else {
		App::import('Vendor', 'jsmin');
		$output = JSMin::minify($input);
	}
	$ratio = 100 - (round(strlen($output) / strlen($input), 3) * 100);
	$output = "/* File: $name, Ratio: $ratio% */\n". $output;
	return $output;
}	
/**
 * Cache the asset
 * @param string $path
 * @param string $content
 * @return string
 */
public function cacheAsset($path, $content) {
	if (!is_dir(dirname($path))) {
		mkdir(dirname($path));
		chmod($path, 0777);
	}
	if (!class_exists('File')) {
		uses('file');
	}
	$cached = new File($path);
	return $cached->write($content);
}
// Do compression and cacheing
if (file_exists($cachePath)) {
	$templateModified = filemtime($filePath);
	$cachedModified = filemtime($cachePath);
	if ($templateModified > $cachedModified) {
		$output = compress($filePath, $fileName, $assetType);
		cacheAsset($cachePath, $output);
	} else {
		$output = file_get_contents($cachePath);
	}
} else {
	$output = compress($filePath, $fileName, $assetType);
	cacheAsset($cachePath, $output);
	$templateModified = time();
}
header("Date: ". date("D, j M Y G:i:s", $templateModified) ." GMT");
header("Content-Type: ". $fileType);
header("Expires: ". gmdate("D, j M Y H:i:s", time() + DAY) ." GMT");
header("Cache-Control: max-age=86400, must-revalidate"); // HTTP/1.1
header("Pragma: cache_asset");        // HTTP/1.0
echo $output; ?>