Zdeněk Wagner - Sjednání obsahu v PHP skriptech

Na stránce o sjednání obsahu (content negotiation) byly ukázány základní principy a byly zveřejněny odkazy na další užitečné informace. Nyní se podíváme, k čemu a jak se dá sjednání obsahu využít ve spojení s dynamickými stránkami vytvářenými v PHP. Nabízejí se nám tyto možnosti:

  1. Internacionalizace PHP skriptů
  2. Simulace sjednání obsahu v případě, že server tento standard neumí nebo není administrátorem povolen.

V tomto dokumentu si postupně popíšeme obě metody. Sice se zaměříme výhradně na PHP, ale obdobný přístup se dá využít i v jiných skriptovacích jazycích, jako jsou ASP, Perl, Python, Ruby apod.

1. Internacionalizace PHP skriptů

PHP skript je obvykle směsí programových instrukcí, v nichž se volají standardní i uživatelské funkce, a kódu HTML. Největší část je tedy společná pro všechny jazykové verze. Proto nechceme vytvářet pro jednotlivé jazyky samostatné soubory odlišené příponami, jako jsme to dělali v případě statických stránek. Odlišení příponami využít chceme, aby fungovalo sjednání obsahu s využitím MultiViews, ale programový kód chceme sdílet. Při kopírování ze souboru do souboru by totiž snadno vznikaly chyby a nekonzistentnosti.

PHP skript zobrazuje texty ve stránkách WWW pomocí funkcí echo a printf. Tyto funkce pracují s řetězci, které budou mít v každé jazykové verzi jiné hodnoty. Jednojazyčná verze používá řetězcové literály. My však použijeme proměnné nebo konstanty definované funkcí define. Programový kód pak bude v jednom společném souboru. Každá jazyková verze bude pouze definovat konstanty a proměnné závislé na jazyku a pak načte společný soubor funkcí require. Vícejazyčný text může vypadat takto:

Český soubor pojmenovaný file.cs.php:

<?php
define(_title_, 'Sjednání obsahu');
require 'incl.file.php';
?>

Anglický soubor pojmenovaný file.en.php:

<?php
define(_title_, 'Content negotiation');
require 'incl.file.php';
?>

Společný soubor pojmenovaný incl.file.php:

<?php
printf('<title>%s</title>', _title_);
?>

Na příslušnou stránku se budete odvolávat jen jako na file bez jakékoliv přípony. Apache pak vybere vhodnou variantu.

Nakonec ještě musíme zabránit přístupu ke společným souborům, protože ty by samostatně nefungovaly. Dosáhneme toho následující direktivou v konfiguračním souboru:

<FilesMatch "incl\.">
  Order allow,deny
  Deny from all
</FilesMatch>

Všimněte si, že incl. je na začátku jména. Pokud by se společný soubor jmenoval file.incl.php a přístup by byl zakázán obdobnou direktivou FilesMatch, pak při hledání file by Apache zkusil též file.incl.php a do chybového souboru by zaznamenal, že prohlížeči byl zákazem v konfiguraci odmítnut přístup k souboru.

Nyní si probereme případ, kdy chceme do souboru vložit dvě velké části kódu HTML. Vytvoříme samostatné soubory file1.cs.html a file2.cs.html pro češtinu, obdobně file1.en.html a file2.en.html pro angličtinu. Když na příslušná místa společného souboru napíšeme virtual('file1'); a virtual('file2');, Apache s použitím sjednání obsahu vloží správné soubory. Bohužel to nefunguje vždy. Uvědomme si, že při hledání URL s názvem file nemusela být nalezena správná jazyková varianta a uživatel si zvolil jazykovou verzi explicitně. Stejně zareaguje Apache na požadavek virtual('file1'); a nabídne nepoužitelné odkazy. Soubor pro daný jazyk by tedy měl definovat řetězcovou konstantu _ext_ obsahující potřebnou příponu a kód HTML by se pak načítal funkcí virtual('file1' . _ext_);.

