jeudi, 25 janvier 2007

Tarpitting, ou comment faire perdre de l'argent aux spammers

Le tarpitting (désolé maman pour l'utilisation d'un mot d'anglais de plus dans mes billets) est un concept émergeant dans le domaine de la protection des mails.

Comme on s'aperçoit qu'on est mal embranché pour réduire la quantité de spam (voir We are loosing this war badly), des solutions en désespoir de cause se mettent petit à petit en place : autant essayer d'ennuyer le plus possible les spammers, en ajoutant un délai lors de la réception d'emails. La constatation est simple : si pour délivrer un mail, on ajoute un temps d'attente d'une seconde, l'utilisateur normal ne sera pas pénaliser car il n'est pas à une seconde près, mais le spammer qui envoie 1'000'000 de spam se verra pénaliser d'un million de seconde d'attente, soit plus de 11 jours. Bien sûr il peutva paralléliser l'envoi de ses mails, mais cela ne va réduire que linéairement son temps de pénalité.

De plus en plus de solution de la sorte voient le jour (principalement les systèmes de greylisting implémentent le tarpitting), et personnellement j'encourage fortement ce genre de solutions : si le mail courant est détecté comme du spam (pour éviter de reproduire ce couteux temps d'attente aux mailing "propres"), je temporise sa réception.

Spammers, chez moi vous allez perdre votre argent, car le temps, c'est de l'argent !

vendredi, 19 janvier 2007

De la sécurité des sessions PHP

Dans la majorité des espaces requierant une authentification sur un site web, le soin du suivi de l'utilisateur est laissé aux sessions, ces petits cookies qui viennent se placer chez le client afin de permettre à l'application web de le reconnaitre lors du passage à la page suivante.

Ce modèle de sécurité a du être imaginé à cause de la nature "connexionless" du protocole HTTP, c'est-à-dire la fermeture de la connexion TCP au serveur entre 2 chargements de page consécutifs (contrairement aux modèles de connexions "continues").

Malheureusement différentes techniques pour voler ces informations de sessions existent, elles se nomment credential token stealing, et sont souvent réalisables grâce à des failles de type XSS.

Cet article va expliquer quel est le point faible des sessions, ainsi que présenter une solution pour en améliorer la sécurité. Un exemple d'implémentation sera une fois de plus donné en PHP.

Problèmes des sessions

Le problème lié aux sessions est que l'identité de l'utilisateur, une fois identifiée, repose entièrement sur ce cookie. Si une personne malveillante parvient à obtenir la valeur du cookie, elle pourra alors se faire passer pour la personne légitime aux yeux de l'application.

Meilleure emprunte (fingerprint)

Une première amélioration est d'associer la valeur du cookie à d'autres éléments qu'une personne malveillante ne peut pas modifier : l'ip de connexion, la signature du navigateur, etc...
Tous ces éléments combinés ensemble donnent ce qu'on appelle l'emprunte de la session, et tous ces éléments sont nécessaires pour pouvoir usurper une identité. La principale difficulté est l'adresse ip de connexion, mais si la personne malveillante est sur le même sous-réseau que la personne légitime, l'adresse ip n'est plus un problème. Autre désavantage d'un fingerprinting étendu, si la personne légitime est sur une connexion à adresse ip dynamique, elle devra se réauthentifier à chaque changement d'adresse ip.

ID de session temporaire

Une autre amélioration est la regénération dynamique de la valeur du cookie. A chaque nouvelle connexion, on test si l'utilisateur est légitime, et on génère une nouvelle valeur qu'on lui envoie.
Si une personne malveillante arrive à voler un cookie, sa valeur ne sera que temporaire et dès le prochain chargement de page, la valeur volée devient obsolète.
Mais cela est aussi vrai à l'inverse, si la personne malveillante arrive à usurper l'identité avant que la personne légitime ne redemande une page, c'est la personne légitime qui sera déconnectée du site.

Implémentation en PHP

Une combinaison des 2 méthodes améliorera la sécurité des sessions, sans pour autant la rendre infaillible.

Voici une petite implémentation en PHP :

