PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Archive for the ‘PHP’ Category

Adress- und Kontaktdaten als VCard exportieren

with 13 comments

VCards sind kleine Dateien, die Personen- und Adressbuch-Daten enthalten. Diese Dateien kann man wunderbar an Emails dranhängen, oder aber als Export + Import zwischen verschiedenen Programmen (Outlook, Thunderbird, Mac OS X Addressbook etc) nutzen. Als Premium-Mitglied bei Xing kann man alle seine Kontakte dort auch exportieren und in die oben genannten Applikationen oder auch in sein Handy importieren.

Wie sieht eine VCard aus? Ungefähr so:

BEGIN:VCARD
VERSION:3.0
N:Kliewe;Michael
FN:Michael Kliewe
ADR;TYPE=HOME,WORK:;;Am Stadtgarten 8a;Oelde;;59302
EMAIL;TYPE=HOME;TYPE=WORK:emailadresse@domain.de
EMAIL;TYPE=PREF:wichtige@emailaddresse.de
END:VCARD

Man sieht, das Format ist schon recht alt (1998), heutzutage würde man wahrscheinlich XML wählen. Aber es ist nun mal DER Standard, wir wollen die Adressdaten aus unserer Applikation als VCard exportieren. Dazu gibt es bereits einige vorgefertigte Klassen (Contact_Vcard_Build, PHP vCard Class 2.0, class vcard), die ich aber hier nicht nutzen möchte, da sie entweder das ältere 2.1 Format nutzen oder kein UTF-8 unterstützen. Hier eine einfache Lösung mit wenigen Zeilen Code:

<?php
header("Content-type:text/x-vCard; charset=utf-8");
header("Content-Disposition: attachment; filename=vcardexport.vcf");

$first = "Michael";
$last = "Kliewe";
$email = "emailadresse@domain.de";
?>
BEGIN:VCARD
VERSION:2.1
N:<?php echo($last); ?>;<?php echo($first); ?>
FN:<?php echo($first); ?> <?php echo($last); ?>
EMAIL;PREF;INTERNET:<?php echo($email); ?>
REV:<?php echo date('Y-m-d H:i:s'); ?>
END:VCARD

Das ist natürlich nur der Anfang und ein erster Test. Aber das Prinzip ist recht einfach: Jede Zeile enthält eine Eigenschaft, wie „Voller Name“, „Email-Adresse“, „Wohnadresse“, „Notizen“, „Urls“, „Fotos“, „Firma“, „Geburtstag“ und so weiter. Key und Value sind durch einen Doppelpunkt getrennt, spezielle Optionen wie beispielsweise das oben sichtbare „PREF“ oder „TYPE“ sind via Semikolon getrennt hinzuzufügen. Weitere Informationen erhält man auf der englischen Wikipedia-Seite (die deutsche ist nicht so umfangreich). Man kann auch mehrere Kontakte in einer vcf-Datei exportieren, und objektorientiert mit dem Zend Framework sieht das ganze dann ungefähr so aus:

public function exportvcardAction()
{
	$this->_helper->layout()->disableLayout();
	$this->_helper->viewRenderer->setNoRender();

	$vcardString = '';
	$contacts = $this->service->getAllContacts();
	foreach ($contacts as $contact) {
		/* @var $contact App_Addressbook_Contact */
		$vcardString .= $contact->getContactAsVcardString() . "\r\n";
	}

	if (count($contacts) == 1) {
		$filename = str_replace(" ", "_", $contacts[0]->getFirstname().' '.$contacts[0]->getSurname());
	} else {
		$filename = 'VCARDS';
	}

	$this->getResponse()->setHeader('Content-Type', 'text/x-vCard; charset=utf-8');
	$this->getResponse()->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'.vcf"');
	$this->getResponse()->setHeader('Content-Length', strlen($vcardString));
	$this->getResponse()->setHeader('Connection', 'close');
	$this->getResponse()->setBody($vcardString);
}

Damit werden nun also alle Kontakte iteriert und die einzelnen VCard-Strings aneinandergehängt. Wir nutzen hier die entsprechenden Funktionen, um das Layout und den ViewRenderer zu deaktiveren und alle Informationen ins Response-Objekt zu schreiben
Die Funktion getContactAsVcardString() habe ich so umgesetzt:

