RSS
 

Archive for the ‘WebDevelopment’ Category

Development vs. Productive Setup

17 Aug

If you want to deploy your cakephp app you usually have to change a few lines of code. But we want to minimize that.

My example setup:
- development: Windows (>= Vista)
- productive: Linux (Debian)

Database Setup

Create a library file and extend this from your DATABASE_CONFIG. This way you can let your app automatically select the correct database var at runtime.

class BASE_CONFIG {
	var $environments = array('default', 'sandbox', 'online');
 
	var $default = array();
 
	/**
	 * switch between local and live site(s) automatically by domain
	 * or manually by Configure::write('Environment.name')
	 * 2009-05-29 ms
	 */
	function __construct() {
		$environment = $this->getEnvironmentName();
		if ($environment && isset($this->{$environment})) {
			$this->default = array_merge($this->default, $this->{$environment});
		}
		$this->test = $this->default;
		$this->test['prefix'] = 'zzz_';
	}
 
 	function getEnvironmentName() {
		$environment = (String)Configure::read('Environment.name');
		if (empty($environment) && !empty($_SERVER['HTTP_HOST'])) {
			$server = (String)$_SERVER['HTTP_HOST'];
			if (!empty($server)) {
				foreach ($this->environments as $e) {
					if (isset($this->{$e}) && isset($this->{$e}['environment']) && $this->{$e}['environment'] == $server) {
						$environment = $e;
						break;
					}
				}
			}
		}
		return $environment;
	}
}

Example for your database.php:

App::import('Lib', 'BaseConfig');
 
class DATABASE_CONFIG extends BASE_CONFIG {
	var $default = array(	// localhost
		'name' => 'default',
		'environment' => 'localhost',
		'driver' => 'mysqli',
		'persistent' => false,
		'host' => 'localhost',
		'login' => 'root',
		'password' => '',
		'database' => 'cake_app',
		'prefix' => 'xyz_',
		'encoding' => 'utf8'
	);
	var $sandbox = array(	// online test
		'name' => 'test',
		'environment' => 'test.domain.com',
		'login' => 'root',
		'password' => '',
		'database' => 'cake_app_test',
	);
	var $online = array(	// online productive
		'name' => 'online',
		'environment' => 'www.domain.com',
		'login' => 'root',
		'password' => '',
		'database' => 'cake_app',
	);
}

The same config file on all 3 locations will now select the one corresponding to the environment url.
You could override this, though, by using Configure::write(‘Environment.name’) – but this is not neccessary if the domain doesnt change too often.

Debug Mode

Put this in your core.php (it should ALWAYS be 0 by default!):

Configure::write('debug', 0);
# Enhancement
if (!empty($_SERVER['HTTP_HOST']) && $_SERVER['HTTP_HOST'] == 'localhost') {
	Configure::write('debug', 2);
}

Debug mode is 0 for all online sites and 2 for your local development site.

Custom “Overrides”

There are many variables you need to switch from local to live apps (google.maps key, other api keys, email server credentials, …)
The quick-and-dirty solution would be something like this:

if ($_SERVER['HTTP_HOST'] == 'localhost') {
	$config['Foo'] = array(1,2,3);
} elseif ($_SERVER['HTTP_HOST'] == 'www.domain.com')) {
	$config['Foo'] = array(1,2,4);
} else {
	$config['Foo'] = array(1,5,6);
}

Very fast very ugly.

A little bit cleaner is using two config files: “configs.php” and “configs_private.php”. The second one is not synched (or in SVN) – it contains passwords and environment specific content.
Include it in your bootstrap AFTER you included the default configs.

As i just mentioned, it has another upside: Beeing a environment based tmp-file it does not store any sensitive information in the SVN (or whatever other backup tool you use).
This way you can easily set up configuration “stubs” in your configs.php and insert the passwords in your private config file.

Folder Setup and .htaccess

You can use the same folder setup:
- cake
- app
- vendors
With …/app/webroot/ as “visible folder” and …/app/webroot/index.php as dispatching script.

In my htaccess file i use “!^localhost” to avoid redirects locally:

RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteCond %{HTTP_HOST} !^localhost [NC]
RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]

This way the same file can be used for both environments.

Linux/Windows differences

Use the constant DS (= Directory Separator) anywhere you can. If you hardcode it with / (Linux) oder \ (Windows) it might break some code. Usually it doesn’t, but it is cleaner to use DS anyway.

Example:

file_put_contents(TMP.'xyz'.DS.'file.txt', 'some text')

Set up a WINDOWS constant in your bootstrap.php – this way you can dynamically decide what function to use or to not use specific lines of code (like console “exec” commands).

if (substr(PHP_OS, 0, 3) == 'WIN') {
	define('WINDOWS', true);
} else {
	define('WINDOWS', false);
}

Example:

if (WINDOWS) {
		//whatever
} else {
		exec($something);
}

Uploading your changes

I use a shell tool i wrote – which uses “rsynch” and only uploads the delta (changes made so far).
Make sure you DON’T upload any tmp stuff or even worse, override environment based files like uploads, cached files etc.
Using batch scripts you can usually exclude some directories.

Final Tips

Remember that linux needs explicit folder permissions. So you need to manually (or per batch script) set /tmp to 777 recursivly as well as any other folder which you want to write into from your scripts.