<?php
/*
* Inspirated from SecureSession class
* initially written by Vagharshak Tozalakyan <vagh@armdex.com>
*/
class SecureSession {

private $_check_browser;
private $_check_ip_blocks = 0;
private $_padding = '*ftt56+g zwc%&gh7/3-lf%254*6c_qm';
private $_regenerate_id = true;
private $_session_var_name = __CLASS__;

public function _construct($check_browser = true,
$check_ip_block = 0, $regenerate_id = true)
{
$this->_check_browser = $check_browser;
$this->_check_ip_block = $check_ip_block;
$this->_regenerate_id = $regenerate_id;

$_SESSION[$this->_session_var_name] = $this->_fingerprint();
$this->_regenerateId();
}

public function isValid()
{
$this->_regenerateId();
return (isset($_SESSION[$this->_session_var_name])
&& $_SESSION[$this->_session_var_name] == $this->_fingerprint());
}

private function _fingerprint()
{
$fingerprint = "";
if ($this->_check_browser) {
$fingerprint .= $_SERVER['HTTP_USER_AGENT'];
}
if ($this->_check_ip_blocks) {
$num_blocks = min(abs(intval($this->check_ip_blocks)), 4);
$blocks = explode('.', $_SERVER['REMOTE_ADDR']);
for ($i=0; $i<$num_blocks; $i++) {
$fingerprint .= $blocks[$i] . '.';
}
}
return sha1($fingerprint . $this->_padding);
}

private function _regenerateId()
{
if ($this->_regenerate_id && function_exists('session_regenerate_id')) {
session_regenerate_id(true);
}
}
}

?>

jeudi, 18 janvier 2007

A quoi juge-t-on qu'on est trop "geek" ?

Voilà que je me surprends à ajouter subtilement des ; à la fin des lignes d'une lettre de demande de congé pour l'armée...


Cette fois mes craintes se justifient : je suis vraiment un geek.

mercredi, 17 janvier 2007

Une authentification sur les serveurs smtp des providers

Non content de perdre la guerre du spam, les providers contre-attaques.

Relayé par Rags, voici le mail explicatif de Green :

Chers clients de green.ch

Dans le cadre d'un projet commun, les 4 grands fournisseurs de service internet en
Suisse (Bluewin, Cablecom, green.ch et Sunrise) mettent en place des mesures pour
combattre l'affluence massive des spams. La 1.ère étape est l'imposition de
l'authentification du serveur SMTP (Simple Mail Transfert Protocole). Cela signifie
que votre programme eMail, pour la récéption et l'envoi des emails, doit toujours
s'authentifier au niveau du serveur mail avec un nom d'utilisateur et mot de passe.

Il est possible que cela soit déjà le cas à votre niveau. Pour être sûr, nous vous
invitons à procéder à une vérification.

Le guide pour le paramétrage exacte ainsi que la configuration de votre compte eMail
se trouve ici: http://dtg.green.ch nous vous invitons à suivre la démarche pas à
pas.

Si vous utilisez exclusivement le Webmail pour vos eMails, vous ne devez rien
entreprendre.

Il est préférable d'effectuer le contrôle tout de suite afin de vous assurer que vos
eMails seront aussi envoyés à l'avenir.

Nous vous remercions de votre coopération.

Avec nos meilleures salutations

Votre équipe de support de green.ch


Les clients des providers vont donc immédiatement devoir modifier les paramètres de leur compte mail sortant afin d'y ajouter une authentification.