public function getContactAsVcardString()
{
	$data['BEGIN'] = 'VCARD';
	$data['VERSION'] = '3.0';
	$data['PRODID'] = '-//company contact export Version 1//EN';
	$data['REV'] = date('Y-m-d H:i:s');
	$data['TZ'] = date('O');

	$data['FN'] = $this->getDisplayName();
	$data['N'] = $this->getSurname().';'.$this->getFirstname();

	if ($this->getTitle() != '') {
		$data['TITLE'] = $this->getTitle();
	}

	foreach ($this->getMailAddresses() as $mailAddress) {
		$data['EMAIL;TYPE=internet'] = $mailAddress;
	}

	if ($this->getNotice() != '') {
		$data['NOTE'] = $this->getNotice();
	}

	$data['END'] = 'VCARD';

	$exportString = '';
	foreach ($data as $index => $value) {
		$exportString .= $index . ':' . $value . "\r\n";
	}

	return $exportString;
}

Als Ergebnis erhält man eine Datei zum Download mit allen $contacts incl. Details.

Written by Michael Kliewe

März 31st, 2010 at 8:44 am

Posted in PHP

Tagged with , , ,

Mit PHP eine eBay-Auktion starten

with 7 comments

eBay, wie viele andere große Webseiten und Dienste bietet Entwicklern natürlich auch eine API, mit der man leicht eigene Applikationen erstellen und eBay steuern kann. Nahezu alles, was man bei eBay auf der Webseite machen kann, kann man auch via API steuern, beispielsweise Verkaufen, Kaufen, Bieten, Bewerten, Suchen usw.

Um auf die API zuzugreifen müssen wir uns als „eBay Developer“ registrieren, um danach die Application-Keys (DEVID, AppID, CertID) zu erhalten. Wir erstellen uns erst einmal Sandbox-Keys, um unser Script in einer Testumgebung zu testen und kein Geld für die Tests bezahlen zu müssen. Wenn das Script erfolgreich läuft, kann man die Sandbox-Keys durch Production-Keys austauschen und richtig loslegen.

Da wir ja eine eBay-Auktion starten möchten, müssen wir uns noch einen User-Token erstellen, damit wir „im Auftrag eines eBay-Accounts“ arbeiten können.

Wir erstellen vorerst einen Testbenutzer für die Sandbox. Später werden wir einen Produktionsaccount (einen richtigen Account) nutzen und dafür einen User-Token erstellen.

Nachdem wir einen TESTUSER angelegt haben und für diesen Testuser ein User-Token generiert haben, können wir unser Script schreiben. Das folgende Script habe ich aus den PHP-Code-Samples abgeleitet. Dort findet man auch noch sehr viele andere Beispiele der API, die aber leider veraltet sind. So musste ich diverse Änderungen an den Beispiel-Codes machen, um es überhaupt ans Laufen zu bekommen (Paperless Zahlungsmöglichkeit hinzufügen, ReturnPolicy+ShippingDetails+DispatchTimeMax hinzufügen etc.).

Diese Klasse ist nur ein Testscript, es fehlen Setter und Getter usw., aber für Euch soll es reichen 😉

<?php
$addItem = new eBayAddItem();
$addItem->callEbay();
$addItem->printResult();

class eBayAddItem
{
	private $_siteId = 77;	// default: Germany
	private $_environment = 'sandbox';   // toggle between sandbox and production
	private $_eBayApiVersion = 661;
	private $_call = 'AddItem';
	private $_keys = array(
		'production' => array(
			'DEVID' 	=> '',
			'AppID' 	=> '',
			'CertID' 	=> '',
			'UserToken'	=> '',
			'ServerUrl' => 'https://api.ebay.com/ws/api.dll'
			),
		'sandbox' => array(
			'DEVID' 	=> '6daxxxxxxxxxxxxxxxxxxxxxxxxxx1e4622',
			'AppID' 	=> 'Mixxxxxxxxxxxxxxxxxxxxxxxxxxxxxx930',
			'CertID' 	=> '68xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx00e',
			'UserToken'	=> 'AgAxxxxxxxxlaaaangxxxxxxxxxxIrGgYZ',
			'ServerUrl' => 'https://api.sandbox.ebay.com/ws/api.dll'
		)
	);