Also clear the cache after updating! Otherwise your model relations as well as some constants will be outdated and cause errors or complete failure.

 
No Comments

Posted in CakePHP

 

How to implement Captchas properly

09 Aug

What is a captcha?

They protect forms on websites from spammers and bots (@see Wikipedia for details). The main idea: Display some kind of code a human can easily read and submit but a computer can not.

How NOT to implement captchas

This part is even more important, because there is not only one correct way, but even more wrong ways to go here.

Don’t use sessions unless you really have to.
Pages that use something like $_['Captcha']['field'] and override this one on every form, really freak me out! It makes working with two or more tabs impossible, because they override each others captcha values all the time, resulting in a ****** mess.
You could use an array like structure, but your captcha session array can get pretty big in a short amount of time.

More helpful are captchas which use the current form and some hash values based on the fields + current timestamp. It should not be possible to “guess” or “calculate” the hash value. So there is no way to use future “hashs”.
You could use older hashs (like from last week), though. But most bots are programmed to just post right away. They would have to save possible valid “scenarios” for later usage.
Mabye we can come up with something fail-prove later on. For now we want to effectively prevent spam bots to submit their crap without annoying normal users.
The second part is always the hardest. Me – for example – I really hate those image captchas which you can barely read. I often times have to repeat or reload it twice in order to succeed. Annoying comes not even close.

Why do we need captchas

First of all, they make sure that there is no bot (automatic program) posting “spam” or whatever.
But sometimes you just want to add captchas to prevent users from doing some action too often (like friendship requests in a community site etc).

A good example what happens if you dont use captchas, is bakery.cakephp.org.
Some articles have like 36 comments, of which all 36 are SPAM. This is a desaster.
And in this case you even have to be logged in to submit a comment.
The argument that forms for which you need to be logged in don’t need captchas is not contemporary anymore.

Passive Captchas

I already talked about using some hash values based on the fields + current timestamp.
This can be used to generate passive captchas. They are similar to the cake core component “security” which adds some hidden fields to make sure that the fields have not been tempered with.

Both are invisible to the user – they dont even notice the passive captcha. Bot bots will soon discover that they are facing a wall.
The difference is, that passive captchas should only valid in a specific timeframe. Too fast (less than 2 seconds) is usually a sign for the work of a bot – humans cannot type that fast.
Too late (> 12 hours?) means you need to revalidate anyway, so we would render the form invalid as well. Well designed forms will keep the posted content, so nothing gets lost.

Another aspect to improve security is to use other user specific fields for the hash value like browser agent (cannot change during posts, but is less secure because it can be modified), IP address (can only be modified by very skilled hackers and therefore is pretty secure), …

Active Captchas

Those are the most commonly used ones. Users either have to read an image, calculate numbers or
interpret a sentence. The first one is not suitable for handicapped people.

Usually they are build as extension on top of passive captchas. We first validate the passive one. If the form is OK, we then validate the user input. If validation passes we render the captcha valid.

I decided to use math captchas. They keep you mentally fit and do what they are supposed to. The only important issue is to make it easy enough. I saw pages using / [division] or numbers above 20 in multiplication or even above 100 for summation – which is total overhead.
But other ones could be used as well – simply by changing configuration settings.

Captcha Behavior

Ok, to sum it up, we want captchas that
- don’t annoy users
- protect as good as absolutely neccessary
- can be used with tabs
- can be easily implemented and configured

The idea is, that we want to add a single form field as well as attach a single behavior to our model.
Thats all there is to it.
That’s why a behaviour in combination with a helper does the trick perfectly.

The code is in my github tools plugin:
captcha behavior
captcha helper

Helper usage (in the view):

echo $this->Captcha->input(); // or input('Modelname') if model is different from the form model

Behavior usage (in the controller):

$this->User->Behaviors->attach('Captcha');
	if ($this->User->save($this->data)) { ... }

Pretty straight forward, isn’t it?

Current weaknesses (apart from its strenghts):
- possible “hash extraction” with unlimited use of those valid hashs (session or db to prevent?)

Final notes

Right now it is mainly used for math captchas (active captchas) and just passive captchas.
Feel free to update the missing parts like providing more captcha types (image, sentence, …) or processing types (session, cookie, …).

 
2 Comments

Posted in CakePHP

 

Extended core validation rules

19 Jul

I18n Translation

Some translate the rules in the view – but it usually creates redundancy. In some projects this might be intentional. I like to keep the error messages centralized, though.

For that, you can just override the core translation rule – add this to app_model.php:

/**
 * Overrides the Core invalidate function from the Model class
 * with the addition to use internationalization (I18n and L10n)
 * @param string $field Name of the table column
 * @param mixed $value The message or value which should be returned
 * @param bool $translate If translation should be done here
 */