Sjednání obsahu v PHP skriptech je využito v programu MemoDisx. Můžete si vyzkoušet demonstrační verzi. Můžete si též stáhnout distribuci programu MemoDisx a podívat se do zdrojových kódů, jak je to uděláno. MemoDisx používá rámy a otvírá se hlavní stránkou s tímto obsahem:

<!--#include virtual="html-head.html" -->
<!--#if expr="$DOCUMENT_URI != /\/index$/" -->
<body>
<p class="error">Please add "English" to the language preferences of your browser and then
<a target="_top" href="./">try again</a>, otherwise MemoDisx will not work properly.</p>

<p>More details can be found in the description of <a
href="http://hroch486.icpf.cas.cz/wagner/content-negotiation">Content Negotiation</a>.</p>

</body>
<!--#else -->
<frameset cols="15%,*">
  <!--#include virtual="noframes" -->
  <frame name="left" src="menu">
  <frame name="right" src="main">
</frameset>
<!--#endif -->
</html>

Proměnná $DOCUMENT_URI obsahuje požadované jméno dokumentu. Pokud nemá příponu, znamená to, že vhodná jazyková verze byla zvolena podle preferencí nastavených v prohlížeči. Pokud příponu má, víme, že uživatel si zvolil jazyk explicitně. Pak bychom ale museli vše programovat složitěji, proto takovou možnost explicitně zakážeme. Ostatně, MemoDisx je aplikace, která má být používána opakovaně. Proto můžeme po uživateli vyžadovat, aby věnoval více času nastavení svého prohlížeče WWW.

2. Simulace sjednání obsahu