	private $_itemStartPrice = 1;
	private $_itemBuyItNowPrice = 10;
	private $_itemTitle = 'New article title2';
	private $_itemDescription = 'Description here2';
	private $_categoryId = 42511;

	private function _getRequestBody()
	{
		$apiValues = $this->_keys[$this->_environment];

		$search = array(
			'%%USER_TOKEN%%', '%%EBAY_API_VERSION%%', '%%START_PRICE%%',
			'%%BUY_IT_NOW_PRICE%%', '%%TITLE%%', '%%DESCRIPTION%%', '%%CATEGORY_ID%%'
		);
		$replace = array(
			$apiValues['UserToken'], $this->_eBayApiVersion, $this->_itemStartPrice,
			$this->_itemBuyItNowPrice, $this->_itemTitle, $this->_itemDescription, $this->_categoryId
		);

		$requestXmlBody = file_get_contents('additem2.xml');
		$requestXmlBody = str_replace($search, $replace, $requestXmlBody);

		return $requestXmlBody;
	}

	public function callEbay()
	{
		$apiValues = $this->_keys[$this->_environment];

		$connection = curl_init();
		curl_setopt($connection, CURLOPT_URL, $apiValues['ServerUrl']);
		curl_setopt($connection, CURLOPT_SSL_VERIFYPEER, 0);
		curl_setopt($connection, CURLOPT_SSL_VERIFYHOST, 0);

		$headers = array (
			'X-EBAY-API-COMPATIBILITY-LEVEL: ' . $this->_eBayApiVersion,
			'X-EBAY-API-DEV-NAME: ' . $apiValues['DEVID'],
			'X-EBAY-API-APP-NAME: ' . $apiValues['AppID'],
			'X-EBAY-API-CERT-NAME: ' . $apiValues['CertID'],
			'X-EBAY-API-CALL-NAME: ' . $this->_call,
			'X-EBAY-API-SITEID: ' . $this->_siteId,
		);

		curl_setopt($connection, CURLOPT_HTTPHEADER, $headers);
		curl_setopt($connection, CURLOPT_POST, 1);

		$requestBody = $this->_getRequestBody();

		curl_setopt($connection, CURLOPT_POSTFIELDS, $requestBody);
		curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1);
		$responseXml = curl_exec($connection);
		curl_close($connection);
		$this->_responseXml = $responseXml;
	}

	public function printResult()
	{
		//Xml string is parsed and creates a DOM Document object
		$responseDoc = new DomDocument();
		$responseDoc->loadXML($this->_responseXml);

		//get any error nodes
		$errors = $responseDoc->getElementsByTagName('Errors');

		//if there are error nodes
		if($errors->length > 0)
		{
			echo '<P><B>eBay returned the following error(s):</B>';
			//display each error
			//Get error code, ShortMesaage and LongMessage
			$code     = $errors->item(0)->getElementsByTagName('ErrorCode');
			$shortMsg = $errors->item(0)->getElementsByTagName('ShortMessage');
			$longMsg  = $errors->item(0)->getElementsByTagName('LongMessage');
			//Display code and shortmessage
			echo '<P>', $code->item(0)->nodeValue, ' : ', str_replace(">", "&gt;", str_replace("<", "&lt;", $shortMsg->item(0)->nodeValue));
			//if there is a long message (ie ErrorLevel=1), display it
			if(count($longMsg) > 0) {
				echo '<BR>', str_replace(">", "&gt;", str_replace("<", "&lt;", $longMsg->item(0)->nodeValue));
			}

		} else { //no errors
			//get results nodes
			$responses = $responseDoc->getElementsByTagName("AddItemResponse");
			foreach ($responses as $response) {
				$acks = $response->getElementsByTagName("Ack");
				$ack   = $acks->item(0)->nodeValue;
				echo "Ack = $ack <BR />\n";   // Success if successful

				$endTimes  = $response->getElementsByTagName("EndTime");
				$endTime   = $endTimes->item(0)->nodeValue;
				echo "endTime = $endTime <BR />\n";

				$itemIDs  = $response->getElementsByTagName("ItemID");
				$itemID   = $itemIDs->item(0)->nodeValue;
				echo "itemID = $itemID <BR />\n";

				$linkBase = "http://cgi.sandbox.ebay.com/ws/eBayISAPI.dll?ViewItem&item=";
				echo "<a href=$linkBase" . $itemID . ">$this->_itemTitle</a> <BR />";

				$feeNodes = $responseDoc->getElementsByTagName('Fee');
				foreach($feeNodes as $feeNode) {
					$feeNames = $feeNode->getElementsByTagName("Name");
					if ($feeNames->item(0)) {
						$feeName = $feeNames->item(0)->nodeValue;
						$fees = $feeNode->getElementsByTagName('Fee');  // get Fee amount nested in Fee
						$fee = $fees->item(0)->nodeValue;
						if ($fee > 0.0) {
							if ($feeName == 'ListingFee') {
								printf("<B>$feeName : %.2f </B><BR>\n", $fee);
							} else {
								printf("$feeName : %.2f <BR>\n", $fee);
							}
						}
					}
				}
			}
		}
	}
}
<?xml version="1.0" encoding="UTF-8"?>
<AddItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
	<RequesterCredentials>
		<eBayAuthToken>%%USER_TOKEN%%</eBayAuthToken>
	</RequesterCredentials>
	<DetailLevel>ReturnAll</DetailLevel>
	<ErrorLanguage>en_US</ErrorLanguage>
	<Version>%%EBAY_API_VERSION%%</Version>
	<Item>
		<BuyItNowPrice currencyID="EUR">%%BUY_IT_NOW_PRICE%%</BuyItNowPrice>
		<Country>DE</Country>
		<Currency>EUR</Currency>
		<Description><![CDATA[%%DESCRIPTION%%]]></Description>
		<DispatchTimeMax>1</DispatchTimeMax>
		<ListingDuration>Days_7</ListingDuration>
		<ListingType>Chinese</ListingType>
		<Location><![CDATA[Oelde, NRW]]></Location>
		<PaymentMethods>PayPal</PaymentMethods>
		<PayPalEmailAddress>mypaypal@domain.de</PayPalEmailAddress>
		<PrimaryCategory>
			<CategoryID>%%CATEGORY_ID%%</CategoryID>
		</PrimaryCategory>
		<Quantity>1</Quantity>
		<RegionID>0</RegionID>
		<ReturnPolicy>
			<ReturnsAcceptedOption>ReturnsNotAccepted</ReturnsAcceptedOption>
		</ReturnPolicy>
		<StartPrice>%%START_PRICE%%</StartPrice>
		<ShippingDetails>
			<ShippingServiceOptions>
				<ShippingService>DE_SonstigeDomestic</ShippingService>
				<ShippingServicePriority>1</ShippingServicePriority>
				<FreeShipping>true</FreeShipping>
			</ShippingServiceOptions>
		</ShippingDetails>
		<Site>Germany</Site>
		<Title><![CDATA[%%TITLE%%]]></Title>
	</Item>