function invalidate($field, $value = null, $translate = true) {
	if (!is_array($this->validationErrors)) {
		$this->validationErrors = array();
	}
	if (empty($value)) {
		$value = true;
	} else {
		$value = (array )$value;
	}
 
	//TODO: make more generic?
	if (is_array($value)) {
		$value[0] = $translate?__($value[0], true) : $value[0];
 
		if (count($value) > 3) { # string %s %s string, trans1, trans2
			$value = sprintf($value[0], $value[1], $value[2], $value[3]);
		} elseif (count($value) > 2) { # string %s %s string, trans1, trans2
			$value = sprintf($value[0], $value[1], $value[2]);
		} elseif (count($value) > 1) { # string %s string, trans1
			$value = sprintf($value[0], $value[1]);
		} else {
			$value = $value[0];
		}
	}
	$this->validationErrors[$field] = $value;
}

Usage (some examples):

var $validate = array(
	'username' => array(
		'notEmpty' => array(
			'rule' => array('notEmpty'),
			'message' => 'valErrEmpty', // some short form to translate
			//normal sentences would of course work, too
		),
	),
	'pwd' => array(
		'between' => array(
			'rule' => array('between', 6, 30),
			'message' => array('valErrBetweenCharacters %s %s', 6, 30), // short form string with variables
			// maybe resulting in something like "Between 6 and 30 chars" as defined in locale.po
		)
	),
	'code' => array(
		'maxLength' => array(
			'rule' => array('maxLength', 5),
			'message' => array('The code cannot be longer than %s chars', 5), // normal text with a variable
		)
	),
	...
);

I like the short forms like “valErrBetweenCharacters %s %s” – they don’t have a certain “grammar”, so if you want to change the translation result “Between %s and %s chars” to “Please insert between %s and %s chars” this can be done with one change in locale.po instead of changing all xxx places you used this rule.

Custom validation rules

Here is one example how to use a custom validation rule with one param (custom rules can be placed in the model for local validation or the app_model for global validation):

var $validate = array(
	'pwd_repeat' => array(
		'validateIdentical' => array(
			'rule' => array('validateIdentical', 'pwd'), // we want to compare this to "pwd"
			'message' => 'valErrPwdNotMatch',
		),
	)
);
 
// in app_model.php:
/**
 * checks if the content of 2 fields are equal
 * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
* //TODO: make it more generic with Model.field syntax
 */
function validateIdentical($field, $compareWith) {
	return ($this->data[$this->alias][$compareWith] === array_shift($field));
}

Multiple validation rules

The Cookbook states:
“By default CakePHP tries to validate a field using all the validation rules declared for it and returns the error message for the last failing rule”
I don’t really like this default behavior – what sense does it make to validate all if only the last error can be returned anyway. But as long as “last” is not true by default, we have to manually set it:

var $validate = array(
    'login' => array(
        'loginRule-1' => array(
            'rule' => 'alphaNumeric',
            'message' => '...',
            'last' => true
         ),
        'loginRule-2' => array(
            'rule' => array('minLength', 8),
            'message' => '...'
        )
    )
);

The rules are validated top down. So if the first one fails, it will now return the error right away. Usually the following validation rules rely on a positive result of the predecessor. At least, thats how you should arrange your rules.

One example:
[email] (in this order – each with a specific error message)
- notEmpty
- email
- undisposable (custom – per vendor)
- notBlocked (custom – per webservice)

As you can see, it would not make sense to check all of them every time. It really slows down the validation – especially if the email address is not even valid. So we first want to check the simple stuff and then move on to the advanced (and sometimes more time consuming) rules. All rules get “last”=>true to achieve that.

Other quite handy custom translation rules

They can be put into app_model.php:

/**
 * checks a record, if it is unique - depending on other fields in this table (transfered as array)
 * example in model: 'rule' => array ('uniqueRecord',array('belongs_to_table_id','some_id','user_id')),
 * if all keys (of the array transferred) match a record, return false, otherwise true
 * @param ARRAY other fields
 * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
 */
