Time for another useful CakePHP code example. This was created for our hosting system at work, and is used to manage DB updates on installs of the hosted websites. I have removed some code to simplify this example, mainly relating to our directory structure and to the ‘global’ database updates. Once you understand the code below, it should be relatively simple to extend it to work with other setups. We also have code to support ‘script’ updates that are held on a Task, but I’ve removed that as well.
Read more »
Author Archives: Utoxin
Cake DB Update Shell
CakePHP, Custom Queries, and the Result Array
Just a quick post about a wonderful trick I stumbled across last night while experimenting with Virtual Fields on models in CakePHP. Since I started using Cake, I’ve been annoyed that when I ran a custom query, the results would come back in the ‘[0]‘ index of the result array, because Cake couldn’t tell what tablename they were from. I’d experimented with a couple different ways to alias them to try and get them to work, but none of them worked. I eventually gave up, and just dealt with it.
So, last night while setting up my first virtual field, I looked at the debug log. There, I discovered this query:
SELECT `Package`.`id`, `Package`.`name`, `Package`.`price`, (CONCAT(`Package`.`name`, " ($", `Package`.`price`, ")")) AS `Package__list_name` FROM `packages` AS `Package`
The key part to notice is the alias it gave to the CONCAT() field. ‘Package__list_name’. That allows Cake to properly identify the model that the field belongs to when retrieving the results, and it can then put it into the array properly, resulting in this array structure:
array ( 0 => array ( 'Package' => array ( 'id' => '1', 'name' => 'Basic MU* Hosting', 'price' => '5.00', 'list_name' => 'Basic MU* Hosting ($5.00)', ), ), 1 => array ( 'Package' => array ( 'id' => '2', 'name' => 'Basic Web Hosting', 'price' => '5.00', 'list_name' => 'Basic Web Hosting ($5.00)', ), ), )
I hope this tip is as useful to others as it will be to me. It may not seem like much, but it will be very nice to be able to place items where I want them in the result arrays.
Ham Radio Update
I’ve been getting much more active in ham radio lately, and have helped a local group I joined put up a website. I also just finished installing an antenna on my car to go with my handheld radio. Feel free to go read my post over on the site I set up for them.
I should also soon have an HF base station. I’m simply waiting for the check for some side work to come in. As soon as it arrives, I’ll be buying an IC-756 radio. It’s about 10 years old, but I couldn’t care less, and can’t wait to be able to listen to and talk to people all over the US and (hopefully) the world.
CakePHP – Updated Cryptable Behavior
Almost 2 years ago, I created the Cryptable behavior, documented here. It worked well, and served it’s purpose, but I missed a feature that I soon wished I’d added. So, today, I was working on a new project, and I decided it was time to fix the behavior to add the missing feature.
The old version used the same Initialization Vector for every string it encrypted. This isn’t HORRIBLE, but it does somewhat reduce the security of the encrypted strings. The ‘best practice’ is to generate a unique IV every time you encrypt something, and just store it with the encrypted string to use when you need to decrypt it later. It’s considered safe to store them together, and even transmit them together in the clear without further encryption.
So, without further ado, here’s the updated code for the behavior:
<?php class CryptableBehavior extends ModelBehavior { public $settings = array(); private $iv_size; private $base64_iv_size; function setup(&$model, $settings) { if (!isset($this->settings[$model->alias])) { $this->settings[$model->alias] = array( 'fields' => array() ); } $this->settings[$model->alias] = array_merge($this->settings[$model->alias], $settings); $this->iv_size = mcrypt_get_iv_size(Configure::read('Cryptable.cipher'), 'cbc'); $this->base64_iv_size = strlen(base64_encode(mcrypt_create_iv($this->iv_size, MCRYPT_DEV_URANDOM))); } function beforeFind(&$model, $queryData) { foreach ($this->settings[$model->alias]['fields'] AS $field) { if (isset($queryData['conditions'][$model->alias.'.'.$field])) { $queryData['conditions'][$model->alias.'.'.$field] = $this->encrypt($queryData['conditions'][$model->alias.'.'.$field]); } } return $queryData; } function afterFind(&$model, $results, $primary) { foreach ($this->settings[$model->alias]['fields'] AS $field) { if ($primary) { foreach ($results AS $key => $value) { if (isset($value[$model->alias][$field])) { $results[$key][$model->alias][$field] = $this->decrypt($value[$model->alias][$field]); } } } else { if (isset($results[$field])) { $results[$field] = $this->decrypt($results[$field]); } } } return $results; } function beforeSave(&$model) { foreach ($this->settings[$model->alias]['fields'] AS $field) { if (isset($model->data[$model->alias][$field])) { $model->data[$model->alias]['cleartext_'.$field] = $model->data[$model->alias][$field]; $model->data[$model->alias][$field] = $this->encrypt($model->data[$model->alias][$field]); } } return true; } public function encrypt($data) { if ($data !== '') { $iv = mcrypt_create_iv($this->iv_size, MCRYPT_DEV_URANDOM); return base64_encode($iv).base64_encode(mcrypt_encrypt(Configure::read('Cryptable.cipher'), Configure::read('Cryptable.key'), $data, 'cbc', $iv)); } else { return ''; } } public function decrypt($data, $data2 = null) { if (is_object($data)) { unset($data); $data = $data2; } if ($data != '') { $iv = base64_decode(substr($data, 0, $this->base64_iv_size)); $data = base64_decode(substr($data, $this->base64_iv_size)); return trim(mcrypt_decrypt(Configure::read('Cryptable.cipher'), Configure::read('Cryptable.key'), $data, 'cbc', $iv)); } else { return ''; } } }
For those using the old behavior, it is possible to upgrade, but you need to do a little bit of work. This amounts to running queries to inject your current IV onto the start of every encrypted field in the database. (PLEASE be careful doing this, and make sure to keep backups.) Also note that you may need to provide more room in the fields, because the data will be somewhat longer now. You can find out how many characters long it will be by running this command in PHP:
echo strlen(base64_encode(mcrypt_create_iv(mcrypt_get_iv_size('CIPHER_HERE', 'cbc'), MCRYPT_DEV_URANDOM)))
The next time the encrypted records are saved, the old default IV will be replaced with a new random one. In fact, any time a record is re-encrypted, it will get a new IV, since I don’t bother with storing the old one when the value is decrypted.
You may also remove the Configure line that holds your old IV, once you’ve injected it into the database on all the records, and tested that the new Cryptable behavior is working as intended.
Please see the original post for any other questions, or feel free to ask in the comments and I’ll do my best to help.
CakePHP + Symlinks = Pain
Now that I’ve had a day or so to recover, I’m going to tell you about what I just spent 2-3 weeks trying to resolve. By way of explanation, our main product at work is a CakePHP based CMS application. It has a lot of neat features, including the ability for users to upgrade to newer versions any time they choose. We keep all versions present in /etc/precious_core/<version_number>/, and each user has a symlink to the relevant directory in their webroot.
When they upgrade, part of the process is to replace that symlink with a new on that points at their new version. For a long time we’ve known there was a problem of some kind related to CakePHP’s cache that developed after upgrades, but it was never a huge problem, so we mostly just ignored it. However, in a recent release, it started causing major problems, and I got tasked with finding and fixing the actual bug. I figured it would take a day or two, and I’d be done with it. Little did I know just how painful this was going to be.
I initially tried several ways of forcing the cache to get cleared when the app was upgraded. That worked well, as far as it went, but then a new problem surfaced. At least half the time, the cache would re-populate with bad data after an upgrade. Some of the cached file paths would be for the wrong version of the central app, for no apparent reason. I tried throwing even more thorough cache clearing at it. Things got a little better, but it still wasn’t working.
Finally, I fully duplicated our production setup on my local dev machine, parallel version directories included, and installed a PHP debugger, so I could step through the code and figure out what exactly was going on. After several hours of research, I determined that the error was happening in this function:
function __map($file, $name, $type, $plugin) {
if ($plugin) {
$this->__map['Plugin'][$plugin][$type][$name] = realpath($file);
} else {
$this->__map[$type][$name] = realpath($file);
}
}
The file paths that were getting inserted into $this->__map were sometimes incorrect. I knew that realpath() could cache it’s data, but I was explicitly calling clearpathcache() during the upgrade process. So why was it getting bad paths sometimes? At this point, Ceeram (@ceeram) of #cakephp on Freenode helped me out by pointing me at this blog: PHP, symlinks, and the realpath cache, which explained perfectly what was going on.
It turns out realpath()’s cache is per-thread, so even though I was clearing it in the apache thread that was doing the upgrade, it still had old data in other apache threads. Possibly very old data sometimes. (I once saw file paths for 3 different versions of our app in one cake_core_file_map during testing.) The simplest solution that I was able to discover was to simply add an apache graceful restart during our upgrade process. The impact of a graceful restart is minimal, but it does cause all apache threads to get closed down and new ones opened, which fixes the problem by forcing all of them to clear their cache.
So, the moral of the story? Well, not sure there is one. Except that sometimes you need a breakpoint debugger even when you’re coding PHP.
Recent Comments