Tento dokument není programátorský toolkit, který byste mohli vzít a přímo použít. Cílem dokumentu je vysvětlení metody. Samozřejmě si můžete vytáhnout příklady kódu, poskládat je do souborů a získat tím funkční řešení. Budete muset nahradit znakové entity odpovídajícími znaky. Abyste to nemuseli provádět ručně. můžete použít jednoduchý perlovský skript (ale ten si nejprve musíte ručně upravit:

#!perl
while (<STDIN>) {
  s/&amp;/&/g;  s/&lt;/</g;  s/&gt;/>/g;  print;
}

V návodu se předpokládá, že jsou registrovány globální proměnné. Lze tedy použít $HTTP_ACCEPT_LANGUAGE a $REQUEST_URI. Pokud by globální proměnné registrovány nebyly, bylo by nutno použít $_SERVER['HTTP_ACCEPT_LANGUAGE'] a $_SERVER['REQUEST_URI'].

Podobně jako při použití běžné metody sjednání obsahu chceme, aby všechny odkazy vedly na hlavní soubor filename.php, který podle požadavků od prohlížeče WWW zvolí správnou jazykovou verzi. Jména souborů s danými jazykovými verzemi však nebudou mít více přípon. Jméno bude mít formát filename-XXX_YYY.EXT, kde XXX a YYY určují jazyk a kódování, EXT je přípona. V příkladech, které zde uvedeme, může přípona být php nebo html. Formát hlavního souboru je velmi jednoduchý:

<?php
require 'engine.php';
if ($inc) {
include $fn;
}
else virtual($fn);
exit;
?>

Vlastní činnost se provádí v souboru engine.php. Ten vrátí jméno souboru s příslušnou jazykovou verzí v proměnné $fn. Je-li nastavena proměnná $inc, jedná se o PHP skript, který je nutno zpracovat příkazem include. V opačném případě použijeme funkci virtual.

Soubor engine.php obsahuje kód, který se přímo provádí. V něm se používají funkce, které jsou definovány v soubory functions.php, takže tento soubor musíme nejprve načíst příkazem require. Žádný z uvedených souborů nesmí být spuštěn přímo, proto na začátek functions.php vložíme:

$bsn = basename($REQUEST_URI);
if ($bsn == 'engine.php' || $bsn == 'functions.php') die('Access not allowed!');

Programový kód v engine.php nejprve najde seznam souborů. Jména kontroluje s regulárním výrazem, který si sám určí.

# Find the list of the files, $re evaluated from $REQUEST_URI
$rq = $REQUEST_URI;
if (substr($rq, -1) == '/') $rq .= 'index.php';
$re = '/^' . substr(basename($rq), 0, -4) . '-(.*)\./';
unset($flist);
$dirhandle = opendir('.');
while ($fn = readdir($dirhandle)) {
  if (preg_match($re, $fn, $m)) {
    $f = array('fn' => $fn, 'ext' => explode('_', $m[1]));
    $flist[] = $f;
  }
}
closedir($dirhandle);
if ($flist) sort($flist);

Dále si zjistíme seznam přijatelných jazyků. Pokud prohlížeč seznam jazyků nepošle, použijeme defaultní hodnotu podobně jako Apache má direktivu LanguagePriority.

# Get the list of acceptable languages
unset($acclang);
if ($HTTP_ACCEPT_LANGUAGE) {
  $acclang = explode(',', $HTTP_ACCEPT_LANGUAGE);
  for ($i = 0; $i < count($acclang);  $i++) {
    $L = explode(';', $acclang[$i]);
    $acclang[$i] = trim($L[0]);
  }
} else $acclang = array('en', 'cs');

Uživatel může požadovat specifickou jazykovou variantu (např. en-zw), kterou na serveru nemáme. Pokud tedy jazykovou variantu nenajdeme, odstraníme specifikace jazykových variant a hledáme znovu.

$xlang = FindLanguage($flist, $acclang);

# Remove language variants and find a file
if ($xlang < 0) {
  for ($i = 0; $i < count($acclang);  $i++) {
    $L = explode('-', $acclang[$i]);
    $acclang[$i] = trim($L[0]);
  }
  $xlang = FindLanguage($flist, $acclang);
}

Funkce FindLanguage je definována v souboru functions.php. Nejprve však musíme definovat seznam známých jazyků a seznam kódování (např. čeština potřebuje ISO-8859-2).

$languages = array(
  'cs' => 'CS',
  'en' => 'EN',
  'fr' => 'FR',
  'de' => 'DE'
);

$encodings = array(
  'iso2' => 'iso-8859-2'
);

# Function for finding a language
function FindLanguage($flist, $acclang) {
$lx = -1;
for ($j = 0;  $lx < 0 && $j < count($acclang);  $j++) {
  for ($i = 0;  $lx < 0 && $i < count($flist);  $i++) {
    $ext = $flist[$i]['ext'];
    for ($k = 0;  $lx < 0 && $k < count($ext);  $k++) {
      if ($acclang[$j] == $ext[$k]) $lx = $i;
    }
  }
}
return $lx;
}

Pokud se nepodaří najít žádnou přijatelnou variantu, vypíšeme seznam a ukončíme skript.

# If no file found, display variants and exit, otherwise fill the file name
if ($xlang < 0) {
?>
<html><body>
<p>No acceptable variant was found. Your browser is set to require text in languages:
<code><?php echo $HTTP_ACCEPT_LANGUAGE; ?></code>
but only the following variants are available:</p>
<ul>
<?php
  for ($i = 0;  $i < count($flist);  $i++) {
    $ext = $flist[$i]['ext'];  $f = $flist[$i]['fn'];
    reset($languages);
    while (list($k, $v) = each($languages)) {
      for ($j = 0;  $f && $j < count($ext);  $j++) {
        if ($ext[$j] == $k) {
          echo "<li><a href=\"$f\">$v</a></li>\n";  $f = false;
        }
      }
    }
  }
?>
</ul>
<p>You can read more about <a title="Content negotiation" target="content_neg"
href="http://hroch486.icpf.cas.cz/wagner/content-negotiation.shtml.en">content negotiation</a> if
you wish to know how to setup your WWW browser correctly.</p>
<hr>
</body></html>
<?php
  exit;
}

Na závěr naplníme proměnné a případně požádáme Apache o vyslání nějaké hlavičky.

$fn = $flist[$xlang]['fn'];
EmitHeader($fn);
$inc = preg_match('/\.php$/', $fn);

Funkce EmitHeader je také definována v souboru functions.php. Některé jazykové varianty specifikují též požadované kódování. Pak je nutno vyslat hlavičku s odpovídající informací. V souboru functions.php máme informace k příslušným příponám přiřazeny.

$headers = array(
  'iso2' => 'Content-Type: text/html; charset=iso-8859-2'
);

# Function for emitting a header
function EmitHeader($fn) {
global $headers;
if (preg_match('/-([^-]+)\./', $fn, $m)) {
  $e = explode('_', $m[1]);
  while (list($k, $v) = each($e)) {
    if ($headers[$v]) header($headers[$v]);
  }
}}

Dynamicky generované stránky se obvykle neukládají ve vyrovnávací paměti. Naše stránky však jsou ve skutečnosti statické, proto můžeme Apache požádat, aby vyslal údaj o expiraci. Máme k tomuto účelu připravenu funkci Expires.

# Longer time units
define(HOUR, 3600);
define(DAY, 24 * HOUR);
define(MONTH, 30 * DAY);

# Expiration
function Expires($after = DAY) {
setlocale(LC_ALL, 'EN_US');
Header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $after) . ' GMT');
}