function validateUnique($arguments, $fields = array(), $options = null) {
	$id = (!empty($this->data[$this->alias]['id'])?$this->data[$this->alias]['id'] : 0);
 
	foreach ($arguments as $key => $value) {
		$fieldName = $key;
		$fieldValue = $value; // equals: $this->data[$this->alias][$fieldName]
	}
 
	if (empty($fieldName) || empty($fieldValue)) { // return true, if nothing is transfered (check on that first)
		return true;
	}
 
	$conditions = array($this->alias.'.'.$fieldName => $fieldValue, // Model.field => $this->data['Model']['field']
		$this->alias.'.id !=' => $id, );
 
	foreach ((array )$fields as $dependingField) {
		if (!empty($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
			$conditions[$this->alias.'.'.$dependingField] = $this->data[$this->alias][$dependingField];
 
		} elseif (!empty($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
			$conditions[$this->alias.'.'.$dependingField] = $this->data['Validation'][$dependingField];
 
		} elseif (!empty($id)) {
			# manual query! (only possible on edit)
			$res = $this->find('first', array('fields' => array($this->alias.'.'.$dependingField), 'conditions' => array($this->alias.'.id' => $this->data[$this->alias]['id'])));
			if (!empty($res)) {
				$conditions[$this->alias.'.'.$dependingField] = $res[$this->alias][$dependingField];
			}
		}
	}
 
	$this->recursive = -1;
	if (count($conditions) > 2) {
		$this->recursive = 0;
	}
	$res = $this->find('first', array('fields' => array($this->alias.'.id'), 'conditions' => $conditions));
	if (!empty($res)) {
		return false;
	}
 
	return true;
}
 
// validateUrl and validateUndisposable are to come

More

See the Cookbook for details on that matter.

 
No Comments

Posted in CakePHP

 

Redirect Root Domain to WWW Subdomain

13 Jul

The Problem

“When you have two different addresses pointing to the same page, like www.example.com/offers.html and example.com/offers.html, many search engines (or so we are led to believe) will treat those two URLs as two separate pages. When you, as a human, see those two pages and notice they are identical, you will automatically realise (correctly) that they are actually the same page. Apparently, the search engines do not make this assumption, and will regard those as different pages with duplicate content.”
(source: www.thesitewizard.com)

I personally think that apart from the SEO problem there shouldn’t be two different urls to the same content in the first place.

So what do we do?

The bad thing to do would be to disable one of the domains (root or www).
We want to select one (www.example.com) as default domain and if someone just enters “example.com” he will be automatically redirected to our default domain.
Again – the bad thing would be to use meta redirects. We want to use so called permanent redirects ( code 301) in order to notify search engines and browsers about the reason we want to redirect.

Using Mod Rewrite

For the cake app to be available per “www.example.com” (and a 301 redirect from mydomain.de) you just need to modify the htaccess file in the /app/webroot/ folder:

<IfModule mod_rewrite.c>
	RewriteEngine On
 
	RewriteCond %{HTTP_HOST} !^www\. [NC]
	RewriteCond %{HTTP_HOST} !^localhost [NC]
	RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]
 
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
</IfModule>

The “localhost” part is not neccessary – but it prevents your local htaccess file to redirect on your development computer. That is if you have your htaccess file in svn or git and therefore both versions (local and stage) are the same.

The reason why i did not lay out the other direction (www to root) is simple: I don’t think this makes sense. Most people not so comfortable with the internet usually type the complete address anyway – they just expect a “www” in front of the domain for the main website.
And with the internet getting more and more complex, some guidelines should just remain. One is that you don’t use root domains for websites. (I could go into details here – about other side effects like “technical” cookie problems occuring with root domain sites etc – but i will leave that out for now)

Using Apache Directives

This is way faster because the server doesn’t have to open the htaccess files for it. It can directly use what it has to have available anyway.

Inside /etc/apache2/sites-available/ there should be your domain file.
Between <Directory> and </Directory> you can add the above lines.
To actually increase the performance we now need to disable htaccess files for this domain.
Add AllowOverride None directly above your new lines. This prevents the server from looking for, opening and processing htaccess files.

Note: Usually this is only available on root servers.

 
 

User Add Console Script

04 Jul

If you have a fresh setup of your app and no users in the database, its not easy to “register” a new one (with the admin role and everything).
The other case would be if you changed the security salt. Now you need new passwords, as well.

The second part is not yet covered by my script – the first one is, though.
The idea is to insert an admin user in order to login for the first time. All you need is a name, an email and a password.

<?php
 
class UserShell extends Shell {
	var $tasks = array();
	var $uses = array('User');
 
	//TODO: refactor (smaller sub-parts)
	function main() {
		if (App::import('Component','AuthExt')) {
			$this->Auth = new AuthExtComponent();
		} else {
			App::import('Component','Auth');
			$this->Auth = new AuthComponent();
		}
 
		while (empty($username)) {
			$continue = $this->in(__('Set \'username\'?', true),array('y', 'n', 'q'), 'y');
			if ($continue == 'q') { die('Abort'); } elseif ($continue == 'n') { break; }
			$username = $this->in(__('Username (2 characters at least)', true));
		}
		while (empty($password)) {
			$password = $this->in(__('Password (2 characters at least)', true));
		}
 
 
 
		if (isset($this->User->Role) && is_object($this->User->Role)) {
			$roles = $this->User->Role->find('list');
 
			if (!empty($roles)) {
				$this->out('');
				pr ($roles);
			}
 
			$roleIds = array_keys($roles);
			while (!empty($roles) && empty($role)) {
				$role = $this->in(__('Role', true), $roleIds);
			}
		} elseif (method_exists($this->User, 'roles')) {
			$roles = User::roles();
 
			if (!empty($roles)) {
				$this->out('');
				pr ($roles);
			}
 
			$roleIds = array_keys($roles);
			while (!empty($roles) && empty($role)) {
				$role = $this->in(__('Role', true), $roleIds);
			}
		}
		if (empty($roles)) {
			$this->out('No Role found (either no table, or no data)');
			$role = $this->in(__('Please insert a role manually', true));
		}
 
		$this->out('');
		$pwd = $this->Auth->password($password);
 
		$data = array('User'=>array(
			'password' => $pwd,
			'active' => 1
		));
		if (!empty($username)) {
			$data['User']['username'] = $username;
		}
		if (!empty($email)) {
			$data['User']['email'] = $email;
		}
		if (!empty($role)) {
			$data['User']['role_id'] = $role;
		}
		$this->out('');
		pr ($data);
		$this->out('');
		$this->out('');
		$continue = $this->in(__('Continue? ', true),array('y', 'n'), 'n');
		if ($continue != 'y') {
			die('Not Executed!');
		}
 
		$this->out('');
		$this->hr();
		if ($this->User->save($data)) {
			$this->out('User inserted! ID: '.$this->User->id);
		} else {
			$this->out('User could not be inserted (email, nick duplicate!!!)');
		}
	}
}
?>

Notes

The code obviously needs some refactoring. And its written for my applications – it might not work out of the box with other cake apps. Maybe we could make it more generic, as well.

It assumes that you have either AuthExt or Auth running for Authentication, that you have a User and a Role model and that the fields are “username”, “email”, “password” and “role_id”. Additionally it sets “active” to 1 – this field is checked in my login procedure.
But this script should be fairly easily adjustable for your needs.

Ideas for further improvement are:
- user add (for adding)
- user edit (for editing specific user/password)
- user reset (to reset all passwords to 123 or whatever)

 
No Comments

Posted in CakePHP

 

Subversion and multiple CakePHP apps

02 Jul

A few days ago i already wrote about how to set up a svn for cake.
In this second part i will cover the aspect of managing several apps.

The Problem

We have several apps, each with its own cake and vendor folder. If you need to update from 1.3.1 to 1.3.2 you would need to do that for each of your cake cores. Woudn’t it be nice if you changed one and all others would be updated automatically?

The Solution

Lets say, our apps have the following svn rep. urls:
http://123.456.789.000/app1
and
http://123.456.789.000/app2

Both have the same structure:
/root/trunk/app/…
(/root/trunk/vendors/…)
(/root/trunk/cake/…)

Both vendor and cake folder we now want to substitute with our shared folders

Sharing Cake Core and Vendors

We now put our cake core in svn under the following url:
http://123.456.789.000/cake13

and vendors:
http://123.456.789.000/vendors

In both apps, we need to edit the properties for the /trunk folder with the following schema:
svn:externals svn_url local_folder
Result:
svn:externals http://123.456.789.000/cake13 cake
svn:externals http://123.456.789.000/vendors vendors

If you update the cake13 or vendors svn, both apps will get the updated version.
Especially if you have more than 2 apps, this will be very helpful.

Extending to other folders

/app/plugins is a very common folder where you should use svn:externals instead of just copying your plugins to all apps. One change in any plugin version will affect the other ones.

/app/libs can have subfolders, too. So /app/libs/lib/… could contain a “library of libs” for all apps.
The same goes for /app/views/helpers, /app/controller/components, /app/models/behaviors/ etc

Your test cases should be in “lib” folders then, too.
E.g:
/app/controller/components/lib/upload.php
/app/tests/cases/components/lib/upload.php
Both inside those shared lib folders which are attached with svn:externals (in this case parent folder “components”).

This might make sense in some cases. Most of the time, though, it’s better to create some kind of “tools” plugin where you can combine all your “library” files (common libs, helpers, components, behaviors, …).
And the test cases are easier managed, as well.

Final tips

You don’t necessarily need seperate svns for each external folder. They could all reside in one.
E.g with “common” svn:

svn:externals http://123.456.789.000/common/cake13 cake
svn:externals http://123.456.789.000/common/subfolder1/subfolder2/vendors vendors

Any svn subfolder is valid

NEW: Use github reps as external svns

Now you can include github reps in svn reps. Its as easy as including
svn:externals https://svn.github.com/[user]/[repository]
in your folder properties.

This is especially useful, if you want to include one of the many cake plugins available at github without manually including it (changes would not be adopted).
Thats how i include my tools plugin in all my apps:
svn:externals https://svn.github.com/dereuromark/tools tools
(in my /app/plugins folder properties)

Note: Currently it does not seem to work the other way around. Although planned to be available very soon, it didn’t work for me committing to such a git-svn rep externally included.
But most of the time, a simple checkout is more than enough. If you need to change the rep, you can do that with git itself and then update the svn rep (it will get the changes then anyway).

 
1 Comment

Posted in CakePHP

 

Code-Completion Console Script

28 Jun

Many good IDEs for Webdevelopment cannot automatically understand the structure of a framework and its classes/objects.
They need a small file initializing the objects so that they know in which context they appear.

E.g.:
$this->Session (Session Helper)
its impossible for the IDE to know that the the helpers are inside the View class.
Same goes for controller components and model behaviors

I wrote a little script which produces mockup code. My IDE “PHPDesigner” works very well with it.

Usage

Just type cake cc inside the cake shell.

Code

<?php
 
App::import('Core', 'Folder');
App::import('Core', 'File');
 
/**
 * Code Completion
 * 2009-12-26 ms
 */
class CcShell extends Shell {
	var $uses = array();
 
	private $content = '';
 
	function main() {
		$this->out('AutoComplete Dump');
 
		//TODO: ask for version (1.2 etc - defaults to 1.3!)
 
		$this->filename = APP.'code_completion__.php';
 
		# get classes
		$this->models();
		$this->components();
		$this->helpers();
		//TODO: behaviors
 
		# write to file
		$this->_dump();
 
		$this->out('...done');
	}
 
	/**
	 * @deprecated
	 * now use: Configure::listObjects()
	 */
	function __getFiles($folder) {
		$handle = new Folder($folder);
		$handleFiles = $handle->read(true, true);
		$files = $handleFiles[1];
		foreach ($files as $key => $file) {
			$file = extractPathInfo('file', $file);
 
			if (mb_strrpos($file, '_') === mb_strlen($file) - 1) { # ending with _ like test_.php
				unset($files[$key]);
			} else {
				$files[$key] = Inflector::camelize($file);
			}
		}
		return $files;
	}
 
 
	public function _getFiles($type) {
    $files = App::objects($type);
    # lib
    $paths = (array)App::path($type.'s');
    $libFiles = App::objects($type, $paths[0] . 'lib' . DS, false);
 
    $plugins = App::objects('plugin');
    if (!empty($plugins)) {
      foreach ($plugins as $plugin) {
         $pluginFiles = App::objects($type, App::pluginPath($plugin) . $type.'s' . DS, false);
          if (!empty($pluginFiles)) {
              foreach ($pluginFiles as $t) {
                  $files[] = $t; //"$plugin.$type";
              }
          }
      }
    }
    $files = array_merge($files, $libFiles);
    $files = array_unique($files);
 
		$appIndex = array_search('App', $files);
		if ($appIndex !== false) {
			unset($files[$appIndex]);
		}
 
		# no test/tmp files etc (helper.test.php or helper.OLD.php)
    foreach ($files as $key => $file) {
			if (strpos($file, '.') !== false || !preg_match('/^[\da-zA-Z_]+$/', $file)) {
				unset($files[$key]);
			}
		}
    return $files;
	}
 
 
	function models() {
		//$files = App::objects('component', null, false);
		$files = $this->_getFiles('model');
		//$files = $this->_getFiles(COMPONENTS);
 
		$content = LF.'<?php'.LF;
		$content .= '/*** model start ***/'.LF;
		$content .= 'class AppModel extends Model {'.LF;
		if (!empty($files)) {
			$content .= $this->_prepModels($files);
		}
		$content .= '}'.LF;
		$content .= '/*** model end ***/'.LF;
		$content .= '?>';
 
		$this->content .= $content;
	}
 
	function components() {
		$files = $this->_getFiles('component');
 
		$content = LF.'<?php'.LF;
		$content .= '/*** component start ***/'.LF;
		$content .= 'class AppController extends Controller {'.LF;
		if (!empty($files)) {
			$content .= $this->_prepComponents($files);
		}
		$content .= '}'.LF;
		$content .= '/*** component end ***/'.LF;
		$content .= '?>';
 
		$this->content .= $content;
	}
 
	function helpers() {
		$files = $this->_getFiles('helper');
 
		$content = LF.'<?php'.LF;
		$content .= '/*** helper start ***/'.LF;
		$content .= 'class AppHelper extends Helper {'.LF;
		if (!empty($files)) {
			$content .= $this->_prepHelpers($files);
		}
		$content .= '}'.LF;
		$content .= '/*** helper end ***/'.LF;
		$content .= '?>';
 
		$this->content .= $content;
	}
 
	function _prepModels($files) {
		$res = '';
		foreach ($files as $name) {
			$res .= '
	/**
	* '.$name.'
	*/
	public $'.$name.';
'.LF;
		}
 
		$res .= '	function __construct() {';
 
		foreach ($files as $name) {
			$res .= '
		$this->'.$name.' = new '.$name.'();';
		}
 
		$res .= '}'.LF;
		return $res;
	}
 
	function _prepComponents($files) {
		$res = '';
		foreach ($files as $name) {
			$res .= '
	/**
	* '.$name.'Component
	*/
	public $'.$name.';
'.LF;
		}
 
		$res .= '	function __construct() {';
 
		foreach ($files as $name) {
			$res .= '
		$this->'.$name.' = new '.$name.'Component();';
		}
 
		$res .= '}'.LF;
		return $res;
	}
 
	function _prepHelpers($files) {
		# new ones
		$res = '';
 
		foreach ($files as $name) {
			$res .= '
	/**
	* '.$name.'Helper
	*/
	public $'.$name.';
'.LF;
		}
 
		$res .= '	function __construct() {';
 
		foreach ($files as $name) {
			$res .= '
		$this->'.$name.' = new '.$name.'Helper();';
		}
 
		# old ones
		$res .= ''.LF;
		/*
		foreach ($files as $name) {
		$res .= '
		$'.lcfirst($name).' = new '.$name.'Helper();
		';
		}
		$res .= LF;
		*/
 
		$res .= '	}'.LF;
 
		return $res;
	}
 
 
	function _dump() {
		$file = new File($this->filename, true);
 
		$content = '<?php exit();'.LF;
		$content .= '//Add in some helpers so the code assist works much better'.LF;
		$content .= '//Printed: '.date('d.m.Y, H:i:s').LF;
		$content .= '?>'.LF;
		$content .= $this->content;
		return $file->write($content);
	}
}
 
?>

Result

The file will look like:

class AppModel extends Model {
	/**
	* Address
	*/
	public $Address;
	...
 
	function __construct() {
		$this->Address = new Address();
		...
	}
}
class AppController extends Controller {
	/**
	* AclComponent
	*/
	public $Acl;
	...
	function __construct() {
		$this->Acl = new AclComponent();
		...
	}
}
class AppHelper extends Helper {
	/**
	* AjaxHelper
	*/
	public $Ajax;
	...
	function __construct() {
		$this->Ajax = new AjaxHelper();
		...
	}
}

Final notes

Feel free to update the TODOs and send me the improved file. A test file would be awesome, too, i guess :)

