PROJET AUTOBLOG


Portail Yosko.net

Site original : Portail Yosko.net

⇐ retour index

Site PHP : "se souvenir de moi" ?

mardi 27 novembre 2012 à 17:00
Pour identifier un utilisateur en PHP, deux principales techniques s'affrontent : les sessions et les cookies
Et si on pouvait profiter des deux réunies, avec des fonctionnalités supplémentaires ? C'est ce que j'ai voulu mettre en place suite à mes discussion avec idleman et sebsauvage.

EDIT 2013-03-16 : une version plus aboutie de ce projet est présentée dans cet article, et est disponible sur GitHub.

Objectifs : que veut-on ?


  1. Distinguer les utilisateurs connectés/déconnectés
  2. Connexions multi-utilisateurs
  3. Connexions depuis différentes machines / différents navigateurs pour un même utilisateur.
  4. Que l'utilisateur puisse conserver une connexion "longue" s'il le souhaite (case à cocher "se souvenir de moi"). Par exemple 1 mois.
  5. Conserver une relative sécurité en évitant de stocker des données sensibles côté client, et en vérifiant l'IP de l'utilisateur

Presque tout cela est déjà possible avec les sessions PHP ($_SESSION). La seule limitation : la session est conservée en mémoire, et sa durée dépend de la configuration PHP et de l'état du serveur.

Solution 1 : augmenter la durée de session


Votre serveur comporte un paramètre PHP nommé gc_maxlifetime, (valeur par défaut : 1440). Cela indique le nombre de secondes (1440s = 24 minutes) au bout desquelles la session peut potentiellement expirer. On peut le modifier directement depuis un script :

ini_set('session.gc_maxlifetime', 3600);    //durée augmentée à 1 heure

Problèmes :
  1. La méthode ini_set n'est pas disponible chez tous les hébergeurs
  2. Il n'est pas forcément judicieux d'étendre trop cette valeur. De ce que j'en ai lu sur le web (ici), cela augmente d'autant la quantité de données stockées dans memcached, et peut présenter une charge non négligeable
  3. Un serveur qui redémarre, un gros site qui nécessite plusieurs serveurs, et il devient difficile de gérer correctement la session.

Solution 2 : cookies & stockage serveur


Note : vous pouvez télécharger une implémentation de cette solution en fin d'article.

On va continuer à utiliser la session PHP "classique", tout en ajoutant par dessus une couche permettant de la relancer lorsqu'elle se termine trop tôt. Ce système de session "maison", basé sur les cookie pour le côté client et sur une sauvegarde sous forme de fichiers texte ou de base de données pour le serveur. Ça donne un peu l'impression de réinventer la roue, mais on est obligé d'y passer.

Résumé du principe :

Mise en oeuvre


Paramètres de notre système :
$config = array();
$config['LTDir'] = 'cache/'; //dossier où seront stockés les fichiers de session
$config['nbLTSession'] = 200; //nombre max de sessions long-terme simultanées
$config['LTDuration'] = 2592000; //durée d'une session long-terme (2592000 = 1 mois)

A partir de là, on va, au début de chacune de nos pages, vérifier l'authentification de l'utilisateur en appelant la fonction suivante (retourne true/false) :

$isLoggedIn = logUser();

Si l'utilisateur n'est pas connecté, on affichera alors l'écran de connexion, sinon on lui affichera un contenu privé et personnalisé.

La fonction en elle-même :

