CMS im Eigenbau Vom Konzept zum fertigen Content Management System. Ein Vortrag von Markus-Hermann Koch 1. Warum _noch_ ein CMS? Was spricht dagegen? • Es gibt bereits eine Vielzahl guter, zum Teil sogar freier CMS. Joomla, Wordpress, Typo 3, ... • Diese werden seit Jahren entwickelt, bieten für vieles gute Standardlösungen und verfügen über ein ausgefeiltes Back End. • Ihre Installation ist mitunter verblüffend einfach. • Der Zeitaufwand einer Neuentwicklung ist hoch. 1. Motivation 2 Was spricht dafür? • Umsetzung selbstgewählter Standards. Nicht nur in punkto HTML, sondern auch in CSS, JavaScript, PHP und allen anderen Komponenten. • Das System wird auf die Inhalte der Site angepasst und nicht andersherum. • Sie verstehen Ihr System und wissen welche Änderungen für einen gewünschten Effekt erforderlich sind. • Man kann dabei einiges lernen! 1. Motivation 3 Warum das Original verwerfen? 1. Motivation 4 1. Motivation 5 Richtlinien für die Neufassung Konsequenzen aus dem Original: • Wartbarkeit. Sämtlicher Quellcode soll mit Hilfe normaler Texteditoren vernünftig lesbar und bearbeitbar sein. • Verzeichnisse sollen automatisch erzeugt werden. • Trennung von Struktur und Textinhalt. • Vermeidung von Redundanz. Insbesondere soll es eine einfach gestrickte, leere HTMLVorlagendatei geben, die die immer gleiche Grundseite enthält und in die die eigentlichen Textinhalte über PHP eingefügt werden. 1. Motivation 6 2. Aufbau der HTML-Vorlage CSS-Zen-Garden: http://www.csszengarden.com/tr/deutsch/ <?php • Einbinden der Initialisierungsskripte und der benötigten Objektklassen. • Auswerten der _GET und _POST-Parameter. Davon abhängig: Welche CSS sollen eingebunden werden? Welche Inhalte werden aus der Datenbank/dem Cache geladen? Ggf. Datenbankinhalte in HTML umsetzen. Alle diese Daten in werden in Variablen abgelegt. (Das hält den HTML-Teil sauber und lässt den HTTP-Header lange variabel.) ?> <html>DIV-orientierter Aufbau in den die Variablen eingefügt werden.</html> 2. Die Vorlage index.php5 7 <?php require_once $_SERVER['DOCUMENT_ROOT'] . '/init.php5'; require_once $i_root . $i_scripts . $i_kDB_fname; ... [Weitere benötigte Objektklassen] ... [Auswerten der _GET- und ggf. _POST-Parameter. Ggf. Auführung zusätzlicher Skripte] ... [Laden der Linklisten, des Hauptinhaltes, inhaltsabhängiger CSS-Anweisungen und ggf. der Werbung aus der Datenbank und/oder dem Cache. Diese Daten werden in Variablen abgelegt] ... [Ggf. Nachbearbeitung dieser Inhalte. Bis zu diesem Zeitpunkt wurde noch kein einziges Zeichen an den Browser geschickt.] ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> 2. Die Vorlage index.php5 8 <head> ... [Metadaten] <!-- Eingebunden werden: common.css; main.css aus htDisplay stammendes CSS --> <link rel='shortcut icon' type='image/x-icon' href='<?php echo $i_protocol.$_SERVER['HTTP_HOST']; ?>/favicon.ico' /> <link rel='stylesheet' type='text/css' href='<?php echo $i_protocol.$i_html.$i_styles_karriere.$i_common_css_fname; ?>' media='all' /> <link rel='stylesheet' type='text/css' href='<?php echo $i_protocol.$i_html.$i_styles_karriere.$i_main_css_fname; ?>' media='all' /> <!--[if IE 6]> <link rel='stylesheet' type='text/css' href='<?php echo $i_protocol.$i_html.$i_styles_karriere. $i_mainIE6_css_fname; ?>' media='<?php echo $i_media; ?>' /> <![endif]--> 2. Die Vorlage index.php5 9 <?if($i_media!=$i_std_media):?> ... [Einbinden von Media-bezogenem CSS] <?endif?> <?php echo $dp->getCssLink(); ?> <title>KARRIERE handbuch - Koch Management Consulting</title> <!-- Java Script dessen einziger Zweck die vertikale Dehnung des Master-Div --> <!-- auf Bildschirmhoehe ist. Nein, height: 100%; tut's nicht! --> <script src='<?php echo $i_protocol.$i_html.$i_scripts.$i_vScale_fname; ?>' type='text/javascript'></script> </head> <body onload='setHeight()'> <div id='master'> <!-- Saemtliche Inhalte umschliessender Master Container --> <div id='title'> <!-- Container fuer das Titelbanner --> <?php print($dp->replaceAliases('{{IMG'.$i_gif_head.';kh}}')); ?> </div> 2. Die Vorlage index.php5 10 <div id='content'> <!-- Container fuer den zentralen Inhalt --> <?php print($cContent); ?> </div> <div id='left'> <!-- Linkblock auf der linken Seite. --> <?php print(staticTools::linkList(1,20,array(2,7,11,14),$activeLink,'kh', NULL,$addLinksLeft,$db)); ?> </div> <div id='right'> <!-- Linkblock auf der rechten Seite. --> <?php print(staticTools::doImpressum($db,$dp,$activeLink)); ?> <?php print(staticTools::linkList(23,40,array(1,4,7),$activeLink,'kh', NULL,$addLinksRight,$db)); ?> </div> <div id='bottom'> <!-- Container fuer die Fussnote --> <?php ... [Logo und Suchfeld einbinden / Debug-Code] ?> </div> 2. Die Vorlage index.php5 11 </div> <!-- End of: master --> <!-... [Einige Kommentare zum Code] Die Vorlage fuer dieses Template lieferte der Css Zen Garden. Eine wunderschoene Site, die sich der Verbreitung von CSS gewidmet hat. Da Sie offenbar Interesse an Quellcode haben, schauen Sie ruhig auch dort einmal vorbei: http://www.csszengarden.com/ P.S.: Diese Site verwendet keine Frames. Es ist also unerheblich ob Frames von Ihrem Browser unterstuetzt werden. ;-) --> </body> </html> 2. Die Vorlage index.php5 12 <div>s wie im Zen-Garden CSS-Zen-Garden: http://www.csszengarden.com/tr/deutsch/ 2. Die Vorlage index.php5 13 Interessante CSS-Eigenschaften Inhalte sollen gut aussehen auf IE 7+, Firefox 2+, Lynx. margin: auto: Zentriert relativ positionierte Blockelemente. float, clear: Erlauben und steuern das horizontale Nebeneinander von relativ positionierten Blockelementen. position: Ob relative, absolute, static oder fixed, mit position lassen sich die Blockelemente im Browser positionieren. overflow: Scrollbars in Blockelementen. Sehr empfehlenswert: http://de.selfhtml.org/ 2. Die Vorlage index.php5 14 3. Das wichtigste PHP Die vier zentralen Bausteine des CMS init.php5: Immer zuerst ausgeführtes Config-Script. Definiert globale, das CMS beschreibende Variablen und ruft seinerseits ein weiteres Script init<Plattform>.php5 auf, welches Daten zur Serverumgebung enthält. class Kdb: Ein eigenes Datenbankobjekt. Sämtliche DB-Zugriffe erfolgen über Instanzen dieses Objektes. class htDisplay und Ableitungen: Verarbeiten die Datenbankinhalte zu HTML-Code oder PDF-Dokumenten. class khAction/fgAction: Enthalten Code für Funktionen die dynamischen Inhalt erzeugen. Diese Klassen werden aktiv, wenn ein GET/POSTParameter ´action´ vorliegt und als Wert den Namen einer definierten Methode enthält. 3. Die Programmstruktur 15 4. Die Datenbankstruktur 4. Die Datenbankstruktur 16 Es kommen vier Arten von Inhalten vor: Standardinhalte, Werbeinhalte, statische Inhalte und dynamische Inhalte. a) Standardinhalte. Aus den SQL-Tabellen index_<clt> und content_<lng>_<clt> Diese bilden das Gros der Inhalte der Site. Die index_<clt>-Tabellen stellen eine inhaltslose Baumstruktur voneinander abhängiger Index-Knoten zur Verfügung. Jedem Knoten ist eine cid (Content-Id) zugeordnet. Die cid entspricht einer Zeilennummer in den Tabellen content_<lng>_<clt>. In den sprachbezogenen Content-Tabellen liegen die eigentlichen Überschriften und Textinhalte. 4. Die Datenbankstruktur 17 Auszüge aus der Index-Content-Struktur. Auszug aus index_kh Auszug aus content_de_kh 4. Die Datenbankstruktur 18 Interessante SQL-Kommandos SHOW COLUMNS FROM <Tabellenname>: Liefert eine Tabelle mit Informationen über die Spalten der Zieltabelle zurück: Datentyp, Defaultwert, Primärschlüssel, ... <Wert1> REGEXP <Wert2>: SQL kennt durchaus reguläre Ausdrücke! <Tabelle1> INNER JOIN <Tabelle2>: Nimmt beide Datensätze und liefert (als Menge betrachtet) ihr kartesisches Produkt zurück. So wird z.B. aus den Datensätzen (a,b,c) und (1,2) der Datensatz (a1,a2,b1,b2,c1,c2). Das ist toll! Bsp.: SELECT ‘title‘ FROM `index_kh` INNER JOIN `content_kh` ON `index_kh`. `cid` = `content_<lng>_kh`. `id` WHERE `index_kh`.`id` = <con>; Liefert für beliebige <lng> den Title eines beliebigen <con> des kh zurück. Sehr empfehlenswert: http://www.sqldocu.com/ Zeigt sehr viele seiner Kommandos auch als Code an: PHP-MyAdmin. 4. Die Datenbankstruktur 19 5. Die Darstellungsklassen <cconv>Disp In den Datenbankeinträgen und im Quellcode so wenig HTML wie möglich. • HTML unterliegt schwankenden Standards. Keine Lust 10.000 Datenbankeinträge nachzubearbeiten! • HTML-Code ist relativ umfangreich und Datenbankeinträge sollten einigermaßen „schlank“ ausfallen. Meine Lösung: • Trage Artikel in vereinfachter, am besten Fließtextform in die Datenbank ein. • Weise dem Artikel eine „Art“ zu. Z.B. twocol für zweispaltige Linklisten oder tabular für Inhalte, die Tabellen enthalten sollen. • Schreibe Ableitungen twocolDisp und tabularDisp der Darstellungsklasse htDisplay, welche alle Fähigkeiten von htDisplay erben, aber diejenige Methode, die dort einfach nur die Inhalte aus der Datenbank holt, überschreibt: htDisplay->conTpl(..) liest einen Datenbankeintrag ein und liefert ihn zurück. twocolDisp->conTpl(..) liest ihn ein und verarbeitet ihn vor dem Zurückliefern. • Bei Bedarf zusätzlich: Verfassen und Einbinden von twocolCon.css 5. Die Darstellungsklassen 20 Aus der Datenbank auf den Monitor: Eine twocol-Linkliste Ebenfalls aus der Db: Der Content Converter für diesen Artikel ist twocol twocolDisp ergänzt den HTML-Header auch um einen Link auf twocolCon.css 5. Die Darstellungsklassen 21 Manchmal praktisch: Ein Artikel - Zwei Displayer Der Artikel, wie er in der Datenbank steht. Für den Browser interpretiert von einem Objekt der Klasse standardDisp: 5. Die Darstellungsklassen 22 Für den Acrobat Reader interpretiert von einem Objekt der Klasse standardPdf: Anmerkung: So etwas geht zügig und vergleichsweise einfach mit der ToolKlasse des PHP-Open Source Projektes fpdf: http://www.fpdf.de/ 5. Die Darstellungsklassen 23 Interessante PHP5-Kommandos $cconv=‘twocolDisp‘; $displayer = new $cconv(...); ... ist ein durchaus funktionierendes Codefragment! Sehr nützlich, wenn man den Namen der zu instanzierenden Klasse vorher nicht kennt. preg_replace(...) und eine Reihe verwandter Funktionen bringen die Möglichkeiten von Perl Regular Expressions nach PHP. (250 mal) header(...) muss aufgerufen werden, bevor das erste Zeichen an den Browser geschickt wurde. Erlaubt die Ergänzung des HTTP-Headers und somit z.B. Weiterleitungen (Location), veränderte MIME-Types (ContentType), Browser-Cache-Kontrolle (Expires, Cache-Control, Pragma: nocache) und vieles mehr. mb_detect_encoding(...), mb_convert_encoding(...): Umkodieren von Textinhalten zum Beispiel von UTF-8 in ASCII. Äußerst empfehlenswert: Programmieren mit PHP von Rasmus Lerdorf et Al. Erschienen im O‘Reilly-Verlag. ISBN-10 3-89721-473-3. 5. Die Darstellungsklassen 24 6. Performance-Fragen Ich bezeichne einen Vorgang als teuer, wenn der Server für seine Bearbeitung stark beansprucht wird. 6. Performance-Fragen 25 Festplatten-Caching: • Nachdem der an den Client zu sendende Inhalt bestimmt wurde diesen parallel auch in einer Datei auf der Festplatte ablegen. • Jedes mal wenn eine Anfrage an den Server geht erst überprüfen, ob der geforderte Inhalt nicht bereits in einer noch einigermaßen aktuellen Cache-Datei vorliegt und ggf. einfach diesen Inhalt laden. Meine Lösung: Die Cache-Dateien für die Hauptinhalte heißen content_<id>_<lng>_<clt>_<media>.cch Sie sind per Default für 24h aktiv und gliedern sich intern in 2 Blöcke: HTML für den Header<CSS==CACHE==CONTENT> HTML für den eigentlichen Inhalt 6. Performance-Fragen 26 Am Rande: Die Standardausgabe von PHP ist der Zielbrowser. PHP bietet aber einige Funktionen, die es erlauben die Ausgabe erst einmal in Variablen abzufangen. PHP-Stichworte dazu: ob_start(), ob_get_contents(), ob_end_clean() Anwendungsbeispiel auf Karrierehandbuch.de: Das Karrierehandbuch verfügt über ein auf bbPress laufendes Forum, dessen Funktionen dazu neigen, ihre Ausgaben direkt per print(...) auszugeben. Mit den ob_-Tools kann ich diese Ausgaben in Variablen abfangen, sie z.B. mit $displayer->replaceAliases(...) nachbearbeiten und erst dann anzeigen lassen. Ohne die gesamte BB-Press-Source überarbeiten zu müssen. BB-Press: http://bbpress.org/ Auf KH: http://www.karrierehandbuch.de/Extern/BBPress/index.php 6. Performance-Fragen 27 Speicher-Caching: Insbesondere die Ergebnisse wiederholt auftretender SQL-Queries (etwa bei einfachen Aliasen) oder auch die Ausgaben komplexerer Methoden können parallel im Speicher abgelegt werden. Wird das Query oder die Methode aufgerufen schaut das Programm immer zuerst, ob bereits eine geeignete Variable mit dem Inhalt vorliegt. Es gibt viele kleine Informationsmethoden im CMS die immer wieder aufgerufen werden. Ohne Cache führt das bei einigermaßen komplexen Seiten schnell zu bis zu ca. 800 winzigen SQL-Queries. Allein durch konsequentes Speichercaching gelang es mir, die Geschwindigkeit der Site mehr als zu verdoppeln. 6. Performance-Fragen 28 Objekt-Recycling: Das CMS enthält einige recht umfangreiche Objektklassen deren Instanzierung bereits teuer ist. Das ist besonders bei kleinen Funktionen, die zuweilen auch ein Displayer- oder ein Datenbankobjekt benötigen, schmerzhaft. Eine Lösung ist die optionale Übergabe einer Objektreferenz, die ggf. das teure new einspart: function tool(...,$dp=NULL,$db=NULL) // ... { if (is_null($dp)) $dp = new htDisplay(..); if (is_null($db)) $db = new Kdb(..); ... return $res; } 6. Performance-Fragen 29 Objektorientierung nutzen! Schlanke Versionen von Klassen: Beispiel: Anlegen einer Log-Klasse • Wird einfach nur ein Inhalt abgerufen, muss die Log-Klasse Ihrer Site nicht viel können. Es genügt, wenn Sie eine Zeile in einer Logdatei richtig zu schreiben vermag. • Lediglich im Backend Ihres CMS benötigen Sie eine deutlich aufwändigere Log-Klasse, die Log-Dateien auch auswerten kann. Programmieren sie diese als Ableitung der einfachen Write-OnlyKlasse! 6. Performance-Fragen 30 Einige von meiner Erfahrung belegte, subjektive Feststellungen: • PHP hat mehr Möglichkeiten, aber SQL arbeitet schneller. • Der Flaschenhals ist die Schnittstelle zwischen PHP und SQL. 10 einfache, SQL-Kommandos sind teurer als 1 „zehnfaches“ SQL-Kommando mit recht komplexem Query. • Die Performance von SQL-JOINS in Kombination mit SQL-REGEXPAusdrücken ist durchaus eindrucksvoll. Beispiel: Die interne Suchmaschine des Karrierehandbuchs verwendet SQL und PHP parallel: Zunächst wird mit einem REGEXP-JOIN eine einfache Vorauswahl an möglichen Artikeln getroffen. Auf diese überschaubare Datenmenge werden dann die mächtigeren preg_Methoden von PHP angewandt, die die Treffer auch bewerten. 6. Performance-Fragen 31 7. All user input is evil Das CMS soll Dritten dienen, eigene Inhalte und ggf. sogar Dateien zu präsentieren. Dabei wird zwangsläufig die Sicherheitsfrage aufgeworfen. Bedrohung: SQL-Injection. Unter SQL-Injection versteht man den Versuch eines Users, durch kunstvolle Eingabe von Daten SQL-Kommandos zu manipulieren. Ein Beispiel: Das Programm erzeugt eine harmlose Anfrage aus einem Parameter <which> SELECT `<which>` FROM `content_de_kh`; Der User übergibt aber: passwd` FROM `userdata` WHERE `login`=´Admin´; -SELECT `passwd` FROM `userdata` WHERE `login`=´Admin´; -- ` FROM `content_de_kh`; 7. All user input is evil 32 Was kann man tun? • Automatisch: Die PHP-Funktion mysql_query(..) lässt nur einen Befehl auf einmal zu. • Gefährliche Zeichen “`´\ -- nur kodiert in die SQL-Maschine einfüttern. • Eben diese Zeichen für die genutzten Queries einsetzen. Also SELECT `broetchen` FROM `baecker`; anstelle von SELECT broetchen FROM baecker; • Regelmäßig Datenbank-Backups ziehen. Hinweis: PHP bietet für die SQL-Schutzkodierung eigens die Kommandos addslashes($text) und stripslashes($text) an. Unicode-Fest ist mysql_real_escape_string($text,$dbResourceId) http://shiflett.org/blog/2006/jan/addslashes-versus-mysql-real-escape-string 7. All user input is evil 33 Bedrohung: Cross Site Scripting. XSS (um es nicht mit CSS zu verwechseln) ist der Versuch eines bösartigen Autors z.B. Java-ScriptFragmente in seine Beiträge einfließen zu lassen, die etwa die Cookies eines diese Beiträge lesenden Admins auslesen und an den Hacker weiterleiten. Letzterer kopiert dann diese Cookies in seine eigenen HTTPRequests und sieht für das CMS nun wie der Admin aus. Was kann man tun? • Javascript aus User-Artikeln herausfiltern. • Cookies an IPs knüpfen. Wer ein Cookie anfordert wird mitsamt seiner IP registriert ($_SERVER['REMOTE_ADDR']). Kommt nun eine Anfrage an den Server wird nicht nur der Wert des Cookies sondern auch die Korrektheit der IP überprüft. Das ist keine Garantie, aber schnell umgesetzt. 7. All user input is evil 34 Bedrohung: User stellen Schadskripte in Ihren Downloadbereichen bereit. Was kann man tun? User-eigene Downloads nur in dafür vorgesehenen Verzeichnissen bereitstellen lassen. Auf Apache-Servern kann man in jedem Verzeichnis eine Datei .htaccess hinterlegen, in der man unter anderem festlegen kann, wie der Server mit zum Beispiel Perl - Dateien verfahren soll. 7. All user input is evil 35 Beispiel für eine solche .htaccess-Datei: <FilesMatch "\.(cgi|pl|...)$"> ForceType application/octet-stream </FilesMatch> Dieses Code-Fragment (die Regexp ist unvollständig) sorgt dafür, dass Scriptdateien bei Aufruf nicht ausgeführt, sondern einfach als Text an den Client gesendet werden. 7. All user input is evil 36 Bedrohung: Abhören des Client beim Austausch vertraulicher Daten. Was kann man tun? • Falls der Server-Dienst dies anbietet: Die Übertragung auf SSL (Secure Socket Layer) setzen und den Client seine Anfragen verschlüsseln lassen. • Die meisten modernen Browser können sich ohne Aufwand für den User von der SSL anbietenden Site einen Public Key herunterladen, um damit zum Beispiel Formulardaten nach RSA zu verschlüsseln. Dazu genügt besucherseitig meist schon das Stellen der Anfrage als https://... anstelle von http://... • Umsetzung: Auf meinem CMS gibt es eine Variable $i_protocol = ‚http://‘ die bei der Erzeugung der internen Links eine wichtige Rolle spielt und deren Wert bei Bedarf auf https:// angepasst wird. FAQ zum Thema SSL: http://www.ccc.de/https/faq?language=de Kostenlose Zertifizierung: http://www.cacert.org/ Nette Einführung: Geheime Botschaften von Simon Singh. 7. All user input is evil 37 8. Dinge, die ich gerne vorher gewusst hätte Regular Expressions sind das Werkzeug für die Bearbeitung von Strings. SQL bietet das Vergleichszeichen REGEXP an. PHP bietet ereg_..()- und preg_..()-Tools. „preg“ steht für Perl Regular Expressions. Die preg-Tools sind besser als die ereg-Varianten. Die Anzahl der tatsächlich ausgeführten SQL Queries klein halten! Speicher- und Festplatten-Caching ist schnell programmiert und super! PEAR-DB wird von Rasmus Lerdorf empfohlen, ist aber bei der Verarbeitung von Strings buggy und führt bei Belastung zu Serverabstürzen. Die mysql_..()-Kommandos sind meiner Erfahrung nach zuverlässig. Auf Apache-Servern lohnt es sich, einmal die Datei httpd.conf entspannt durchzulesen. .htaccess-Dateien erlauben die Konfiguration des Serververhaltens in beliebigen Verzeichnissen 8. Dinge, die ich gerne vorher gewusst hätte 38 Vielen Dank für Ihre Aufmerksamkeit! CMS im Eigenbau 39