For LF constants see my article about “bootstrap goodies”.

 
No Comments

Posted in CakePHP

 

Social Bookmark Helper

27 Jun

If you want to add social bookmarks to your cake projects, this helper will do the trick.

Usage

# Defaults:
$this->Bookmark->getBookmarks();
 
# All:
$this->Bookmark->getBookmarks(null, null, $this->Bookmark->availableBookmarks());
 
# Custom:
$custom = (array)Configure::read('Bookmarks');
//or
$custom = array('Twitter', 'Facebook', ...);
$this->Bookmark->getBookmarks(null, null, $custom);

Link to GIT-Rep

cakephp-bookmark-helper

Updates on bookmarks?

Are there any outdated bookmarks – or even some missing ones? Write me.

 
No Comments

Posted in CakePHP

 

Helper? Component? Lib?

26 Jun

Some ideas what to use if you want to add some additional feature.
Feel free to comment on this below.

Level of Independence

We need to ask ourselves if this feature needs to interact with other cake elements, like controller, some components, models, …

If it needs to save to the session, or if it needs some controller functionality, it will have to be a component.
With initialize(&$controllerReference) and startup(&$controllerReference) this is very easy to accomplish.

But with Cake13 libs have been introduced. Not every piece of “controller” code necessarily needs to be component anymore.
So if you retrieve an RSS feed or get the weather from a weather channel web service you could just make a clean and independent lib class. No need to extend the cake object or even pass the controller reference. Less memory and dependency is a good thing. And its easier to test, anyway.