function logUser() {
global $config;

//démarrer la session PHP
session_start();

//déconnexion en cours ou IP incorrecte
if(isset($_GET['logout'])
|| isset($_SESSION['ip']) && $_SESSION['ip']!=getIpAddress()) {
//suppression de la session "long-terme"
unsetLTSession($_SESSION['uid']);
setcookie('yosloginlt', null, time()-31536000,
dirname($_SERVER['SCRIPT_NAME']).'/',
'', false, true);

//suppression de la session PHP
unset($_SESSION['uid']);
unset($_SESSION['ip']);
unset($_SESSION['login']);
unset($_SESSION['userRelatedInformation']);

session_set_cookie_params(time()-31536000, dirname($_SERVER['SCRIPT_NAME']).'/');
session_destroy();

header("Location: index.php");

//si la session PHP est expirée mais que le cookie de session long-terme existe
} elseif(!isset($_SESSION['uid']) && isset($_COOKIE['yosloginlt'])) {
//récupération des données de session long-terme
$LTSession = getLTSession($_COOKIE['yosloginlt']);
if($LTSession !== false) {
//rétablissement de la session PHP
$_SESSION['uid']=$_COOKIE['yosloginlt'];
$_SESSION['ip']=$LTSession['ip'];
$_SESSION['login']=$LTSession['login'];
} else {
//si le cookie ne correspond à aucune session, on le supprime
setcookie('yosloginlt', null, time()-31536000,
dirname($_SERVER['SCRIPT_NAME']).'/',
'', false, true);
}

//si l'utilisateur est en train de se connecter
} elseif (isset($_POST['submitLogin'])
&& isset($_POST['login']) && trim($_POST['login']) != ""
&& isset($_POST['password']) && trim($_POST['password']) != "") {
//récupération des données utilisateur
$user = getUser($_POST['login']);
//vérification du mot de passe
if(!empty($user) && sha1($_POST['password']) == $user['password']) {
//établissement de la session PHP
$_SESSION['uid']=sha1(uniqid('',true).'_'.mt_rand());
$_SESSION['ip']=getIpAddress();
$_SESSION['login']=$user['login'];

//si l'utilisateur a coché la case "se souvenir de moi"
if(isset($_POST['remember']) && $_POST['remember'] == "remember") {
//enregistrer la session long-terme
$LTSession = array();
$LTSession['login'] = $_SESSION['login'];
$LTSession['ip'] = $_SESSION['ip'];
setLTSession($_SESSION['uid'], $LTSession);

//nettoyage des vieilles sessions
flushOldLTSessions();
}

header("Location: $_SERVER[REQUEST_URI]");
}
}

//si l'utilisateur est connecté
if (!empty($_SESSION['uid'])) {
//mise à jour de la date d'expiration du cookie long-terme
if(isset($_COOKIE['yosloginlt'])
|| isset($_POST['remember']) && $_POST['remember'] == "remember") {
setcookie('yosloginlt', $_SESSION['uid'], time()+$config['LTDuration'],
dirname($_SERVER['SCRIPT_NAME']).'/',
'', false, true);
}
return true;
} else {
return false;
}
}

Récupération de l'utilisateur (à adapter selon votre architecture technique).
function getUser($login) {
return(array("login" => "yosko", "password" => sha1("yosko"));
}

Récupérer l'adresse IP de l'utilisateur, même pour un serveur utilisant un proxy (basé sur cette discussion) :
function getIpAddress(){
foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key){
if (array_key_exists($key, $_SERVER) === true){
foreach (explode(',', $_SERVER[$key]) as $ip){
$ip = trim($ip); // just to be safe
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false){
return $ip;
}
}
}
}
}

Et les fonctions de stockage de session long-terme (à adapter selon si vous utilisez des fichiers ou une base de données) :

function setLTSession($sid, $value) {
global $config;

$fp = fopen($config['LTDir'].$sid, 'w');
fwrite($fp, gzdeflate(json_encode($value)));
fclose($fp);
}

function getLTSession($sid) {
global $config;

$dir = $config['LTDir'];

$value = false;
if (file_exists($dir.$sid)) {

//expiration de la session long-terme
if(filemtime($dir.$sid)+$config['LTDuration'] <= time()) {
unsetLTSession($sid);
$value = false;
} else {
$value = json_decode(gzinflate(file_get_contents($dir.$sid)), true);
//mise-à-jour de la date de modification
touch($dir.$sid);
}
}
return($value);
}

function unsetLTSession($sid) {
global $config;

if (file_exists($config['LTDir'].$sid)) {
unlink($config['LTDir'].$sid);
}
}

function flushOldLTSessions() {
global $config;

$dir = $config['LTDir'];

//liste des fichiers de session
$files = array();
if ($dh = opendir($dir)) {
while ($file = readdir($dh)) {
if(!is_dir($dir.$file)) {
if ($file != "." && $file != "..") {
$files[$file] = filemtime($dir.$file);
}
}
}
closedir($dh);
}

//tri par date (plus récents en premier)
arsort($files);

//vérification de chaque fichier
$i = 1;
foreach($files as $file => $date) {
if ($i > $config['nbLTSession'] || $date+$config['LTDuration'] <= time()) {
unsetLTSession($file);
}
++$i;
}
}

