Wie mittlerweile in mehreren Quellen erwähnt, haben bisherige Zend Framework Versionen (kleiner 1.7.7) eine Sicherheitslücke in der Klasse Zend_Filter_StripTags. Hier können bei Nutzung einer Whitelist trotz Filterung unerlaubte Tags eingeschleust werden. Besonders kritisch ist das bei Anwendungen wie diesen Blog. Es wird empfohlen auf die aktuellste Version zu patchen, oder zumindest die StripTags Datei mit einer gepatchten Version zu ersetzen.
Weitere Infos gibt es im Zend Framework Forum.
Vor kurzem bin ich in Thomas Weidners Blog auf einen Hinweis gestoßen, dass sich fehlende Übersetzungen problemlos loggen lassen. Dazu übergibt man dam Zend_Translate Objekt einfach einen Logger, in dem dann, im Falle einer fehlenden Übersetzung, eine Meldung “Untranslated message: xyz” geschrieben wird. Eine sehr praktische Möglichkeit, da bei größeren Projekten praktisch laufend neue Texte hinzugefügt und dann doch manchmal einzelne Übersetzungen vergessen werden.
In meiner zentralen Bootstrap Datei bereite ich in der Regel den Logger vor (oder hole mir den Logger aus dem Cache). Ebenfalls wird dort das Übersetzungsobjekt vorbereitet. Hier kann man also in einem Zug und ohne großen Aufwand den Logger für das Zend_Translate Objekt bekannt machen.
An dieser Stelle das Beispiel aus der Zend Framework Dokumentation:
$translate = new Zend_Translate('gettext', $path, 'de');
// Eine Log Instanz erstellen
$writer = new Zend_Log_Writer_Stream('/path/file.log');
$log = new Zend_Log($writer);
// Diese der Übersetzungs-Instanz hinzufügen
$translate->setOptions(array(
'log' => $log,
'logUntranslated' => true));
$translate->tranlate('unbekannter String');
Das Thema Benutzerrechte ist ja ein eher lästiges Thema, sollte aber schon möglichst früh in die Planung einer Applikation einfließen. Nach einigen Überlegungen und Diskussionen von verschiedenen Vorgehensweisen habe ich mich für einen recht einfachen Ansatz entschieden. Das Prinzip will ich hier kurz beschreiben, vielleicht ist es ja für den ein oder anderen interessant.
Die Lösung sollte möglichst wartungsfrei und einfach sein, zudem sollte natürlich die ACL Komponente vom Zend Framework zum Einsatz kommen. Folgende Rahmenbedingungen gelten für diese Lösung:
- Es gibt Benutzer und Gruppen
- Ein Benutzer/Gruppe hat ein Recht oder hat es nicht (keine eingeschränkten Rechte oder feinere Abstufungen)
- Ein Benutzer ist in keiner, einer oder mehreren Gruppen
- Ein Recht das einmal vergeben wurde kann nicht mehr wiederufen werden (z.B. durch eine Mitgliedschaft in einer weiteren Gruppe die das Recht explizit nicht hat)
Zend_Acl sieht Rollen und Ressourcen vor. Für Ressourcen können schließlich Rechte vergeben werden. Eine Rolle ist hier ein Benutzer oder eine Gruppe. Die Ressourcen ergeben sich aus den verfügbaren Modulen, Controller und Actions des MVC Patterns. Sollen also Rechte vergeben werden (z.B. in der Administration der Gruppen), so werden alle Module, Controller und Actions ausgelesen. Für diese können dann die Rechte gesetzt und in der Datenbank gespeichert werden. Der Benutzer hat erst einmal keine Rechte und für jede Berechtigung muss explizit ein Eintrag in einer Tabelle erzeugt werden. Hierfür wird eine Tabelle mit folgender Struktur angelegt:

