Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
495 lines (446 sloc) 10.2 KB
<?php
/**
* Antwort auf HTTP-Requests
*
* Nur einsetzen, wenn man damit *alle* Header generieren will.
* @author Thomas Scholz <info@toscho.de>
*
* Um ein einfaches HTML-Dokument zu verschicken:
$response = new HTTP_Response;
* Fertig.
*/
//error_reporting(E_ALL | E_STRICT);
class HTTP_Response {
/**#@+
* @access private
*/
private
$mime,
$charset = 'utf-8',
$compression = TRUE,
$allow_cache = TRUE,
$now,
$lastmod, // Wird vom Konstruktor gesetzt.
$lastmod_gmt,
$max_expires, // Maximale Verfallszeit.
$cache_control = array(), // Kombination mehrerer Werte
$headers = array(); // Zusätzliche Header
/**#@-*/
/**
* Konstruktor
*
* @param bool $auto TRUE = Sendet die Header automagisch.
* @param string $mime MIME-Typ
* @return void
*/
public function __construct($auto = TRUE, $mime = 'text/html')
{
$this->now = time();
$this->set_max_expires(3600 * 24 * 3 + $this->now); // Drei Tage
if ( '127.0.0.1' == $_SERVER["REMOTE_ADDR"] )
{
$this->set_max_expires(10 + $this->now); // 10 Sekunden
}
$this->set_mime($mime);
/* Sonst befüllt der Server das eventuell mit der Adresse, die ohne
* den Einsatz von mod_rewrite gälte. */
header('Content-Location: http://'
. $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], TRUE );
if ( $auto )
{
$this->send();
}
}
/**
* Fügt neue Header hinzu.
*
* @param string $name Feldname
* @param string $value Feldwert
* @return void
*/
public function add_header($name, $value)
{
$this->headers[$name] = $value;
return;
}
/**
* Entfernt Header.
*
* @param string $name Feldname
* @return void
*/
public function remove_header($name)
{
unset ( $this->headers[$name] );
return;
}
/**
* Verfallsdatum setzen
*
* @param int $int Zeit ins Sekunden
* @return void
*/
public function set_max_expires($int)
{
$this->max_expires = (int) $int;
return;
}
/**
* Verfallsdatum holen
*
* @return integer
*/
public function get_max_expires()
{
return $this->max_expires;
}
/**
* Errechnet das Verfallsdatum: Jetzt + Alter der jüngsten
* Datei in Sekunden.
*
* @return integer
*/
public function get_real_expires()
{
$t1 = ($this->now - $this->lastmod) + $this->now;
$t2 = $this->get_max_expires();
return ($t1 < $t2) ? $t1 : $t2;
}
/**
* Setzt den Mime-Typen, bsp.: 'text/html'
*
* @todo Negotiation für alle MIME-Typen.
* @param string $mime MIME-Typ
* @param bool $negotiate Soll notfalls ein anderer MIME-Typ benutzt werden?
* Derzeit nur XHTML
* @return void
*/
public function set_mime($mime, $negotiate=TRUE)
{
if ( ('application/xhtml+xml' != $mime)
or (FALSE === $negotiate) )
{
$this->mime = $mime;
return;
}
/* Aha, da will jemand XHTML verschicken. Erstmal sehen, ob das geht. */
if ( $this->ua_accepts_xhtml() )
{
$this->mime = $mime;
}
else
{
$this->mime = 'text/html';
}
return;
}
public function get_mime()
{
return $this->mime;
}
/**
* Setzt die Zeichenkodierung, bsp: 'utf-8'
*
* @param string $charset
* @return void
*/
public function set_charset($charset)
{
$this->charset = $charset;
return;
}
/**
* Gibt die Zeichenkodierung zurück.
*
* @return string
*/
public function get_charset()
{
return $this->charset;
}
/**
* Erlaubt (TRUE) oder verbietet die Kompression per gzip
*
* @param bool $c
* @return void
*/
public function set_compression($c)
{
$this->compression = $c;
return;
}
/**
* Sagt, ob komprimiert werden soll (g-zip).
*
* @return bool
*/
public function get_compression()
{
return $this->compression;
}
/**
* Erlaubt oder verbietet clientseitiges Caching
*
* @param bool $c
* @return void
*/
public function set_caching($c)
{
$this->allow_cache = $c;
return;
}
/**
* Sagt, ob die Ressource beim Client gecacht werden darf.
*
* @return bool
*/
public function get_caching()
{
return $this->allow_cache;
}
/**
* Erzeugt den Cache-Control-Header, in dem mehrere Werte kombiniert
* werden können.
*
* @return void
*/
private function cache_control()
{
if ( empty ( $this->cache_control ) )
{
return;
}
$c = array_unique($this->cache_control);
header ( 'Cache-Control: ' . implode(', ', $c) );
return;
}
private function send_mime()
{
$mime = $this->get_mime() . ( is_null( $this->get_charset() )
? '' : ';charset=' . $this->get_charset() );
/* Wichtig ist das TRUE hinten, sonst wird ein eventuell zuvor gesetzter
* Content-Type nicht überschrieben.
*/
header('Content-Type: ' . $mime, TRUE);
return;
}
/**
* Sendet die Header.
* TODO: Parameter für das Absenden separater Header einbauen.
*
* @return void
*/
public function send()
{
$this->lastmod = $this->real_lastmod();
$this->lastmod_gmt = gmdate("D, d M Y H:i:s", $this->lastmod) . ' GMT';
$this->send_mime();
// Zusätzliche Header
if ( !empty ( $this->headers ) )
{
foreach ( $this->headers as $key => $val )
{
header($key . ': '. $val, TRUE);
}
}
/* Wir gehen zunächst davon aus, daß die Ressource zum ersten Mal
* ausgeliefert wird. */
$fresh = TRUE;
/* Das prüfen wir aber. */
if ( $this->allow_cache )
{
$etag = md5($this->lastmod_gmt); /* Ein ekliger Hack. */
header('ETag: "' . $etag . '"');
$this->cache_control[] = 'max-age=' . $this->get_real_expires();
header('Expires: '
. gmdate("D, d M Y H:i:s", $this->get_real_expires() ) . ' GMT');
$fresh = $this->_validate_cache($etag);
}
/* Der Cache ist jetzt bereits validiert worden. Inhalte wollen wir
* nicht mehr versenden. */
if ( !$fresh )
{
die();
}
/* Okay, wir verschicken doch was. */
$this->compress();
$this->cache_control();
return;
}
/**
* Schaltet bei Bedarf den ob_gzhandler an.
*
* @return void
*/
private function compress()
{
if ( !is_null($this->compression)
and !zlib_get_coding_type()
and 'HTTP/1.1' == $_SERVER['SERVER_PROTOCOL']
)
{
ob_start( array ( 'ob_gzhandler', 9 ) );
header('Vary: Accept-Encoding');
// Sonst cacht der IE Win nicht.
$this->cache_control[] = 'private';
//$this->send_mime();
}
return;
}
/**
* Validiert den Client-Cache
*
* @param string $etag MD5
* @return void
*/
private function _validate_cache($etag)
{
$fresh = TRUE;
if ( isset ( $_SERVER['HTTP_IF_NONE_MATCH'] )
and strpos($_SERVER['HTTP_IF_NONE_MATCH'], $etag) )
{
$fresh = FALSE;
}
if ( isset ( $_SERVER['HTTP_IF_MODIFIED_SINCE'] )
and strstr($_SERVER['HTTP_IF_MODIFIED_SINCE'], $this->lastmod_gmt) )
{
$fresh = FALSE;
}
if ( TRUE == $fresh )
{
/* in send() wird $fresh jetzt auf TRUE gesetzt, damit der
* Inhalt verschickt werden kann.
*/
return TRUE;
}
$this->send_mime();
if ( strstr(PHP_SAPI, 'cgi') )
{
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
echo "\r\n\r\n";
}
else
{
if ( version_compare( PHP_VERSION, '4.3.0', '>=' ) )
{
header('Not Modified', TRUE, 304);
}
else
{
header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
}
}
$this->send_mime();
die();
return FALSE;/* Wir senden nix. */
}
/**
* Gibt den Zeitpunkt der letzten Änderung aller verwendeten
* Dateien als UNIX-Timestamp zurück.
* @return integer
*/
public function real_lastmod()
{
$arr_all_files = get_included_files();
if ( !array_key_exists(1, $arr_all_files) )
{
return getlastmod();
}
foreach ( $arr_all_files as $val )
{
$all_dates[] = filemtime($val);
}
/* Fix für http://bugs.php.net/bug.php?id=14837 */
if ( strstr(PHP_SAPI, 'cgi')
and
isset ( $_SERVER["SCRIPT_FILENAME"]) )
{
$all_dates[] = filemtime($_SERVER["SCRIPT_FILENAME"]);
}
// Änderungen an dieser Klasse selbst werden nicht berücksichtigt.
$all_dates[] = filemtime(__FILE__);
sort($all_dates);
return end($all_dates);
}
/**
* Redirect permanent or temporarly.
*
* @param string $url Target
* @param boolean $permanent TRUE (default) = 301, FALSE = 302
*
* @return void
*/
public function redirect($url, $permanent=TRUE)
{
$prefix = 'http://' . $_SERVER['HTTP_HOST'] . '/';
if ( !stristr($url, $prefix) )
{
$url = $prefix . ltrim($url, '/');
}
// 302 == temporarly, 301 == permanent.
header("Location: " . $url, TRUE, '30'. ($permanent ? '1' : '2') );
die ( ($permanent ? 'Ab jetzt' : 'Derzeit')
. " hier: <a href='$url'>$url</a>." );
return;
}
public function clean_request()
{
$this->remove_index_checks();
$this->remove_get_params();
return;
}
public function remove_index_checks($exclude_list = array() )
{
$crap = array( 'index', 'index.php', 'index.html', 'index.htm',
'default.html', 'index.cgi', '%20');
$url_arr = explode('/', $_SERVER['REQUEST_URI']);
$url_last = array_pop($url_arr);
if ( in_array($url_last, $crap) )
{
$this->redirect( implode('/', $url_arr) );
}
return;
}
// Aufruf mit Parameter
public function remove_get_params($exclude_list = array() )
{
if ( strstr($_SERVER['REQUEST_URI'], '?') )
{
$tmp = explode('?', $_SERVER['REQUEST_URI']);
$this->redirect($tmp[0]);
die(); // Sonst verhindern folgende Header eventuell den Redirect.
}
return;
}
/**
* Prüft, ob an den UA XHTML ausgeliefert werden darf.
* Gibt TRUE zurück, wenn ja, FALSE, wenn nicht.
*
* @return boolean
*/
function ua_accepts_xhtml() {
/* Das Caching nach Content-Negotiation gelieferter Ressourcen erfordert
* Features aus HTTP 1.1: Vary, ETag, If-Match und If-None-Match */
if ( 'HTTP/1.1' != $_SERVER['SERVER_PROTOCOL']
or !isset ( $_SERVER['HTTP_ACCEPT'] ) )
{
return FALSE;
}
/* Geckos vor rv:1.0 haben ein paar Crashbugs mit XHTML.
* @todo Opera 7 und tiefer abfangen; die sollten auch kein
* XHTML bekommen. */
if ( isset ( $_SERVER['HTTP_USER_AGENT'] )
and strpos($_SERVER['HTTP_USER_AGENT'], 'rv:0') )
{
return FALSE;
}
/* Behauptet der UA, XHTML zu können? */
if ( preg_match('|application/xhtml\+xml(?!\s*;\s*q=0)|',
$_SERVER['HTTP_ACCEPT']) )
{
return TRUE;
}
/* In dubio pro HTML. */
return FALSE;
}
}