Conclusion


Et voilà \o/ (qu'est-ce que vous espériez, comme conclusion ? :-P)

Plus sérieusement, je suis sûr que ce script est encore imparfait. Mais il ne demande qu'à être amélioré, alors n'hésitez surtout pas à me faire part de vos idées. Entre autres, je ne suis pas expert en sécurité, et je ne serais donc pas surpris qu'il y a quelques failles dans ce système...

Télécharger une implémentation fonctionnelle : yoslogin-v1.zip (5ko)

Sources :

Améliorations possible :

DDb : travaillez votre mémoire onirique

mardi 13 novembre 2012 à 20:35
Dans le but de noter mes rêves et ceux de mes amis, j'ai réalisé un petit CMS pour les noter. Je vous présente aujourd'hui le résultat de mon travail : DDb.

DDb (la Dream Database, aka base de données de rêves) est un petit outil en ligne permettant de noter ses rêves et ceux de ses amis, et de les consulter.

En effet, le meilleur moyen de se souvenir de ses rêves c'est, paraît-il, de prendre l'habitude de les noter. Mais cela peut avoir d'autres utilités, comme aider à faire des rêves lucides (où l'on est conscient d'être dans un rêve, ce qui offre l'opportunité de modifier le cours du rêve).

ddb-dream.jpg

Grâce à la DDb, travaillez sur vos rêves et partagez-les entre amis. Vous pouvez ainsi les ajouter, les modifier, les supprimer, les rechercher sur divers critères. Je serais presque tenté d'ajouter que "la seule limite est votre imagination", mais ça serait sans doute un peu prétentieux.

ddb-add-or-edit.jpg

EDIT : DDb est réalisé en HTML5 / PHP / PDO-SQLite (merci H3 pour la remarque :-P)
EDIT bis : DDb comporte désormais un flux RSS et une option de connexion "se souvenir de moi"