Gibt es z.B: für ein Blogsystem ein Modul admin mit dem Controller articles und den Actions write, delete und list, so sind verschiedene Berechtigungen denkbar. Oben stehender Auszug zeigt drei Beispieldatensätze in der Tabelle für Berechtigungen, die auf das Beispiel passen könnten:
- Benutzer 1 darf alle Actions in allen Controller des Moduls admin ausführen
- Die Gruppe 55 darf alle Actions im Controller articles des Moduls admin ausführen
- Benutzer 12 darf nur die Action list im Controller articles des Moduls admin ausführen
Eine Prüfung der Rechte kann zentral an einer Stelle erfolgen. Hier kann ein Plugin beim Front Controller registriert werden. Ich bevorzuge es hier den Zend_Action_Controller in eine eigenen BasisController Klasse abzuleiten und dort in dem Hook init verschiedene Dinge, wie eben diese Berechtigungen zu prüfen. Alle eigenen Controller werden dann wiederum vom BasisController abgeleitet. So wird vor der Ausführung einer jeden Action zuerst die Berechtigung geprüft und ggf. mit einer Fehlerseite abgebrochen, falls diese fehlt.
Um eine Berechtigung zu prüfen, kann die Komponente Zend_Acl genutzt werden. Dies ist allerdings sehr aufwendig, deshalb ist an dieser Stelle unbedingt ein Caching notwendig. Mit der Zend_Cache Komponente ist das allerdings sehr einfach: es wird einmal das ACL Objekt erzeugt und dann für die Dauer der Session gecached (oder direkt in der Session des Benutzers gespeichert).
Ebenso müssen die überhaupt verfügbaren Rechte nicht extra konfiguriert werden, sondern ergeben sich automatisch aus den vorhandenen Modulen, Controller und Actions. Um diese für eine Rechtevergabe in der Administrationsoberfläche auszulesen, verwende ich das seit PHP 5 verfügbare Reflection. Um also alle Module, Controller und Actions zu erhalten kann folgende Funktion genutzt werden:
/**
* returns all available modules, controllers and actions
*
* @param Zend_Controller_Front $fc the current front controller
* @return array
*/
protected function getRessources($fc) {
$ressources = array();
// search all registered modules
foreach($fc->getControllerDirectory() as $module => $dir) {
$ressources[$module] = array();
// search all controller files
foreach(scandir(realpath(dirname(__FILE__)) . $dir) as $file) {
$currentFile = realpath(dirname(__FILE__)) . $dir . "/" . $file;
// search in all controller files for actions
if(is_file($currentFile) && strpos($file,"Controller")!==false) {
require_once($currentFile);
$classname = $module . "_" . str_replace(".php","",$file);
$controller = new ReflectionClass($classname);
$methods = array();
foreach($controller->getMethods() as $method) {
// don't include error handler
if($method->getName()=="errorAction")
continue;
$length = strpos($method->getName(),"Action");
if($length!==false)
$methods[] = substr($method->getName(),0,$length);
}
if(count($methods)>0) {
$controllername = strtolower(substr($file,0,strpos($file,"Controller")));
$ressources[$module][$controllername] = $methods;
}
}
}
}
return $ressources;
}
Als Administrationsoberfläche kann ich den Tree von Extjs empfehlen, der auch Checkboxes unterstützt:

