FRAMES POUR PHD EN PHP



Les principes et les problèmes principaux des frames ont été expliqués dans les deux articles précédents. Il reste donc à mettre au point un système de navigation satisfaisant. Les solutions proposées ici ne sont pas les seules possibles et elles impliquent le recours au php. L'espace de discussion en bas de l'article est consacré aux solutions alternatives et à la critique.

Le premier problème exposé faisait état de l'explosion du nombre de fichiers sur disque. Pour résoudre cet inconvénient, quelques lignes de code à placer dans le fichier Texte et dans celui de la définition des cadres suffisent. La solution consiste à adapter la définition du cadre à chaque nouveau chargement d'un fichier à l'écran. Dans les exemples qui suivent, seul le Cadre3 et les fichiers qui s'y trouvent font l'objet de commentaires, mais tous peuvent fonctionner de la même manière :



Il existe deux stratégies possibles pour réaliser cette prouesse :

1) On peut procéder en transmettant le nom du fichier à placer dans la définition du cadre en tant que variable. Le fichier chargé en Cadre3 transmet son nom à DefCadre qui le réutilise ensuite dans la définition.

Démonstration 6 : Ce lien pointe sur le fichier Texte6.php qui commande le chargement du cadre-parent DefCadre6.php. La donnée "Texte6.php" est transmise à DefCadre6.php en tant que variable "$NomDuFichier" et insérée dans la définition du cadre. Le code est le suivant :

Dans le fichier Texte6.php :

<Head>
< ?php
//*Déterminer le nom et le chemin du fichier
$NomDuFichier = "http://www.Site.com$PHP_SELF";

//*Envoyer le nom du fichier avec l'instruction javascript de chargement du cadre
Print "
<script language=\"javascript\">\n
if(parent.frames.length == 0) parent.location.href = \"DefCadre6.php?Texte=$NomDuFichier\"\n
</script>\n";
?>
</Head>

Le php s'exécute le premier et détermine l'instruction javascript à exécuter. En voici le résultat :

<Head>
...
<script language="javascript">
if(parent.frames.length == 0) parent.location.href = "DefCadre6.php?Texte=http://www.Site.com/Texte6.php"
</script>
...
</Head>

La variable Texte est récupérée dans le fichier DefCadre6.php par la méthode GET :

<html>
<frameset rows="25%,75%*">
<frame name=Cadre1 src=Menu6.php scrolling=no>
<frameset cols="20%,80%*">
<frame name=Cadre2 src=SousMenu6.php>
<?php
//* Définir la variable Texte
$Texte=$_GET[Texte];
//* Vérifier qu'une variable a bien été reçue
if(strlen($Texte)<2)
{
//* Si la variable n'est pas définie, c'est que l'internaute a appelé DefCadre6.php plutôt que Texte6.php.
//*Dans ce cas le fichier à charger par défaut en Cadre3 est Texte6b.php.
Print "<frame name=Cadre3 src=Texte6b.php>\ n
</frameset>\n";
}
else
{
//* Charger le fichier Texte6.php défini par la variable NomDuFichier
Print "<frame name=Cadre3 src=$Texte>\n
</frameset>\n";
}
</frameset>
//* A ne pas oublier, c'est le bon endroit
mysql_close();
?>
<noframes>
<body lang=FR>
<p >Cette page utilise des cadres mais votre navigateur ne les prend pas en charge. Pour une version sans frames <a href="Txt1NoFrame.html" target = "_Self">cliquez ici</a>.</p>
</body>
</noframes>
</frameset>
</html>

C'est tout !

Le système permet d'économiser des centaines de définitions de cadre. Mais cette situation n'est pas aussi parfaite qu'il y paraît. Lorsque le cadre est chargé le premier, la variable NomDuFichier n'est pas déclarée. Et dans le cas d'un raffraîchissement d'écran par la touche [F5] ou l'icône d'actualisation, il arrive que la variable soit perdue. De plus, la navigation, tant dans l'historique que la mémoire cache, ne fonctionne pas correctement. On retrouve ces imperfections sur la quasi-totalité des sites qui utilisent les jeux de cadres. Les présentations Powerpoint de Microsoft écrites en javascript n'échappent pas à la règle malgré l'extrême complexité du code généré.

