ZF : ACL et ressources multiple

Lorsque l’on utilise des ACL dans Zend Framework, une chose assez embêtante est de devoir tout mettre en place1. Pour ma part, j’ai par facilité voulu ajouté le support de ressource multiple.

Avant de commencer, il convient de contextualisé les choses, les ACL de ZF pouvant être utilisé de bien des manière.

Dans le cas qui nous intéresse, j’ai simplement défini ceci :

  • les ressources2 = module.controller
  • les privilèges3 = action

Ce que je voulait c’est pouvoir définir une ressource pour tous les contrôleurs. La syntaxe évidente qu’il m’est venu est la suivante : module.*

Dans ma classe qui étend Zend_ACL j’ai simplement fait ceci :

    public function isAllowed($role = null, $resource = null, $privilege = null)
    {
    	if (null === $resource)
    		return parent::isAllowed($role, $resource, $privilege);
 
    	$resources = $this->getResourcesPossibility($resource);
    	foreach($resources as $resource)
    	{
    		if ($this->has($resource) && parent::isAllowed($role, $resource, $privilege))
    		{
    			return true;
    		}
    	}
    	return false;
    }
 
    public function getResourcesPossibility($resource = null)
    {
    	$ret = array($resource);
    	if (null !== $resource)
    	{
    		$resources = explode('.', $resource);
    		$cptRessources = count($resources);
    		if ($cptRessources >= 2)
    		{
    			$resources[ $cptRessources - 1 ] = '*';
    		}
    		$ret[] = implode('.', $resources);
    	}
    	return $ret;
    }

Ceci peut bien entendu être enrichi mais permet au moins de profiter de l’utilisation des ACL dans le menu et sur des aides de vue qui serait éventuellement définie comme expliqué dans la plupart des tutoriaux.

Pour en savoir plus sur les ACL et Zend Framework, je vous renvoi a un très bon article.

  1. surtout si on utilise l’aide de vue pour généré un menu (Zend_navigation)
  2. resource
  3. privilege

ZF : génération automatique du fichier de navigation et ACL

Lors de l’utilisation du mécanisme d’ACL et de génération de menu dans Zend Framework, il est intéressant de limiter l’affichage de ce menu en utilisant les ressources et privilèges associer.

Pour ma part, j’utilise un fichier XML pour construire mon menu, mon sitemap, … 1. Et comme beaucoup je génère mon projet ZF à l’aide de Zend_Tool. Je trouvais donc dommage de devoir réécrire pratiquement la même chose que ce que j’avais déclaré dans Zend_Tool pour reconstruire mon menu. J’ai donc décidé de rapidement écrire un petit script qui reprendrait le fichier XML du projet et le transformerait en menu …

À noter que le script devrait certainement être amélioré, mais que cela permet un gain de temps considérable …

Téléchargement

  1. Comme expliquer dans le manuel http://framework.zend.com/manual/fr/zend.navigation.html

Réécriture d’URL, alias et plusieurs développeurs sur Apache

Lorsque l’on travaille à plusieurs sur un projet, c’est toujours intéressant. Malheureusement, cela peut aussi entrainer divers problèmes. Je vais tenter de vous expliquer un obstacle qui peut vite devenir très chi*nt…

Pour présenter cette problématique, je vais prendre exemple sur ce que je développe actuellement. Le site en cours de création se base sur zend framework et nécessite une réécriture d’URL. Il faut savoir qu’Apache utilise le chemin physique1 comme base pour calculer le chemin vers le fichier réécrit sauf si on lui précise une directive RewriteBase différente. Le problème survient à cet endroit, plusieurs développeurs entrainent plusieurs machines et donc plusieurs configurations différentes!

La solution de base est que chaque personne utilisant un alias Apache définit un RewriteBase. Cependant, cela veut dire qu’a chaque nouvelle version du fichier .htaccess il faut redéfinir celui-ci.

La réponse la plus simple consiste à utiliser un RewriteCond sur l’hostname du serveur et bien entendu à l’utiliser lors de l’accès aux tests locaux ou non …

Exemple de configuration :

  • nom du serveur : grummfy
  • URL appelée : http://grummfy/serveur/dev/projet/example.com/… (Si vous utilise http://localhost/ la directive HTTP_HOST vaudra localhost)
  • ALIAS : /serveur/ => /media/data/serveur/

Le fichier .htacccess contiendra ceci :
SetEnv APPLICATION_ENV development
php_value session.auto_start 0
php_flag magic_quotes_gpc off
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteCond %{HTTP_HOST} grummfy
RewriteRule ^.*$ /serveur/dev/projet/example.com/index.php [NC,L]
RewriteRule ^.*$ index.php [NC,L]