An dieser Stelle möchte ich kein gesamtes Codebeispiel geben, da die Integration ins eigene Projekt und die eigene Umsetzung durchaus variieren kann. Was ich hier als Grundidee vermitteln will ist die Ausrichtung der ACL an der MVC Struktur. Einmal ins eigene Projekt eingebaut ist keine weitere Anpassung mehr notwendig.
Über Feedback, Bedenken und Anmerkungen freue ich mich, da diese Lösung pragmatisch, aber sicherlich auch verbesserungswürdig ist.
flickr ist eine Seite, die ich laufend beobachtet, aber nie so richtig genutzt habe. Bei meinen Streifzügen durch das Netz hab ich sie dann mal wieder seit einiger Zeit in Augenschein genommen und wollte mir die beliebtesten Bilder herausfischen. Es zeigt sich, dass man sehr gut nach einzelnen Stichworten suchen kann, aber als Übersicht über die beliebtesten Bilder nur einen Kalender Interesting findet. Dieser listet eine Übersicht über alle Tage und bietet einen Link zu den 500 beliebtesten Fotos des jeweiligen Tages. Ich hätte mir aber ein Best of über alle Jahre gewünscht.
Erster Gedanke ist also auf diesen Kalender basierend die beliebtesten Bilder zu ermitteln, sprich alle Tage mittels einem Skript parsen und dann jeweils die 10 beliebtesten Bilder auszugeben. Das Ergebnis ist dann eine Seite mit den schönsten Bilder eines Monats, wobei von jedem Tag die 10 beliebtesten Bilder zu sehen sind. Was sich kompliziert anhört ist mit dem Zend Framework in wenigen Zeilen passiert. Da es ein super Beispiel für Zend_Dom und Zend_Http ist, hab ich mich entschlossen die kurze Lösung hier zu präsentieren.
Folgende Dinge müssen im Skript ablaufen:
- Der Start und End Zeitpunkt werden gesetzt
- In einer Schleife werden die nächsten Schritte wiederholt, so lange das aktuelle Datum nicht größer als das Enddatum ist.
- Die URL zu der Kalenderseite auf flickr wird erzeugt (diese hat immer den gleichen Aufbau: http://www.flickr.com/explore/interesting/2008/12/01/page1)
- Der Inhalt der Seite wird mittels Zend_Http geladen
- Die Links zu den Bilder und Bildseiten werden mittels Zend_Dom_Query ausgelesen und in ein Array gesteckt
- Der Tag wird um eins erhöht und es geht bei 2 weiter
Im ersten Schritt wird also der Start und Endzeitpunkt festgelegt. Dafür ist Zend_Date zuständig.
$start = new Zend_Date();
$start->set('01.11.2008',Zend_Date::DATE_MEDIUM);
$stop = new Zend_Date();
$stop->set('05.11.2008',Zend_Date::DATE_MEDIUM);
Die Methode set erwartet einen String, der dann je nach zweiten Parameter geparst wird (so ist als zweiter Parameter z.B. auch Zend_Date::DATE_SHORT möglich, das dem Format dd.MM.YY entspricht).
Als zweiten Schritt wird geprüft ob $start nach $end liegt. Dafür nutzt man die Methode compare des Zend_Date Objektes. -1 als Ergebnis bedeutet das $start zeitlich vor $stop liegt, 1 das $start nach $stop liegt und 0 das $start gleich $stop ist.
while($start->compare($stop)!=1) {
Nun wird die URL erzeugt und die Zielseite mittels Zend_Http geladen:
$url = 'http://www.flickr.com/explore/interesting/' . $start->ToString("YYYY/MM/dd");
$client = new Zend_Http_Client($url);
$response = $client->request();
$flickr = $response->getBody();
Wird ein Objekt vom Typ Zend_Http_Client instanziiert, so muss als Konstruktorparameter die Ziel-URL angegeben werden. Die Methode request fordert die Seite dann an. Hier kann sehr genau festgelegt werden was im Header stehen soll und es besteht auch die Möglichkeit Cookies für einen Request zu setzen. Der HTTP Client von Zend ist mit sehr vielen Funktionen ausgestattet, welche in der Doku sehr gut erklärt werden. Als Ergebnis liefert die Methode request ein Objekt vom Typ Zend_Http_Response, das sämtliche Informationen der Rückantwort enthält (Header und natürlich den Content). Der HTML Text der Zielseite lässt sich mittels getBody als String auslesen.
Zend_Dom hilft uns nun die wichtigen Informationen (die Bilder sowie die Links zu den Detailseiten der Bilder) zu extrahieren. Es parst den Text als eine DOM Struktur und bietet über das Objekt Zend_Dom_Query die Möglichkeit mittels CSS Selektoren oder XPath Elemente abzufragen. Vor allem das Arbeiten mit CSS Selektoren ist an der Stelle ein Luxus, da sehr viele bereits mit CSS oder auch JavaScript Frameworks Erfahrung haben und so intuitiv Zend_Dom_Query verwenden können. In diesem Beispiel werden die Bilder über ihren CSS Klassennamen selektiert:
$dom = new Zend_Dom_Query($flickr);
$images = $dom->query('.pc_img');
$links = $dom->query('.pc_m a');
$images und $links sind Objekte vom Typ Zend_Dom_Query_Result, welches das Iterable Interface implementiert und somit in einer foreach Schleife durchlaufen werden kann. Das wird auch gleich genutzt um das Zielarray zu befüllen (optional könnte man das Ergebnis auch in eine Datenbank schreiben oder direkt ausgeben):
foreach($images as $image) {
$photos[] = array(
"src" => $image->getAttribute("src"),
"title" => $image->getAttribute("alt"),
"href" => $links->current()->getAttribute("href"),
);
$links->next();
}
Schließlich wird der Tag um eins erhöht und das Ganze kann von vorne beginnen:
$start->add('1', Zend_Date::DAY);
Am Ende hat man dann das Array, welches auf vielfältige Weise weiterverarbeitet werden kann. In diesem Beispiel wird es einfach nur ausgegeben.
Das gesamte Skript kann hier heruntergeladen werden. Es ist eine sehr reduzierte Version, die nur das Wesentliche enthält. Für den Eigengebrauch habe ich ein komfortables Skript geschrieben, das zuvor eine JavaScript basierte Zeitauswahl bietet und auch größere Mengen (ganze Jahre) von flickr laden kann (mit Statusanzeige, Browsefunktion etc.). Wer sich hier interessiert kann sich gerne melden.
Ralf hat in seinem Blog bereits vor einiger Zeit über Performanceoptimierung geschrieben. Dieses Thema will ich hier noch einmal kurz aufgreifen, da es aus meiner Sicht für Frameworks, wie das Zend Framework extrem wichtig geworden ist. Von entscheidender Bedeutung ist hier aus meiner Sicht das Caching, welches bei Seiten wie diesen Blog extrem wirkungsvoll ist: es wird nur selten was geändert, aber häufig gelesen (ok, so häufig bei meinem Blog nun auch nicht, aber das Verhältnis ist entscheidend
.
Mein Favorit ist hier memcached, aber auch dateibasiertes Caching ist sinnvoll, wenn man sich hier Datenbankabfragen sparen kann (insbesondere wenn man davon ausgehen kann, dass die Dateien im Dateisystemcache des Betriebssystem liegen und somit nicht einmal ein Festplattenzugriff notwendig ist).
Zwei Artikel beschreiben sehr schön worauf man achten sollte:
- Optimizing your (ZF) web application
- Zend Framework performance
- Hinweise in der offiziellen Zend Framework Dokumentation
Wer den letzten Rest an Performance aus seiner Applikation herausholen will, dem sei die Seite PHP Benchmark empfohlen. Hier werden verschiedene Codefragmente hinsichtlich ihrer Performance verglichen.
Seit heute ist die Version 1.7 des Zend Frameworks online. Ich bin gespannt was sich da hinsichtlich der Performance getan hat. Hier wurde ja in den letzten Wochen bereits kräftig geworben, dass sich da einiges verbessern sollte. Aber auch die Liste an neuen Features ist wieder ziemlich lange:
- Zend_Amf with support for AMF0 and AMF3 protocols
- Dojo Toolkit 1.2.1
- Support for dijit editor available in the Dojo Toolkit
- Zend_Service_Twitter
- ZendX_JQuery in extras library
- Metadata API in Zend_Cache
- Google book search API in Zend_Gdata
- Preliminary support for GData Protocol v2 in Zend_Gdata
- Support for skip data processing in Zend_Search_Lucene
- Support for Open Office XML documents in Zend_Search_Lucene indexer
- Performance enhancements in Zend_Loader, Zend_Controller, and server components
- Zend_Mail_Storage_Writable_Maildir enhancements for mail delivery
- Zend_Tool in incubator
- Zend_Text_Table for formatting table using characters
- Zend_ProgressBar
- Zend_Config_Writer
- ZendX_Console_Unix_Process in the extras library
- Zend_Db_Table_Select support for Zend_Paginator
- Global parameters for routes
- Using Chain-Routes for Hostname-Routes via Zend_Config
- I18N improvements
- Application wide locale for all classes
- Data retrieving methods are now static
- Additional cache handling methods in all I18N classes
- Zend_Translate API simplified
- File transfer enhancements
- Support for file elements in subforms
- Support for multifile elements
- Support for MAX_FILES_SIZE in form
- Support for breaking validation chain
- Support for translation of failure ,messages
- New IsCompressed, IsImage, ExcludeMimeType, ExcludeExtension validators
- Support for FileInfo extension in MimeType validator
- Zend_Db_Table_Select adapater for Zend_Paginator
- Support for custom adapters in Zend_Paginator
- More flexible handling of complex types in Zend_Soap
Siehe auch http://devzone.zend.com/article/4045-Zend-Framework-1.7.0-is-now-available
Seit der Version 1.6 bietet das Zend Framework eine sehr komfortable Möglichkeit Loggingausgaben direkt an Firefox zu senden, ohne den eigentlichen Seiteninhalt zu verändern. Dazu müssen die Erweiterungen Firebug und FirePHP installiert sein. Die eigentliche Log Information, die im PHP Code erzeugt wird, wird dabei über den HTTP Header zu Firebug gesendet.
Von einer Methode eines Zend_Controller_Action heraus lässt sich dies mit wenigen Codezeilen realisieren (nutzt man den MVC Rahmen des Frameworks nicht, so sind einige zusätzliche Schritte notwendig, ein Beispiel ist hier in der Doku zu finden):
$logger = new Zend_Log(new Zend_Log_Writer_Firebug());
$logger->log('Das ist eine Lognachricht!', Zend_Log::INFO);
$logger->log('Das ist eine noch wichtigere Lognachricht!', Zend_Log::WARN);
$logger->log('Das ist eine extrem wichtige Lognachricht!', Zend_Log::ERR);
Dies führt zu folgender Ausgabe in Firebug

Wirft man einen Blick auf die HTTP Antwort des Servers, so findet man die zusätzlichen Zeilen im Header, welche die Logging Informationen im JSON Format enthalten.
HTTP/1.x 200 OK
Date: Wed, 22 Oct 2008 16:33:37 GMT
Server: Apache/2.2.9 (Win32) DAV/2 mod_ssl/2.2.9 OpenSSL/0.9.8h mod_autoindex_color PHP/5.2.6
X-Powered-By: PHP/5.2.6
X-Wf-Protocol-1: http://meta.wildfirehq.org/Protocol/JsonStream/0.1
X-Wf-1-Structure-1: http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1
X-Wf-1-Plugin-1: http://meta.firephp.org/Wildfire/Plugin/ZendFramework/FirePHP/0.1
X-Wf-1-1-1-1: |[{"Type":"INFO"},"Das ist eine Lognachricht!"]|
X-Wf-1-1-1-2: |[{"Type":"WARN"},"Das ist eine noch wichtigere Lognachricht!"]|
X-Wf-1-1-1-3: |[{"Type":"ERROR"},"Das ist eine extrem wichtige Lognachricht!"]|
X-Wf-1-Index: 3
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html
Ein sehr gutes Beispiel ist auch in der Zend Dokumentation zu finden.