<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Aidan Lister</title>
	<atom:link href="http://aidanlister.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://aidanlister.com</link>
	<description>Code is poetry</description>
	<lastBuildDate>Sun, 14 Feb 2010 08:52:55 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.1</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Retrieve time from an NTP server</title>
		<link>http://aidanlister.com/2010/02/retrieve-time-from-an-ntp-server/</link>
		<comments>http://aidanlister.com/2010/02/retrieve-time-from-an-ntp-server/#comments</comments>
		<pubDate>Sun, 14 Feb 2010 08:52:37 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Code Repository]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=422</guid>
		<description><![CDATA[<code>ntp_time()</code> allows you to quickly retrieve the time from an NTP server as a unix timestamp.]]></description>
			<content:encoded><![CDATA[<p>There&#8217;s a lot of code floating around the internet to communicate with NTP servers, unfortunately none of the ones I found actually worked. Here&#8217;s a low level implementation:</p>
<p><?php<br />
/**<br />
 * Retrieve time from an NTP server<br />
 *<br />
 * @param string $host The NTP server to retrieve the time from<br />
 * @return int The current unix timestamp<br />
 */<br />
function ntp_time($host) {</p>
<p>  // Create a socket and connect to NTP server<br />
  $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);<br />
  socket_connect($sock, $host, 123);</p>
<p>  // Send request<br />
  $msg = "\010" . str_repeat("\0", 47);<br />
  socket_send($sock, $msg, strlen($msg), 0);</p>
<p>  // Receive response and close socket<br />
  socket_recv($sock, $recv, 48, MSG_WAITALL);<br />
  socket_close($sock);</p>
<p>  // Interpret response<br />
  $data = unpack('N12', $recv);<br />
  $timestamp = sprintf('%u', $data[9]);</p>
<p>  // NTP is number of seconds since 0000 UT on 1 January 1900<br />
  // Unix time is seconds since 0000 UT on 1 January 1970<br />
  $timestamp -= 2208988800;	</p>
<p>  return $timestamp;<br />
}<br />
?></p>
<p>You can use any NTP host you want, ideally something local to you. Otherwise, you can use a random server from the <a href="http://www.pool.ntp.org">NTP Server Pool</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2010/02/retrieve-time-from-an-ntp-server/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Dealing with human readable network addresses</title>
		<link>http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/</link>
		<comments>http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/#comments</comments>
		<pubDate>Sun, 18 Oct 2009 12:58:22 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Housekeeping]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=400</guid>
		<description><![CDATA[<code>network_expand_range</code> and <code>network_compact_range</code> allow the expansion and contraction of IP addresses or ranges as human readable input.]]></description>
			<content:encoded><![CDATA[<p>Let&#8217;s say we need to build a list of network addresses to whitelist access to a restricted area, or blacklist a troublesome spammer. It&#8217;s handy to be able to enter data in a variety of formats, including hostnames, IP addresses, and IP ranges.</p>
<p>Usually we will store this information as a <em>long</em>, then we can easily see if a requesting IP address falls within our list of networks simply by numerical comparison, specifically: start_range < requesting_ip < end_range.</p>
<p>However, our user doesn't want to enter the list of IPs as a <em>long</em> so we need tools to convert between an attractive input format, and an array of start and end ranges.</p>
<p>To accomplish this, we define two functions <code>network_expand_range</code> and <code>network_compact_range</code>. Let&#8217;s have a look:</p>
<pre class="brush: php;">
/**
 * Take small human readable network and give dotted quad range
 *
 * Acceptable inputs:
 *   1.1.1.1 - 1.1.255.255
 *   2.2.2.0-255
 *   3.3.3.*
 *   4.4.*.*
 *   5.5.5.20-40
 *   google.com
 *
 * @author Aidan Lister &lt;aidan@php.net&gt;
 * @link http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/
 * @param input $string A newline separated list of network ranges
 * @return array Start and end addresses in dotted quads, or false
 */
function network_expand_range($network)
{
    // Sanity check
    if (empty($network)) {
        return false;
    }

    $has_star = strpos($network, '*');
    $has_dash = strpos($network, '-');

    // Last character of a domain must be in the alphabet
    if (ctype_alpha($network[strlen($network)-1])) {
        if (!$address = gethostbyname($network)) {
          return false;
        }
        return array($address, $address);
    } 

    // Simple
    if ($has_star === false &amp;&amp; $has_dash === false) {
        $res   = long2ip(ip2long($network));
        $start = $end = $res;
        if ($res === '0.0.0.0') {
            return false;
        }
    }

    // Using a star
    if ($has_star !== false) {
        $start = long2ip(ip2long(str_replace('*', '0', $network)));
        $end   = long2ip(ip2long(str_replace('*', '255', $network)));
        if ($start === '0.0.0.0' || $end === '0.0.0.0') {
            return false;
        }
    }

    // Using a dash
    if ($has_dash !== false) {
        list($start, $end) = explode('-', $network);

        // Check whether end is a full IP or just the last quad
        if (strpos($end, '.') !== false) {
            $end = long2ip(ip2long(trim($end)));
        } else {
            // Get the first 3 quads of the start address
            $classc = substr($start, 0, strrpos($start, '.'));
            $classc .= '.' . $end;
            $end = long2ip(ip2long(trim($classc)));
        }

        // Check for failure
        $start = long2ip(ip2long(trim($start)));
        if ($start === '0.0.0.0' || $end === '0.0.0.0') {
            return false;
        }
    }

    return array($start, $end);
}
</pre>
<p>Next, the sister function to reverse the expansion process:</p>
<pre class="brush: php;">
/**
 * Convert start and end address into small human readable string
 *
 * For example, $s=1.1.1.1 and $e=1.1.255.255 into 1.1.*.*
 *
 * @author Aidan Lister &lt;aidan@php.net&gt;
 * @link http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/
 * @param int $start The start address
 * @param int $end The end address
 * @return string A start and end range as the small formatted string
 */
function network_compact_range($start, $end)
{
    if ($start === $end) {
        $output = $start;
    } else {
        $s = explode('.', $start);
        $e = explode('.', $end);
        if ($s[0] === $e[0] &amp;&amp; $s[1] === $e[1] &amp;&amp; $s[2] === $e[2]) {
            if ($s[3] === '0' &amp;&amp; $e[3] === '255') {
                $s[3] = '*';
                $output = implode('.', $s);
            } else {
                $s[3] = sprintf('%s-%s', $s[3], $e[3]);
                $output = implode('.', $s);
            }
        } else {
            $output = $start .' - '. $end;
        }
    }

    return $output;
}
</pre>
<p>To show how it all works, let&#8217;s look at some examples:</p>
<pre class="brush: php;">
// Example input (maybe from a textarea, database or file)
$input = array();
$input[] = '1.1.1.1 - 1.1.255.255';
$input[] = '2.2.2.0-255';
$input[] = '3.3.3.*';
$input[] = '4.4.*.*';
$input[] = '5.5.5.20-40';
$input[] = 'google.com';

echo &quot;The input is:\n&quot;;
foreach ($input as $network) {
    echo &quot;$network\n&quot;;
}

echo &quot;\n&quot;;
echo &quot;Expanded into ranges:\n&quot;;
foreach ($input as $network) {
    list($start, $end) = network_expand_range($network);
    echo &quot;$start - $end\n&quot;;
}

echo &quot;\n&quot;;
echo &quot;As a long:\n&quot;;
foreach ($input as $network) {
    list($start, $end) = network_expand_range($network);
    echo ip2long($start) . ' - ' . ip2long($end) . &quot;\n&quot;;
}

echo &quot;\n&quot;;
echo &quot;Back into compacted form:\n&quot;;
foreach ($input as $network) {
    list($start, $end) = network_expand_range($network);
    echo network_compact_range($start, $end), &quot;\n&quot;;
}
</pre>
<p>The output of this would be something like:</p>
<pre class="brush: plain;">
The input is:
1.1.1.1 - 1.1.255.255
2.2.2.0-255
3.3.3.*
4.4.*.*
5.5.5.20-40
google.com

Expanded into ranges:
1.1.1.1 - 1.1.255.255
2.2.2.0 - 2.2.2.255
3.3.3.0 - 3.3.3.255
4.4.0.0 - 4.4.255.255
5.5.5.20 - 5.5.5.40
74.125.53.100 - 74.125.53.100

As a long:
16843009 - 16908287
33686016 - 33686271
50529024 - 50529279
67371008 - 67436543
84215060 - 84215080
1249719652 - 1249719652

Back into compacted form:
1.1.1.1 - 1.1.255.255
2.2.2.*
3.3.3.*
4.4.0.0 - 4.4.255.255
5.5.5.20-40
74.125.53.100
</pre>
<p>Hope you find this useful.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/10/dealing-with-human-readable-network-addresses/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Executing a callback for all files in a directory</title>
		<link>http://aidanlister.com/2009/06/executing-a-callback-for-all-files-in-a-directory/</link>
		<comments>http://aidanlister.com/2009/06/executing-a-callback-for-all-files-in-a-directory/#comments</comments>
		<pubDate>Tue, 16 Jun 2009 23:22:56 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Functions]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=381</guid>
		<description><![CDATA[<code>directory_walk</code> allows running an arbitrary callback on all files in a deep directory structure.]]></description>
			<content:encoded><![CDATA[<p>A common question when dealing with deep directory structures concerns how a function can be applied recursively to all the files in the target directory, regardless of the depth. This function, <code>directory_walk</code>, builds an internal stack (uses no recursion) and iteratively applies the user supplied callback providing a fast and flexible approach.</p>
<pre class="brush: php;">
/**
 * Allows running a callback on all files in a deep directory structure
 *
 * @author Aidan Lister &lt;aidan@php.net&gt;
 * @param string $dirname The directory to walk
 * @param callable $callable The callable to execute on all files found
 * @param mixed $arg{n} Extra parameters to be passed to the callable
 * @return mixed The return value of the last callable run
 */
function directory_walk($dirname, $callable)
{
    $ignore = array('.', '..', '.DS_Store');
    $args = func_get_args();
    array_shift($args);

    // Sanity check
    if (!file_exists($dirname)) {
        return false;
    }

    // Create and iterate stack
    $stack = array($dirname);
    while ($entry = array_pop($stack)) {
        if (is_link($entry)) continue;

        // Run the action
        if (is_file($entry)) {
          $ret = call_user_func_array($callable, array($entry) + $args);
          continue;
        }

        // Add the directory into the stack
        $dh = opendir($entry);
        while (false !== $child = readdir($dh)) {
            if (in_array($child, $ignore)) continue;
            $child = $entry . DIRECTORY_SEPARATOR . $child;
            $stack[] = $child;
        }
        closedir($dh);
    }

    return $ret;
}
</pre>
<p>So, for some examples. We&#8217;ll start simple and simply print the directory:</p>
<pre class="brush: php;">
$func = create_function('$file', 'echo &quot;found $file&quot;;');
directory_walk('target-dir', $func);
</pre>
<p>Which for me outputs:</p>
<pre class="brush: plain;">
found target-dir/foo
found target-dir/bar
found target-dir/baz
found target-dir/baz/ding
found target-dir/baz/dong
found target-dir/baz/dong/witch
</pre>
<p>What if we wanted to add a .txt extension to all of these files? We could write:</p>
<pre class="brush: php;">
function my_rename($entry, $extension) {
    if (is_file($entry)) {
        rename($entry, $entry . $extension);
    }
}
directory_walk('target-dir', 'my_rename', '.txt');
</pre>
<p>Another example might be deleting all the .svn or CVS folders in a directory. We could write:</p>
<pre class="brush: php;">
function delsvn($file) {
  $ext = substr($file, strlen($file)-4,strlen($file));
  if ($ext === '.svn') rmdirr($ext);
}
directory_walk('test', 'delsvn');
</pre>
<p>Note that the above example used the <a href="/2004/04/recursively-deleting-a-folder-in-php/">recursive delete function</a> also.</p>
<p>Happy stacking.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/06/executing-a-callback-for-all-files-in-a-directory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Why I&#8217;ll never eBay again</title>
		<link>http://aidanlister.com/2009/06/why-ill-never-ebay-again/</link>
		<comments>http://aidanlister.com/2009/06/why-ill-never-ebay-again/#comments</comments>
		<pubDate>Tue, 16 Jun 2009 19:34:19 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Housekeeping]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=370</guid>
		<description><![CDATA[I listed my old Macbook Pro 15" laptop for sale on eBay today. I paid my fees. I reviewed my add. I was happy for about 6 minutes, until I received a barrage of emails notifying me my account was suspended for "abuse" of their terms and conditions.

My account was locked down, leaving me unable to even log in to respond to the situation! All my bids on other items were revoked. Is this what online selling has become? If so, no thanks.]]></description>
			<content:encoded><![CDATA[<p>I listed my old Macbook Pro 15&#8243; laptop for sale on eBay today. I paid my fees. I reviewed my add. I was happy for about 6 minutes, until I received the following barrage of emails:</p>
<blockquote><p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br />
MC010 A26 TKO NOTICE:  Restored Account- aidanlister<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br />
Hello aidanlister (aidanlister@gmail.com),</p>
<p>It appears that your account was accessed by an unauthorised third party to list items without your permission. At this time we have taken several steps to secure your eBay account. Be assured that your credit card and banking information is safe as this information is kept encrypted on a secure server and cannot be viewed by anyone.</p></blockquote>
<p>Followed by:</p>
<blockquote><p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br />
MC040 NOTICE: eBay Auction(s) Cancelled &#8211; User Agreement &#8211; Abusing eBay</p>
<p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br />
Hello aidanlister (aidanlister@gmail.com),</p>
<p>Due to the suspension of your account you are prohibited from using eBay in any way. This includes registering a new account.</p>
<p>Please note that this does not relieve you of your obligation to pay any fees you may owe to eBay.</p></blockquote>
<p>Evidently listing an item for sale constitutes abuse, but it&#8217;s nice to know they won&#8217;t charge me for it.</p>
<blockquote><p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br />
MC045 aidanlister: Your eBay automatic payment request<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br />
Hello aidanlister (aidanlister@gmail.com),</p>
<p>We have temporarily disabled your automatic payment method for 30 to 60 days. During this time, eBay will not attempt to collect automatic payment for your invoices.</p></blockquote>
<p>Okay I thought, there&#8217;s a lot of scammers in the world &#8230; I&#8217;m sure they are just taking precautions. Great, I like precautions. I bet there&#8217;s a way to clear all this up in My eBay and I&#8217;ll chalk one up to the Nigerians.</p>
<p>Oh, how wrong I was. Not only has my password been changed, clicking on &#8220;Forgotten password&#8221; (and filling in my entire life history) sends me this beauty of an email:</p>
<blockquote><p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br />
Forgotten Password<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;</p>
<p>Dear aidanlister,</p>
<p>This email was sent automatically by eBay in response to your request to recover your password. This is done for your protection; only you, the recipient of this email can take the next step in the password recover process.</p></blockquote>
<p>Okay simple one-time login, this should be no problem then pow:</p>
<p><a href="http://aidanlister.com/wp-content/uploads/2009/06/eBay.png"><img class="size-medium wp-image-369" title="eBay - The ultimate user experience" src="http://aidanlister.com/wp-content/uploads/2009/06/eBay.png" alt="Please log in (with your password) to change your password" width="500" /></a></p>
<p>You want me to SIGN IN WITH MY PASSWORD &#8230; TO CHANGE MY PASSWORD? I mean, great odens raven guys, what the hell?</p>
<p>As I furiously write all of this in an anonymous customer support request (because I can&#8217;t log in to my account) I ponder; for a company that spends literally millions every year on ensuring users feel safe on their website, if this is the best they can do, then good luck in the future because I&#8217;m done. I&#8217;ll never eBay again.</p>
<p>What do people sell stuff on these days? I&#8217;d give craigslist a shot, but I&#8217;m worried my ad might be misconstrued sexually.</p>
<p><strong>Update</strong>: 17th June 2009<br />
I received the following email from eBay today:</p>
<blockquote><p>
Thank you for writing to eBay&#8217;s Customer Support. My name is Emma and I<br />
will be assisting you with your account.</p>
<p>Please complete the following steps to secure your account:</p>
<p>1. Change the password on your personal email account. It&#8217;s likely the<br />
unauthorised person has access to your email account. By changing the<br />
password, we can safely send you instructions and it will help prevent<br />
your eBay account from being accessed by someone else again.</p>
<p>2. Please contact our Account Security Live Help team
</p></blockquote>
<p>How ridiculous. Not only can I not log into my account, eBay are still under the impression that someone else listed the laptop for sale. I quite clearly stated in both the subject, and the first lines of my support request that this wasn&#8217;t the case.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/06/why-ill-never-ebay-again/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Implementing row level access control in CakePHP</title>
		<link>http://aidanlister.com/2009/05/implementing-row-level-access-control-in-cakephp/</link>
		<comments>http://aidanlister.com/2009/05/implementing-row-level-access-control-in-cakephp/#comments</comments>
		<pubDate>Sat, 02 May 2009 10:01:34 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[CakePHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=327</guid>
		<description><![CDATA[The ACL Component in CakePHP is very powerful and can be used to solve a wide variety of access control problems. In this tutorial, we provide a step-by-step guide for implementing row level access control to a model.]]></description>
			<content:encoded><![CDATA[<p>The ACL Component in CakePHP is very powerful and can be used to solve a wide variety of access control problems. In this tutorial, we provide a step-by-step guide for implementing row level access control to a model. We will assume at least a basic understanding of ACL and Auth in CakePHP.</p>
<p>The example I am using is an editing platform. At the top of the tree are <code>Volumes</code>, each of which contain many <code>Papers</code>. <code>Authors</code> have access to papers, while <code>Editors</code> have access to volumes and all the papers there-in. Because we have two trees, it&#8217;s important to design our ACL tree well or it quickly becomes unmanageable.  We&#8217;ll call the top of our tree <code>Papers</code>, but this choice is arbitrary.</p>
<p>We create the following ACO hierarchy: Papers/<code>Volume</code>/<code>Paper</code></p>
<p>This tree structure allows you to provide editors access to a volume, which<br />
automatically gives access to the papers inside. This is the beauty of<br />
ACLs.</p>
<p>To create the ACO tree for an existing dataset, we can develop our own cake shell tool. I will assume you have already created the ARO tree for your users and groups.</p>
<pre class="brush: php;">
class AcltoolShell extends Shell
{
    /**
     * Build a complete ACO tree for two linked models
     *
     * Usage: $ cake acltool aco_models
     */
    function aco_models()
    {
        $this-&gt;out('Starting models sync');
        $Paper  = ClassRegistry::init('Paper');
        $Volume = ClassRegistry::init('Volume');

        // Create the root node
        $root_alias = 'papers';
        $this-&gt;Aco-&gt;create();
        $this-&gt;Aco-&gt;save(array('parent_id' =&gt; null, 'model' =&gt; null, 'alias' =&gt; $root_alias));
        $aco_root = $this-&gt;Aco-&gt;id;

        // Iterate all the volumes
        $volumes = $Volume-&gt;findAll();
        foreach ($volumes as $volume) {
            // Create a node for the volume
            $this-&gt;out(sprintf('Created Aco node: %s/%s', $root_alias, $volume['Volume']['number']));
            $this-&gt;Aco-&gt;create();
            $row = array('parent_id' =&gt; $aco_root, 'foreign_key' =&gt; $volume['Volume']['id'], 'model' =&gt; 'Volume', 'alias' =&gt; $volume['Volume']['number']);
            $this-&gt;Aco-&gt;save($row);
            $parent_id = $this-&gt;Aco-&gt;id;

            // Iterate all the papers
            $papers = $Paper-&gt;find('all', array('conditions' =&gt; array('volume_id' =&gt; $volume['Volume']['id']), 'recursive' =&gt; -1));
            foreach ($papers as $paper) {
                // Create a node for the paper
                $this-&gt;out(sprintf('Created Aco node: %s/%s/%s', $root_alias, $volume['Volume']['number'], $paper['Paper']['slug']));
                $this-&gt;Acl-&gt;Aco-&gt;create();
                $row = array('parent_id' =&gt; $parent_id, 'foreign_key' =&gt; $paper['Paper']['id'], 'model' =&gt; 'Paper', 'alias' =&gt; $paper['Paper']['slug']);
                $this-&gt;Acl-&gt;Aco-&gt;save($row);
            }
        }
    }
}
</pre>
<p>Once our ACO tree is built, we need to give our users permissions. Again we will use a cake shell tool.</p>
<pre class="brush: php;">
    /**
     * Grant user access to two related models
     *
     * Usage: $ cake acltool vol_perms
     */
    function vol_perms()
    {
        // Row level access for volumes
        $this-&gt;out('Creating row-level permissions for volumes');
        $Volume = ClassRegistry::init('Volume');
        $volumes = $Volume-&gt;findAll();
        foreach ($volumes as $vol) {
            $this-&gt;out(sprintf('- Entering volume number %s', $vol['Volume']['number']));
            $Volume-&gt;id = $vol['Volume']['id'];
            foreach ($vol['User'] as $user) {
                $this-&gt;out(sprintf('-- Granting access to %s', $user['name']));
                $User-&gt;id = $user['id'];
                $this-&gt;Acl-&gt;allow($User, $Volume);
            }
        }
    }
}

?&gt;
</pre>
<p>We need to inform our models about our chosen ACO structure. We do this for Papers and Volumes in the same way we would for Users and Groups.</p>
<pre class="brush: php;">
models/volume.php
class Volume extends AppModel
{
    /**
     * Describe our ACO tree
     */
    function parentNode()
    {
        return null;
    }
}
</pre>
<pre class="brush: php;">
models/paper.php
class Paper extends AppModel
{
    /**
     * Describe our ACO tree
     */
    function parentNode()
    {
        if (!$this-&gt;id &amp;&amp; empty($this-&gt;data)) {
            return null;
        }
        $data = $this-&gt;data;
        if (empty($this-&gt;data)) {
            $data = $this-&gt;read();
        }
        if (empty($data['Paper']['volume_id'])) {
            return null;
        } else {
            return array('Volume' =&gt; array('id' =&gt; $data['Paper']['volume_id']));
        }
    }
}
</pre>
<p>Unfortunately Auth does not provide a way to automatically handle model access. We use <code>beforeFilter</code> to implement access control manually in the necessary controllers.</p>
<p>First we check that they&#8217;re not an admin, then we apply our Acl check. This relies on the fact that a) access is blocked to users by the &#8216;controllers&#8217; Aco tree and b) access is granted to editors/volumes to this controller by the &#8216;controllers&#8217; Aco tree. Both of these constraints are enforced by Auth (with $this->Auth->authorize = &#8216;actions&#8217;).</p>
<pre class="brush: php;">
class PapersController extends AppController
{
    /**
     * Row level access checking
     */
    function beforeFilter()
    {
        $methods = array('admin_edit', 'admin_view', 'admin_delete');
        if (isset($this-&gt;params['pass'][0]) &amp;&amp; in_array($this-&gt;params['pass'][0], $methods)) {
            $aco = $this-&gt;Acl-&gt;Aco-&gt;findByModelAndForeignKey('Paper', $this-&gt;params['pass'][0]);
            $aro = $this-&gt;Acl-&gt;Aro-&gt;findByModelAndForeignKey('User', $this-&gt;Auth-&gt;user('id'));
            if (!$this-&gt;Acl-&gt;check($aro['Aro'], $aco['Aco'])) {
                $this-&gt;Session-&gt;setFlash($this-&gt;Auth-&gt;authError);
                $this-&gt;redirect(array('su' =&gt; true, 'controller' =&gt; 'papers', 'action' =&gt; 'index'));
            }
        }
    }
}
</pre>
<p>The only thing remaining is displaying a list of papers and volumes<br />
that a user has access to, instead of all the papers/volumes. This is quite difficult for large trees because it&#8217;s simply not what ACL&#8217;s are designed for. There&#8217;s <a href="https://trac.cakephp.org/ticket/6153">work being done</a> in Cake 1.3.x.x to build an access cache which would solve this problem, but in the mean time:</p>
<pre class="brush: php;">
class PapersController extends AppController
{
    /**
     * Display a list of papers for which the user has access to view
     */
    function admin_index()
    {
        $papers = array();
        $user_id = $this-&gt;Auth-&gt;user('id');
        $nodes = $this-&gt;Acl-&gt;Aro-&gt;findByForeignKeyAndModel($user_id, 'User');
        foreach ($nodes['Aco'] as $node) {
            if ($node['model'] === 'Paper') {
                $papers[] = $node['foreign_key'];
            }

            // Get children from volumes
            if ($node['model'] === 'Volume') {
                $children = $this-&gt;Acl-&gt;Aco-&gt;children($node['id']);
                foreach ($children as $child) {
                    $papers[] = $child['Aco']['foreign_key'];
                }
            }
        }
        $conditions = array('Paper.id' =&gt; $papers);

        if ($this-&gt;Auth-&gt;user('group_id') == 1) {
            $conditions = null;
        }

        $this-&gt;set('papers', $this-&gt;paginate($conditions));
    }
}
</pre>
<p>The same applies to the volumes controller, but a little simpler as<br />
you don&#8217;t need the hierarchy.</p>
<pre class="brush: php;">
class VolumesController extends AppController
{
    /**
     *
     */
    function admin_index()
    {
        $volumes = array();
        $user_id = $this-&gt;Auth-&gt;user('id');
        $nodes = $this-&gt;Acl-&gt;Aro-&gt;findByForeignKeyAndModel($user_id, 'User');
        foreach ($nodes['Aco'] as $node) {
            if ($node['model'] === 'Volume') {
                $volumes[] = $node['foreign_key'];
            }
        }
        $conditions = array('Volume.id' =&gt; $volumes);

        if ($this-&gt;Auth-&gt;user('group_id') == 1) {
            $conditions = null;
        }

        $this-&gt;set('volumes', $this-&gt;paginate($conditions));
    }
}
</pre>
<p>And that&#8217;s it, we&#8217;ve implemented complete row-level access control in CakePHP. Cake&#8217;s AclComponent is incredibly flexible and very powerful, as you can see you can design quite clean and fine-grained permissions quickly and efficiently.</p>
<p>I hope this tutorial helps other developers out there with similar problems, and as always let me know if you have comments or suggestions.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/05/implementing-row-level-access-control-in-cakephp/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Creating a community in five minutes with CakePHP</title>
		<link>http://aidanlister.com/2009/05/creating-a-community-in-five-minutes-with-cakephp/</link>
		<comments>http://aidanlister.com/2009/05/creating-a-community-in-five-minutes-with-cakephp/#comments</comments>
		<pubDate>Fri, 01 May 2009 11:22:44 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=302</guid>
		<description><![CDATA[CakePHP's automatic hashing makes things a lot harder than they need to be, and simple tasks (e.g. a registration page) become annoyingly difficult.

Here, we build a complete community based website in five minutes using Cake best practices, with the following features:
<ul>
    <li>Account registration</li>
    <li>Login and logout</li>
    <li>Account management page</li>
    <li>Password retrieval</li>
</ul>]]></description>
			<content:encoded><![CDATA[<p>CakePHP&#8217;s automatic hashing makes things a lot harder than they need to be, and simple tasks (e.g. a registration page) become annoyingly difficult.</p>
<p>Here, we build a complete community based website in five minutes using Cake best practices, with the following features: account registration, login and logout, account management page, and password retrieval.</p>
<p>We will take the following steps:</p>
<ol>
<li>Bake a new Cake website</li>
<li>Build an account registration page</li>
<li>Create a login page, and optionally add a <code>lastlogin</code> field</li>
<li>Add change password functionality to our account page</li>
<li>Finish with the password retrieval feature</li>
</ol>
<p>At any time you can <a href='http://aidanlister.com/wp-content/uploads/2009/05/building-a-community-in-cakephp.zip'>download the source code</a> used in this tutorial. Let&#8217;s get started!</p>
<h3>Step 1. Bake a new Cake website</h3>
<p>I&#8217;m going to assume that you have a working <a href="http://book.cakephp.org/view/108/The-CakePHP-Console">cake shell installation</a>, so change to your DocumentRoot and execute:<br />
<code>$ cake bake community</code></p>
<p>This will create a <code>community</code> folder with all the necessary files for your new website. If you visit http://example.com/community, you should see the &#8220;all green&#8221; indicating you have a working cake installation.</p>
<p>Next, we&#8217;ll create the <code>users</code> table by executing the following SQL:</p>
<pre class="brush: sql;">
CREATE TABLE `users` (
  `id` int(11) NOT NULL auto_increment,
  `created` datetime,
  `modified` datetime,
  `lastlogin` datetime,
  `name` varchar(100),
  `email` varchar(200),
  `username` varchar(50),
  `password` varchar(42),
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`)
)
</pre>
<p>That&#8217;s it for the setting up &#8230; Let&#8217;s move on.</p>
<h3>Step 2. Build an account registration page</h3>
<p>We&#8217;ll get straight into it by creating the users controller, model and the register view.</p>
<p>Because of CakePHP&#8217;s automatic hashing, we have to do all our validation work on the <code>password_confirm</code> field, as the <code>password</code> field contains only the hash of the users password.</p>
<pre class="brush: php;">
models/user.php:
class User extends AppModel
{
    /**
     * Standard validation behaviour
     */
    var $validate = array(
        'name' =&gt; array(
            'length' =&gt; array(
                'rule'      =&gt; array('minLength', 5),
                'message'   =&gt; 'Please enter your full name (more than 5 chars)',
                'required'  =&gt; true,
            ),
        ),
        'username' =&gt; array(
            'length' =&gt; array(
                'rule'      =&gt; array('minLength', 5),
                'message'   =&gt; 'Must be more than 5 characters',
                'required'  =&gt; true,
            ),
            'alphanum' =&gt; array(
                'rule'      =&gt; 'alphanumeric',
                'message'   =&gt; 'May only contain letters and numbers',
            ),
            'unique' =&gt; array(
                'rule'      =&gt; 'isUnique',
                'message'   =&gt; 'Already taken',
            ),
        ),
        'email' =&gt; array(
            'email' =&gt; array(
                'rule'      =&gt; 'email',
                'message'   =&gt; 'Must be a valid email address',
            ),
            'unique' =&gt; array(
                'rule'      =&gt; 'isUnique',
                'message'   =&gt; 'Already taken',
            ),
        ),
        'password' =&gt; array(
            'empty' =&gt; array(
                'rule'      =&gt; 'notEmpty',
                'message'   =&gt; 'Must not be blank',
                'required'  =&gt; true,
            ),
        ),
        'password_confirm' =&gt; array(
            'compare'    =&gt; array(
                'rule'      =&gt; array('password_match', 'password', true),
                'message'   =&gt; 'The password you entered does not match',
                'required'  =&gt; true,
            ),
            'length' =&gt; array(
                'rule'      =&gt; array('between', 6, 20),
                'message'   =&gt; 'Use between 6 and 20 characters',
            ),
            'empty' =&gt; array(
                'rule'      =&gt; 'notEmpty',
                'message'   =&gt; 'Must not be blank',
            ),
        ),
    );

    /**
     * Ensure two password fields match
     *
     * @param   array   data provided by the controller
     * @param   string  the original (already hashed) password fieldname
     * @param   bool    whether the password field has been hashed,
     *                  only hashed when a username input is present
     */
    function password_match($data, $password_field, $hashed = true)
    {
        $password         = $this-&gt;data[$this-&gt;alias][$password_field];
        $keys             = array_keys($data);
        $password_confirm = $hashed ?
              Security::hash($data[$keys[0]], null, true) :
              $data[$keys[0]];
        return $password === $password_confirm;
    }
}
</pre>
<p>Next we create our view at <code>views/users/register.ctp</code>. Because we&#8217;ve done all our validation on the <code>password_confirm</code> field, the error messages (password too short, etc.) will appear under <code>password_confirm</code>. This isn&#8217;t ideal from the user interface perspective; luckily we can just swap the input labels and noone is the wiser.</p>
<pre class="brush: php;">
views/users/register.ctp:
&lt;?php
echo $form-&gt;create(array('action' =&gt; 'register'));
echo $form-&gt;input('name');
echo $form-&gt;input('email');
echo $form-&gt;input('username');
echo $form-&gt;input('password_confirm', array('label' =&gt; 'Password', 'type' =&gt; 'password'));
echo $form-&gt;input('password', array('label' =&gt; 'Password Confirm'));
echo $form-&gt;end('Register');
?&gt;
</pre>
<p>Finally we create our users controller at <code>controllers/users_controller.php</code>. We define a <code>beforeFilter</code> method to make our registration page publicly accessible, our <code>register</code> action, and a blank <code>login</code> action.</p>
<pre class="brush: php;">
controllers/users_controller.php:
class UsersController extends AppController
{
    var $components = array('Auth');

    /**
     * Runs automatically before the controller action is called
     */
    function beforeFilter()
    {
        $this-&gt;Auth-&gt;allow('register');
        parent::beforeFilter();
    }

    /**
     * Registration page for new users
     */
    function register()
    {
        if (!empty($this-&gt;data)) {
            $this-&gt;User-&gt;create();
            if ($this-&gt;User-&gt;save($this-&gt;data)) {
                $this-&gt;Session-&gt;setFlash(__('Your account has been created', true));
                $this-&gt;redirect('/');
            } else {
                $this-&gt;Session-&gt;setFlash(__('Your account could not be created. Please, try again.', true));
            }
        }
    }

    /**
     * Account login page
     */
    function login()
    {

    }

    /**
     * Log a user out
     */
    function logout()
    {
       return $this-&gt;redirect($this-&gt;Auth-&gt;logout());
    }
}
</pre>
<p>The next issue with Auth arises when the user has entered a valid password, but has failed to validate the entire form &#8211; for example, if they picked a username that is in use and must be re-entered. As the password has already been replaced by the hashed password, the form will be redisplayed with the hashed password in the <code>password</code> input. If the user attempts to resubmit the form without retyping their password, form validation will fail on the <code>password_confirm</code> field. Even worse, if you&#8217;re not using a <code>password_confirm</code> field, the user will simply be unable to log in to their newly created account.</p>
<p>Thus, we need some hackery to make sure encrypted passwords are not sent back to the user. We can do this in <code>AppController</code>, by writing a <code>beforeRender</code> method.</p>
<pre class="brush: php;">
app_controller.php:
class AppController extends Controller
{
    /**
     * Before Render
     */
    function beforeRender()
    {
        unset($this-&gt;data['User']['password']);
        unset($this-&gt;data['User']['password_confirm']);
    }
}
</pre>
<p>And that&#8217;s it, we have a complete working registration page. We can view this completed page by visiting <code>/users/register</code> and create our first account.</p>
<h3>Step 3. Create a login page</h3>
<p>Next up is our login page. We need to modify the default layout which lives at <code>views/layouts/default.ctp</code>. We replace the <code>content</code> div which allows <code>Auth</code> to display messages to the user.</p>
<pre class="brush: php;">
views/layouts/default.ctp:
&lt;div id=&quot;content&quot;&gt;
    &lt;?php $session-&gt;flash(); ?&gt;
    &lt;?php $session-&gt;flash('auth'); ?&gt;
    &lt;?php echo $content_for_layout; ?&gt;
&lt;/div&gt;
</pre>
<p>Next we create a very simple login page:</p>
<pre class="brush: php;">
views/users/login.ctp:
&lt;?php
echo $form-&gt;create(array('action' =&gt; 'login'));
echo $form-&gt;input('username');
echo $form-&gt;input('password');
echo $form-&gt;end('Login');
?&gt;
</pre>
<p>And that&#8217;s it &#8211; it doesn&#8217;t get much simpler. We can view our new login page at <code>/users/login</code>.</p>
<p>If we wanted to track the time the user logged in last, we can do some extra work:</p>
<p>Adding to the <code>AppController</code>, we set <code>Auth->autoRedirect</code>:</p>
<pre class="brush: php;">
app_controller.php:
class AppController extends Controller
{
    ...

    /**
     * Before Filter
     */
    function beforeFilter()
    {
        $this-&gt;Auth-&gt;autoRedirect = false;
    }
}
</pre>
<p>Then we modify our login controller to do the necessary work:</p>
<pre class="brush: php;">
controllers/users_controller.php:
class UsersController extends AppController
{
    ...

    /**
     * Ran directly after the Auth component has executed
     */
    function login()
    {
        // Check for a successful login
        if (!empty($this-&gt;data) &amp;&amp; $id = $this-&gt;Auth-&gt;user('id')) {
            // Set the lastlogin time
            $this-&gt;User-&gt;id = $id;
            $this-&gt;User-&gt;saveField('lastlogin', date('Y-m-d H:i:s'));

            // Redirect the user
            $url = array('controller' =&gt; 'users', 'action' =&gt; 'account');
            if ($this-&gt;Session-&gt;check('Auth.redirect')) {
                $url = $this-&gt;Session-&gt;read('Auth.redirect');
            }
            $this-&gt;redirect($url);
        }
    }
}
</pre>
<p>Not a lot of additional work, and it will come in handy if you need to prune inactive accounts. Note we can&#8217;t this part of the step yet, as it&#8217;s expecting an <code>/users/account</code> page to exist (which we create next).</p>
<h3>Step 4. Add change password functionality</h3>
<p>Cake&#8217;s form validation functionality is somewhat limited, in that it only allows you to define validation rules at the model level. Often, you will want to validate data at a per-form level instead. To achieve this, we can dynamically modify the validation rules before form validation takes place. We dive into the user model again:</p>
<p>Note: If we simply added the <code>password_old</code> rule to our validation behaviour, other forms that didn&#8217;t include <code>password_old</code> would not validate (because we have set <code>required = true</code>). If we removed <code>required = true</code>, a POST request can easily be forged bypassing this security check.</p>
<pre class="brush: php;">
models/user.php:
class User extends AppModel
{
    /**
     * Extra form dependent validation rules
     */
    var $validateChangePassword = array(
        '_import' =&gt; array('password', 'password_confirm'),
        'password_old' =&gt; array(
            'correct' =&gt; array(
                'rule'      =&gt; 'password_old',
                'message'   =&gt; 'Does not match',
                'required'  =&gt; true,
            ),
            'empty' =&gt; array(
                'rule'      =&gt; 'notEmpty',
                'message'   =&gt; 'Must not be blank',
            ),
        ),
    );

    /**
     * Dynamically adjust our validation behaviour
     *
     * Look for an _import key in new ruleset, and import
     * those rules from the base validation ruleset
     *
     * @param   string  array key of the validation ruleset to use
     */
    function useValidationRules($key)
    {
        $variable = 'validate' . $key;
        $rules = $this-&gt;$variable;

        if (isset($rules['_import'])) {
            foreach ($rules['_import'] as $key) {
                $rules[$key] = $this-&gt;validate[$key];
            }
            unset($rules['_import']);
        }

        $this-&gt;validate = $rules;
    }

    /**
     * Ensure password matches the user session
     *
     * @param   array   data provided by the controller
     */
    function password_old($data)
    {
        $password = $this-&gt;field('password',
            array('User.id' =&gt; $this-&gt;id));
        return $password ===
            Security::hash($data['password_old'], null, true);
    }

    ...
}
</pre>
<p>Our change password logic now becomes very simple. We validate, then update the password and redirect the user.</p>
<p>Note: In this case Cake decides not to automatically hash the users password field, so we must perform the hashing manually. This happens because Cake will only hash the password if both the <code>username</code> and <code>password</code> key are set.</p>
<pre class="brush: php;">
controllers/users_controller.php:
class UsersController extends AppController
{
    /**
     * Account details page (change password)
     */
    function account()
    {
        // Set User's ID in model which is needed for validation
        $this-&gt;User-&gt;id = $this-&gt;Auth-&gt;user('id');

        // Load the user (avoid populating $this-&gt;data)
        $current_user = $this-&gt;User-&gt;findById($this-&gt;User-&gt;id);
        $this-&gt;set('current_user', $current_user);

        $this-&gt;User-&gt;useValidationRules('ChangePassword');
        $this-&gt;User-&gt;validate['password_confirm']['compare']['rule'] =
            array('password_match', 'password', false);

        $this-&gt;User-&gt;set($this-&gt;data);
        if (!empty($this-&gt;data) &amp;&amp; $this-&gt;User-&gt;validates()) {
            $password = $this-&gt;Auth-&gt;password($this-&gt;data['User']['password']);
            $this-&gt;User-&gt;saveField('password', $password);

            $this-&gt;Session-&gt;setFlash('Your password has been updated');
            $this-&gt;redirect(array('action' =&gt; 'account'));
        }
    }

    ...
}
</pre>
<p>We create a template for the account page as follows:</p>
<pre class="brush: php;">
views/users/account.ctp:
&lt;h2&gt;Account Page&lt;/h2&gt;
&lt;h3&gt;Change your password&lt;/h3&gt;
&lt;p&gt;You are &lt;?php echo $current_user['User']['name']; ?&gt; who last logged in &lt;?php echo $current_user['User']['lastlogin']; ?&gt;.&lt;/p&gt;

&lt;?php
echo $form-&gt;create(array('action' =&gt; 'account'));
echo $form-&gt;input('password_old',     array('label' =&gt; 'Old password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $form-&gt;input('password_confirm', array('label' =&gt; 'New password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $form-&gt;input('password',         array('label' =&gt; 'Re-enter new password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $form-&gt;end('Update Password');
?&gt;
</pre>
<p>The user is now able to change their password. We can test this by logging in at <code>/users/login</code> which will take us to our new account page.</p>
<h3>Step 5. Create a password retrieval page</h3>
<p>In our final step, we allow the user to retrieve a forgotten password. We do this by emailing the user a token. The user clicks back to our website with the token, and gets emailed a replacement password.</p>
<p>We create a <code>models/token.php</code> to handle the token generation, information storage and retrieval.</p>
<pre class="brush: php;">
models/token.php:
&lt;?php
class Token extends AppModel
{
    /**
     * Create a new ticket by providing the data to be stored in the ticket.
     */
    function generate($data = null)
    {
        $data = array(
          'token' =&gt; substr(md5(uniqid(rand(), 1)), 0, 10),
          'data'  =&gt; serialize($data),
        );

        if ($this-&gt;save($data)) {
            return $data['token'];
        }

        return false;
    }

    /**
     * Return the value stored or false if the ticket can not be found.
     */
    function get($token)
    {
        $this-&gt;garbage();
        $token = $this-&gt;findByToken($token);
        if ($token) {
          $this-&gt;del($token['Token']['id']);
          return unserialize($token['Token']['data']);
        }

        return false;
    }

    /**
     * Remove old tickets
     */
    function garbage()
    {
        return $this-&gt;deleteAll(array('created &lt; INTERVAL -1 DAY + NOW()'));
    }
}
</pre>
<p>We&#8217;ll also create the necessary database table.</p>
<pre class="brush: sql;">
CREATE TABLE `tokens` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `created` datetime default NULL,
  `modified` datetime default NULL,
  `token` varchar(32) default NULL,
  `data` text,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `token` (`token`)
)
</pre>
<p>We next update the users controller to make the <code>recover</code> and <code>verify</code> actions public, then we define our <code>recover</code> and <code>verify</code> actions:</p>
<pre class="brush: php;">
controllers/users_controller.php:
&lt;?php
class UsersController extends AppController
{
    var $components = array('Auth', 'Email');

    ...

    /**
     * Runs automatically before the controller action is called
     */
    function beforeFilter()
    {
        $this-&gt;Auth-&gt;allow('register', 'recover', 'verify');
        parent::beforeFilter();
    }

    /**
     * Allows the user to email themselves a password redemption token
     */
    function recover()
    {
        if ($this-&gt;Auth-&gt;user()) {
            $this-&gt;redirect(array('controller' =&gt; 'users', 'action' =&gt; 'account'));
        }

        if (!empty($this-&gt;data['User']['email'])) {
            $Token = ClassRegistry::init('Token');
            $user = $this-&gt;User-&gt;findByEmail($this-&gt;data['User']['email']);

            if ($user === false) {
                $this-&gt;Session-&gt;setFlash('No matching user found');
                return false;
            }

            $token = $Token-&gt;generate(array('User' =&gt; $user['User']));
            $this-&gt;Session-&gt;setFlash('An email has been sent to your account, please follow the instructions in this email.');
            $this-&gt;Email-&gt;to = $user['User']['email'];
            $this-&gt;Email-&gt;subject = 'Password Recovery';
            $this-&gt;Email-&gt;from = 'Support &lt;support@example.com&gt;';
            $this-&gt;Email-&gt;template = 'recover';
            $this-&gt;set('user', $user);
            $this-&gt;set('token', $token);
            $this-&gt;Email-&gt;send();
        }
    }

    /**
     * Accepts a valid token and resets the users password
     */
    function verify($token = null)
    {
        if ($this-&gt;Auth-&gt;user()) {
            $this-&gt;redirect(array('controller' =&gt; 'users', 'action' =&gt; 'account'));
        }

        $Token = ClassRegistry::init('Token');
        if ($data = $Token-&gt;get($token)) {
            // Update the users password
            $password = $this-&gt;User-&gt;generatePassword();
            $this-&gt;User-&gt;id = $data['User']['id'];
            $this-&gt;User-&gt;saveField('password', $this-&gt;Auth-&gt;password($password));
            $this-&gt;set('success', true);

            // Send email with new password
            $this-&gt;Email-&gt;to = $data['User']['email'];
            $this-&gt;Email-&gt;subject = 'Password Changed';
            $this-&gt;Email-&gt;from = 'Support &lt;support@example.com&gt;';
            $this-&gt;Email-&gt;template = 'verify';
            $this-&gt;set('user', $data);
            $this-&gt;set('password', $password);
            $this-&gt;Email-&gt;send();
        }
    }
}
</pre>
<p>Because we are generating new passwords for users, we need a function to do this. Alternative approaches, like <a href="http://pear.php.net/package/Text_Password">PEAR&#8217;s Text/Password</a> can be substituted if you like.</p>
<pre class="brush: php;">
models/user.php:
class User extends AppModel
{
    ....

    /**
     * Generate a random pronounceable password
     */
    function generatePassword($length = 10) {
        // Seed
        srand((double) microtime()*1000000);

        $vowels = array('a', 'e', 'i', 'o', 'u');
        $cons = array('b', 'c', 'd', 'g', 'h', 'j', 'k', 'l', 'm', 'n',
            'p', 'r', 's', 't', 'u', 'v', 'w', 'tr',
            'cr', 'br', 'fr', 'th', 'dr', 'ch', 'ph',
            'wr', 'st', 'sp', 'sw', 'pr', 'sl', 'cl');

        $num_vowels = count($vowels);
        $num_cons = count($cons);

        $password = '';
        for ($i = 0; $i &lt; $length; $i++){
            $password .= $cons[rand(0, $num_cons - 1)] . $vowels[rand(0, $num_vowels - 1)];
        }

        return substr($password, 0, $length);
    }
</pre>
<p>We define two email templates for recovery and verification respectively:</p>
<pre class="brush: php;">
views/elements/email/text/recover.ctp:
Dear &lt;?php echo $user['User']['name']; ?&gt;,

Someone is attempting to reset your password.

Your username for this account is: &lt;?php echo $user['User']['username']; ?&gt;

If you wish to continue, you may reset your password by
following this link:

    &lt;?php echo Router::url(array('controller' =&gt; 'users', 'action' =&gt; 'verify', $token), true); ?&gt;

If you did not initiate this action, please contact
support. You can log in to change your password
at this address:

    &lt;?php echo Router::url(array('controller' =&gt; 'users', 'action' =&gt; 'login'), true); ?&gt;

Thanks,
Support
</pre>
<pre class="brush: php;">
views/elements/email/text/verify.ctp:
Dear &lt;?php echo $user['User']['name']; ?&gt;,

Your password has been reset, please use the following
details to log into our site.

    Username: &lt;?php echo $user['User']['username']; ?&gt;

    Password: &lt;?php echo $password; ?&gt;

Please change your password to something more memorable.
You can log in to change your password at this address:

    &lt;?php echo Router::url(array('controller' =&gt; 'users', 'action' =&gt; 'login'), true); ?&gt;

Thanks,
Support
</pre>
<p>And finally we define our two templates for the recovery and verification views:</p>
<pre class="brush: php;">
views/users/recover.ctp:
&lt;h2&gt;Recover Password&lt;/h2&gt;

&lt;?php
echo $form-&gt;create('User', array('action' =&gt; 'recover'));
echo $form-&gt;input('email');
echo $form-&gt;end('Recover');
?&gt;
</pre>
<pre class="brush: php;">
views/users/verify.ctp:
&lt;h2&gt;Recover Password&lt;/h2&gt;

&lt;?php if (isset($success)): ?&gt;
    &lt;div class=&quot;message&quot;&gt;Access verified. Your new password has been emailed to you.&lt;/div&gt;
    &lt;p&gt;A new password has been generated for your account and mailed to you. After you've logged in, you should change your password to something memorable via the account information page.&lt;/p&gt;
&lt;?php else: ?&gt;
    &lt;div class=&quot;warning&quot;&gt;Invalid token. This page has expired, or the link was not copied from your email client correctly.&lt;/div&gt;
    &lt;p&gt;Make sure you have copied the entire link correctly, pasting it together if the link was split over two lines. If you're copying the link correctly and still can't get access, please contact us.&lt;/p&gt;
&lt;?php endif; ?&gt;
</pre>
<p>And that&#8217;s it &#8230; we&#8217;ve developed a complete community orientated website for CakePHP in less time it takes than to make a cup of tea (if you&#8217;re very slow at making tea).</p>
<p>On a side note: you may have noticed that CakePHP&#8217;s automatic hashing makes things a lot harder than they should be. In my opinion, automatic hashing directly goes against Cake&#8217;s mantra of &#8220;If it&#8217;s easy to do in Cake, then it doesn&#8217;t belong in core&#8221;, and I hope the core development team rethink in the idea in Cake 1.3.x.x. Although forcing developers to automatically hash their users password is a nice idea, the current implementation a major stumbling block for new bakers. We shouldn&#8217;t need a <a href="http://book.cakephp.org/view/565/Troubleshooting-Auth-Problems ">section in the manual</a> about common problems the user will encounter trying to do simple tasks. Instead, we should fix the cause of the common problems. It is, however, a testament to Cake&#8217;s flexibility that we are able to work around most of these issues quite elegantly. It&#8217;s also worth noting that there are several ways to <a href="http://teknoid.wordpress.com/2008/10/08/demystifying-auth-features-in-cakephp-12/">override this behavior</a>.</p>
<p>I hope you&#8217;ve enjoyed this tutorial, please feel free to leave comments, suggestions or improvements.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/05/creating-a-community-in-five-minutes-with-cakephp/feed/</wfw:commentRss>
		<slash:comments>15</slash:comments>
		</item>
		<item>
		<title>Repository content moved to the blog</title>
		<link>http://aidanlister.com/2009/04/repository-content-moved-to-the-blog/</link>
		<comments>http://aidanlister.com/2009/04/repository-content-moved-to-the-blog/#comments</comments>
		<pubDate>Wed, 08 Apr 2009 14:12:41 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Housekeeping]]></category>

		<guid isPermaLink="false">http://aidanlister.com/2009/04/repository-content-moved-to-the-blog/</guid>
		<description><![CDATA[I have just finished moving all of my repository content to separate posts in WordPress. I also decided to get a little tricky and manually imported all of the old comments into WordPress too.]]></description>
			<content:encoded><![CDATA[<p>I have just finished moving all of my repository content to separate posts in WordPress. I also decided to get a little tricky and manually imported all of the old comments into WordPress too.</p>
<p>Moving the comments was fairly complicated as I didn&#8217;t record a name or a website, which happens to be the two required fields for WordPress. Using some SQL magic I split the email into name@url, which has worked reasonably well.</p>
<p>I&#8217;ve still got lots and lots of cleaning up to do, so excuse the mess. Once it&#8217;s all done, I will redirect all of the incoming repository links to their relevant blog post.</p>
<p>Also I should take this opportunity to heap praise on the WordPress team &#8211; guys, you have built an amazing piece of software, it&#8217;s an absolute pleasure to use (as long as you don&#8217;t look inside the codebase).</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/04/repository-content-moved-to-the-blog/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Better Error Handling with CakePHP</title>
		<link>http://aidanlister.com/2009/04/better-error-handling-with-cakephp/</link>
		<comments>http://aidanlister.com/2009/04/better-error-handling-with-cakephp/#comments</comments>
		<pubDate>Mon, 06 Apr 2009 07:10:49 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[CakePHP]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=5</guid>
		<description><![CDATA[CakePHP is a wonderful framework, but it really drops the ball when it comes to practical error management. In production environments (DEBUG = 0), only 404 or 500 errors are displayed to the user, and no errors are written to the log files.

To solve these two problems we override php's error handler to enable production error logging, and cake's error handler to allow forward facing error pages.]]></description>
			<content:encoded><![CDATA[<p>CakePHP is a wonderful framework, but it really drops the ball when it comes to practical error management. In production environments (DEBUG = 0), only 404 or 500 errors are displayed to the user, and no errors are written to the log files.</p>
<p>This means runtime errors (e.g. an unexpected divide-by-zero) are not logged, and service errors (e.g. when a paypal checkout fails) are not explained to the user.</p>
<p>To solve these two problems we override php&#8217;s error handler to enable production error logging, and cake&#8217;s error handler to allow forward facing error pages.</p>
<p>Part 1: To enable production error logging, we can override cake&#8217;s production error handling code by conditionally setting DISABLE_DEFAULT_ERROR_HANDLING. This code only kicks in when cake is in production and logs all notices and warnings in the cake log file.</p>
<pre class="brush: php;">
app/config/bootstrap.php
&lt;?php
/**
 * Handle logging errors in production mode
 */
if (Configure::read() === 0) {
    // Disable the default handling and include logger
    define('DISABLE_DEFAULT_ERROR_HANDLING', 1);
    uses('cake_log');
    error_reporting(E_ALL);

    /**
     * A function to directly log errors
     *
     * @param $errno The error number
     * @param $errstr The error description
     * @param $errfile The file where the error occured
     * @param $errline The line of the file where the error occured
     * @return bool Success
     */
    function productionError($errno, $errstr, $errfile, $errline) {
        // Ignore E_STRICT and suppressed errors
        if ($errno === 2048 || error_reporting() === 0) {
            return;
        }

        // What type of error
        $level = LOG_DEBUG;
        switch ($errno) {
            case E_PARSE:
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
                $error = 'Fatal Error';
                $level = LOG_ERROR;
            break;
            case E_WARNING:
            case E_USER_WARNING:
            case E_COMPILE_WARNING:
            case E_RECOVERABLE_ERROR:
                $error = 'Warning';
                $level = LOG_WARNING;
            break;
            case E_NOTICE:
            case E_USER_NOTICE:
                $error = 'Notice';
                $level = LOG_NOTICE;
            break;
            default:
                return false;
            break;
        }

        // Log
        CakeLog::write($level, sprintf('%s (%d): %s in [%s, line %d]',
            $error, $errno, $errstr, $errfile, $errline));

        // Die if fatal
        if ($level === LOG_ERROR) {
            die();
        }

        return true;
    }

    // Use the above handling
    set_error_handler('productionError');
}
?&gt;
</pre>
<p>Part 2: To enable forward facing user error pages, we can define an AppError class. This code allows you to set up custom error handlers, and decide how you want to handle each &#8211; be it publicly displayed, logged or mailed to the site managers.</p>
<pre class="brush: php;">
app/app_error.php:
&amp;lt;?php
class AppError extends ErrorHandler
{
    /**
     * List of errors which are displayed, even in production mode
     */
    var $displayErrors = array('logic', 'paypal', 'payflow');

    /**
     * List of errors which, when occur, information is emailed to
     * the site administrator
     */
    var $emailErrors   = array();

    /**
     * List of errors which, when occur, will result in log entries
     */
    var $logErrors     = array('logic', 'system');

    /**
     * A string containing people to be notified in the event of an
     * error. The string must be acceptable input for the mail function
     */
    var $siteManager   = 'aidan@php.net';

    /**
     * Override the default cakeError error handling behaviour
     *
     * By setting the debug switch, the page will be publically visisble
     * Alternatively, or in conjunction with, we can log and notify the
     * site owner
     */
    function __construct($method, $messages)
    {
        if (in_array($method, $this-&gt;displayErrors)) {
            Configure::write('debug', 1);
        }

        if (in_array($method, $this-&gt;logErrors)) {
            $bt = debug_backtrace();
            $errfile = $bt[1]['file'];
            $errline = $bt[1]['line'];
            $parameters = str_replace(&quot;\n&quot;, '',
                print_r($messages, true));
            $error = sprintf('%s: Called with parameters (%s) in [%s, line %d]',
                $method, $parameters, $errfile, $errline);
            CakeLog::write(LOG_ERROR, $error);
        }

        if (in_array($method, $this-&gt;emailErrors)) {
            $this-&gt;_notify($method, $messages);
        }

        // Handle as normal
        parent::__construct($method, $messages);
    }

    /**
     * Send out email when an error occurs
     */
    function _notify($method, $messages)
    {
        $subject  = '[CakePHP] Site Error';
        $headers  = 'From: cakephp@' . $_SERVER['HTTP_HOST'] . &quot;\r\n&quot;;
        $headers .= 'Reply-To: cakephp@' . $_SERVER['HTTP_HOST'];
        $message  = 'An occurred error at ' . $_SERVER['HTTP_HOST'] . &quot;.\n\n&quot;;
        foreach ($messages as $key =&gt; $value) {
            $message .= sprintf(&quot;    %s: %s\n&quot;, $key, $value);
        }

        mail($this-&gt;siteManager, $subject, $message, $headers);
    }

    /**
     * A fatal logic error occured
     *
     * @param $params Expects keys [message]
     */
    function logic($params)
    {
        $this-&gt;controller-&gt;set('message', $params['message']);
        $this-&gt;_outputMessage('logic');
    }
}
?&amp;gt;
</pre>
<p>Both of these solutions are independent of each other, and provide a way to practically manage error handling in your CakePHP deployment.</p>
<p>I have also opened a <a href="https://trac.cakephp.org/ticket/6165">RFC</a> to get this behavior included in cake&#8217;s core.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/04/better-error-handling-with-cakephp/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>It begins &#8230;</title>
		<link>http://aidanlister.com/2009/04/it-begins/</link>
		<comments>http://aidanlister.com/2009/04/it-begins/#comments</comments>
		<pubDate>Sat, 04 Apr 2009 19:34:25 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Housekeeping]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=1</guid>
		<description><![CDATA[I'm told I'm not a very funny person. Nor is my writing of particular quality. However, after camping this domain name for the last eight years I've finally decided to do something useful (perhaps debatable) with the space and start my own blog.]]></description>
			<content:encoded><![CDATA[<p>I&#8217;m told I&#8217;m not a very funny person. Nor is my writing of particular quality. However, after camping this domain name for the last eight years I&#8217;ve finally decided to do something useful (perhaps debatable) with the space and start my own blog. My repository still lives at the <a title="usual address" href="http://aidanlister.com/repos/">usual address</a>, but I will be upgrading it shortly. Happy reading!</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2009/04/it-begins/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>A very simple unassuming ICS parser</title>
		<link>http://aidanlister.com/2004/04/a-very-simple-unassuming-ics-parser/</link>
		<comments>http://aidanlister.com/2004/04/a-very-simple-unassuming-ics-parser/#comments</comments>
		<pubDate>Mon, 05 Apr 2004 19:34:25 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Code Repository]]></category>
		<category><![CDATA[Classes]]></category>
		<category><![CDATA[PHP]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=138</guid>
		<description><![CDATA[<code>ICSParser</code> is a very simple unassuming ICS parser which can be used to quickly retrieve information from Google Calendar or Apple's iCal.]]></description>
			<content:encoded><![CDATA[<pre class="brush: php;">
/**
 * A very simple unassuming ICS parser.
 *
 * Reads a ICS file and splits the information into useable arrays.
 * Does not try and interpret the specification, just provides a raw data interface.
 *
 * @author      Aidan Lister &lt;aidan@php.net&gt;
 * @version     1.0.0
 * @link        http://aidanlister.com/repos/v/iCalReader.php
 */
class ICSReader
{
    /**
     * Element array of data source
     *
     * @var     array
     * @access  private
     */
    var $_source = '';

    /**
     * Line number to be parsed next
     *
     * @var     int
     * @access  private
     */
    var $_linenum = 0;

    /**
     * Parsed data
     *
     * @var     array
     * @access  private
     */
    var $_data = array();

    /**
     * Constructor
     *
     * @param   string  Path to file to be parsed
     * @access  public
     */
    function iCalReader($source)
    {
        $source = file_get_contents($source);
        $source = preg_split('#\n(?!s)#', $source);
        $this-&gt;_source = array_map('trim', $source);

        $this-&gt;_data['meta'] = $this-&gt;_parseMeta();
        $this-&gt;_data['events'] = $this-&gt;_parseEvents();
    }

    /**
     * Return parsed meta information
     *
     * @access  public
     * @return  array   Parsed meta information
     */
    function getMeta()
    {
        return $this-&gt;_data['meta'];
    }

    /**
     * Return parsed event information
     *
     * @access  public
     * @return  array   Parsed event information
     */
    function getEvents()
    {
        return $this-&gt;_data['events'];
    }

    /**
     * Parse meta information
     *
     * @access  private
     */
    function _parseMeta()
    {
        // Init
        $meta = array();
        $i = 0;

        // Sanity check
        if ($this-&gt;_source[$i] !== 'BEGIN:VCALENDAR') {
            return false;
        }

        // Iterate source
        $i++;
        while (isset($this-&gt;_source[$i])) {
            $line = $this-&gt;_source[$i];
            list($key, $value) = explode(':', $line, 2);

            // Meta information
            $meta[$key] = $value;

            $i++;

            // Check next line for EOM
            if ($this-&gt;_source[$i] === 'BEGIN:VEVENT') {
                break;
            }
        }

        $this-&gt;_linenum = $i;
        return $meta;
    }

    /**
     * Parse events information
     *
     * @access  private
     */
    function _parseEvents()
    {
        // Init
        $events = array();
        $i = $this-&gt;_linenum;
        $j = 0;

        // Iterate source
        while (isset($this-&gt;_source[$i])) {
            $line = $this-&gt;_source[$i];
            list($key, $value) = explode(':', $line, 2);

            // Event information
            $events[$j++] = $this-&gt;_parseEvent();
            $i = $this-&gt;_linenum;

            // Check next line for EOC
            if ($this-&gt;_source[$i] === 'END:VCALENDAR') {
                break;
            }
        }

        return $events;
    }

    /**
     * Parse a single events information
     *
     * @access  private
     */
    function _parseEvent()
    {
        // Init
        $event = array();
        $i = $this-&gt;_linenum;

        // Sanity check
        if ($this-&gt;_source[$i] !== 'BEGIN:VEVENT') {
            return false;
        }

        $i++;
        while (isset($this-&gt;_source[$i])) {
            $line = $this-&gt;_source[$i];
            list($key, $value) = explode(':', $line, 2);

            // Event information
            if ($key === 'DESCRIPTION') {
                $value = str_replace(
                            array(&quot;\r\n&quot;, '\n', ',', ';'),
                            array('', &quot;\n&quot;, ',', ';'),
                            $value);
            }
            $event[$key] = $value;
            $i++;

            // Check next line for EOE
            if ($this-&gt;_source[$i] === 'END:VEVENT') {
                $this-&gt;_linenum = $i + 1;
                return $event;
            }
        }

        // Parsing error (End tag not found)
        return false;
    }
}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2004/04/a-very-simple-unassuming-ics-parser/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
	</channel>
</rss>