Funkce LangLinks, jak název napovídá, připravuje odkazy na další jazykové varianty.

# Links to languages
function LangLinks() {
global $fn, $REQUEST_URI, $languages;
echo '<div id="langlinks">&nbsp;';
$rq = $REQUEST_URI;
if (substr($rq, -1) == '/') $rq = index.php;
else $rq = basename($rq);
if (!$fn) $fn = $rq;
if (preg_match('/^([^-]+)-[^-]+$/', $fn, $m)) $rq = $m[1];
$re = '/^' . $rq . '-(.*)\./';
$dirhandle = opendir('.');
while ($f = readdir($dirhandle)) {
  if (preg_match($re, $f, $m)) {
    if ($f != $fn) {
      $ext = explode('_', $m[1]);
      while (list($k, $v) = each($ext)) {
        if ($languages[$v]) {
          $lang = $languages[$v];
          printf('<a title="%s" href="%s">%s</a>&nbsp;', $lang, $f, $lang);
        }
      }
    }
  }
}
closedir($dirhandle);
echo "</div>\n";
}

Aby indexovací roboty nehodnotily stránku podle názvů či zkratek jazyků, jsou odkazy uvedeny na konci stránky, ale kaskádovým stylem se zobrazí na začátku. Kaskádový styl pak obsahuje:

h1 {
  font-size: 180%;
  margin-top: 2.5em;
  font-family: "Tms Rmn", "Times New Roman", "Times Roman", serif
}

#langlinks {
  position: absolute; top:0.5em; right:0;
  background-color: rgb(153,0,51);
  color: rgb(204,221,255);
  width: 100%;
  text-align: right;
  font-family: "Lucida Sans", Helvetica, Arial, sans-serif;
  font-weight: bold;
  font-size: 125%;
  padding-color: rgb(153,0,51);
  margin-color: rgb(153,0,51);
}

Pokud soubory s jednotlivými jazykovými variantami zapíšeme jako PHP skripty, pak jejich struktura bude:

<?php
if (!function_exists('FindLanguage')) {
require 'functions.php';
EmitHeader(basename($REQUEST_URI));
}
Expires();
?>
<html>
<head>
...
<link rel="STYLESHEET" type="text/css" href="style.css" />
</head>
<body>
...
<?php LangLinks(); ?>
</body>
</html>

Takové skripty se chovají obdobně jako standardní technika sjednání obsahu.

Friday, 09-Sep-2005 10:19:14 CEST