</AddItemRequest>

Eine riesen Hilfe war die API-Dokumentation zur AddItem Funktion. Dort ist ein XML-Beispiel, alle Tags sind anklickbar, man erhält für jedes Tag eine Beschreibung und die möglichen und erlaubten Werte.

Vielleicht schreibe ich bald noch einen Artikel darüber, wie man seine Auktionen überwacht (Höchstgebote auflisten) und den Auktionsgewinner automatisch per Email kontaktiert.

Written by Michael Kliewe

März 30th, 2010 at 9:01 am

Posted in PHP

Tagged with , , ,

Buchvorstellung: PHP Sicherheit

with 8 comments

Ein weiteres Werk von meinem Berg ist durchgearbeitet: Das 333 Seiten starke Standard-Werk zu „PHP Sicherheit“ von Christopher Kunz und Security-Guru Stefan Esser, den viele wahrscheinlich von seinem „Month of PHP Bugs“ oder der PHP-Erweiterung Suhosin kennen. Vor mir liegt die 3. Auflage aus dem Jahr 2008, und ich kann auch schon mal vorweg nehmen, dass die Themen heute noch brandaktuell sind und wahrscheinlich durch 5.3 keine gravierenden neuen Tipps dazugekommen wären.

Was erwartet man bei einem solchen Titel? Eine Liste von PHP-Core-Sicherheitslücken, Hinweise wie „vertraue keinen Usereingaben“, oder Aussagen „PHP ist garnicht so unsicher wenn man es richtig benutzt“? Ja, unter anderem, und noch viel mehr.