Helpers are used if the result is in relation to the view – in other words if it gets printed/echoed right away. If you want to retrieve some web service information and save it to the database use a component instead.

Database related?

Often times we need to adjust some model data, we either use a component first and then pass it to the model or we use beforeValidate() and beforeSave() in the model. Same goes for the other direction (from model to controller): afterFind() or a component call afterwards.
This is fine for custom changes. As soon as it could be something useful for several models, it might make sense to build a behavior. The code gets cleaner and your models more powerful.

Examples would be:
“Last Editor/Last Change”, “Geocoding”, “Auto-Capitalize first letter of name/title”, “Format/Localize Date/Time”, …

Reducing code redundancy

Now that we have a vague understanding where to use what type of tool, we should think about cutting down the redundancy.
Lets say we use the vendor class “phpThump”. We would have to write two wrappers. one for the helper (display images in the view) and one for the component (uploading images and resizing), maybe even for some behavior (validating + uploading + resizing). This wrapper handles default values from Configure::read() and other cake related settings.
In this scenario we should build one single library in /libs, maybe called “phpthumb_lib.php”.
Here we put our wrapper with our custom functions.
Then we build a helper (view side) as well as a component or a behavior (controller side). They will import and use the library file. This is a cleaner approach because changes in the library class will be available in all files it is used in.
Bonus: The main library file is easier to test. And therefore testing the other classes afterwards is easier, too.