J'ai essayé de faire en sorte que l'installation soit la plus simple possible (vu qu'il s'agit d'un petit script, ça aurait été dommage de compliquer les choses). Pour plus d'info, rendez-vous sur la page dédiée au projet :

ddb-logo.png

>> Aller sur la page de DDb <<

Et surtout, si vous rencontrez le moindre problème ou que vous avez des idées d'améliorations, n'hésitez pas à m'en faire part !

Avator, nouvel outil de création d'avatar (beta)

vendredi 2 novembre 2012 à 12:40
Je reviens (enfin !) de vacance, et après en avoir terminé avec une semaine de boulot assez chargée, je trouve enfin le temps de vous écrire à nouveau. Et ça faisait un sacré petit bout de temps que je voulais présenter ce projet.

Rappellez-vous, après l'adaptation d'Anime Girl Generator en C#.Net, je vous avais évoqué mon envie d'en faire une variante en C++/Qt multiplateforme et open-source. Si je n'ai pas immédiatement envisagé de la réaliser, j'ai commencé à travailler sur un autre outil qui a finalement dérivé pour fourni des fonctionnalités apparentées. J'ai l'honneur de vous présenter : Avator !

En réalité, j'étais parti sur un outil de gestion des couleurs en Pixel-Art suite à mes recherches sur le sujet. Mais une fois le premier jet en place, j'ai re-codé la plupart des fonctionnalités qui m'intéressaient de ma version d'AGG.

Avator, un "avatar generator"

Exemple davatar realise avec Avator 07

>> Aller sur la page d'Avator <<

Du coup, je suis prêt à vous présenter aujourd'hui une première version d'Avator, dont voici les principales fonctionnalités :

Et un petit aperçu, un :

Apercu de lecran principal dAvator

L'interface sera probablement revue dans la version finale,
pour plus de simplicité et d'accessibilité

La gestion des couleurs dans Avator étant un peu particulière, elle ne peut s'utiliser que sur des images parfaitement compatible.
Mais si on omet cette fonctionnalité, Avator reste compatible avec les ressources existantes suivantes :

Si vous avez des questions, des remarques, des préréglages de couleurs à proposer ou des ressources particulières que vous souhaiteriez utiliser dans Avator, n'hésitez pas à me contacter.

Apparté sur les couleurs dans Avator

Pour ceux que ça intéresse (je ne voudrais pas saouler les autres avec mes propos techniques) :

La gestion des couleurs est un peu particulière, puisqu'elle a été pensée pour mes travaux en pixel art. Elle peut même sembler complexe de prime abord, mais les variations d'ombre et de lumière d'une même couleur sont calculée selon un procédé artistique et mathématique censé faciliter vos choix.

Apercu de lecran de conversion des images dans Avator

Pour jouer avec les couleurs dans Avator, les images doivent être en "couleurs indexées".
Un outil de conversion est donc intégré pour réaliser cela

Si j'ai abordé les couleurs sous cet angle, c'était en réalité pour répondre à un besoin personnel : je galère toujours à sélectionner mes couleurs pour mes dessins en pixel art, et j'ai justement l'intention d'en faire beaucoup dans un avenir proche (j'ai hâte de vous montrer ça, mais y'a encore un sacré boulot à abattre avant que ça ne devienne possible).

En gros, chaque zone de couleur comporte un maximum de 6 variantes (en dégradé, exprimant les zones d'ombres et de lumières). Plutôt que de refaire une rotation basique de couleur comme le fait AGG, j'ai préféré me baser sur mes travaux récents pour essayer d'obtenir toujours des couleurs "réalistes". En effet, la rotation donnait parfois des résultats très artificiels et assez dégueulasse...

PS : un grand merci à tous ceux qui ont accepté de tester ça en avant-première, et ont gentiment essuyé les premiers plâtres. On ne m'a remonté que peu de bugs, mais je suis sûr qu'il en reste !

RSS est mort, longue vie à RSS !

samedi 6 octobre 2012 à 00:35

Cela fait plusieurs mois que j'entend parler de l'obsolescence soit-disant avérée (ou tout du moins programmée) de RSS et de son compagnon, Atom. Tout récemment encore, Google a prit des décisions sur feedburner qui laissent entendre qu'ils souhaitent s'en éloigner (ce n'est pas forcément plus mal, puisqu'il s'agissait d'une "petite" partie, très fermée, des flux).

De mon côté, j'ai tendance à ressentir chaque jour un peu plus l'utilité de ces flux d'information. Je n'ai commencé à les utilisé qu'il y a peu (début 2010), et je peux de moins en moins m'en passer. J'agrémente jour après jour mon aggrégateur d'agrébles sources d'informations (ouais je sais, mes allitérations sont nazes...).

Il y a bien quelques outils qu'on tend à considérer comme des concurrents : les microblogs et autres outils de repost permettent à autrui d'aggréger l'information à votre place, ne vous en laissant que la crème de la crème comme susbtrat à votre vision du monde (ou de la toile). Mais n'est-ce pas là leur plus grand défaut ? Tout l'intérêt que je vois à mes flux, c'est que je les choisis, les prend de divers horizons, que je cherche à conserver un esprit critique sur mes sources d'informations (pas que j'y arrive particulièrement, hein... surtout si on considère que 80% de mes flux concernent les lolcats et les mèmes...). Et puis même pour suivre des microblogs, je passe par des flux.

J'en viens à me demander si ceux qui déclarent RSS comme mort n'y cherchaient pas, à tort, un outil supplémentaire pour faire des sous, et n'y auraient vu qu'un moyen de perdre des visiteurs sur leur site... Bref des gens qui ne veulent pas qu'on leur impose de fournir leurs données sous forme de flux parce que ça n'est pas assez rentable (et qu'on ne me parle pas de feedburner)...

feed.png

Je déteste les article sans illustration, donc voici une icône RSS

Mais bon, trève de discours sombres, et sommes toutes assez peu vérifiable, et passons à la partie la plus intéressante : mes flux !
Je me suis dit qu'il pourrait être intéressant de vous en partager quelques-uns (juste une 60aine parmi les 256 auxquels je suis actuellement abonné) :

J'en ai probablement oublié des très importants (voir vitaux), mais on va s'arrêter là pour l'instant.

Keep Calm and Rock On.

EDIT : promis, idleman, la prochaine fois je fais plus court... Ah mince, non, je vais devoir présenter Avator, et ça va être dur de faire concis... -__-

xkcd: Click and Drag

mercredi 19 septembre 2012 à 19:10
Le dernier strip d'xkcd est vraiment fun. Un strip dynamique gigantesque, bourré de détails marrant cachés.
Bon, je sais que je me suis déjà fait griller par certains, mais j'ai quand même décidé d'aller au bout de mon petit jeu : je me suis amusé à vous en faire une miniature complète du strip d'xkcd.

xkcd-click-and-drag-thumb.png

Cliquez pour agrandir un peu (~5200x2400 pixels)


Pour la réaliser, j'ai observé le code de la page, et j'ai constaté, comme azt.tm avant moi, que l'affichage se basait sur des images de carrées de 2048 pixels de côté, et dont les noms sont basés sur un système d'orientation nord-sud (haut-bas de l'image) et ouest-est (gauche-droite).

Le javascript utilisé par Randall Munroe semblait indiquer qu'il y avait des coordonnées allant de jusqu'à 14 nord, 48 est, 25 sud et 33 ouest.

J'ai donc réalisé un petit script qui m'a sorti les liens vers toutes les images supposées exister, entre (33 ouest - 14 nord) et (48 est - 25 sud). Soit un total de 81 par 39 images, donc 3159 images de 2048 pixels, soit.... 165888 par 79872 pixels !
Bon, ok, jusqu'ici, on s'en fout un peu.

Ensuite, j'ai utilisé l'addon de Firefox nommé DownThemAll, pour télécharger automatiquement toutes ces images. On constate finalement que certaines n'existent pas, dans les zones où l'auteur n'a rien dessiné. Il n'y a finalement "que" 225 images au total. Les autres redirigent vers une 404, et DownThemAll a été assez intelligent pour ne pas les télécharger. Bon la méthode est assez bourrine, mais je suis sûr que son serveur est dimenssionné pour suporter une telle charge. O:-P

Un petit coup d'imagemagick pour batchresize tout ça en carrés de 64px :
mogrify -resize 3.125% *.png

Il ne reste plus qu'à pondre un petit code pour rassembler le tout en une image. J'ai d'abord pensé à la commande montage d'imagemagick, mais soit elle n'est pas assez puissante, soit je n'ai pas trouvé les fonctionnalités que je cherchais.

Du coup, j'ai fait ça en C++/Qt :

#include <QImage>
#include <QPainter>
#include <QFile>
#include <QDebug>

int main(int argc, char *argv[]) {
const int TILE_SIZE = 64;
// const int TILE_SIZE = 2048;
const int NORTH_SIZE = 14;
const int EAST_SIZE = 33;
const int SOUTH_SIZE = 23;
const int WEST_SIZE = 48;

QImage *resultImage = new QImage(TILE_SIZE * (EAST_SIZE + WEST_SIZE) ,TILE_SIZE * (NORTH_SIZE + SOUTH_SIZE), QImage::Format_ARGB32);
resultImage->fill(Qt::gray);
for(int i=0; i < (EAST_SIZE+WEST_SIZE); i++) {
for(int j=0; j < (NORTH_SIZE+SOUTH_SIZE); j++) {
QString filePath = "thumbs/";
// QString filePath = "images/";
if(j < NORTH_SIZE) {
filePath += QString::number(NORTH_SIZE-j) + "n";
} else {
filePath += QString::number(j-NORTH_SIZE+1) + "s";
}
if(i < EAST_SIZE) {
filePath += QString::number(EAST_SIZE-i) + "w";
} else {
filePath += QString::number(i-EAST_SIZE+1) + "e";
}
filePath += ".png";
QFile imageFile(filePath);
if(imageFile.exists()) {
qDebug() << filePath;
QPainter painter;
painter.begin(resultImage);
painter.drawImage(i*TILE_SIZE, j*TILE_SIZE, QImage(filePath));
painter.end();
}
}
}
resultImage->save("result.png");
}

Je vous proposerais bien d'exécuter le même code directement sur les images en 2048 pixels, mais je pense que la mémoire pourrait avoir quelques soucis.

PS : et j'allais presque oublier le lien vers le strip !!! Allez jouer avec sur xkcd !