Das Inhaltsverzeichnis liest sich wie eine Offenbarung, wirklich alle wichtigen Schlagwörter sind vertreten und machen Lust auf Details. Zuerst gehen die Autoren auf allgemeine Sicherheitsprinzipien ein, helfen dabei wie man seinem Chef erklärt warum Sicherheit viel wert ist und Zeit sowie Geld kosten darf (und muss!). Wichtige Mailinglisten wie Full Disclosure und BugTraq werden genannt, um immer auf dem Laufenden zu sein.

Im zweiten Kapitel werden Methoden aufgezeigt, wie Angreifer Informationen über Webserver, Datenbankversionen, PHP-Versionen und -Erweiterungen, verwendete Software, Pfade usw. sammeln und ausnutzen. Im darauf folgenden Kapitel werden Möglichkeiten erläutert, wie Angreifer $_GET, $_POST, $_COOKIE und sogar $_SERVER Variablen manipulieren, um Angriffe wie „HTTP Response Splitting“, „Remote Command Execution“, „Cookie Poisioning“, „Mail-Header-Manipulation“ und vieles mehr durchführen können. Natürlich werden auch Lösungen präsentiert.

(Quelle: http://projects.webappsec.org/Web-Application-Security-Statistics#Summary)

Ein ganzes Kapitel beschäftigt sich dann mit dem Web-Sicherheitsproblem Nummer 1: Cross-Site Scripting (CSS/XSS) inklusive dem Thema „Cross-Site Request Forgery“, sowie der Nummer 3: SQL Injection. Das nächste Kapitel beschäftigt sich mit Authentisierung/Authentifizierung. Ein weiteres Kapitel umfasst das Thema Session (also u.a. Session Bruteforcing, Hijacking, Fixation). Jeweils gibt es auch Anleitungen, wie man diese Gefahren abwehrt.

Dann beginnen die Kapitel, die sich mit grundsätzlichen Lösungen zur Härtung der Installation beschäftigen: die PHP-Extension ext/filter wird erläutert, suExec, der Safe-Mode, open_basedir, Code-Sandboxing mit runkit, mod_chroot und vielen vielen Einstellungen und Modulen mehr.

Das Kapitel 11 beschäftigt sich, man hätte es fast vermisst, mit Suhosin. Installation, Konfiguration und Nutzen werden umfassend erläutert. Das letzte Kapitel bilden die Filtermodule mod_security und mod_parmguard. Doch das Buch ist noch nicht am Ende, im Anhang befinden sich noch Checklisten zu sicheren Webapplikationen, die wichtigen Optionen der php.ini zusammengefasst sowie die Liste aller aktuellen Gefahren, denen man sich stellt im Internet.

Tja, was soll ich sagen, es ging durch wie Butter. Die Grundlagen sind natürlich extrem wichtig, um die Auswirkungen zu verstehen und das Vorhandensein der Lücken in der eigenen Software prüfen zu können. Ich wette, dass dem ein oder anderen Leser beim Durcharbeiten der Satz „Oha, meine Anwendung ist dagegen anfällig“ durch den Kopf geht. Interessant finde ich aber auch die Checkliste, die jeder PHP-Programmierer (eigentlich jeder Webentwickler) bei einem (Re)Launch durchgehen sollte, solch eine Liste fehlt einfach, um sie methodisch Punkt für Punkt durchgehen zu können.

Voraussetzung zum Verständnis ist natürlich ein gewisses Vorwissen im PHP-Bereich sowie ein selbst installierter Webserver, ohne ein paar Grundlagen in den Bereichen versteht man die Auswirkungen von vielen Dingen wahrscheinlich nicht. Dieses Grundlagenbuch sollte jeder gelesen haben, wenn dann noch alle gewissenhaft die Lösungen befolgen wäre das Web um vieles sicherer. Ich überlege gerade, was ich zu meckern hätte… aber mir fällt nichts ein. Keine Rechtschreibfehler, praxisnahe Beispiele, Vollständigkeit. Bin sprachlos.

Pflichtlektüre!

Written by Michael Kliewe

März 29th, 2010 at 7:33 am

Posted in PHP

Tagged with , , ,

Zend_View Output Filter: Whitespaces aus HTML entfernen

with 22 comments

Zend Framework implementiert das Intercepting-Filter-Pattern, und dadurch ist es sehr einfach nach der Generierung des HTML-Codes (Postprocessor) einen Filter einzufügen, der die Ausgabe noch verändert. Natürlich gibt es auch Präprozessor-Filter, in denen man häufig die Authentifizierung prüft oder Eingabeparameter validiert.

Vorstellen könnte man sich beispielsweise einen Filter, der bestimmte Textblöcke ersetzt, um zum Beispiel aus „mailto:“-Links Bilder zu generieren oder sie anders zu schützen, beispielsweise mittels Javascript.

Man kann sich auch einen Filter vorstellen, der das HTML-Ergebnis durch den HTML-Purifier schickt, ihn also W3C-kompatibel macht. Dies sollte man jedoch vermeiden wenn eben möglich und seinen HTML-Code auch vorher bereits korrekt generieren.

In diesem Artikel möchte ich einen ganz einfachen Filter einrichten, der alle Tabs, Leerzeichen und Umbrüche entfernt, sodass der komplette HTML-Code in einer Zeile steht und keine unnötigen Whitespaces enthält. Man kann damit den Traffic um ca. 3-15% (nicht gzip’t) verringern, nur durch das Entfernen von Tabs und Leerzeichen! Es ersetzt natürlich nicht die Aktivierung von gzip, um „richtig“ Traffic zu sparen und die Webseite schneller zu machen.

Wir schreiben uns also einen einfachen Filter, der die Aufgabe erfüllt:

<?php
class App_View_Filter_Minify implements Zend_Filter_Interface
{
	public function filter($string)
	{
		return preg_replace(
			array('/>\s+/', '/\s+</', '/[\r\n]+/'),
			array('>', '<', ' '),
			$string
		);
	}
}

Die filter() Methode bekommt den Ausgabestring übergeben, wir filtern alle Whitespaces heraus und geben das Ergebnis zurück. Wir sind hier etwas vorsichtig und filtern nur direkt vor oder nach HTML-Tags.

Diesen Filter müssen wir nun noch einbauen, und das geht wie folgt in der Bootstrap.php, wo wir im View-Objekt den Filter aktivieren:

/**
 * Add Output filters to View
 *
 * @return void
 */
protected function _initViewFilter()
{
	$view = $this->getResource('view');
	$view->addFilterPath('App/View/Filter', 'App_View_Filter_')
		->addFilter('Minify');
}

Wir fügen dem View-Objekt also einen Pfad hinzu, wo wir den Filter abgelegt haben, und übergeben dann noch den Namen des Filters. Das Zend Framework wird dann nach der Abarbeitung der ControllerAction und dem Rendern des View-Scripts diesen Filter benutzen und die Ausgabe verändern.

Ergebnis ist dann eine einzelne Zeile HTML-Code ohne Whitespaces. Hier ein paar Ergebnisse dieses doch sehr einfachen Filters:

Größe vorher (KB)Größe nachher (KB)Ersparnis (%)
6,15,59,9
100,990,210,6
4,64,28,7
18,918,61,6

Written by Michael Kliewe

März 25th, 2010 at 9:19 am

Speichern der Session-Daten im Memcached

with 16 comments

Normalerweise speichert PHP die Session-Daten lokal auf der Festplatte, und zwar im Verzeichnis das in der php.ini unter session.save_path eingetragen ist (z.B. /tmp unter Linux). Das sollte man jedoch dringend anpassen wenn man mehrere Applikationen auf einem Webserver laufen hat, da sonst alle Applikationen dort ihre Session-Daten im selben Verzeichnis ablegen. Wer sich in Applikation 1 einloggt ist auch direkt in Applikation 2 eingeloggt (natürlich nur wenn beide Applikationen ähnliche Daten in der Session speichern, zum Beispiel die UserId).

Also sollte man für jede Applikation einen eigenen Pfad angeben, um die Daten zu trennen. Das macht man normalerweise mit

ini_set('session.save_path', '/application1/sessiondata/);

Der Webserver muß dort selbstverständlich Lese- und Schreibrechte besitzen.

Doch das lokale Speichern auf der Festplatte hat auch Nachteile. Einerseits ist die Geschwindigkeit nicht so berauschend, andererseits möchte man eventuell einen zentralen Speicher nutzen falls man mehrere Webserver hinter einem einfachen Loadbalancer hat, der die einzelnen Requests jeweils auf einen zufälligen Webserver verteilt.

Spätestens dann benötigt man einen zentralen Session-Speicher. Im Zend Framework gibt es bereits eine Lösung mit Hilfe einer Datenbank. Ich möchte hier die Memcached-Lösung vorstellen in Verbindung mit Zend_Session. Ganz ähnlich funktioniert auch die native Lösung mit Hilfe der session_set_save_handler() Funktion.

Wir benötigen zuerst eine Klasse, die einige wichtige Funktionen zur Verfügung stellt:

<?php
class App_Session_SaveHandler_Memcached implements Zend_Session_SaveHandler_Interface
{
	/**
	 * @var int
	 */
	private $_maxlifetime = 3600;
	/**
	 * @var Zend_Cache_Core
	 */
	private $_cache;

	public function __construct(Zend_Cache_Core $cacheHandler) {
		$this->_cache = $cacheHandler;
	}

	public function open($save_path, $name) {
		return true;
	}

	public function close() {
		return true;
	}

	public function read($id) {
		if (!($data = $this->_cache->load('SessionData_'.$id))) {
			return '';
		} else {
			return $data;
		}
	}

	public function write($id, $sessionData) {
		$this->_cache->save($sessionData, 'SessionData_'.$id, array(), $this->_maxlifetime);
		return true;
	}

	public function destroy($id) {
		$this->_cache->remove('SessionData_'.$id);
		return true;
	}

	public function gc($notusedformemcache) {
		return true;
	}
}

Nun können wir diese Klasse als SaveHandler nutzen:

$cache = Zend_Cache::factory(
				'Core',
				$backend,
				$frontendOptions,
				$backendOptions
);

Zend_Session::setSaveHandler(new App_Session_SaveHandler_Memcached($cache));
Zend_Session::setOptions(
	array(
		//'cookie_secure' 	=> true,	// only for https
		'name' 				=> 'AppName1',
		'cookie_httponly'	=> true,
		//'gc_maxlifetime'	=> 60*60,
));
Zend_Session::start();

Nun werden die Session-Daten nicht mehr auf der Festplatte gespeichert, sondern im zentralen Memcached abgelegt. Falls die PHP-Session-Funktionen genutzt werden statt Zend_Session, ist die Lösung fast genauso einfach und die App_Session_SaveHandler_Memcached Klasse mit ein paar Anpassungen nutzbar.

EDIT: Statt dieses in PHP geschriebenen Save-Handlers kann man auch direkt den Handler auf „memcache“ setzen und den Server mittels „save_path“ definieren. Funktioniert genauso, es hat nur den einzigen Nachteil dass man den Key nicht frei wählen kann. Mehrere Memcached-Server kann man einfach kommasepariert auflisten:

Zend_Session::setOptions(
	array(
		'name' 			=> 'App1',
		'save_handler' 	=> 'memcache',
		'save_path'		=> 'tcp://'.$this->_applicationIni->cache->memcached->ip.':' .
								$this->_applicationIni->cache->memcached->port.
								'?persistent=1&amp;weight=1&amp;timeout=1&amp;retry_interval=15',
		'cookie_httponly'=> true,
		'gc_maxlifetime'=> 60*60,
	)
);
Zend_Session::start();

Falls die Fehlermeldung „Cannot find save handler ‚memcache'“ kommt mußt man die memcache-Extension neu kompilieren, dann mit Session-Handler-Support:

pecl uninstall memcache
pecl install memcache
Enable memcache session handler support? [yes] : yes

Danke an Ulf für den Druck, dass ich das auch mal ausprobiere 😉

Written by Michael Kliewe

März 24th, 2010 at 6:23 am