Generally speaking, all web services should be some kind of library file (some would make a data source out of it). It doesn’t matter then if we use it in components or helpers, because it will fit either way.
A helper in a controller, though, is not really a nice thing.

Thats – by the way – something i don’t like about the core helpers. They have functionality which is often useful in the controller (replacing a timestamp with localized date in session flash messages).
So there should be a library called “Time” or whatever which is then extended or used in the helper.
But, if needed, it can be used in the controller, as well. Same goes for “Text” and “Number”.

Plugin or not?

If your feature is not site-specific but very generic it probably makes sense to build a plugin.
This way all other apps can easily use the same plugin. Additionally, test cases, assets etc are all combined in one folder – clean and extendable.

Examples:
A bookmark helper usually can be used in several apps, whereas a custom helper with two functions for this one app will not be very useful anywhere else.

 
No Comments

Posted in CakePHP

 

Tools Plugin – Part1: CodeKey

25 Jun

In this first part of the presentation of my tools plugin I will show you how you can easily manage (store, retrieve, validate) “code keys”.
They are useful in the registration process of users, or if you want to send some double opt in confirmation emails, etc.

function newKey($type, $key = null, $uid = null, $content = null) {}

If you submit a uid it can increase security by validation only if the pair uid + key are both valid.
As 4th parameter any string content can be stored. This comes in handy if you use it for changing email addresses. The new one will be stored in content until validation is complete and will then replace the old one.

function useKey($type, $key, $uid = null) {}

Usage

register_account(){}

# get new code with key "activate" and specific user id bound to it
$this->CodeKey = ClassRegistry::init('CodeKey');
$cCode = $this->CodeKey->newKey('activate', null, $uid);

This code can be used to send an activation email with a link to click on.

activate_account($keyToCheck){}

...
$this->CodeKey = ClassRegistry::init('CodeKey');
$key = $this->CodeKey->useKey('activate', $keyToCheck);
 
if (!empty($key) && $key['CodeKey']['used'] == 1) {
	# warning flash message: already activated
} elseif (!empty($key)) {
	# continue with activation
} else {
	# error flash message: invalid key
}
...

Plugin Model Code

<?php
 
class CodeKey extends ToolsAppModel {
 
