ZfP-Sonderpreis der DGZfP beim Regionalwettbewerb Jugend forscht LANDAU OOPS - Content-Management an einem praktischen Beispiel Daniel Lindenkreuz Schule: Otto-Hahn-Gymnasium Westring 11 76829 Landau Jugend forscht 2011 OOPS! Content Management Systeme an einem praktischen Beispiel Besondere Lernleistung von Daniel Lindenkreuz Betreut von Jürgen Kohlhepp MSS 12 Schuljahr 2009 / 2010 Erklärung Hiermit versichere ich, dass ich die besondere Lernleistung selbständig angefertigt, keine anderen als die angegebenen Hilfsmittel benutzt und alle Stellen der Facharbeit, die wörtlich oder sinngemäß aus anderen Quellen (auch aus dem Internet) stammen oder anderen Werken entnommen wurden, als solche gekennzeichnet und mit genauer Angabe der Fundstelle versehen habe. Verwendete Informationen aus dem Internet sind dem Lehrer vollständig im Ausdruck bzw. auf elektronischem Datenträger zur Verfügung gestellt worden. Edesheim, den 23. September 2010 (Daniel Lindenkreuz) ! 2 Inhaltsverzeichnis Über Content Management Systeme 4 Lizenzierung und Hosting 5 Weitere Entwicklung von OOPS 6 Datenbankstruktur 7 Vererbung 8 Hübsche URLs 9 Der Dispatcher 11 Eintragsfabrik und Eintragstypen 13 MVC – Architektur mit Model-View-Controller 14 HTML, CSS und JavaScript 16 AJAX 17 Dependencies 18 Sicherheit 19 Anleitung zum Editieren von Inhalten 20 Mein Fazit über OOPS 22 ! 3 Über Content Management Systeme Content Management Systeme dienen der Verwaltung und Instandhaltung von Websites. Das derzeit am meisten verbreitete Einsatzgebiet sind Blogs oder schlichtweg NewsSeiten, die regelmäßig Updates – meist nicht nur von einem Benutzer – brauchen. Vor allem bei mehreren Nutzern, die gegebenenfalls verschiedene Rechte benötigen, wollen Websiteinhaber oftmals nicht den Zugang zum Dateisystem per FTP freigeben. Hier ist es gebräuchlich, ein CMS einzurichten, welches Informationen und Inhalte in einer Datenbank (oftmals MySQL) speichert, eine Benutzeroberfläche über den Browser und in Sonderfällen sogar einen eigenen Client (z.B. Adobe Contribute) bietet und somit beschriebene “Rohzugänge” überflüssig macht. Nutzer melden sich direkt auf der Website an und erhalten somit Zugriff auf Administratorfunktionen, die nach Bedarf durch Nutzerrechte limitiert sind. Ein Content Management System macht im Allgemeinen beim Nutzer die Kenntnis von Programmier- oder Skriptsprachen wie HTML oder CSS unnötig, da Texte in einem Rich Text Editor – ähnlich wie Word – eingegeben werden und sämtliche Logik vom CMS selbst übernommen wird. Designer können jedoch zumindest auf “rohen” CSS-Code zugreifen, um Änderungen effektiv und schnell durchzusetzen. Zu den führenden Content Management Systemen gehören Wordpress, Movable Type, Joomla, ExpressionEngine, Drupal, Typo3 und Blogger. Diese Systeme werden mehrheitlich für Blogs verwendet; manche bieten eine Erweiterungsschnittstelle an und können dadurch auch attraktive Features wie Online-Shops oder Fähigkeiten sozialer Netzwerke bereitstellen. Das Content Management System liegt auf einem entfernten Server, der mit einem seperaten Datenbankserver verbunden ist. Es bereitet die Inhalte vor und gibt sie als HTML-Seite an den Client-Computer weiter – eine scheinbar ganz normale Seite im Browser. Obwohl OOPS auf einem Mac entwickelt wurde, kann es problemlos plattformunabhängig zum Beispiel auf einem Windows-Rechner bedient werden, da der Code vom Server und nicht vom Client ausgeführt wird. Einzig und alleine können auf verschiedenen Browsern Unterschiede bei der Ausgabe erkennbar sein, was allerdings am jeweiligen Browser hängt. Nicht empfohlen sind Internet Explorer 6 und 7, wohingegen Firefox, Safari und Chrome alles korrekt darstellen. ! 4 Lizenzierung und Hosting Die meisten erhältlichen CMS sind gegebenenfalls nach einem abgeschlossenen Kauf als Download verfügbar und oftmals PHP-basiert, da die PHP-Software open source und daher auf so gut wie allen Webservern vorinstalliert ist. Lizenzierung kann für Entwickler kostenpflichtiger CMS schnell zum Problem werden. Kompiliert man die fertige Applikation z.B. mit phc1 und liefert diese im kompilierten Zustand aus, so benötigt der Kunde technische Kenntnisse und gegebenenfalls rootZugang zum Server, welchen Hoster bei einfachen Paketen wie Webspace vorenthalten. Stattdessen würde vom Kunden zumindest Zugang zu einem vServer – einem virtualisierten Server – vorausgesetzt, welcher höhere monatliche Kosten verursacht und somit das Produkt aufgrund zu hoher Anforderungen unattraktiv machen würde. Liefert man das Produkt unkompiliert, also in (ursprünglicher) Skriptform aus, so muss man einen rechtssicheren Lizenzvertrag ausarbeiten, sodass im Falle einer Rechtsverletzung zum Beispiel durch Weiterverbreitung des Codes schnell und effektiv gehandelt werden kann. Da eine Weiterverbreitung jedoch fast nie festgestellt werden kann und sämtliche Schutzmaßnahmen durch Code von erfahrenen Anwendern entfernt werden können, entscheiden sich manche Anbieter, das Produkt gar nicht erst an den Kunden weiterzugeben, sondern das CMS auf Firmenservern zu hosten. Weil – wie bereits erwähnt – durch Content Management Systeme ein Dateisystemzugang überflüssig ist, haben Kunden somit auch keine Gelegenheit, Code einzusehen, zu kopieren oder zu ändern. Gegen eine monatliche Gebühr erhalten Kunden Zugang zur Benutzeroberfläche des CMS und Speicherplatz für eigene Inhalte wie Bilder oder andere Dateien. Kunden haben außerdem die Möglichkeit, eigene Domänen auf die Adresse des Hosters durch Domain Mapping per DNS (Dynamic Name Server) weiterzuleiten. Beispiel: http:// kunde.net wird weitergeleitet zu http://kunde.hoster.com. Bei einer Weiterleitung dieser Art wird schlussendlich im Browser kein Adresswechsel angezeigt – Besucher von http:// kunde.net/blog greifen unbewusst auf http://kunde.hoster.com/blog zu, ohne letztere Adresse im Adressfeld des Browsers angezeigt zu bekommen. Bei Verletzungen von AGB oder Nutzungsbedingungen kann der Anbieter des CMS schnell handeln: da er alleine den vollen Zugang zu FTP und MySQL hat und im CMS 1 ! http://www.phpcompiler.org/documentation.html 5 wahrscheinlich als Super-Admin registriert ist, kann er ohne Probleme unverzüglich den Account des Störers sperren. Die beschriebenen Lizenzierungsvarianten sind Optionen für die weitere Entwicklung von OOPS, die im Folgenden erklärt wird. Weitere Entwicklung von OOPS OOPS wird vermutlich nach einer weiteren Entwicklungsphase für Hosting auf einem Firmenserver konzipiert werden. Es wird mehrere Websites in einer Datenbank unterstützen und somit eine manuelle mehrfache Installation überflüssig machen. Kunden werden online automatisch freigeschaltete Accounts erstellen können. Außerdem wird die Bearbeitung des Designs der Website benutzerfreundlicher gestaltet werden, sodass Nutzer auch ohne CSS-Kenntnisse Seiten umgestalten können. Die gesamte Benutzeroberfläche könnte überarbeitet und mehr Grafiken integriert werden. Es wird zusätzliche Eintragstypen wie Diskussion, Kontaktformular und nach Möglichkeit auch einen Videoplayer geben. Soziale Netzwerke wie Twitter oder Facebook können aufgrund der flexiblen Programmarchitektur schnell integriert werden. ! 6 Datenbankstruktur OOPS ist objektorientiert konzipiert und dies spiegelt sich auch in der Datenbankstruktur wieder. Jedes Objekt, welches abgespeichert werden muss, befindet sich in einer Zeile in der Datenbank. Es existieren verschiedene Objekttypen wie Einträge, die später als HTML ausgegeben werden, Benutzer und Benutzerrechte (elementär vertreten, aber derzeit noch ohne Benutzeroberfläche). Jeder Objekttyp hat eine eigene Tabelle in der Datenbank, welche mit einem Präfix (in oops.config.php definiert, normalerweise oops_) versehen ist, sodass Konflikte mit anderen installierten Webapplikationen vermieden werden. Jede Tabelle hat ein Feld namens ID vom Typen Integer mit der Länge 11. Das MySQLSonderfeature auto_increment sorgt dafür, dass bei der Erzeugung eines neuen Eintrags automatisch eine noch nicht verwendete ID vergeben wird. Um auf ein Objekt in der Datenbank zuzugreifen, habe ich die Klasse XCObject (definiert in oops.object.php) geschaffen. Sie bietet eine Basis für sämtliche Zugriffe auf Eigenschaften in der Datenbank und zur Reduzierung der Datenlast ein minimales Cache-System. Außerdem lassen sich mit ihr Zeilen in der Datenbank erzeugen und löschen. Jedoch findet der Zugriff auf den MySQL-Server hier nicht statt – diese Funktionalität bietet die Klasse XCDatabase (definiert in oops.database.php), welche imstande ist, gültige MySQL Query Strings zu generieren, per mysqli eine Verbindung zum Datenbankserver hat und somit auch direkt auf Einträge zugreifen kann. Um nur eine einzige Verbindung während des gesamten Programmablaufs zu haben, wird in oops.globals.php eine globale Instanz von XCDatabase erzeugt, auf welche in Methoden von XCObject zugegriffen wird. Einträge in der Datenbank werden ausschließlich explizit mit der Funktion create(); erzeugt. XCDatabase bietet einige Hilfsfunktionen wie makeCommaString(), makeConjunctString(), getRows() und getObject(), die zum Generieren des MySQL Query Strings benötigt werden. Wichtiger ist jedoch, dass sämtliche Funktionen zunächst einen Query String generieren und dieser dann zentral über query() ausgeführt wird, was aus statistischen Gründen nützlich ist: es lässt sich leicht ein Zähler implementieren oder zum Debugging eine Ausgabe der Abfragen machen. Funktionen, die die Datenbankklasse unterstützt, sind: SELECT, UPDATE, INSERT, DELETE. Durch XCDatabase und XCObject wird es ermöglicht, später extrem klaren Code zu schreiben, um auf Objekte und Eigenschaften in der Datenbank zurückzugreifen. Möchte man den Titel eines Blogeintrags mit der ID 4 erfahren, so reicht folgender Abschnitt: $entry = new XCBlogEntry(4); echo $entry->getProperty(‘title’); ! 7 Vererbung In obigem Beispiel fällt bereits XCBlogEntry auf. Aufgrund einer Kette von Vererbungen führe ich nun ein Schaubild zur besseren Visualisierung auf. Durch diese Vererbung erhält XCBlogEntry Elternklasse aller Objekte, Zugriff auf Datenbank XCEntryController alle Fähigkeiten der geerbten Klassen. XCEntryController stellt Funktionen für den schlussendlichen HTML-Output bereit und Vorlagen, die später geerbt werden können ist auf einer MVC-Struktur aufgebaut, die XCTemplateable später erklärt wird, XCTemplateable verarbeitet die Designvorlagen, XCEntry Programmlogik eines einzelnen Eintrags: Eigenschaften, Output etc. definiert die Programmlogik eines einzelnen XCEntry Eintrags und legt den HTML-Output genauer fest und letztendlich bietet XCBlogEntry Blogspezifischen Output wie Anzeige des Spezielle Funktionen für Blogs: Kommentare etc. XCBlogEntry Veröffentlichungsdatums, Autors etc. Hier kann man deutlich erkennen, dass objektorientierte Programmierung tatsächlich nützlich ist und ein Programm zwar umfangreicher, jedoch wesentlich flexibler machen kann. Vererbung ist hier essentiell – selbst Eigenschaften für den Editor und die Typisierung von Kinder-Elementen werden vererbt. Dennoch befinden sich sämtliche Funktionen, die Inhaltsfragmente ausgeben können, in der “Überklasse”, XCEntryController. Warum das? Manche Seitentypen haben ähnliche Strukturen und da alle Typen sowieso von XCEntryController erben, sind in jener Klasse sämtliche Funktionen verfügbar, die in jeder Klasse genutzt werden können – aber letztendlich beim Output nicht unbedingt genutzt werden müssen. ! 8 Beispiel einer Seitenhierarchie: Hübsche URLs - Home - Blog - Archiv - Produkte - Produkt 1 - Features - Design - Produkt 2 ... Adressen oder URLs von Websites machen unbewusst einen Teil des ersten Eindrucks der jeweiligen Firma aus. Es ist schnell klar, ob sich die Firma Mühe gibt, Klarheit über die Inhalte ihrer Website zu verschaffen oder ob es dem Programmierer der Website schlichtweg egal war, aussagekräftige URLs einzubauen. Leider haben - Kontakt - Anfahrt heutzutage noch viele Seiten die schlechte - Impressum Angewohnheit, unlesbare und komplizierte URLs zu haben, die man sich nicht merken kann und die auch im Suchmaschinen-Ranking unvorteilhaft sind. Man nehme Hewlett-Packard, einen Global Player unter Computerherstellern und einen Link zu einem Produkt im Online-Shop: http://www.shopping.hp.com/webapp/shopping/computer_can_series.do? storeName=computer_store&category=desktops&a1=Category&v1=All-in-One +PCs&series_name=200xt_series&jumpid=in_R329_prodexp/hhoslp/psg/desktops/All-inOne_PCs/200xt_series Eine wesentlich hübschere, kürzere und lesbarere Alternative wäre: http://www.shopping.hp.com/200xt-series Dies ist technisch relativ einfach umzusetzen, vor allem für ein Programmiererteam, das vermutlich aus mehr als 20 Experten besteht. Dennoch schien die Firma keinen sonderbar großen Wert darauf zu legen, wie ihre URLs beim Endkunden ankommen. Argumente in URLs (?storeName=computer_store&category=desktops&a1=Category&v1=Allin-One+PCs) machen sich nicht nur ästhetisch nicht gut, sondern können auch leicht zu Sicherheitsproblemen führen, wenn diese ohne vorherige Filterung direkt in die HTMLSeite – oder noch schlimmer, in eine MySQL-Abfrage – eingespeist werden. Dies ist auch als Code Injection2 bekannt. Aus diesen Gründen habe ich mich entschieden, hübsche URLs in meinem Content Management System zu implementieren. Dabei wird auch die Hierarchie von Inhalten berücksichtigt – es folgt ein Beispiel. 2 ! http://en.wikipedia.org/wiki/Code_injection 9 Will man mehr über das Design von Produkt 1 erfahren, so gelangt man zur URL http:// kunde.com/produkte/produkt-1/design. Dies ist wesentlich einfacher zu verstehen und zu merken als IDs in der URL an den Server zu vergeben, wie in http://kunde.com/index.php? menu1=3&menu2=1&menu3=2. Dies kann leider auch zu Redundanz in der Datenbank führen – dies habe ich aber vermieden, indem jeder Artikel eine parent_id hat und nur ein “Bruchstück” der URL in der Datenbank gespeichert wird. In unserem Beispiel könnte der Eintrag zu produkte/ produkt-1/design die Eigenschaften parent_id = 3 und uid (Unique ID) = ‘design’ haben. Der Rest wird von PHP eingelesen und verarbeitet. Eine solche URL-Struktur könnte man natürlich auch per Dateisystem erreichen, indem man für jede Seite einen Ordner mit einer index.html-Seite anlegt und die Ordner nach Hierarchie verschachtelt, doch verursachen Dateisystemzugriffe generell Probleme unter anderem durch Benutzerrechte; vor allem, wenn mehrere Websites von einer Installation des CMS verwaltet werden. Außerdem kann es vorkommen, dass das Dateisystem an seine Grenzen gelangt: Bei Windows beispielsweise können Ordner- und Dateinamen maximal 255 Zeichen lang, eine URL zu einem Element im Dateisystem maximal etwa 32,000 Zeichen lang sein3. Es ist technisch problemloser, Requests über eine .htaccess-Datei an die index.php-Datei weiterzuleiten, wo dann sämtlicher Code verarbeitet wird und Inhalte ausgegeben werden. Da die .htaccess-Datei jedoch nicht einfach zu verstehen ist, werde ich kurz auf den Codeausschnitt eingehen, der die Weiterleitung vornimmt. <IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule (.*) index.php [L] </IfModule> Zunächst wird überprüft, ob das für die Weiterleitung benötigte Modul mod_rewrite.c installiert ist. Wenn dies der Fall ist, wird die RewriteEngine, welche URLs “umschreiben” wird, aktiviert und zwei Bedingungen für einen Rewrite gesetzt: Die angefragte URL darf weder auf eine Datei (!-f) noch auf einen Ordner (Directory, !-d) verweisen, denn sonst 3 ! http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx 10 wäre ein Zugriff auf zusätzliche Dateien wie JavaScripts, Bilder oder statische CSS unmöglich. Sämtliche URLs (Regular Expression: (.*)), die diese Voraussetzungen erfüllen, werden zu index.php intern weitergeleitet, d.h., es findet kein vom Besucher bemerkbarer Adresswechsel statt. [L] kennzeichnet, dass dies die letzte verarbeitete RewriteRule ist. Durch diese Weiterleitungstechnik habe ich außerdem vermieden, verschiedene PHPDateien für Aktionen wie view, edit, delete, add, comment etc. anzulegen. Stattdessen laufen sämtliche Requests wie beschrieben über eine einzige Datei – index.php –, werden von einem Dispatcher analysiert und von dort aus Funktionsaufrufe gemacht. Der Dispatcher Dispatch heißt Abfertigung. oops.url.dispatcher.php stellt Funktionen bereit, welche die vom Nutzer aufgerufene URL genauer analysieren. Wird zum Beispiel http://kunde.com/ blog/edit aufgerufen, so schneidet getPathUrl(); http://kunde.com ab, da diese Adresse konstant ist und für den weiteren Programmverlauf irrelevant ist. getBasePath(); gibt den Pfad des Ordners zurück, in welchem sich das komplette System möglicherweise befindet. Ist das komplette CMS inklusive index.php beispielsweise im Verzeichnis http://kunde.com/cms/, so gibt getBasePath(); /cms/ zurück. Dies wird später benötigt, um relative URLs korrekt auszugeben und somit auch auf statische Dateien im Dateisystem zugreifen zu können, wenn das CMS gegebenenfalls in einem Unterverzeichnis gespeichert ist. Die wohl wichtigste Funktion, dispatch();, greift auf getPathUrl(); zurück und nimmt dessen Rückgabewert auseinander. Letztendlich gibt sie eine Instanz von XCEntryController zurück, welche dann innerhalb eines Methodenaufrufs die komplette, vom Nutzer gewünschte Seite als HTML ausgibt. Die Struktur einer URL ist hier nicht nur von der Hierarchie der Inhalte abhängig, sondern kann auch Aktionsaufrufe zum Bearbeiten (edit), Löschen (delete), Hinzufügen (add) und Ein- und Ausloggen (login, logout) beinhalten. Diese Aktionskürzel finden sich mit einem vorausgehenden Slash (/) am Ende eines gewöhnlichen Seitenaufrufs. Beispiel: blog/roundup-vom-dezember/edit Im Code befinden sich außerdem rudimentäre Ansätze zur Auswertung zusätzlicher Aktionskürzel wie feed, media oder posts. Diese Auswertungen werden in der Zukunft implementiert. ! 11 Eine foreach-Schleife überprüft die URL auf Aktionsaufrufe; wird solch ein Aufruf gefunden, so wird eine entsprechende Flag gesetzt, die später auf das zurückzugebende XCEntryController-Objekt übertragen wird. Wenn ein Bruchstück der URL ($chunk) keinen Funktionsaufruf oder ein ähnliches Schlüsselwort beinhaltet, wird es in das Array $entry_chunks für weitere Verarbeitungen übertragen. OOPS bietet momentan eine rudimentäre Unterstützung für User-Profile. Da User jedoch in der Datenbank aus logischen Gründen seperat von regulären Einträgen wie Blogs abgespeichert sind (Tabellen oops_users, oops_entries), kann über die normale XCEntryController-Klasse, welche auf letztere Tabelle in der Datenbank zurückgreift, nicht auf User-Daten zugegriffen werden. Die Separierung von Nutzerprofilen und regulären Einträgen wird daher bereits im Dispatcher vorgenommen und ein von XCEntryController erbendes, aber strukturell unterschiedliches XCUserPage-Objekt wird gegebenenfalls erzeugt. Dies liegt damit zusammen, dass User-Profile von der Hierarchie ausgeschlossen sind und nicht etwa Unter-Element einer Blog-Seite sein können (und somit auch kein Elternelement haben können). Hierbei hat jeder Nutzer einen Spitznamen (shortname in der Datenbank). Dieser Spitzname findet sich in der URL des Nutzerprofils wieder und führt auf die interne numerische ID des Nutzers zurück. Nach der Auswertung der Aktionsaufrufe und gegebenenfalls des Nutzerprofilaufrufs führt das Programm den übrig bleibenden Teil der URL, $entry_url, auf ein Objekt in der Datenbank zurück und versucht, eine Instanz von XCEntryController zu erstellen. Dies geschieht in der Funktion getEntryFromUrl();, welche zunächst prüft, ob der Eintrag als Startseite festgelegt ist, welche einen Aufruf nicht explizit benötigt. Beispiel: http:// kunde.com/roundup-vom-dezember/ muss den Eintrag http://kunde.com/blog/roundupvom-dezember/ hervorrufen, wenn die Eigenschaft vom Eintrag “Blog” frontpage = 1 ist, unabhängig von dessen UID. Es wird daher davon ausgegangen, dass der Eintrag ein Unterelement der Startseite oder die Startseite selbst ist. Falls ein Unterelement der Startseite mit dem ersten Bruchstück (hier: roundup-vom-dezember) als UID nicht existiert, so wird das CMS nach einem Eintrag suchen, der sich auf der höchsten Ebene der Hierarchie (wie “Blog”) befindet und somit kein Unterelement von “Blog” ist. Sofern nach dieser ersten Auswertung noch Bruchstücke der URL übrig bleiben, wird in einer foreach-Schleife versucht, jeweils das nächste Unterelement, dessen UID = $chunk, zu finden. Wenn dies zu keinem Ergebnis führt, wird ein Pseudo-Eintrag mit der Flag ! 12 $entry->attributes[‘not_found’] = true generiert. Dieser Eintrag wird auch dargestellt, dennoch zeigt er sinngemäß keinen Inhalt und stattdessen eine “Eintrag nicht gefunden”Meldung an. Außerdem findet sich der Pseudo-Eintrag nicht in der Datenbank. Eintragsfabrik und Eintragstypen In getEntryFromUrl(); wird eine Funktion namens produceEntry($id); aufgerufen, welche über einen durch Include-Reihenfolge bedingten Umweg, mit dem man weitere 25 Seiten füllen könnte, ebenso in $entry->getChild(); aufgerufen wird. produceEntry(); ist eine sogenannte Factory-Funktion. Sie wertet die übergebene numerische ID aus und ruft Typ und Untertyp aus der Tabelle oops_entries in der Datenbank ab. Optional können Typ und Untertyp explizit per Argument an die Funktion übergeben werden, was allerdings nur benötigt wird, wenn ein neuer Eintrag erzeugt werden soll. Auf das Erzeugen von Einträgen werde ich später nochmals eingehen. Der schlichte Zweck von produceEntry(); ist es, ein Objekt der entsprechenden Klasse zurückzugeben. Wenn zum Beispiel in der Datenbank unter type ‘page’ und unter subtype ‘blog’ verzeichnet ist, wird produceEntry(); eine Instanz von XCBlogPage erzeugen und zurückgeben. Derzeit implementierte Eintragstypen sind: • XCHtmlPage: Statische HTML-Seite • XCBlogPage: Blog-Seite mit Untereinträgen • XCGalleryPage: Galerie-Seite mit Bildern • XCLinkPage: Weiterleitung zu einem Link • XCBlogEntry: Einzelner Blogeintrag • XCGalleryEntry: Foto in einer Galerie Im Code sind bereits Ansätze für die spätere Entwicklung vorgesehen; es sollen später bei Gelegenheit noch Diskussionsseiten mit verschiedenen Themen (XCDiscussionPage) und Seiten einer Benutzergruppe (XCGroupPage) eingeführt werden. Man sieht hier äußerst deutlich, dass das Programm von Grund auf so konzipiert wurde, dass es letztendlich möglichst einfach zu erweitern ist. Dies habe ich auch während der Entwicklung der Galerieseiten gemerkt, welche an sich wesentlich schneller als das “Fundament”, welches ! 13 schwer auf MVC basiert, implementiert waren. MVC ist das nächste größere Feature, welches der Code für OOPS bietet. MVC – Architektur mit Model-View-Controller In Webapplikationen und Webframeworks heutzutage sehr gängig und bei vielen Programmierern beliebt, bietet eine auf MVC basierende Programmarchitektur eine solide Grundlage für produktive Entwicklungsvorgänge und zudem eine gute Voraussetzung für Entwicklung in kleinen bis mittleren Teams. Das Model ist hierbei die logische Struktur einer Klasse oder eines Objektes, welches sich auch in der Datenbank widerspiegelt und somit bei korrekter Implementierung problemlos synchronisiert werden kann. Streng genommen hat es keinen direkten Zugriff auf die Datenbank, speichert und verarbeitet dennoch wichtiges logisches Material. View heißt Präsentation oder Ansicht und ist dafür zuständig, die Daten visuell zu repräsentieren – sprich, die Ausgabe zum Beispiel in HTML oder CSS zu verwalten. Der View-Teil von MVC dient also als Benutzeroberfläche und kann auch Eingaben annehmen, verarbeitet diese aber nicht oder nur unwesentlich (URL-Encoding etc.). Der Controller übernimmt den größten Teil der Arbeit, verarbeitet Benutzereingaben und dient in vielen Implementierungen als Kommunikationsglied zwischen View und Model. Diese Prinzipien von MVC klingen möglicherweise wie ein Regelwerk, sind es aber per Definition nicht – jede Implementierung von MVC hat ihre eigenen Rafinessen und das Web ist derzeit nahezu von MVC-Frameworks überflutet (CakePHP, Kohana, CodeIgniter uvm.). Es ist also offensichtlich, wie viel Aufwand nötig ist, um ein zum eigenen Programmierstil passendes Framework zu finden und dieses zuvor selbstverständlich ausgiebig zu testen. Aus jenem Grund war die Versuchung groß, ein eigenes Konzept, das sich an MVC anlehnt, zu finden und zu implementieren – was schlussendlich zwar ein größerer Aufwand war als erwartet, sich aber in vieler Hinsicht gelohnt hat. Das für OOPS abgeleitete MVC-Konzept sieht folgende “Änderungen” vor: • Controller: Der XCEntryController ist das Kernstück der Anwendung. Er verarbeitet hauptsächlich die im Hintergrund extrem komplexe, aber im Vordergrund so einfach wie möglich gehaltene Benutzeroberfläche, die intelligent auf Änderungen oder Fehler reagiert und mit einem eigens programmierten Notifikationssystem den Benutzer über ! 14 kritische Daten informiert. Beispielsweise sorgt der Controller für die codeweise Darstellung der Bearbeitungsformulare, behandelt aber auch die Nutzerrechte. Über den Controller laufen quasi sämtliche Daten; er ist mit zwei Views verbunden und hat einen Connector, der ihn an die Datenbank anbindet. Außerdem leitet er die Daten aus der Datenbank direkt zum Modell weiter, das den logischen Aufbau von Seitenstrukturen (Titel, Inhalte, Veröffentlichungsdetails) aufrechterhält und unter anderem auch vom Benutzer eingegebene Daten automatisiert verarbeitet und in die Datenbank einspeist. • Model: Das Modell hinter der Anwendung ist der XCPreferencesDispatcher; jeder Entry, also jede Seite, jeder Untereintrag, jede Bildseite, besitzt einen Dispatcher, der eigentlich ein um zahlreiche Funktionen erweitertes Array in Klassenform ist. Es können Eigenschaften vererbt werden (beispielsweise hat jeder Eintragstyp einen Titel und eine eindeutige ID / UID für den dazugehörigen Link). Der Dispatcher ist also eine Sammlung von eintragsspezifischen Eigenschaften, die an die Datenbank angebunden sind und mit wiederum eigenen XCPreferenceController und XCPreferenceView-Objekten eine weitere, kleinere und vereinfachte MVC-Struktur darstellen. • View: Jeder Seitentyp zeigt schlussendlich auf dem Bildschirm eine unterschiedliche Struktur an. Die Reihenfolge von Elementen wie Titel, Body-Text und teils Bildmaterial ist nicht immer gleich; Blogseiten bieten zum Beispiel ein zusätzliches Kommentarformular. Jedoch ändert sich das Grundgerüst einer Website, das Template, im besten Falle nie – Navigation, Header, Content und Footer nehmen gewöhnlicherweise einen festen Platz ein. Daher hat jeder XCEntryController gleich zwei Views: ein View-Objekt für den “Rahmen” und ein View-Objekt für den Inhalt dieses Rahmens. Der Rahmen bleibt gleich und ändert sich nicht durch Vererbung, während sich der Inhalt großzügig der Vererbung bedient. • Connector: Der Connector stellt das Verbindungsglied zwischen Controller und Datenbank dar. Er ist vom Typ XCNullObject und bietet die Möglichkeit, auf “Roheigenschaften” eines Eintrags zuzugreifen, ohne ein einziges bisschen SQL zu schreiben. Das globale Datenbankobjekt generiert selbst automatisch MySQL-Code, mithilfe eines Methodenaufrufs (z.B. $db->select();) kann ein ganzes, wohlsortiertes Array an Daten entnommen werden. Basisfunktionen wie diese sind besonders wichtig – vor allem bei MySQL klaffen oft Sicherheitslücken, die zum fatalen Ende einer Datenbank und somit der ganzen Anwendung führen können. Daher war es besonders wichtig, eine teils aufwändigere Implementierung der Code generierenden Programmteile zu verwenden. ! 15 Wie man sieht, weicht meine Implementierung von MVC zwar durchaus von den standardisierten Modellen ab, aber die Leistung der Anwendung kann den Industriestandard WordPress bezüglich Renderzeiten von Webseiten sogar toppen – Tests haben eine etwa dreifache Geschwindigkeit ergeben. Die dabei verwendete Installation von WordPress hatte keine aktivierten Plugins und ausschließlich Standardinhalte. HTML, CSS und JavaScript Die primäre Aufgabe von OOPS ist es jedoch nicht, mit komplexen Codearchitekturen zu prahlen, sondern Inhalte effizient, schnell und korrekt wiederzugeben und ansehnlich zu präsentieren. Bereits oben erwähnt, sorgt ein globales Template für die nötige Konsistenz einer Website. Dennoch ist dieses Grundgerüst extrem flexibel; mit wenigen Mausklicks lassen sich über das Admin-Panel komplett andere Layouts einstellen, die manuell umzuprogrammieren erfahrungsgemäß fast 10 Minuten Aufwand bedürfen. Um dies verständlicher zu machen, habe ich eine Skizze des Grundlayouts angefertigt. Der Header ist der auffälligste Bereich und enthält gewöhnlicherweise entweder ein Banner-Bild oder besteht aus einem großen Schriftzug. Meistens befindet sich dort ein Firmenlogo, da das menschliche Auge den oberen linken Bereich einer Website zuerst wahrnimmt. Um den Platz effizienter zu nutzen, besteht die Möglichkeit, den Navigationsbereich in den Header zu verlegen und diesen horizontal auszubreiten. Dabei spielt nicht nur die semantische Reihenfolge der Elemente im HTMLQuellcode eine Rolle, sondern meistens auch der dazugehörige CSS-Code. Stylesheets sind nicht nur für Farben und Grafiken zuständig, sondern machen heutzutage fast ein komplettes Layout aus. Da CSS eine der zentralen Rollen einer Website spielt, muss das CMS in der Lage sein, gültigen CSS-Code zu generieren, der zumindest das Grundlayout mit Spalten, Zeilen, Header, Content und Footer definiert. Dies geht problemlos mit OOPS – bei mehrspaltigen Layouts werden sogar Breiten einiger Bereiche automatisch berechnet. Die Verarbeitung dieses Codes findet ebenfalls im XCEntryController statt. ! 16 HTML-Code wird – kurz gesagt – ständig während der Laufzeit von einem XCEntryController produziert und von einem XCEntryView ausgegeben. Es werden betrachtlich viele <div>-Elemente verwendet, da diese die neutralsten Elemente sind und keine vom User-Agent Stylesheet vorgegebene CSS-Informationen haben – browserunabhängig. Dies hat bei der Entwicklung einen enormen Vorteil, da man sozusagen mit einem leeren Blatt Papier arbeitet und mit CSS problemlos eine einheitliche Benutzeroberfläche zusammenbaut. HTML und CSS hängen also stark zusammen – nichts wirklich neues. Das, was Leben in die Benutzeroberfläche bringt, ist JavaScript. Mit JavaScript lassen sich Elemente im DOM (Document Object Model, eigentlich in der HTML-Struktur) manipulieren, also entfernen, duplizieren, hinzufügen oder einfach nur verändern. Die JavaScript-Syntax ist zwar nicht sonderlich schwer, jedoch ist die DOM-Manipulation mit “Hausmitteln” umständlich und lange zu implementieren. Um dies wesentlich einfacher zu gestalten, gibt es ein Framework namens jQuery. Mit jQuery kann man per JavaScript mit CSS-Selektoren auf zutreffende Elemente im DOM zugreifen, wiederum deren CSS-Eigenschaften verändern, hübsche Effekte einbauen und vieles mehr. jQuery ist eigentlich nichts mehr als eine sehr große Erweiterung (und Vereinfachung) der Möglichkeiten mit JavaScript. AJAX Der wichtige Teil von jQuery ist jedoch nicht die Animation, sondern die Fähigkeit, mit AJAX-Requests zu arbeiten. Diese Requests erlauben Zugriffe auf eine andere Website, die unabhängig von der aktuell geöffneten Website im Hintergrund laufen können und Daten an die eigentliche Website zurückgeben. Somit ist es möglich, dass man zum Beispiel Menüpunkte in der Navigation in OOPS verschieben kann und ohne die Seite neu zu laden die neue Anordnung speichern kann. Eine weitere Verwendung von AJAX ist das Admin-Panel, welches bei authentifizierten Nutzern von oben ausklappt. Bei AJAX war die größte Schwierigkeit, eine einheitliche Begriffsgebung für Befehle zu finden, da man sonst sehr schnell den Überblick verliert und nicht mehr weiß, welche JavaScript-Funktion zu welchem Programmteil in PHP gehört. Ansonsten war jQuery eine sehr große Hilfe, die die Kommunikation zwischen Client und Server zu einem (relativen) Kinderspiel machte. Ein weiterer Nachteil der Kommunikation mit den zwei Programmiersprachen war, dass Funktionen in ihrer Logik teilweise doppelt vorkamen und wenn beispielsweise ein ! 17 Begriff im Output von PHP geändert wurde, die dazugehörige JavaScript-Funktion nicht mehr funktionierte. Die Konzentration musste somit auf zwei “Hälften” des Codes gerichtet werden, die redundant waren und sehr anfällig gegen Bugs waren. Es gelang mir jedoch, einen großen Teil der JavaScript-Programmlogik in PHP auszulagern und somit dieses Problem zu minimieren. Dadurch macht JavaScript einen eher kleinen Anteil an meinem Code aus. JavaScript (CodeMirror-Bibliothek) sorgt außerdem dafür, dass man Texte ähnlich wie in Microsoft Word mit Formatierung bearbeiten kann und zusätzlichen CSS-Code im AdminPanel mit Syntax Highlighting eingibt, was die Entwicklung eines Templates vereinfacht. Dependencies Die dritte und letzte Hürde bei der Entwicklung bezog sich ebenfalls auf die Kommunikation zwischen PHP und JavaScript – allerdings hinsichtlich der Includes: Um Bandbreite zu sparen und die Seite schneller zu laden, sollten JavaScript-Dateien, die ausschließlich für Administratoraktionen benötigt werden, auch nur dann geladen werden, falls der Benutzer solch eine Aktion anfordert. Dies erforderte seitens PHP eine komplexe Verarbeitung dieser Abhängigkeiten, dieser “Dependencies”. Über die globale Variable $site wurden bei JavaScript-nutzenden Programmteilen manuell Hinweise auf die jeweils benötigten Javascripts gegeben und diese dann schlussendlich beim Rendering der kompletten Seite als Referenzen in den HTML-Code eingespeist. Dabei wird unter anderem auch die mehrmalige Verwendung von manchen Javascripts, wie zum Beispiel der jQuery-Bibliothek berücksichtigt – mehrfach vorhandene Scripts werden vor der Ausgabe entfernt, um Übersicht beizubehalten und wiederum Bandbreite zu sparen. Scripts werden “intelligent” nachgeladen, beispielsweise wenn ein Editor geöffnet wird. Die Editor-Logik befindet sich in oops.editor.js, wohingegen sich das für den Editor benötigte Javascript-Plugin in jHtmlArea-0.7.0.js befindet. Es kann zu Fehlern kommen, wenn ein Script per AJAX doppelt geladen wird – daher werden bereits geladene Scripts vor dem Ladevorgang durch AJAX aus dem zu ladenden Teil entfernt. Hierbei hilft JSON, eine alternative Notation von Javascript-Objekten, die die Skript-URLs vom übrigen Inhalt separiert. Ein JSON-Objekt könnte zum Beispiel so aussehen: {"dependencies":{"js":["/resources/scripts/jquery/jquery-1.4.2.min.js", ...]},"html": "<div class=‘editor’><p>Editor</p></div>"} ! 18 Sicherheit Heutzutage ist das Internet voller Spambots und Hacker. Gegen Spambots hat man weniger Chancen als gegen Hacker, jedoch stellen Hacker eine weitaus größere Gefahr für das System, für den Server und oftmals auch für Besucher dar: über Sicherheitslücken eines Content Management Systems können Profis bösartigen Code einschleusen, der sich auf Computern, die eine Seite auf dem Server aufrufen, verbreitet. Zunächst gilt es, zu verhindern, dass Logins gefälscht werden. Ein Computer identifiziert sich nach einem Login durch einen lokalen Cookie mit dem Server. Dieser Cookie beinhaltet meistens einen eindeutigen Code – jedoch sind Cookies lokal gespeichert und können vom Client gefälscht werden. Um solche Änderungen nutzlos zu machen, befindet sich die Original-ID in der Datenbank, welche ausschließlich vom System ausgelesen werden kann. Zudem hat OOPS ein gruppengesteuertes Benutzerrechtesystem, das im aktuellen Zustand zwischen Besuchern und Administratoren unterscheidet. Besucher dürfen standardmäßig Inhalte sehen, aber nicht ändern oder löschen. Administratoren haben selbstverständlich Vollzugriff auf sämtliche Optionen, darunter auch das Design der Seite. Vor jeder verändernden Aktion werden Benutzerrechte geprüft und, falls diese nicht ausreichen, eine entsprechende Fehlermeldung angezeigt. Somit können unautorisierte Nutzer keine unautorisierten Aktionen durchführen. Eine weitere mögliche Schwachstelle ist die Übertragung von Daten. Über ein Kommentarformular beispielsweise kann entweder Spam eingespeist oder eine SQLInjection durchgeführt werden. Deswegen ist es wichtig, vor dem Speichern in der Datenbank HTML-Tags zu entfernen, die Links zu bösartigen Seiten oder sogar JavaScripts beinhalten könnten sowie die Rohdaten zu “escapen”, um SQL-Injections zu verhindern. “Escaping” heißt hier, Anführungszeichen und sonstige SQL-spezifische Delimiter der Inhalte mit einem Escape-Zeichen (einem Backslash \) zu versehen, sodass diese nicht fälschlicherweise (und fatalerweise) als SQL-Delimiter interpretiert werden. Somit wird verhindert, dass bösartiger SQL-Code durch eine Injection eingeschleust wird. ! 19 Anleitung zum Editieren von Inhalten Egal wie einfach und selbsterklärend eine Benutzeroberfläche gestaltet sein sollte, bedarf es meistens immer trotzdem einer kurzen Anleitung zur Verwendung. Um Änderungen an Inhalten vorzunehmen, muss man sich zunächst einloggen. Dazu klickt man auf den Login-Link in der Menüleiste oder hängt an eine beliebige Seite “login” an das Ende der Adresszeile des Browsers. Beispiel: “http://localhost/galerie/” -> “http:// localhost/galerie/login” Nach erfolgreicher Anmeldung fällt das Admin-Panel rechts oben auf der Seite auf; von dort aus kann man den Site Admin-Modus aktivieren, welcher ein Panel oben einblendet, mit welchem man seitenumfassende Einstellungen vornehmen kann und beispielsweise das Design modifizieren kann. “Navigation bearbeiten” aktiviert einen Modus, der sämtliche Menüs verschiebbar macht. Per Drag&Drop lassen sich so Menüeinträge neu anordnen. Um die eigentlichen Inhalte zu bearbeiten, fährt man einfach mit der Maus über den zu editierenden Teil und erhält dort außerdem die Möglichkeit, den betreffenden Eintrag zu löschen oder gegebenenfalls ein Unterelement hinzuzufügen. ! 20 Der Site Admin-Modus erlaubt es zudem, strukturelle Änderungen an der Website durchzuführen. Mit den Einstellungen “Navigationslayout” und “Spaltenlayout” lässt sich die Anordnung von Navigation und Seiteninhalten anpassen. Die Navigation befindet sich bei der Einstellung “1” unter dem Header-Bild, bei der Einstellung “2” über dem Header-Bild und bei der Einstellung “3” in einer der Spalten. Die Schemen weiter unten erläutern die Einstellungen für das Spaltenlayout und stellen die Seitenstruktur mit Header, Content, Footer und den jeweiligen Spalten dar. Der eigentliche Inhalt nimmt in jedem Falle die größte Spalte ein. 1 4 ! 2 3 5 6 21 Mein Fazit über OOPS Das Projekt war inhaltlich äußerst anspruchsvoll, da extrem viele logische Zusammenhänge zwischen verschiedenen darstellbaren und nicht-darstellbaren Elementen herrschen, die berücksichtigt werden müssen, um Fehler zu vermeiden oder gar die Datenbank versehentlich zu zerstören. Redundanz ist bei Content-ManagementSystemen ein großes Manko – ich habe versucht, über eine einheitliche Tabellenform mit ID und UID als wiederkehrende Elemente, diesem Problem entgegenzuwirken. Die Datenbank ist nach zahlreichen Versuchen intakt geblieben. Meine Recherchen über die theoretische Funktionsweise von Content-ManagementSystemen waren zeitlich stark ausgedehnt; es fiel mir auf, dass erfolgreiche Softwarefirmen wie Squarespace oder Automattic, welche CMS entwickeln, kaum Rückschlüsse auf das innere “Wesen” ihrer Produkte zulassen. Anscheinend machen Details in der Implementierung tatsächlich einen Unterschied zwischen einem erfolgreichen Produkt und einer mittelmäßigen Anwendung. Man legt großen Wert auf Benutzerfreundlichkeit – an dieses Prinzip habe ich mich bei der Entwicklung meines Content-Management-Systems gehalten und hoffe, dass Dritte es ebenso leicht wie ich bedienen können. Eine große Fülle an Features kann OOPS zwar leider noch nicht bieten, jedoch ist die Codebasis – das umfangreiche, MVC-ähnliche Framework, welches ich programmiert habe – beeindruckend und bietet eine hervorragende Grundlage für weiterführende Entwicklung und Implementierung zusätzlicher Funktionen. Eine klare Struktur des Codes lag mir sehr am Herzen, da ich selbst später wieder irgendwann die Entwicklung meiner Anwendung fortsetzen und mich wahrscheinlich nicht mehr gut an meine damalige Denkweise erinnern werde. Diese Dokumentation und die zahlreichen, wenn auch größtenteils in Englisch verfassten Kommentare im Quellcode könnten mir und möglichen Ko-Entwicklern später als Gedankenstütze dienen, um die Anwendung weiter zu verbessern und Kundenwebsites damit zu hosten. Daniel Lindenkreuz, September 2010 ! 22 Anhang Repräsentative Ordnerstruktur des Projektes Plugins sind Bibliotheken von Dritten, Nutzung ist genehmigt /Users/daniel/Sites/url-test-mvc/classes /Users/daniel/Sites/url-test-mvc/classes/languages /Users/daniel/Sites/url-test-mvc/classes/plugins /Users/daniel/Sites/url-test-mvc/classes/plugins/csstidy /Users/daniel/Sites/url-test-mvc/classes/xc.admin.view.php /Users/daniel/Sites/url-test-mvc/classes/xc.array.extensions.php /Users/daniel/Sites/url-test-mvc/classes/xc.config.php /Users/daniel/Sites/url-test-mvc/classes/xc.database.php /Users/daniel/Sites/url-test-mvc/classes/xc.dependencies.php /Users/daniel/Sites/url-test-mvc/classes/xc.entry.controller.php /Users/daniel/Sites/url-test-mvc/classes/xc.entry.view.php /Users/daniel/Sites/url-test-mvc/classes/xc.front.css.php /Users/daniel/Sites/url-test-mvc/classes/xc.functions.php /Users/daniel/Sites/url-test-mvc/classes/xc.globals.php /Users/daniel/Sites/url-test-mvc/classes/xc.hierarchy.logic.php /Users/daniel/Sites/url-test-mvc/classes/xc.include.php /Users/daniel/Sites/url-test-mvc/classes/xc.init.php /Users/daniel/Sites/url-test-mvc/classes/xc.media.controller.php /Users/daniel/Sites/url-test-mvc/classes/xc.misc.view.php /Users/daniel/Sites/url-test-mvc/classes/xc.notification.view.php /Users/daniel/Sites/url-test-mvc/classes/xc.object.php /Users/daniel/Sites/url-test-mvc/classes/xc.output.php /Users/daniel/Sites/url-test-mvc/classes/xc.preferences.dispatcher.php /Users/daniel/Sites/url-test-mvc/classes/xc.preferences.value.controller.php /Users/daniel/Sites/url-test-mvc/classes/xc.preferences.view.php /Users/daniel/Sites/url-test-mvc/classes/xc.rights.php /Users/daniel/Sites/url-test-mvc/classes/xc.session.php /Users/daniel/Sites/url-test-mvc/classes/xc.site.php /Users/daniel/Sites/url-test-mvc/classes/xc.template.php /Users/daniel/Sites/url-test-mvc/classes/xc.tidy.phpx ! 23 /Users/daniel/Sites/url-test-mvc/classes/xc.translator.php /Users/daniel/Sites/url-test-mvc/classes/xc.url.dispatcher.php /Users/daniel/Sites/url-test-mvc/content /Users/daniel/Sites/url-test-mvc/content/gallery /Users/daniel/Sites/url-test-mvc/resources /Users/daniel/Sites/url-test-mvc/resources/css /Users/daniel/Sites/url-test-mvc/resources/css/clearfix.css /Users/daniel/Sites/url-test-mvc/resources/css/editor.css /Users/daniel/Sites/url-test-mvc/resources/css/jquery-ui.css /Users/daniel/Sites/url-test-mvc/resources/css/reset.css /Users/daniel/Sites/url-test-mvc/resources/css/ui.css /Users/daniel/Sites/url-test-mvc/resources/images /Users/daniel/Sites/url-test-mvc/resources/scripts /Users/daniel/Sites/url-test-mvc/resources/scripts/codemirror /Users/daniel/Sites/url-test-mvc/resources/scripts/jhtmlarea /Users/daniel/Sites/url-test-mvc/resources/scripts/jquery /Users/daniel/Sites/url-test-mvc/resources/scripts/oops /Users/daniel/Sites/url-test-mvc/resources/scripts/oops/oops.admin.js /Users/daniel/Sites/url-test-mvc/resources/scripts/oops/oops.editor.js /Users/daniel/Sites/url-test-mvc/resources/scripts/uploadify /Users/daniel/Sites/url-test-mvc/index.php /Users/daniel/Sites/url-test-mvc/playground_site_header.gif Anmerkung zum abgedruckten Quellcode: Vervielfältigung ohne Genehmigung des Verfassers nicht erlaubt! Copyright (c) 2010 Daniel Lindenkreuz. Alle Rechte vorbehalten. Die Datenbankquelle wird seperat geliefert. ! 24