Deckblatt Landau.indd

Werbung
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
Herunterladen