Le fait d’utiliser le rewrite flag « L » permet de sortir de la réécriture d’URL. Si le hostname du serveur n’est pas grummfy il appliquera la règle par défaut, à savoir tenter de trouver index.php dans /media/data/serveur/dev/projet/example.com/ comme si on avait effectuer un appel depuis http://media/data/serveur/dev/projet/example.com/

  1. physical-directory-path

WebHook Google Code – recevoir un mail à chaque commit

Dans Google code il y a la possibilité d’utiliser un gestionnaire de version tel que subversion (svn) ou mercurial (hg). C’est bien pratique, mais malheureusement, de base, rien n’est prévu pour prévenir (excepté par flux RSS) les gens de ces mises à jour. Cependant, Google code permet d’utiliser un webhook en post commit.

Qu’est-ce qu’un webhook?

Un webhook c’est un « crochet web », c’est-à-dire une URL a appelée après (avant ou pendant) une action X. Dans notre cas, après chaque commit une URL est appelée.

Utilisation

Voici un exemple de code que j’utilise pour plusieurs de mes projets :

<?php
// project name
$projects = array('mon-super-projet');
 
// google code webhook key
$keys = array(
	'b-box'	=> 'Top-Secret_key_fourni_par_google-dans-l-adminsitration'
);
 
//user agent from google code
$useragent = 'Google Code Project Hosting (+http://code.google.com/p/support/wiki/PostCommitWebHooks)';
 
//email of all owner (eg. project chief)
$owners = array('vous@example.com');
 
//email of all team members except owners
$users = array('toi@example.com');
 
//sender of email
$sender = 'WebHook mailer<webmaster@exemple.com>';
 
//----------------------------------------------------------------------
$project = (isset($_GET['p']))?$_GET['p']:'';
$revision = (isset($_GET['r']))?intval($_GET['r']):-99;
$data = file_get_contents('php://input');
$digest = (isset($_SERVER['HTTP_GOOGLE_CODE_PROJECT_HOSTING_HOOK_HMAC']))?$_SERVER['HTTP_GOOGLE_CODE_PROJECT_HOSTING_HOOK_HMAC']:'';
 
//----------------------------------------------------------------------
/**
 * Send a mail
 * @param string $from email of the sender : sample@example.com or "name"<sample@example.com>;
 * @param array $to [a] => list of email [cc], [bcc] (hidden), ...
 * @param string $subject
 * @param string $body
 * @return bool true if success
 */
function mailer($from, array $to, $subject, $body)
{
	if (empty($to))
	{
		return false;
	}
 
	$headers = 'From: ' . $from . "\n";
 
	$a = '';
 
	if (isset($to['a']) &amp;&amp; !empty($to['a']))
	{
		$a = implode(',', $to['a']);
	}
 
	if (isset($to['bcc']) &amp;&amp; !empty($to['bcc']))
	{
		$headers .= 'Bcc: ' . implode(',', $to['bcc']) . "\n";
	}
 
	if (isset($to['cc']) &amp;&amp; !empty($to['cc']))
	{
		$headers .= 'Cc: ' . implode(',', $to['cc']) . "\n";
	}
 
	$headers .= 'MIME-Version: 1.0' . "\n";
	$headers .= 'Content-Type: text/plain; charset="UTF-8"' . "\n";
	$headers .= 'Content-Transfer-Encoding: 8bit' . "\n";
	$headers .= 'X-Mailer: PHP/' . phpversion();
 
	return mail($a, '[webhook]' . $subject, $body, $headers);
}
 
function failed($test_id, $msg)
{
	global $sender, $owners;
 
	$msg .= "\n--\nWebHook mail from the Google code project";
 
	mailer($sender, array('bcc' => $owners), 'failed test #' . $test_id, $msg);
 
	die('KO');
}
 
function get_ip()
{ 
	return (isset($_SERVER['HTTP_X_FORWARDED_FOR']))?$_SERVER['HTTP_X_FORWARDED_FOR']:(isset($_SERVER['HTTP_CLIENT_IP']))?$_SERVER['HTTP_CLIENT_IP']:$_SERVER['REMOTE_ADDR'];
}
 
