<?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>Thu, 08 Mar 2012 06:12:01 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3</generator>
		<item>
		<title>Getting up and running with virtualenv on Mac OSX Lion.</title>
		<link>http://aidanlister.com/2011/11/getting-up-and-running-with-virtualenv-on-mac-osx-lion/</link>
		<comments>http://aidanlister.com/2011/11/getting-up-and-running-with-virtualenv-on-mac-osx-lion/#comments</comments>
		<pubDate>Wed, 09 Nov 2011 19:57:31 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Housekeeping]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=519</guid>
		<description><![CDATA[I recently purchased a new Macbook Air and had forgotten all of the various steps to get virtualenv up and running. Using the native Python packaged with OSX resulted in Could not call install_name_tool &#8212; you must have Apple&#8217;s development tools installed which I found confusing given that, you know, I have Xcode installed. Resorting [...]]]></description>
			<content:encoded><![CDATA[<p>I recently purchased a new Macbook Air and had forgotten all of the various steps to get <em>virtualenv</em> up and running. Using the native Python packaged with OSX resulted in <em>Could not call install_name_tool &#8212; you must have Apple&#8217;s development tools installed</em> which I found confusing given that, you know, I have Xcode installed.</p>
<p>Resorting to my old friend <a href="http://macports.com">MacPorts</a>, it took me a few tries and plenty of googling to get up and running. To save you some time should you be in a similar position, here are the commands you will need;</p>
<p>The steps required install Python, easy_install, pip and virtualenv in Mac OSX Lion:<br />
<code>$ sudo port install python27<br />
$ sudo port select --set python python27<br />
$ sudo port install py27-distribute<br />
$ PYDIR=`which python`;<br />
$ echo "export PATH=`dirname $PYDIR`:\$PATH" >> ~/.profile<br />
$ source ~/.profile<br />
$ sudo easy_install -U pip<br />
$ sudo pip install -U virtualenv</code></p>
<p>There&#8217;s a bit of magic in there to add pip and easy_install into your path, I found this solution to be nicer than symlinking them to your /usr/bin folder. Once this is done, you are ready to rock:</p>
<p><code>$ virtualenv --no-site-packages --distribute hooray</code></p>
<p>Another reader has pointed out this alternative:<br />
<code>git clone https://github.com/gregglind/virtualenv.git<br />
cd virtualenv<br />
git checkout feature/install_name_tool<br />
sudo python setup.py install</code></p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2011/11/getting-up-and-running-with-virtualenv-on-mac-osx-lion/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Drupal meets geolocation with Quova</title>
		<link>http://aidanlister.com/2011/09/drupal-meets-geolocation-with-quova/</link>
		<comments>http://aidanlister.com/2011/09/drupal-meets-geolocation-with-quova/#comments</comments>
		<pubDate>Wed, 07 Sep 2011 23:54:12 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=507</guid>
		<description><![CDATA[<a href="http://developer.quova.com">Quova</a> - the worlds largest geolocation provider - has sponsored the development of a suite of <a href="http://drupal.org">Drupal</a> modules to bring geolocation to Drupal.

With their <a href="http://drupal.org/project/quova">suite of modules</a> site developers can now easily create locale aware, geo-targeted compelling experiences in Drupal.]]></description>
			<content:encoded><![CDATA[<p><a href="http://developer.quova.com">Quova</a> &#8211; the worlds largest geolocation provider &#8211; has sponsored the development of a suite of <a href="http://drupal.org">Drupal</a> modules to bring geolocation to Drupal.</p>
<p>With their <a href="http://drupal.org/project/quova">suite of modules</a> site developers can now easily create locale aware, geo-targeted compelling experiences in Drupal.</p>
<p>The suite of modules for both Drupal 6 and 7 includes:</p>
<ul>
<li><em>Quova Core</em> &#8211; Provides an API to interact with the Quova IP Info service, and helpers for location based customizable content.</li>
<li><em>Quova Context</em> &#8211; Built in context support allows you to use country and line speed as a context argument. This allows you to easily display localised blocks, display country dependent blocks, or swap out bandwidth sensitive content.</li>
<li><em>Quova Locale</em> &#8211; Provides automatic language switching based on the users country and region.</li>
<li><em>Quova Weather</em> &#8211; Integrates with the Weather module to provide a block showing weather at the users current location.</li>
<li><em>Rules</em> &#8211; Built in rules support allows you to use country as a rules condition allowing location based site customizations or geofencing. Combined with <a href="http://drupal.org/project/commerce">Drupal Commerce</a>, this allows you to enable and disable payment processors depending on the users location.</li>
<li><em>Views / Location</em> &#8211; Built in helpers to allow you to interface easily with Views and the Location module. For example this can be used to automatically display branches closest to a potential customer.</li>
<li><em>Ad</em> &#8211; Allows geotargeting of advertising using Quova and the Ad module.</li>
</ul>
<p>For more information, visit the <a href="http://drupal.org/project/quova">project page</a> or check out the <a href="http://drupal.org/node/1263112">documentation</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2011/09/drupal-meets-geolocation-with-quova/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Interleaving numbers</title>
		<link>http://aidanlister.com/2010/11/interleaving-numbers/</link>
		<comments>http://aidanlister.com/2010/11/interleaving-numbers/#comments</comments>
		<pubDate>Thu, 25 Nov 2010 10:54:13 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Code Repository]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=459</guid>
		<description><![CDATA[<code>interleave()</code> allows you to quickly calculate the Morton Interleave of two numbers.]]></description>
			<content:encoded><![CDATA[<p>When you have two numbers that need to be indexed together for speedy lookups there are a variety of mechanisms that can be used, a fast and efficient mechanism is called the <a href="http://www.codexon.com/posts/morton-codes">Morton Interleave</a>.</p>
<pre class="brush: php; title: ; notranslate">
/**
 * Calculate a Morton Interleave for two numbers.
 *
 * @param    int   $x   The first number
 * @param    int   $y   The second number
 * @return   int      The Morton Interleave
 * @author   Aidan Lister &lt;aidan@php.net&gt;
 * @link     http://aidanlister.com/2010/11/interleaving-numbers/
 */
function interleave($x, $y) {
    $result = 0;
    $position = 0;
    $bit = 1;

    while ($bit &lt;= $x || $bit &lt;= $y) {
        if ($bit &amp; $x)
            $result |= 1 &lt;&lt; (2*$position+1);
        if ($bit &amp; $y)
            $result |= 1 &lt;&lt; (2*$position);

        $position += 1;
        $bit = 1 &lt;&lt; $position;
    }
    return $result;
}
</pre>
<p>A common use case for the interleave is storing a conversation between two users. For example, to retrieve all messages between user 231 and user 119 one would normally query the database with <code>WHERE (sender_id=231 AND receiver_id=119) OR (sender_id=119 AND receiver_id=231)</code>.</p>
<p>When using an interleave field, you could do something like <code>WHERE conversation_id=48447</code>. The number 48447 being the interleave, calculated as:</p>
<pre class="brush: php; title: ; notranslate">
$uids = array(231, 119);
interleave(max($uids), min($uids));
</pre>
<p>This means faster queries and cleaner code!</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2010/11/interleaving-numbers/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Better integration with Facebook for Django</title>
		<link>http://aidanlister.com/2010/11/better-integration-with-facebook-for-django/</link>
		<comments>http://aidanlister.com/2010/11/better-integration-with-facebook-for-django/#comments</comments>
		<pubDate>Thu, 18 Nov 2010 16:39:30 +0000</pubDate>
		<dc:creator>Aidan Lister</dc:creator>
				<category><![CDATA[Programming]]></category>

		<guid isPermaLink="false">http://aidanlister.com/?p=464</guid>
		<description><![CDATA[I have released <a href="https://github.com/aidanlister/django-facebook">django-facebook</a> which offers total and seamless integration with Facebook for your social apps.]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve really been enjoying working with <a href="http://djangoproject.com/">Django</a> for the last few months, it&#8217;s an incredibly powerful and elegant framework and I sorely wish there was an equivalent in PHP.</p>
<p>This said, I was sadly unimpressed with the social landscape support. Facebook&#8217;s own <a href="">Python SDK</a> has a slew of bugs and is not actively maintained, I recommend people use <a href="https://github.com/dziegler/python-sdk">David Ziegler&#8217;s fork</a>.</p>
<p>For integration with Django, there was <a href="https://github.com/tschellenbach/Django-facebook">Django-facebook</a> which had good intentions but doesn&#8217;t quite cut the mustard, <a href="https://github.com/sciyoshi/pyfacebook/">Pyfacebook</a> which is no longer actively maintained and does not work with the new Graph API, and <a href="https://github.com/flashingpumpkin/django-socialregistration">Social registration</a> which was the best of the bunch, but still offered poor integration with the django authentication system.</p>
<p>To this end, I have released <a href="https://github.com/aidanlister/django-facebook">django-facebook</a> which offers total and seamless integration with Facebook for your social apps.</p>
<p>It has a bunch of nifty tools including template tags, a nice middleware, an authentication backend and some <a href="https://github.com/aidanlister/django-facebook/blob/master/README.md">actual documentation</a>!</p>
<p>Spread the word, and watch this space.</p>
]]></content:encoded>
			<wfw:commentRss>http://aidanlister.com/2010/11/better-integration-with-facebook-for-django/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<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>
<pre class="brush: php; title: ; notranslate">
/**
 * Retrieve time from an NTP server
 *
 * @param    string   $host   The NTP server to retrieve the time from
 * @return   int      The current unix timestamp
 * @author   Aidan Lister &lt;aidan@php.net&gt;
 * @link     http://aidanlister.com/2010/02/retrieve-time-from-an-ntp-server/
 */
function ntp_time($host) {

  // Create a socket and connect to NTP server
  $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
  socket_connect($sock, $host, 123);

  // Send request
  $msg = &quot;&#92;&#48;10&quot; . str_repeat(&quot;&#92;&#48;&quot;, 47);
  socket_send($sock, $msg, strlen($msg), 0);

  // Receive response and close socket
  socket_recv($sock, $recv, 48, MSG_WAITALL);
  socket_close($sock);

  // Interpret response
  $data = unpack('N12', $recv);
  $timestamp = sprintf('%u', $data[9]);

  // NTP is number of seconds since 0000 UT on 1 January 1900
  // Unix time is seconds since 0000 UT on 1 January 1970
  $timestamp -= 2208988800;	

  return $timestamp;
}
</pre>
<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; title: ; notranslate">
/**
 * 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; title: ; notranslate">
/**
 * 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; title: ; notranslate">
// 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; title: ; notranslate">
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; title: ; notranslate">
/**
 * 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; title: ; notranslate">
$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; title: ; notranslate">
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; title: ; notranslate">
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; title: ; notranslate">
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="http://aidanlister.com/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; title: ; notranslate">
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; title: ; notranslate">
    /**
     * 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; title: ; notranslate">
models/volume.php
class Volume extends AppModel
{
    /**
     * Describe our ACO tree
     */
    function parentNode()
    {
        return null;
    }
}
</pre>
<pre class="brush: php; title: ; notranslate">
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; title: ; notranslate">
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; title: ; notranslate">
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; title: ; notranslate">
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><strong>Update</strong>: Now compatible with CakePHP 1.3.x. You can also <a href="http://github.com/aidanlister/cakecommunity">download the source code directly</a> from GitHub.</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; title: ; notranslate">
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; title: ; notranslate">
models/user.php:
&lt;?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; title: ; notranslate">
views/users/register.ctp:
&lt;?php
echo $this-&gt;Form-&gt;create(array('action' =&gt; 'register'));
echo $this-&gt;Form-&gt;input('name');
echo $this-&gt;Form-&gt;input('email');
echo $this-&gt;Form-&gt;input('username');
echo $this-&gt;Form-&gt;input('password_confirm', array('label' =&gt; 'Password', 'type' =&gt; 'password'));
echo $this-&gt;Form-&gt;input('password', array('label' =&gt; 'Password Confirm'));
echo $this-&gt;Form-&gt;end('Register');
</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; title: ; notranslate">
controllers/users_controller.php:
&lt;?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.', 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; title: ; notranslate">
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; title: ; notranslate">
views/layouts/default.ctp:
&lt;div id=&quot;content&quot;&gt;
    &lt;?php echo $this-&gt;Session-&gt;flash(); ?&gt;
    &lt;?php echo $this-&gt;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; title: ; notranslate">
views/users/login.ctp:
&lt;?php
echo $this-&gt;Form-&gt;create(array('action' =&gt; 'login'));
echo $this-&gt;Form-&gt;input('username');
echo $this-&gt;Form-&gt;input('password');
echo $this-&gt;Form-&gt;end('Login');
</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; title: ; notranslate">
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; title: ; notranslate">
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
            $fields = array('lastlogin' =&gt; date('Y-m-d H:i:s'), 'modified' =&gt; false);
            $this-&gt;User-&gt;id = $id;
            $this-&gt;User-&gt;save($fields, false, array('lastlogin'));

            // 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; title: ; notranslate">
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; title: ; notranslate">
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; title: ; notranslate">
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 $this-&gt;Form-&gt;create(array('action' =&gt; 'account'));
echo $this-&gt;Form-&gt;input('password_old',     array('label' =&gt; 'Old password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $this-&gt;Form-&gt;input('password_confirm', array('label' =&gt; 'New password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $this-&gt;Form-&gt;input('password',         array('label' =&gt; 'Re-enter new password', 'type' =&gt; 'password', 'autocomplete' =&gt; 'off'));
echo $this-&gt;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; title: ; notranslate">
models/token.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;delete($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; title: ; notranslate">
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; title: ; notranslate">
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_str = null)
    {
        if ($this-&gt;Auth-&gt;user()) {
            $this-&gt;redirect(array('controller' =&gt; 'users', 'action' =&gt; 'account'));
        }

        $Token = ClassRegistry::init('Token');

        $res = $Token-&gt;get($token_str);
        if ($res) {
            // Update the users password
            $password = $this-&gt;User-&gt;generatePassword();
            $this-&gt;User-&gt;id = $res['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 = $res['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', $res);
            $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; title: ; notranslate">
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; title: ; notranslate">
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; title: ; notranslate">
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; title: ; notranslate">
views/users/recover.ctp:
&lt;h2&gt;Recover Password&lt;/h2&gt;

&lt;?php
echo $this-&gt;Form-&gt;create('User', array('action' =&gt; 'recover'));
echo $this-&gt;Form-&gt;input('email');
echo $this-&gt;Form-&gt;end('Recover');
?&gt;
</pre>
<pre class="brush: php; title: ; notranslate">
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>37</slash:comments>
		</item>
	</channel>
</rss>