Ce ne sont que quelques raisons parmi d'autres pour envisager une seconde stratégie qui est plus performante mais qui a le défaut de requérir de multiples accès à la base.

2) Une table des sessions est renseignée. Elle comporte au moins deux colonnes :

-L'IP de l'internaute, colonne : AdresseIP - indexée
-Le nom du fichier Texte, colonne : NomDuFichier.   

Le principe consiste à renseigner la table avec l'information la plus récente concernant le fichier Texte actif. Lors de son chargement, il inscrit son propre nom et l'IP de l'internaute dans la table. Il vérifie auparavant qu'aucun enregistrement n'est déjà renseigné sans quoi une mise à jour suffit. Le code du fichier Texte6.php est le suivant :

<Head>
<Emplacement des balises Meta>
.
< ?php
//*Capturer le nom du fichier et l'adresse IP
$NomDuFichier = "http://www.Site.com$PHP_SELF";
$AdresseIP = $REMOTE_ADDR;
//*Définir les requêtes
$SqlSelect = "SELECT IP FROM Session WHERE AdresseIP='$AdresseIP'";
$SqlUpdate = "UPDATE Session SET NomDuFichier='$NomDuFichier' WHERE AdresseIP='$AdresseIP'";
$SqlInsert = "INSERT INTO Session (NomDuFichier,AdresseIP) VALUES ('$NomDuFichier','$AdresseIP')";
//*Lancer les requêtes
$query = Mysql_query($SqlSelect) or die('Select incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
$row = Mysql_Numrow($query);
//*S'il y a plusieurs enregistrements, mise à jour de la table Session
If ($row == 1)
{
Mysql_query($SqlUpdate) or die('Update incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
}
//*S'il n'y a aucun enregistrement, ajouter à la table Session
If ($row == 0)
{
Mysql_query($SqlInsert) or die('Insert incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
}
//*Cette dernière instruction est une précaution juste au cas où une erreur se serait produite dans la procédure précédente, ce qui n'arrive que très rarement : Tout est effacé puis réinséré.
If ($row > 1)
{
$SqlDelete="Delete FROM Session where AdresseIP = '$AdresseIP'" ;
Mysql_query($SqlDelete) or die('Delete incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
Mysql_query($SqlInsert) or die('Insert2 incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
}
?>
.
<Définition des styles>
</Head>

Une fois cette opération réalisée, le fichier de définition des cadres, DefCadre6.php dans notre exemple, doit récupérer la donnée dans la table Session :


<html>
<frameset rows="25%,75%*">
<frame name=Cadre1 src=Menu6.php scrolling=no>
<frameset cols="20%,80%*">
<frame name=Cadre2 src=SousMenu6.php>
<frameset cols="1*,4*">
  <frame name=Cadre2 src=SousMenu6.php>
< ?php
$AdresseIP = $REMOTE_ADDR;
$SqlSelect = "Select NomDuFichier from Session where AdresseIP = '$AdresseIP'" ;
$result = mysql_query($SqlSelect) ;
$row = mysql_num_rows($result);
$Texte = mysql_result($result, '0', 'NomDuFichier');
//* Vérifier que le résultat est valide
if strlen($Texte>2)
{
Print "<frame name=Cadre3 src=$Texte scrolling=no>
</frameset>";
}
else
{
//* Si $Texte6 n'est pas défini, Texte1 s'y colle
Print "<frame name=Cadre3 src=Texte1.php scrolling=no>\
</frameset>";
}
?> 
</frameset>
<noframes>
<body lang=FR>
<p >Cette page utilise des cadres mais votre navigateur ne les prend pas en charge. Pour une version sans frames <a href="Txt1NoFrame.html" target = "_Self">cliquez ici</a>.</p>
</body>
</noframes>
</frameset>
</html>

Voilà c'est presque fini ! Les pages articles de Nostradamia font office de démonstration (pas les intros et les quatrains).

Le choix de l'une ou l'autre stratégie est avant tout dicté par des considérations philosophiques. La première méthode s'avère de loin la plus simple mais aussi la plus limitée. Avec la seconde stratégie, les contraintes techniques sont éliminées hormis celles liées à des accès fréquents à la base. Mais que se passe-t-il lorsqu'un internaute cache son IP ? Et comment réagissent les moteurs de recherche que l'on sait changer d'adresses IP à tout bout de champ ?

La question de fond est de comprendre pourquoi les internautes cachent leurs IP. Leurs motifs ne semblent pas des plus respectables. Aussi, c'est sans état d'âme que l'on peut leur interdire la navigation. Mais si on souhaite tout de même la permettre, on peut définir une IP "Unknown" définie dans chacun des fichiers DefCadre6.php et Texte6.php :

< ?php
//*Capturer le nom du fichier et l'adresse IP suite au select du fichier de définition de cadre
if(strlen($PHP_SELF)<2)
{
$NomDuFichier=$Unknown;
}
else
{
$NomDuFichier = "http://www.Site.com$PHP_SELF";
}
?>

Mais on peut aussi laisser les choses dans l'état puisqu'une IP égale à '' (blanc) revient au même qu'à "Unknown". Ceci permet de naviguer sur le site sans ennui, pourvu qu'il n'y ait qu'un seul interaute sans IP en même temps. S'ils sont plusieurs, chacun voit s'afficher les pages demandées par les autres anonymes. Un système sophistiqué pourrait les différencier. Il s'agit d'une question de choix et non d'une limite technique.

Certains navigateurs et serveurs renouvellent les IP à chaque nouvelle page. La seule chose en cause est la fréquence de renouvellement. Si l'IP utilisée lors de l'ouverture de Texte6.php est différente de celle de DefCadre6.php, alors la navigation devient impossible et l'internaute se voit en permanence renvoyé à la page Texte1.php. En revanche, si son renouvellement est plus parsimonieux, sa navigation n'est pas affectée.

Là aussi il convient de se poser la question des raisons et des conséquences. Des navigateurs peuvent y avoir recours pour cacher leurs IP. Il peut aussi s'agir d'une question de technologie de serveur ou de sécurité. Interdire ce type de navigation aurait pour conséquence de gêner les travaux d'indexation des moteurs comme Google ou FreeFind qui peuvent changer d'IP nombre de fois lors de leur unique visite mensuelle, ce que l'on veut éviter à tout prix. Il faut donc faire en sorte que les moteurs de recherche ne chargent pas les cadres ce qui sera traité plus loin en même temps que le sujet du référencement. Quant aux très rares internautes qui changent d'IP plus vite que la lumière (1/5000), Nostradamia prend le parti de ne rien faire pour eux, ni contre eux. Qu'advienne la navigation que pourra !

La méthode et la fréquence d'épuration de la table Session dépend du nombre de visites sur le site. A moins de 200 visites par jour, un script peut tout simplement vider la table aux heures creuses. Au delà, le critère de l'activité des IP doit s'appliquer. Un "Timestamp" peut être utilisé ou un champ "Heure" rajouté à la table Session mis à jour à chaque transaction. Dans ces deux cas, 2 heures d'inactivité peuvent être considérées comme l'évidence d'une déconnexion. Les scripts sont programmés et lancés automatiquement depuis le serveur.

Le système de navigation de la seconde stratégie est déjà presque opérationnel mais il reste à ajuster le comportement du navigateur lors des déplacements dans l'historique.

Dans la démonstration 6 il était demandé de cliquer sur un lien du Cadre2 qui remplaçait Texte6.php par Texte6B.php en Cadre3. Puis il fallait rafraîchir l'écran pour constater l'instabilité des cadres. Le problème est résolu en démonstration 7. Mais il est désormais demandé de revenir en arrière par la touche arrière de l'historique juqu'à réafficher Texte6.php, puis de rafraichir l'écran. Texte6B.php revient au lieu de Texte6.php. C'est l'effet cache.

En effet, lors du déplacement dans l'historique, les pages se rechargent à partir de la mémoire cache si bien que le code php ne se recalcule pas et l'inscription dans la table Session ne s'effectue pas. Mais la définition des cadres est elle recalculée et provoque l'ouverture du dernier fichier inscrit. La solution requiert de réinscrire au fur et à mesure des clics tous les fichiers, y compris ceux mouvementés depuis la mémoire cache. La contrainte donnée est de ne pas avoir recours à une balise meta no-cache dans l'entête des fichiers Textes qui pénaliserait l'internaute.

Un fichier "Pull" peut s'en charger. Il ne contient que des instructions php et son rôle se limite à mettre à jour les informations Session avec le nom du fichier actif. Ce fichier est no-cache mais tous les autres, Texte, Menu et SousMenus sont dotés de la propriété cache. Le pull peut renfermer tout le code php de Texte défini à ce stade. Il s'ouvre dans un cadre d'où il charge le document à afficher à sa place, l'opération ne prenant que quelques fraction de secondes. Sinon il peut s'agir d'un cadre caché qui évite d'interférer avec l'affichage de l'utilisateur. Le nom du fichier à inscrire est transmis par Texte dans le lien par lequel il appelle Pull.php. C'est ce qu'illustre la démonstration 8 :

Dans Texte8.php :

<?php
//*Texte8.php attache son nom en tant que variable au lien qui ouvre pull.php
$NomDuFichier = "http://www.Site.com$PHP_SELF";
$Pull = "Pull.php?Texte=".$NomDuFichier;

//*Cette instruction ouvre le fichier Pull.php en Cadre2 et lui transmet la variable
print \"SCRIPT Language=\"javascript\">
window.open(\"$Pull\",\"Cadre2\");
</SCRIPT>";
?>


Dans Pull.php :

<Html>
<Head >
<?php
$AdresseIP = $REMOTE_ADDR;
$NomDuFichier = "$_GET[Texte]";

//*La suite est en tout point identique à ce qu'elle était en démonstration 6 à l'exception de la dernière instruction
//*Définir les requêtes
$SqlSelect = "SELECT IP FROM Session WHERE AdresseIP='$AdresseIP'";
$SqlUpdate = "UPDATE Session SET NomDuFichier='$NomDuFichier' WHERE AdresseIP='$AdresseIP'";
$SqlInsert = "INSERT INTO Session (NomDuFichier,AdresseIP) VALUES ('$NomDuFichier','$AdresseIP')";
//*Lancer les requêtes
$query = Mysql_query($SqlSelect) or die('Select incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
$row = Mysql_Numrow($query);
//*S'il y a plusieurs enregistrements, mise à jour de la table Session
If ($row == 1)
{
Mysql_query($SqlUpdate) or die('Update incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
}
//*S'il n'y a aucun enregistrement, ajouter à la table Session
If ($row == 0)
{
Mysql_query($SqlInsert) or die('Insert incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
}
//*Cette dernière instruction est une précaution juste au cas où une erreur se serait produite dans la procédure précédente, ce qui n'arrive que très rarement : Tout est effacé puis réinséré.
If ($row > 1)
{
$SqlDelete="Delete FROM Session where AdresseIP = '$AdresseIP'" ;
Mysql_query($SqlDelete) or die('Delete incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
Mysql_query($SqlInsert) or die('Insert2 incomplet - Veuillez raffraichir l'écran !<br>'.mysql_error());
}
?>
window.open("SousMenu8.php", "Cadre2")
</Head >
</Html>


Il reste un tout petit détail pour parfaire le système. Puisque le fichier Pull doit s'ouvrir dans un cadre et que c'est lui qui se charge de l'inscription dans la table Session, qu'adviendrait-il si le fichier Texte n'avait pas encore chargé son cadre ? On aurait un fichier ouvert sans contenu dans une fenêtre ce qui ne serait pas très élégant. De plus l'instruction "window.open" pourrait être bloquée par un navigateur s'il interdisait les pop-ups. La conséquence en serait que la procédure ne s'exécute pas.

Le code suivant y remédie en ouvrant pull.txt dans la fenêtre active lorsque le cadre n'est pas encore chargé :

<?php
//*Texte8.php attache son nom en tant que variable au lien qui ouvre pull.php
$NomDuFichier = "http://www.Site.com$PHP_SELF";
$Pull = "Pull.php?NomDuFichier=".$NomFichier;

Print "
<script language=\"javascript\">\n
If (parent.frames.length = = 0)\n
//*Si le cadre n'est pas chargé
{\n
window.location.href=\"Pull.php?Texte=$NomDuFichier\"\n
}\n
//*Le fichier de procédure Pull.php s'ouvre dans la fenêtre actuelle. Il inscrit le nom du fichier et l'iP de l'internaute dans la table Session. Il charge le cadre qui recharge Texte8.php qui à son tour rouvre Pull.php, mais cette fois-ci en Cadre2, qui à son tour ouvre SousMenu8.php. Si la mémoire cache est activée les temps de réponse ne sont pas dégradés.
else\n
//*Si le cadre est chargé Pull.php s'ouvre en Cadre2 ou dans un cadre spécifique
{\n
window.open(\"Pull.php?Texte=$NomDuFichier\",\"Cadre2\")\n
}\n
?>

Le dernier cas de figure est exceptionnel. C'est celui où un fichier, Texte1.php par exemple, en charge un autre, Texte2.php, dans un iframe ? Texte2.php s'inscrit dans la table Session en remplacement de Texte1.php car il s'est chargé en second. Hors, c'est bien Texte1.php qui doit s'afficher en cas de rafraîchissement. La plus stable et cohérente solution consiste à transmettre en tant que variable dans le lien de l'iframe l'information comme quoi il s'agit d'un fichier chargé dans un iframe. Texte n'a qu'à tester la valeur "yes" de la variable et s'interdire l'exécution du script :

Cette procédure n'a d'intérêt que pour les fichiers contenus dans des iframes qui inscrivent leur nom dans la table Session :

Dans la section body de Texte1.php :

<body>
.
<iframe src=Texte2?ProcIframe=yes name=Cadre5 .>
.
</body>

Et dans la section head de Texte1.php :
<head>
.
< ?php
Conection à la base;
$ProcIframe=$_GET[ProcIframe];
if($ProcIframe=='yes');
{}
else
{
include(Procedures/Pull.txt);
}
$_GET[ProcIframe]='';
?>

Noter : La variable ProcIframe est modifiée en '' à la sortie de la procédure pour garantir que le test d'égalité ne retourne pas un résultat "vrai" durant tout le reste de la session. La variable est réinitialisée à sa valeur 'yes' en cas de rafraîchissement de Texte1.php.


Les autres aspects

Pour permettre l'ajout aux favoris d'une page framée, voici le code HTML à placer sur chaque page Texte à l'intérieur de la section <body>:

<a href="javascript:window.external.AddFavorite(document.URL,document.title);">Ajouter cette page aux favoris</a>

Et pour les problèmes de navigation dans l'historique qui obligent l'internaute au recours à de nombreux clics lorsque plusieurs pages de cadres ont été mouvementées, il faut aussi se résoudre à afficher un bouton dans la page de Texte. Le principe consiste à lancer autant de retour arrière dans l'historique que nécessaire pour revenir à la page Texte précédente. Et pour ce faire, une procédure javascript placée dans le fichier Texte suffit. En principe, lorsque la page Texte qui contient la procédure est mouvementée, la procédure doit s'arrêter. Mais dans les faits, elle va au delà et poursuit ses retours en arrière après que Texte ait été remplacé. Il faut introduire une temporisation ce qui peut se faire en testant une variable définie comme égale au nom du fichier Texte. Dès que Texte est fermé, la variable chute et la procédure s'interrompt.

En voici le code :

<?
print "<SCRIPT Language=\"javascript\">\n
function Retour()\n
{\n
var NomFicphp = \"$NomFichier\";\n
var NomFicJava = document.URL;\n
if (NomFicJava == \"$NomFichier\"){history.back()};\n
if (NomFicJava == \"$NomFichier\"){history.back()};\n
if (NomFicJava == \"$NomFichier\"){history.back()};\n
if (NomFicJava == \"$NomFichier\"){history.back()};\n
//*4 fois pour un maximum de 4 fichiers mouvementés à chaque clic
}\n
</SCRIPT>\n";
?>

Noter : La procédure ne fonctionnerait pas dans une boucle "for" ou "while", d'où l'obligation de répéter l'instruction "window.history.back()". De plus, les variantes comme "history.go(-1)" ne fonctionnent plus dès qu'une variable se calcule dans la procédure (!). Et enfin, Netscape et IE réagissent différemment. Aussi la syntaxe requise est précisément celle exposée. A ce stade de son développement elle ne garantit pas que la navigation avant dans l'historique fonctionne. Ce second aspect de la procédure ne sera donc exposé qu'après sa mise au point.

Il faut désormais organiser le système de fichier sur disque. La solution la plus simple et la moins coûteuse consiste à placer tous les fichiers Texte, Menus, SousMenus et Pull dans un seul et même répertoire. Mais la clarté d'administration pour les grands sites en pâtit. Une alternative consiste à dédier un répertoire aux procédures. Et elles peuvent être rappatriées dans Texte par une instruction "include" ou "require" :

Le code de Texte.php est le suivant :

<head>
< ?php
Procédure de conection à la base;
include(Procedures/Pull.txt);
?>
</head>

L'extension txt garantit que le code php s'exécute dans le fichier appelant. Avec une extension php il s'exécuterait dans le répertoire source. Etant donné que Texte doit déterminer son propre nom pour le transmettre à Pull, la procédure doit se calculer dans Texte. Le fichier Pull.txt doit néanmoins démarrer et se terminer par les délimiteurs <? et ?> de php :

Le fichier Pull.txt placé dans le répertore des procédures :

< ?
//*Ici, la Procédure Pull s'insère dans l'état exact où elle a été défine plus haut, sans toutefois l'instruction de connection à la base.
?>

L'avantage est que la publication d'un texte ne prend que quelques instants. Une fois mis au point le HTML, il ne reste qu'à inclure ces deux lignes dans chaque fichier Texte, les mêmes toujours au même endroit. Le système de navigation avec frames se transforme en une véritable machine à publier et dote l'administrateur d'une grande réactivité. Et avec les procédures ainsi centralisées il bénéficie, en cas de modifications à apporter à Pull.txt, de l'effet de levier sur le site tout entier.

Noter : Penser à restreindre le CHMOD du répertoire dédié aux procédures en lecture seule et à un accès local seulement.

(Reste à traiter : Le référencement des frames et à mettre au point les démos 7 et 8)

Récapitulatif

Ce système de navigation avec frames règle les problèmes du raffraîchissement d'écran y compris lors de la navigation dans l'historique, celui de la multiplication des fichiers sur disque, du nombre de clics arrière, de l'administration des fichiers, du placement du fichier actif dans les favoris. C'est une véritable machine à publier, mais aussi, une machine à gaz - la faute aux navigateurs.

En espérant contribuer positivement à l'évolution de la technologie des frames,

Merci de partager vos réflexions, et en particulier pour la fonction "avant" de l'historique dont les subtilités échappent pour l'instant à mes compétences. Ne peut-on pas forcer le navigateur à prendre note des mouvements arrières à chaque étape de la procédure pour qu'il soit en mesure de refaire le chemin inverse ? Dans l'état actuel de la procédure la fonction est inopérante, sans doute parce que les déplacements dans l'historique ont été trop rapides pour laisser le temps au navigateur de les inscrire dans le fichier dédié à cet effet.


Et bon surf.

Le "GrandPontif" de Nostradamia