	var $name = 'CodeKey';
 
	var $displayField = 'key';
	var $order = array('CodeKey.created' => 'ASC');
 
	private $defaultLength = 22;
 
	var $validate = array(
		'type' => array(
			'notEmpty' => array(
				'rule' => array('notEmpty'),
				'message' => 'valErrMandatoryField',
			),
		),
		'key' => array(
			'isUnique' => array(
				'rule' => array('isUnique'),
				'message' => 'key already exists',
			),
			'notEmpty' => array(
				'rule' => array('notEmpty'),
				'message' => 'valErrMandatoryField',
			),
		),
		'content' => array(
			'maxLength' => array(
				'rule' => array('maxLength', 255),
				'message' => array('valErrMaxCharacters %s', 255),
				'allowEmpty' => true
			),
		),
		'used' => array('numeric')
	);
 
	//var $types = array('activate');
 
	/**
	 * stores new key in DB
	 * @param string type: neccessary
	 * @param string key: optional key, otherwise a key will be generated
	 * @param mixed user_id: optional (if used, only this user can use this key)
	 * @param string content: up to 255 characters of content may be added (optional)
	 * NOW: checks if this key is already used (should be unique in table)
	 * @return string key on SUCCESS, boolean false otherwise
	 * 2009-05-13 ms
	 */
	function newKey($type, $key = null, $uid = null, $content = null) {
		if (empty($type)) {		//  || !in_array($type,$this->types)
			return false;
		}
 
		if (empty($key)) {
			$key = $this->generateKey($this->defaultLength);
			$keyLength = $this->defaultLength;
		} else {
			$keyLength = mb_strlen($key);
		}
 
		$data = array(
			'type' => $type,
			'user_id' => (string)$uid,
			'content' => (string)$content,
			'key' => $key,
		);
 
		$this->set($data);
		$max = 99;
		while (!$this->validates()) {
			$data['key'] = $this->generateKey($keyLength);
			$this->set($data);
			$max--;
			if ($max == 0) { //die('Exeption in CodeKey');
			 	return false;
			}
		}
 
		$this->create();
		if ($this->save($data)) {
			return $key;
		}
		return false;
	}
 
	/**
	 * usesKey (only once!) - by KEY
	 * @param string type: neccessary
	 * @param string key: neccessary
	 * @param mixed user_id: needs to be provided if this key has a user_id stored
	 * @return ARRAY(content) if successfully used or if already used (used=1), FALSE else
	 * 2009-05-13 ms
	 */
	function useKey($type, $key, $uid = null) {
		if (empty($type) || empty($key)) {
			return false;
		}
		$conditions = array('conditions'=>array($this->alias.'.key'=>$key,$this->alias.'.type'=>$type));
		if (!empty($uid)) {
			$conditions['conditions'][$this->alias.'.user_id'] = $uid;
		}
		$res = $this->find('first', $conditions);
		if (empty($res)) {
			return false;
		} elseif(!empty($uid) && !empty($res[$this->alias]['user_id']) && $res[$this->alias]['user_id'] != $uid) {
			// return $res; # more secure to fail here if user_id is not provided, but was submitted prev.
			return false;
		} elseif ($res[$this->alias]['used'] == 1) {
			return $res;
		}
 
		# actually use key
		if ($this->spendKey($res[$this->alias]['id'])) {
			return $res;
		}
		$this->log('VIOLATION in CodeKey Model (method useKey)');
		return false;
	}
 
	/**
	 * sets Key to "used" (only once!) - directly by ID
	 * @param id of key to spend: neccessary
	 * @return boolean true on success, false otherwise
	 * 2009-05-13 ms
	 */
	function spendKey($id = null) {
		if (empty($id)) {
			return false;
		}
		$this->id = $id;
		if ($this->saveField('used', 1)) {
			return true;
		}
		return false;
	}
 
	/**
	 * remove old/invalid keys
	 * does not remove recently used ones (for proper feedback)!
	 * @return boolean success
	 * 2010-06-17 ms
	 */
	function garbigeCollector() {
		$conditions = array(
			$this->alias.'.created <'=>date(FORMAT_DB_DATETIME, time()-MONTH),
		);
		return $this->deleteAll($conditions, false);
	}
 
 
	/**
	 * @param length (defaults to defaultLength)
	 * @return string codekey
	 * 2009-05-13 ms
	 */
	function generateKey($length = null) {
		if (empty($length)) {
			$length = $defaultLength;
		} else {
			if ((class_exists('CommonComponent') || App::import('Component', 'Common')) && method_exists('CommonComponent', 'generatePassword')) {
				return CommonComponent::generatePassword($length);
			} else {
				return $this->_generateKey($length);
			}
		}
	}
 
	/**
	 * backup method - only used if no custom function exists
	 * 2010-06-17 ms
	 */
	function _generateKey($length = null) {
		$chars = "234567890abcdefghijkmnopqrstuvwxyz"; // ABCDEFGHIJKLMNOPQRSTUVWXYZ
		$i = 0;
		$password = "";
		$max = strlen($chars) - 1;
 
		while ($i < $length) {
			$password .= $chars[mt_rand(0, $max)];
			$i++;
		}
		return $password;
	}
 
}
?>
 
No Comments

Posted in CakePHP