Cette authentification a deux effets :
  • premièrement elle est ABSOLUMENT INUTILE contre l'envoi de spam, puisque les malwares qui envoient du spam passent très majoritairement par des open-proxies, ou se connectent directement sur le smtp du MX du domaine.
  • deuxièmement elle force à avoir une adresse email @<super_provider_de_luxe>, ce qui rend les clients dépendant d'eux, car il est pénible de changer une adresse email déjà diffusée à tous ses amis/contacts. Elle empêche de ce fait le confort d'utilisation que nous fourni des adresses email comme Gmail (à noter que le webmail n'est pas touché par cette restriction).

A mon humble avis, cette solution a dû être trouvée par les économistes du top management et ne sert qu'à se donner un semblant de paraitre d'essayer de combattre le spam, tout en fidélisant de force leur clients.

De ce point de vue, chapeau, il n'aurait jamais été si facile de faire passer la pilule sans cet argument de combattre le spam. D'un point de vu efficacité réelle, autant dire que c'est même pas un pet de constipé dans l'eau, c'est de la poudre aux yeux sans poudre. J'espère simplement que vous, chers providers, avez pensé à renforcer vos équipes de
hotline
, et que vous les avez former pour répondre à la question : "Je reçois toujours autant de spam, que faire ?".

Je serais tellement content de pouvoir expliquer mon poing point de vue à un décideur d'une de ces entreprises...

mardi, 16 janvier 2007

Les 7 règles fondamentales en sécurité

Très souvent oubliées ou minimisées, voici les 7 règles fondamentales en sécurité :

  1. Least privilege : on ne donne que le privilege minimum
  2. Defense in depth : on protége à tous les niveaux
  3. Choke point : on emprunte qu’un seul chemin pour aller au but
  4. Weakest link : la sécurité globale est égale à la sécurité du maillon le plus faible
  5. Default deny : plutôt que d’énumérer les cas pas permis, au risque d’en oublier, on énumère les cas permis.
  6. User participation : on éduque les utilisateurs par rapport à la sécurité
  7. Simplicity : on applique le principe de simplicité
A appliquer sans modération !

lundi, 15 janvier 2007

Statistiques fournies par FeedBurner

Je suis relativement content de mon blog sur Blogger.com, les principaux problèmes que j'y vois sont :

  • Pas la possibilité de faire des trackbacks
  • Pas de statistiques de fréquentation.
Le premier problème n'a pas l'air prêt de se régler, le deuxième peut en revanche être contourner en passant par Feedburner.

Chose faite, il serait bien d'actualiser vos bookmarks de feed et s'abonner à http://feeds.feedburner.com/BenoitPerroud plutôt que l'alternative de Blogger.

dimanche, 14 janvier 2007

Form flooding

Les formulaires sont un des points vulnérables d'une application web, car c'est notamment à travers eux que les "clients" peuvent injecter des données dans l'application.

De plus, même si le formulaire est suffisamment protégé contre l'injection de données, qu'elle soit XSS, SQL, string format ou autre, le formulaire reste vulnérable à un flood :

Comment réagit votre formulaire si un client bien authentifié décide de poster 1'000'000 de fois le formulaire ?


Ce genre d'attaque peut avoir des effets très néfastes...

Il y a plusieurs méthodes pour s'en prémunir, et je vais en présenter une qui, contrairement au captcha, ne requiert pas d'intervention de la part de l'utilisateur (on ne peut pas faire de captcha dans un webservice...), mais protège tout de même notre formulaire.

On va donc attribuer à chaque formulaire un identifiant unique, qui est entré dans une table (ou en var de session) conjointement avec un timestamp. L'identifiant nous permettra d'éviter (au pire de remarquer) la soumission multiple du même formulaire, et grâce au timestamp, nous pourrons mesurer le temps entre le chargement de la page contenant le formulaire et son renvoi au serveur, temps qui ne devrait pas être inférieur à une voir plusieurs secondes pour un utilisateur humain, selon la taille du formulaire.

En recevant le formulaire, le serveur peut donc contrôler si :

1. Le formulaire est valide, i.e. l'identifiant du formulaire est valide
2. Le formulaire n'a pas été envoyé plusieurs fois.
3. Le délai de soumission du formulaire n'est pas trop élevé pour une session donnée.

L'implémentation de cette solution est elle aussi multiple, mais j'en donne un exemple ci-dessous :


__toString(); ?> ...
*
* if (isset($_POST)) {
* $canary = Form_Protector::factory($_POST);
* if (!$canary->is_valid()) {
* if ($canary->exists()) {
* // le canary n'existait pas dans la db, afficher un message d'erreur et recharger la page
* } else {
* // le formulaire a été posté trop rapidement, on peut donc compter le nombre de soumissions durant les 5 dernières minutes, et prendre une des actions suivante
* // --> soit on blacklist l'ip un moment,
* // --> soit on sleep(30) pour temporiser (tarpitting)
* }
* } else {
* // le canary est valide, tout est bien dans le meilleur des mondes.
* }
* }
*/

define("_TIME_TO_SUBMIT_FORM", 2); // temps que l'utilisateur fait pour submiter un formulaire
class Form_Protector {

protected $_canary;
protected $_ip;
protected $_date_request;
protected $_date_request;
protected $_exists = false;

public static $input_name = 'form_protector_canary';

protected function __construct($canary = 0, $ip = "") {
$this->_ip = $_SERVER['REMOTE_ADDR'];
if ($canary === 0) {
$this->_canary = rand();
$this->_insert();
} else {
$this->_canary = $canary;
if ($ip !== "") $this->_ip = $ip;
$this->_load();
}
}

protected function _load() {
$query = sprintf("SELECT * FROM form_protector
WHERE canary = %d AND ip = '%s'" . int_val($this->_canary),
mysql_real_secape($this->_ip));
// query,
// load $this->_date_request, $this->_date_request = time();
// si pas un champ est retourné, on passe $this->_exists à true; et on UPDATE ... SET date_response = $this->_date_request
}

protected function _insert() {
$this->_date_request = time();
$query = sprintf("INSERT INTO form_protector (canary, ip, date_request) VALUES (%d, %s, %d)", int_val($this->_canary), mysql_real_secape($this->_ip), $this->_date_request);
// query
// si pas d'erreur : $this->_exists = true;
}

public function is_valid() {
if ($this->_date_request + _TIME_TO_SUBMIT_FORM <>_exists;
}
public function __toString() {
return '<input value="' . $this->_canary . '" name="form_protector_canary" type="hidden">';
}

public static factory($params = NULL) {
if (is_array($params) && isset($params[self::$name])) {
$canary = $params[self::$name];
} else {
if ($params === NULL) {
$canary = 0;
} else {
$canary = $params;
}
}
return new Form_protector($canary);
}
}

?>

We are losing this war badly

Ou "Quand les RFCs sont trop difficiles à comprendre..."

Quand un serveur SMTP se connecte à un autre pour envoyer un mail, il doit s'annoncer. Cela fait parti du protocole SMTP définit dans la RFC 2821

- Connexion TCP à <server_destinataire> (telnet <server_destinataire> 25 pour simuler le comportent)
- HELO <server_name> (ou EHLO dans le cas de ESMTP)

Ce HELO <server_name> est une petite politesse introduite dans le protocole, mais qui permet, en plus de choisir la version de SMTP lors de l'échange, d'ajouter des tests pour détecter le spam.

En effet, à l'heure actuelle la plupart des moteurs SMTP utilisés pour envoyer du spam ne s'annoncent pas correctement. Le <server_name> est très souvent remplacé par une chaine de caractères aléatoires.
Si on part du principe qu'un serveur mail légitime a une adresse ip fixe, la correspondance entre l'ip inverse de <server_name> et de l'ip de la connexion du serveur serait un très bon test pour contrer le spam. Le problème devient un cauchemar quand même des providers ne configurent par correctement leurs serveurs SMTP :

Received: from smtp-auth-be-03.sunrise.ch (mail-proxy-be-01.sunrise.ch [194.158.229.48])
(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
by dns3.omne-serveurs.net (Postfix) with ESMTP id 342991EEE10
for ; Sun, 31 Dec 2006 02:16:16 +0100 (CET)


Dans cet exemple, il s'agit d'un serveur Sunrise qui s'annonce smtp-auth-be-03.sunrise.ch, et dont l'ip est 194.158.229.48. C'est presque juste, le seul détail est que le reverse de 194.158.229.48 est mail-proxy-be-01.sunrise.ch.

Received: from swip.net (mailfe05.tele2.ch [212.247.154.136])
by dns3.omne-serveurs.net (Postfix) with ESMTP id 0173F763C3C
for ; Wed, 10 Jan 2007 07:54:31 +0100 (CET)


Dans ce deuxième exemple, la configuration est pire : le serveur qui se connecte avec l'ip 212.247.154.136, s'annonce comme étant swip.net, alors que le reverse de swip.net est 212.247.156.1.

En résumé, parce que les personnes qui administrent des serveurs mails ne sont pas un peu plus scrupuleux, comme il est dit dans cet article
We are losing this war badly

jeudi, 11 janvier 2007

Nabuchodonosor, Roi de Babylone, écrivez-moi cela en 4 lettres.


Nabuchodonosor, Roi de Babylone, écrivez-moi cela en 4 lettres.


Cette expression est ma dernière signature en date, et comme d'habitude personne ne comprend son sens profond, alors je me vois contrains de l'expliquer.

Le but de cette phrase est de faire paraitre quelque chose compliqué alors qu'elle ne l'est en réalité pas (tout le monde peut écrire cela en 4 lettres : c e l a. Pas besoin d'être roi de Babylone pour le faire).

Il en est de même dans beaucoup de domaine de l'ingénierie, où des gens ont la fâcheuse tendance à compliquer des concepts simples (pour justifier leur travail, pour faire plus vendeur, ou pour n'importe quelle autre raison qui m'échappe). Il y a bien évidement des cas où cette complication est nécessaire (pour un obfuscateur de code source par exemple), mais retenons et appliquons la devise d'Albert Einstein :

Faites les choses aussi simple que possible, mais pas plus simple.

mardi, 9 janvier 2007

Optimisation du nombre de requêtes SQL dans les collections d'objets et relation n-m

L'orientation objet de PHP n'est plus contestable, mais les problèmes d'optimisation persistent.

Par exemple le malheureusement célèbre n+1 pattern, qui fait que pour afficher une liste de n objets, le + 1 étant la requête qui sélectionne tous les ids des objets à instancier, n + 1 requêtes SQL seront effectuées.

De même dans des relations n-m, la majorité des implémentations sélectionnent les n objets (en n + 1 requêtes donc), puis pour chacun on sélectionne les m objets de la relation. On obtient donc n * m + 1 requêtes.

Le but de cet article est de présenter deux techniques qui, combinées, réduisent les n * m + 1 requêtes en n + m + 1.

Les deux techniques que je vais illustrer ici se nomment object caching et grouped fetching. Elles se combinent très bien, ce qui permet d'optimiser drastiquement les performances d'un script PHP, du points de vue I/O (moins de requêtes), vitesse d'exécution et même mémoire utilisée (les objets ne sont pas dupliqués).

Object caching :

Le principe de l'object caching est de rendre le constructeur de l'objet privé (au pire protégé) et de l'instancier au moyen d'une factory. Puis on ajoute à la classe un tableau statique dans lequel les références des objets instanciés seront placés. La factory va donc regarder dans le tableau de références si l'objet existe, et si ça n'est pas le cas elle va le créer, l'ajouter au tableau et le retourner.


class A {
protected static $objects_cache = array();

public static function factory($id, $class = __CLASS__)
{
if (array_key_exists($id, $class::$objects_cache)) {
return $class::$objects_cache[$id];
} else {
$o = new $class($id);
$class::$objects_cache[$id] = $o;
return $o;
}
}
}


Cette solution pourrait encore être améliorée si les objets pouvaient être partagés entre toutes les instances de PHP. Ce n'est pas le cas à cause de l'architecture share nothing de PHP, et c'est un des points forts des serveurs d'applications.
Un autre désavantage de ce concept, si on a un script qui tourne suffisamment longtemps pour que le grabage collector se lance, est que tous les objets créés sont toujours référencés, même ceux qui pourraient être des candidats potentiels à la finalisation. Ce problème est résolu dans d'autres langages, en Java par exemple grâce aux références faibles (WeakReference).

Grouped fetching

Le principe du grouped fetching, dérivé d'une solution proposée par notre ami Colder, est de charger les données des objets de manière asynchrone et en bloque. Quand un objet est instancié, il est marqué comme non chargé, et sa référence est placée dans un tableau global de la classe. Puis lors d'un accès à un champ non chargé, la classe va sélectionner dans la base de données tous les objets instanciés mais pas encore chargés. Plus on retarde les accès aux attributs d'un objet, plus on va paralléliser les requêtes SQL.


class A {
private static $_to_load = array();
private $_is_loaded = false;
public function __construct($id)
{
$this->id = $id;
self::$_to_load[$id] = $this;
}

public function __get($attribut)
{
if (!$this->_is_loaded) {
self::_groupedLoad();
}
return $this[$attribut];
}

private function _setLoaded()
{
$this->is_loaded = true;
}

private static function _groupedLoad()
{
$ids = implode(', ', self::$_to_load);
$query = 'SELECT * FROM ' . self::$_table . ' WHERE id IN ( ' . $ids . ' ) ';
$res = db_query($query);
while ($row = $res->getNext()) {
self::_to_load[$row['id']]->_initByArray($row);
self::_to_load[$row['id']]->_setLoaded;
}
}
}


L'overhead de ce concept est très faible (un tableau de références supplémentaire), et le nombre d'accès à la base de données sont grandement réduit. Mais le problème de la finalisation des objets se repose aussi.

lundi, 8 janvier 2007

Lettre au père Noël

Trouvé sur linuxfr, tellement plein de vrai que je suis obligé de broadcaster :

Lettre au père Noël

dimanche, 7 janvier 2007

Samourai

Et voici un autre jeu à boire intéressant, appris sur le tas dans de sombres circonstances, et dont l'issue fut fatale pour plus d'un d'entre nous. Il s'appelle Samourai , et le principe est très simple : des personnes autour d'une table devant accomplir chacun leur tour une ou plusieurs actions.

Les actions sont simples, elles consistent à désigner la personne suivante à réaliser l'action, à boire un verre de vodka cul-sec réaliser un gage ou faire un action collective.

Le nom des actions étant normalement en japonais, je me permets d'y faire ici une transcription phonétique.

L'action la plus simple consiste à désigner son voisin direct par un coup de coude latéral, le poing du bras donnant le coup de coude dans la paume l'autre main, en criant "Hi-ha". La seule contrainte sur cette action est qu'elle doit se faire dans la même direction que le coup de coude reçu (donc on reçoit un coup de coude du coté gauche et on renvoie un coup de coude sur notre droite, respectivement le coté gauche de notre voisin).

La deuxième action permet de désigner n'importe quelle personne autour de la table. Cela se fait en tendant sa main bien droite en direction de cette personne et en criant "Katana".

L'action "Oups" permet de faire sauter le tour du voisin dans lequel l'action de se déplace. Elle se réalise en décrivant un cercle de la taille d'un ballon de foot avec ses 2 mains.

L'action pour bloquer le coup de coude et faire repartir le mouvement dans le sens contraire s'appelle "Wasaï". Elle se réalise en plaçant devant soi son avant-bras à la verticale et son poing contre son coude opposé, le tout en criant "Wasaï". Si on reçoit un "Hi-ha" par la gauche, on doit lever son avant-bras droite, de manière à avoir le coude gauche à l'horizontal pour répondre à son voisin de gauche.

Les 2 prochaines actions peuvent être exécuter en plus de l'action de base, et tous les autres joueurs doivent y répondre :
Si le jour crie "Aligato", les autres joueurs joignent leurs mains dans un signe de prière, s'inclinent vers le joueur qui a prononcé ce mot et réponde "Aligato san".
De même si le joueur crie "Samourai", les autres joueurs imitent l'action de resserrer un noeud autour de la taille en criant "Hou".

Puis vient le moment du gage. Le joueur qui exécute incorrectement un action cité doit donc boire un verre exécuter un gage. Avant son gage, il doit prononcer "Arakiri", que tout le monde doit applaudir, puis il doit remercier Yogi à la fin du gage. Si ces 2 parties du gage sont mal exécutées, le gage entier doit être refait.

Dernière action en date, similaire à "Katana", le joueur désigne un autre joueur en criant "Nikon". Le joueur visé doit alors viser un autre joueur en imitant d'avoir un appareil photo dans les mains et crie "click-clack".

A noter qu'il serait judicieux d'introduire progressivement les règles, avec dans l'ordre :

  1. "Hi-ha"
  2. "Wasaï"
  3. "Oups"
  4. "Samourai"
  5. "Katana"
  6. "Aligato"
  7. "Nikon"


Bonne guerre Bon jeu !

De l'art d'éviter les requêtes SQL inutiles

Dans le domaine de l'optimisation, une chose simple à faire est d'essayer de réduire au strict minimum le nombre de requêtes à faire à la base de données.

Je souhaite simplement rendre attentif au problème posé par les procédures embarquées et autres tirggers :

Dans le cas d'une mise-à-jour d'un objet mappé sur une table d'une base de données, les champs de l'objet sont remplacés (après nettoyage...) par ceux du formulaire. On pourrait donc penser qu'il est inutile de faire un SELECT juste après un UPDATE, car les données mises-à-jour sont celles fournies.

Cela est vrai sans compter sur les triggers déclenchés en cas de mises-à-jour : sans un SELECT après l'UPDATE, certains champs peuvent contenir des données fausses car non traitées par le trigger. Les champs auto-timestamp de Mysql sont un exemple tout simple de trigger à ne pas oublier...

jeudi, 4 janvier 2007

Bonne année (2007) !

Version geek :

<?php

if (date('n') === 1
&& ($day = date('j')) >= 1 && $day < 10) {
echo '<p>Bonne année ' . date('Y') . ' à tous !</p>';
}

?>


Version "Le Chat" :
Meilleurs voeux pour toute la vie.. Comme ça c'est fait une
fois pour toutes !