//----------------------------------------------------------------------
if ($useragent != $_SERVER['HTTP_USER_AGENT'])
{
	// failed 1
	failed(1, 'User agent is bad : ' . htmlspecialchars($_SERVER['HTTP_USER_AGENT']) . "\n\nFrom : " . get_ip());
}
elseif (empty($project) || !in_array($project, $projects))
{
	// failed 2
	failed(2, 'No project set : ' . htmlspecialchars($project) . "\n\nFrom : " . get_ip());
}
else
{
	$hmac = hash_hmac('md5', $data, $keys[ $project ]);
	$data = json_decode($data, true);
 
	if (empty($digest) || $digest != $hmac)
	{
		// failed 3
		failed(3, 'Bad digest : ' . $digest . ' vs ' . $hmac . "\n\nFrom : " . get_ip());
	}
	elseif (intval($data['revision_count']) != count($data['revisions']))
	{
		// failed 4
		failed(4, 'Bad count : ' . count($data['revisions']) . ' vs ' . intval($data['revision_count']) . "\n\nFrom : " . get_ip());
	}
	else
	{
		$mail_body = '';
		foreach($data['revisions'] as $_revision)
		{
			$mail_body .= 'Revision : ' . "\t" . htmlentities($_revision['revision']) . ' from ' . htmlentities($_revision['author']) . ' at ' . date('Y-m-d H:i', intval($_revision['timestamp'])) . "\n";
			$mail_body .= 'Added : ' . "\t" . implode("\n\t\t", htmlentities($_revision['added'])) . "\n";
			$mail_body .= 'Modified : ' . "\t" . implode("\n\t\t\t", htmlentities($_revision['modified'])) . "\n";
			$mail_body .= 'Removed : ' . "\t" . implode("\n\t\t\t", htmlentities($_revision['removed'])) . "\n\n";
//			$mail_body .= 'URL : ' . "\t\t" . htmlentities($_revision['url']) . "\n\n";
			$mail_body .= 'Message : ' . "\t" . htmlentities($_revision['message']) . "\n\n\n";
//			$_revision['path_count'];
		}
		$mail_body .= "\n--\nWebHook mail from the Google code project : " . $project . "\nhttp://code.google.com/p/" . $project . "/\n";
 
		mailer($sender, array('bcc' => $owners + $users), '[' . $project . ']New revision #' . $revision, $mail_body);
	}
}
 
exit('OK');
 
# EOF

Plus d’info : PostCommitWebHooks

Espace de nom PHP et chargement automatique

PHP 5.3 ajoute une notion intéressante : les espaces de nom (ou namespace en anglais). Les espaces de nom permettent de séparer différents … « espace« , permettant ainsi d’avoir deux classe portant le même nom. Idéal pour l’utilisation de framework mais aussi de « l’isolation » de certains composants. Voyons voir comment créer un chargeur automatique (ou autoloader) comprenant les espaces de nom.

Si vous êtes sous Ms Windows, aucun problème un simple spl_autoload_register(); suffit. Malheureusement, sous *nix un bug existe il faudra donc implémenté une solution maison.

Le code

Ce code provient du « PHP Standards Working Group » :

function autoload($className)
{
	$className = ltrim($className, '\\');
	$fileName  = '';
	$namespace = '';
	if ($lastNsPos = strripos($className, '\\'))
	{
		$namespace = substr($className, 0, $lastNsPos);
		$className = substr($className, $lastNsPos + 1);
		$fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
	}
	$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
 
	require $fileName;
}

Pour l’utiliser, il faut définir deux choses :

  1. Ne pas oublier de modifier l’include path si nécessaire.
  2. Ajouter cette fonction au chargeur de classes présent.

L’exemple

Voici un exemple un peu plus complet.
index.php

<?php
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/lib/');//on ajoute le dossier lib dans l'include path
spl_autoload_register();//sur windows ceci devrait suffire .
//le code pour les autres
//----------------------------------------------------------
function autoload($className)
{
	$className = ltrim($className, '\\');
	$fileName  = '';
	$namespace = '';
	if ($lastNsPos = strripos($className, '\\'))
	{
		$namespace = substr($className, 0, $lastNsPos);
		$className = substr($className, $lastNsPos + 1);
		$fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
	}
	$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
 
	require $fileName;
}
spl_autoload_register('autoload');
//----------------------------------------------------------
//fin du code de fix
 
use grummfy\test\Test;
Test::sayHello();
 
\grummfy\std\Test::sayHello();
 
# EOF

./lib/grummfy/test/Test.php

<?php
namespace grummfy\test;//pour rappel ceci doit-être la première instruction php (et on ne doit pas avoir de HTML avant)
echo 'Je suis inclus (' . __FILE__ . ')!';
class Test
{
	public static function sayHello()
	{
		echo 'Bonjour depuis ' . __CLASS__;
	}
}
 
# EOF

./lib/grummfy/std/Test.php

<?php
namespace grummfy\std;
echo 'Je suis inclus (' . __FILE__ . ')!';
class Test
{
	public static function sayHello()
	{
		echo 'Bonjour depuis ' . __CLASS__;
	}
}
 
# EOF

Le résultat devrait être :

Je suis inclus (/.../lib/grummfy/test/Test.php)!
Bonjour depuis grummfy\test\Test
Je suis inclus (/.../lib/grummfy/std/Test.php)!
Bonjour depuis grummfy\std\Test