Desktophintergrund zu wxBitmap
Thursday, 16. June 2011
Um in C den Desktophintergrund in ein dann anderweitig nutzbares wxBitmap zu bekommen, kann man so vorgehen:
wxBitmap* backImage = new wxBitmap(); WnckScreen *screen = wnck_screen_get_default (); backImage->SetPixmap( gdk_pixmap_foreign_new( wnck_screen_get_background_pixmap(screen) ) );
wnckScreen aus der libwnck, gdk-pixmap-foreign-new, SetPixmap fand ich nicht in der Doku.
Den Wechsel des Hintergrunds kann man mit GSignals so fangen:
g_signal_connect (G_OBJECT(defaultScreen), "background-changed", G_CALLBACK (callback), ca);
Wenig Code, doch gar nicht so einfach zu finden.
RSA-SHA256-Signatur mit PHP5
Saturday, 26. March 2011
Ich dachte ja, ich mag PHP. Aber wenn man in solche Fallen wie heute rennt, hört das ganz schnell auf.
Folgendes Szenario: Ein Server (node.js) soll sich bei einem Client ausweisen. Also erstellt der Server eine Zufallszahl, signiert die, schickt die Zahl und die Signatur an den Client. Im Grunde so:
function generateNonce() { return Math.floor(Math.random()*100001); } function generateSignature(nonce) { var signer = crypto.createSign('RSA-SHA256'); signer.update(""+nonce); var private_key = fs.readFileSync('./privateKey.pem', "utf8"); return signer.sign(private_key, output_format='base64'); }
Jetzt müsste nur noch der PHP-Client das mit dem PublicKey entschlüsseln und schauen, ob die Signatur gilt. Sollte ja nicht wirklich schwer sein, openssl kann das ja - doch die Dokumentation dazu ist unterstes Niveau. Dort wird nichtmal angegeben, in welchem Format (hex, base64?) die Signatur erwartet wird. Dank diesem (Signatur als reiner String, also selbst dekodieren) und diesem Kommentar (erklärt, im Gegensatz zur Dokumentation verständlich, wie man überhaupt Schlüssel lädt) ließ sich das jetzt nach Stunden des Ausprobierens so zusammensetzen:
$nonce = $_POST['nonce']; $sig = base64_decode($_POST['sig']); $publicKey = openssl_get_publickey(file_get_contents(dirname(__FILE__). '/publicKey.pem')); $valid = openssl_verify($nonce, $sig, $publicKey, "RSA-SHA256");
HTML reparieren mit JTidy
Friday, 18. February 2011
Vermutlich kommt sxp mit ungültigem XML oder seltsamen HTML besser zurecht als die meisten XML-Parser. Manchmal sind die Fehler aber zu gravierend, oder man will Fehlerquellen minimieren. Dann ist JTidy praktisch: Der Fork von HTML Tidy behebt HTML-Fehler schlichtweg und liefert sauberen Code zurück. Wenn man mit Seiten aus der echten Welt arbeitet statt mit vermutlich sauberen API-Ausgaben ist sowas einfach notwendig.
Zwei Beispiele von der "HTML Tidy"-Seite:
<a href="#refs">References<a>
<a href="#refs">References</a>
<h1><i>italic heading</h1> <p>new paragraph
<h1><i>italic heading</i></h1> <p>new paragraph
Ich weiß nicht mehr, welche Seite und welche Codestelle so zerschossen war, dass ich es initial gebraucht habe. Ich weiß noch, dass es bei den Arbeiten am Blogzähler war und das jTidy das Problem löste. Seitdem lasse ich es einfach standardmäßig über die Seiten laufen.
HTML-Boxen gleicher Größe: boxHeight
Thursday, 17. February 2011
Das heute vorgestellte Projekt ist eine Nummer kleiner. BoxHeight ist ein kleines Stück Javascript, dem man ids von Elementen gibt, die daraufhin auf die gleiche Größe (die größte) gesetzt werden. Das ist praktisch wenn man divs hat, die nebeneinander sind und, unabhängig von der Länge des enthaltenen Textes, gleich groß sein sollen.
Ohne Angleichung |
Mit boxHeight |
Natürlich gibt es dafür auch jQuery-Plugins, doch da das noch nicht genutzt wurde wollte ich es nicht für eine solche Kleinigkeit (und da man den Code auch selbst schreiben könnte) einbinden.
Xpath für XML als String unter Java - StringyXmlReader
Wednesday, 16. February 2011
Der StringyXmlParser ist von mir. Er versteht XML als String, iteriert dort drüber, um schließlich einen String oder einen Vektor von Strings zurückzugeben.
Bevor ich das weiter erkläre sollte ich die Vorgeschichte erzählen, bevor mir hierfür später mal der Kopf abgerissen wird. Eine Api gab XML zurück und das musst geparst werden. Also schaute ich mir an, was es dafür unter Java so alles gibt. Zuerst fand ich SAX, und auch wenn das inzwischen alles seinen Sinn hat: Die impliziten Funktionsaufrufen bei den Events habe ich damals schlichtweg nicht verstanden.
Dann versuchte ich, ein DOM-Objekt zu erstellen, und bekam dort immer null zurück, in ähnliche Fehler rannte ich noch bei anderen Wegen. Schießlich wurde mir das zu blöd, die Auflistung der vorhandenen Tools ist doch eine Aufzählung des Elends. Ich suchte noch eine Weile und habe dabei wahrscheinlich eine äußerst einfache Lösung übersehen (nicht vergessen: Ich rannte in Bugs oder machte etwas falsch, sodass solche netten Lösungen nicht funktionierten). Schließlich beschloss ich, mir eben selbst den Komfort zu bauen den es unter Bash schon lange gibt: Ein Tool, das XML als String und einen XPath-Ausdruck frisst und daraufhin einen Wert zurückgibt.
getNode(xml, xpath)
Also zum StringyXmlParser. Die grundsätzliche Benutzung sollte aus folgendem Beispiel klarwerden:
String xml = "<root>" + "<group>" + "<field>" + "abc" + "</field>" + "<field>" + "def" + "</field>" + "</group>" + "</root>"; StringyXmlParser sxp = new StringyXmlParser(); Vector<String> nodes = sxp.getNode(xml, "/root/group"); for (String node: nodes) { assertTrue(node.equals("<group><field>abc</field><field>def</field></group>")); }
getValue(xml)
Will man nicht einen kompletten Node, sondern den Inhalt eines Nodes:
String xml = "<root id=\"1\" value=\"abc def\" data=\"12dv\" >" + "<group>" + "<field>" + "abc" + "</field>" + "</group>" + "</root>"; StringyXmlParser sxp = new StringyXmlParser(); String value = sxp.getValue(sxp.getNode(xml, "/root/group/field").get(0)); assertTrue(value.equals("abc"));
getAttribute(xml, id)
Ebenso einfach kommt man an Attribute:
String xml = "<root id=\"1\" value=\"abc def\" data=\"12dv\" >" + "<group>" + "<field />" + "</group>" + "</root>"; StringyXmlParser sxp = new StringyXmlParser(); String attribute1 = sxp.getAttribute(xml, "id"); String attribute2 = sxp.getAttribute(xml, "value"); assertTrue(attribute1.equals("1")); assertTrue(attribute2.equals("abc def"));
searchNode(xml, nodeName)
Natürlich gibt es auch die Möglichkeit, einfach nach einem Node zu suchen:
String xml = "<root>" + "<group>" + "<field>" + "abc" + "</field>" + "<field>" + "def" + "</field>" + "</group>" + "</root>"; StringyXmlParser sxp = new StringyXmlParser(); Vector<String> nodes = sxp.searchNode(xml, "group"); for (String node: nodes) { assertTrue(node.equals("<group><field>abc</field><field>def</field></group>")); }
searchComplex(xml, nodeName1, value, nodeName2, rückgabetyp, exakteGleichheit)
Oder auch komplizierter: In der Gruppe, bei der ein Node (field1) einen bestimmten Wert hat (abc), wird der Wert des benachbarten Nodes (field2) zurückgegeben (oder der ganze Node, wenn rückgabeytp false ist). Das brauchte ich oft:
String xml = "<root>" + "<group>" + "<field1>" + "abc" + "</field1>" + "<field2>" + "def" + "</field2>" + "</group>" + "</root>"; StringyXmlParser sxp = new StringyXmlParser(); String value = sxp.searchComplex(xml, "field1", "abc", "field2", true, false).get(0); assertTrue(value.equals("def"));
Abschlussworte
Ist das nun die Erleuchtung, der definitive Weg um mit XML-Dateien umzugehen? Eher nicht. Ich benutze den Parser, um sowohl HTML als auch XML auszulesen, z.B. nutzt der Blogzähler ihn um auf den Blogs nach Faktoren für eine bestimme Blogplattform zu suchen. Das funktioniert ziemlich gut, also: Dort funktioniert sxp ziemlich gut. Doch wenn man in den Code schaut wird man feststellen, dass er keineswegs garantiert funktionieren wird - er betrachtet XML eben als String und geht dementsprechend dort durch. Das kann scheitern (stringy bedeutet nicht umsonst auch zäh). Dagegen finde ich es aber äußerst interessant und vermisste das früher oft, dass sxp einen Node einfach wieder als XML frisst und man darauf direkt wieder einen Xpath-Ausdruck anwenden kann. Das ist der Vorteil, wenn man syntaktische Korrektheit schlichtweg ignoriert.
An sich ist die Benutzung des StringyXmlParsers ein angenehmer und sehr einfacher Weg, mit XML umzugehen. Ich bin recht sicher, dass ich ihn noch eine Weile als eines meiner Standardwerkzeuge unter Java benutzen werde, solange ich nicht in unheilbare Bugs oder gravierende Performance-Probleme renne (die sich bisher nicht abzeichneten). Nebenbei: Lizenz ist die GPL.
Download: StringyXmlParser-0.1.tar.gz
DB4O
Tuesday, 15. February 2011
Db4o ist eine freie Datenbank für Objekte. In meiner Datenbankvorlesung habe ich noch gelernt, dass sowas zwar faszinierend, aber esoterischer Quatsch ist, der in der Praxis aus diversen Gründen nicht funktioniert. Es mag auch durchaus sein, dass es Gründe gibt, db4o nicht einzusetzen - bei meinem (natürlich vergleichsweise kleinem) Projekt konnte ich keine finden.
Speichern
Nochmal: db4o ist eine Datenbank für Objekte. Man speichert ein Objekt hinein und bekommt zu einem beliebigen späteren Zeitpunkt genau das gleiche Objekt zurück. Das Speichern ist simpel:
Cpu cpu = new Cpu(); ObjectContainer cpuDB = Db4o.openFile("cpus");; cpuDB.store(cpu);
Hier wurde eine Datei als Datenbank verwendet, "cpus", und dort hinein das neuerstelle Objekt gespeichert.
Select
Aber Speichern hilft nicht, wenn es nicht einfache Wege gibt, wieder das Objekt zu holen. db4o kennt zwei, und der erste ist wirklich simpel: queryByExample. Man erstellt ein Objekt, bei dem das Suchattribut gesetzt ist, alles andere steht auf default und wird ignoriert. Dann übergibt man das der Datenbank und bekommt all die Objekte mit dem gleichen Attribut zurück. Ein Beispiel:
Cpu exampleCpu = new Cpu(); exampleCpu.setSocket("A"); ObjectContainer cpuDB = Db4o.openFile("cpus"); ObjectSet<Cpu> cpuResult = cpuDB.queryByExample(exampleCpu); // ==>Nur Cpus mit Sockel A
Und ein Select *, also alle Cpus? Das geht über die Klasse:
ObjectSet<Cpu> cpuResult = cpuDB.queryByExample(Cpu.class);
Alternativ gibt es noch die native queries. Bei ihnen läuft ein Matcher über die Datenbank:
// Nur Cpus günstiger als 200: List<Cpu> result = container.query(new Predicate<Cpu>() { public boolean match(Cpu cpu) { return cpu.price < 200; } });
Editieren
Eine Objektdatenbank, oder zumindest db4o, hat eine andere Definition von Identität als eine SQL-Datenbank. Es wird nicht einfach ein unique key auf id gesetzt und dann verhindert die Datenbank, dass so etwas nochmal eingesetzt wird, und wird bei db4o ein Objekt gespeichert, das die gleichen Attribute wie ein anderes hat, ist ein solches Objekt eben zweimal vorhanden. Trotzdem kann man ein Objekt holen, editieren und mit den Änderungen speichern, ohne dass es Duplikate gibt:
private void updatePrice(Cpu cpu, double price, ObjectContainer cpuDB) { ObjectSet<Cpu> found = cpuDB.queryByExample(cpu); cpu = found.get(0); cpu.price = price; cpuDB.store(cpu); }
Löschen folgt dem gleichen Prinzip:
public void delete(Cpu cpu) { ObjectContainer cpuDB = Db4o.openFile(path+"cpus"); ObjectSet found = cpuDB.queryByExample(cpu); cpuDB.delete(found.get(0)); cpuDB.close(); }
Fazit
Db4o hat mich ziemlich begeistert. Ich musste einige Objekte speichern und wieder holen, und mir war klar, dass sich die Struktur dieser Objekte im Laufe der Entwicklung noch häufig ändern wird. Jedesmal ein SQL-Schema abzuändern oder neu zu erstellen wäre grausam gewesen. Überhaupt ein SQL-Schema zu erstellen wäre grausam gewesen. So brauchte ich mich darum nicht kümmern: Objekt gespeichert, Objekt geholt. Änderte sich mal was an den Objekten, wurde die alte Datenbank eben gelöscht, erstellt sich ja von selbst neu.
Ich kann die Performance nicht bewerten - es ist möglich, dass db4o bei großen Datenmengen gegen eine SQL-Datenbank verliert. Ich kann nicht sagen, ob es ansonsten gewichtige Gründe gibt, diese Objektdatenbank zu meiden, ich spare mir hier das Aufzählen der theoretischen Gegenargumente. Aber ich kann sagen: Bei mir hat db4o wunderbar funktioniert und die Entwicklung erheblich vereinfacht. Wenn ich nochmal Daten speichern will, die als Objekt bereits vorliegen und so wieder gebraucht werden, würde ich es nochmal hiermit versuchen.
Javascript: Element aus Dom entfernen
Friday, 30. April 2010
Ich benutze eigentlich immer den gleichen "Trick", den ich mal irgendwo aufgeschnappt habe, um Elemente zu löschen:
p.parentNode.removeChild(p);
Was mir jetzt nicht klar war: Es ist ziemlich einfach, diese Funktion jedem Objekt zur Verfügung zu stellen, sodass man das nicht immer neu eintippen muss, sondern nur eine Funktion aufruft:
Object.prototype.remove = function() { this.parentNode.removeChild(this); }
Soll nun irgendwas weg geht das einfachst:
var comment = document.getElementById('comment_1'); comment.remove();
PS: Gut, sowas erklären andere für verboten. Ist aber auch kein Problem, daraus ne normale Funktion zu machen.
Curl ignoriert Range
Friday, 22. January 2010
Ich habe gestern nochmal ein Update des Autotitel-Plugins veröffentlicht. Eigentlich sollte nur eine Konfigurationsoption eingefügt werden, aber dabei fiel mir auf, dass curl unter PHP seine eigene Range-Option nicht beachtet.
$ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RANGE, "0-4096"); $page = curl_exec($ch); curl_close($ch);
$page sollte hiernach nicht mehr als 4096 Byte groß sein, wird aber beliebig größer. Daher ist nun wieder
file_get_contents($url, 0, null, -1, 4096);
der Standardweg, nur wenn das scheitert wird es nochmal mit curl versucht.
Eigene Pluginseiten in Serendipity
Friday, 15. January 2010
Nachtrag 2: Einem Plugin eigene Seiten im Adminbereich zu geben ist relativ simpel. Soll die Seite aber im offenen Bereich des Blogs dargestellt werden, wird das Verfahren etwas komplizierter. Denn anstatt einfach auf das Event zu warten, das beim Besuchen unserer Seite geworfen wird, muss bei jedem Seitenaufruf geschaut werden, ob dies nicht unsere Seite ist.
Das listsearch-Plugin dient mir als Beispiel, wobei ein Großteil des Codes direkt dem contactform-Plugin entnommen ist.
Prüfen
Kern des ganzen ist also zu erkennen, ob dies die richtige Seite ist. Deshalb ist selected() die wichtigste Funktion. Sie gibt immer dann true aus, wenn der Seitenname "searchresults" an der richtigen Stelle in der URL ist.
function selected() { global $serendipity; if (!empty($serendipity['POST']['subpage'])) { $serendipity['GET']['subpage'] = $serendipity['POST']['subpage']; } if ( $serendipity['GET']['subpage'] == '/index.php?/searchresults' || preg_match('@^' . preg_quote($serendipity['serendipityHTTPPath'].'searchresults') . '@i', $serendipity['GET']['subpage']) ) { return true; } return false; }
Im richtigen Moment Darstellen
Leider ist das Zeigen der Seite nicht ganz einfach. Obwohl es im Grunde nur ein Event ist:
case 'entries_header': if ($this->selected()) { $this->show(); }
Wobei show() dann die Seite ausgibt und weiter unten beschrieben wird. Doch es müssen noch zwei weitere Events gefangen werden, damit alles passt. Um den Seitentitel zu setzen, ist 'genpage' gefragt:
case 'genpage': $args = implode('/', serendipity_getUriArguments($eventData, true)); if ($serendipity['rewrite'] != 'none') { $nice_url = $serendipity['serendipityHTTPPath'] . $args; } else { $nice_url = $serendipity['serendipityHTTPPath'] . $serendipity['indexFile'] . '?/' . $args; } if (empty($serendipity['GET']['subpage'])) { $serendipity['GET']['subpage'] = $nice_url; } if ($this->selected()) { $serendipity['head_title'] = QUICKSEARCH; $serendipity['head_subtitle'] = htmlspecialchars($serendipity['blogTitle']); } break;
Wichtiger noch als ein guter Titel ist, dass außer unserem Inhalt keine anderen Beiträge auftauchen. Dafür muss 'clean_page' gesetzt werden:
case 'entry_display': if ($this->selected()) { if (is_array($eventData)) { $eventData['clean_page'] = true; // This is important to not display an entry list! } else { $eventData = array('clean_page' => true); } } return true; break;
show()
Nun ist klar, wann wir etwas anzeigen. Im Idealfall wird aber nun nicht einfach irgendwas ausgegeben, sondern die Ausgabe Smarty übergeben. Daher ist show() gar nicht so simpel, deshalb hier nur der elementare Teil:
function show() { ... if (!headers_sent()) { header('HTTP/1.0 200'); header('Status: 200 OK'); } if (!is_object($serendipity['smarty'])) { serendipity_smarty_init(); } $serendipity['smarty']->assign( array( 'results' => $results ) ); $template = 'listsearch.tpl'; $tfile = serendipity_getTemplateFile($template, 'serendipityPath'); #serendipity_getTemplateFiles returns filename if no file found #in the templates instead of false (?) if ($tfile == $template) { $tfile = dirname(__FILE__) . "/$template"; } $inclusion = $serendipity['smarty']->security_settings[INCLUDE_ANY]; $serendipity['smarty']->security_settings[INCLUDE_ANY] = true; $content = $serendipity['smarty']->fetch('file:'. $tfile); $serendipity['smarty']->security_settings[INCLUDE_ANY] = $inclusion; echo $content; }
Es wird der richtige header für eine gefundene Seite ausgegeben, Smarty initialisiert falls es noch nicht getan wurde und danach die listsearch.tpl mit den gefundenen Beiträgen ($results) gefüllt. An dieser Stelle könnte man natürlich noch viel mehr übergeben, so reicht der echte Code an dieser Stelle noch die Trefferanzahl weiter.
Nun kann die Seite "index.php?/searchresults" des Blogs aufgerufen werden.
Eigene Seiten in Serendipitys Adminbereich
Wednesday, 13. January 2010
Nachtrag 1: Es ist ein leichtes, eigene Seiten per Plugin zu bauen. Insbesondere im Adminbereich, worum es im Folgenden gehen wird. Eigene Seiten befüllen zu können ermöglicht es, alles mögliche umzusetzen.
Link einsetzen
Zuerst wird der Link gesetzt. Den kann man direkt in das Adminmenü integrieren. Es muss zuerst das Event 'backend_sidebar_entries' registriert werden und dort der Code ausgegeben werden. Hier als Beispiel für dbclean:
case 'backend_sidebar_entries': echo '<li class="serendipitySideBarMenuLink serendipitySideBarMenuEntryLinks"> <a href="?serendipity[adminModule]=event_display&serendipity[adminAction]=dbclean"> ' .PLUGIN_EVENT_DBCLEAN_NAME .' </a> </li>'; return true; break;
Menü anzeigen
Nun wird also im Menü direkt auf dbclean verwiesen. Also muss das Event, ebenfalls vorher registriert, gefangen werden, wenn diese Seite aufgerufen wird:
case 'backend_sidebar_entries_event_display_dbclean': $this->displayMenu(); return true; break;
Das wars schon. Die Funktion displayMenu() kann nun den gesamten Code der Seite ausgeben - mittels eines Plugins wurde der Adminbereich erweitert.
Bash: Fehlermeldungen ausgeben
Thursday, 7. January 2010
Fehlermeldungen aus vorhandenen Programmen umzuleiten geht mit 2>, das findet man auch recht einfach im Netz. Doch wie gibt man so eine Fehlermeldung auf stderr überhaupt aus? Vielleicht ist das zum einen zu einfach und zum anderen etwas, mit dem sich der typische Nutzer nicht herumschlägt. Es geht so:
echo "Errormessage" >&2
local ist unwichtig
Monday, 4. January 2010
Wer den Fehler entdeckt, wird der Aussage im Titel garantiert zustimmen.
#!/bin/bash getPrim() { local prim=$RANDOM until isPrim $prim;do prim=$RANDOM done echo $prim } isPrim() { local prim=$1 local i=2 while [[ $i -lt $prim ]];do if [ $(($prim % $i)) == 0 ];then notprim=true break fi let i++ done if [ "$notprim" == true ];then return 1 else return 0 fi }
Einführung: Serendipity-Plugins schreiben
Wednesday, 23. December 2009
Was bringt die Rohform, wenn man nicht weiß, wie man etwas damit anfangen kann? Ich will hier zeigen, wie Serendipity-Plugins funktionieren und ein paar der typischen Probleme ansprechen. Gedacht ist das als verfrühtes kleines Weihnachtsgeschenk, für die langen und ruhigen Weihnachtsfeiertage ;)
Ein Wort zu PHP: Davor sollte man keine Angst haben. Es mag in manchen Kreisen als Skriptkiddie-Sprache gelten, warum auch immer. Wahrscheinlich spielt da rein, dass es nicht die schönste aller Sprachen ist, mit Semikolon-Zwang, der Mischung aus funktionaler und objektorientierter Programmierung und $-Variablenbezeichnung. Aber es tut seinen Job, und wer etwas Bash und eine beliebige objektorientierte Sprache kennt wird sich zurechtfinden. Bash mit Objekten beschreibt PHP ganz gut.
Diese Anleitung ist als Ergänzung/Erklärung zur umfassenden Auflistung der Plugin-API auf s9y.org zu verstehen.
Funktionsweise
Ein Plugin kann nur dort ansetzen, wo das Mutterprogramm es zulässt. Bei Serendipity sind dafür eine Vielzahl von Eintrittspunkten definiert, die Event Hooks (oder einfach Events). An bestimmten Punkten im Programmablauf, z.B. jedes Mal wenn im Adminbereich ein Kommentar gelistet wird, wird solch ein Event geworfen. Ein Plugin kann das Event fangen, die geworfenen Daten manipulieren und zurückgeben.
Das bedeutet aber nicht, dass keine echte eigene Funktionalität möglich ist. Ein Plugin kann problemlos eigene Seiten anlegen und auf denen machen, was es will.
Es muss noch unterschieden werden zwischen den Seitenleistenpugins (serendipity_plugin) und den Event-Plugins (serendipity_event). Letztere sind die "echten" Plugins mit voller Funktionalität. Die Seitenleistenplugins sind nur dazu da, Informationen in der Seitenleiste auszugeben und nicht dafür, alle möglichen Events zu fangen. Der grundsätzliche Aufbau ist aber gleich.
Aufbau
Ein Plugin ist ein PHP-Objekt, das von serendipity_event erbt. In den folgenden Abschnitten wird dann die Funktionalität eingebaut.
Definitionen
Die Funktion introspect(&$propbag) definiert die ganzen grundlegenden Eigenschaften des Plugins, inklusive den Konfigurationsoptionen und den genutzten Events. Was hier nicht angelegt wird, kann später nicht genutzt werden.
function introspect(&$propbag) { global $serendipity; $propbag->add('name', PLUGIN_EVENT_PLUGINNAME_NAME); $propbag->add('description', PLUGIN_EVENT_PLUGINNAME_DESC); $propbag->add('author', 'Your Name'); $propbag->add('version', '0.1'); $propbag->add('requirements', array( 'serendipity' => '0.8')); $propbag->add('event_hooks', array('frontend_display' => true)); $propbag->add('configuration', array('directblock')); }
Mittels $propbag->add() werden die verschiedenen Eigenschaften des Plugins definiert. Man sieht die ganzen grundlegenden Beschreibungen wie $propbag->add('name', 'PLUGIN_EVENT_PLUGINNAME_NAME'), wovon es natürlich noch einige weitere gibt. Wichtig und an sich selbsterklärend sind die Voraussetzungen zur Installation des Plugins.
Am allerwichtigsten ist jedoch das Hinzufügen der Events, die später genutzt werden sollen. $propbag->add('event_hooks', array('frontend_display' => true)) fügt den Event frontend_display hinzu, das Plugin soll also immer dann etwas tun, wenn eine normale Blogseite (wie dieser Artikel) aufgerufen wird.
Doch zuerst zur Konfiguration: $propbag->add('configuration', array('directblock')) fügt die Option 'directblock' hinzu, jedoch ohne anzugeben, was das für eine Art von Option ist. Das folgt im nächsten Abschnitt.
Konfiguration
Alle angelegten Optionen werden in der Funktion introspect_config_item($name, &$propbag) ausgewertet und mit Funktionalität versehen.
function introspect_config_item($name, &$propbag) { global $serendipity; switch($name) { case 'directblock': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_EVENT_PLUGIN_DIRECTBLOCK); $propbag->add('description', PLUGIN_EVENT_PLUGIN_DIRECTBLOCK_DESC); $propbag->add('default', false); break; default: return false; } return true; }
Mit der switch/case-Anweisung werden alle Optionen durchgegangen. Hier gibt es nur eine, directblock, also wird nur diese gefunden. Ihr wird ein Typ vergeben, der Typ Boolean, ein Name, Beschreibung und ein Standardwert. Je nach Typ wird in der grafischen Konfiguration ein anderes Objekt daraus, das hier wird ein Radiobutton mit "Nein" und "Ja" zur Auswahl.
Im folgenden Verlauf des Programms können die Optionen dann jederzeit mit $this->get_config('option', 'default') abgefragt werden, aber meistens wird es so sein, dass sie direkt bei der Abfrage der Events gebraucht werden - dort wird die Funktionalität spezifiziert.
Events
Um die Events kümmert sich die Funktion event_hook($event, &$bag, &$eventData, $addData = null). Ein Beispiel:
function event_hook($event, &$bag, &$eventData, $addData = null) { global $serendipity; $hooks = &$bag->get ( 'event_hooks' ); if (isset ($hooks[$event])) { switch ($event) { case 'external_plugin' : switch ($eventData) { case 'learncomment': //do something break; } return true; break; default : return false; break; } } else { return false; } }
Zuerst wird geguckt, ob überhaupt Events vorhanden sind. Wenn dem so ist und das Event 'external_plugin' (eines der wichtigsten, da freien Events, dazu später mehr) eintritt und dann dort der mitgegebene String '$eventData' 'learncomment' lautet, dann macht das Plugin irgendwas.
Statt 'external_plugin' hätte man auch das Event von oben, 'frontent_display', fangen können. An diesem sieht man etwas besser, wie $event_Data verwendet wird:
case 'frontend_display': foreach ($this->markup_elements as $temp) { if (serendipity_db_bool($this->get_config($temp['name'], true)) { $element = $temp['element']; $eventData[$element] = $this->_s9y_markup($eventData[$element]); } }
Das ist ein Auszug aus dem Code des s9ymarkup-Plugins, das standardmäßig in jedem Serendipity-Blog aktiviert ist. Wichtig ist hier nur, wie die Ausgabe angefasst wird. $eventData[$element] steht z.B. für 'body', verweist also auf den Blogtext eines Eintrags. Der wird manipuliert und dann wieder $eventData[$element] zugewiesen, also überschrieben. Und schon ist das Prinzip einer Auszeichnungssprache umgesetzt, das Plugin hat die eigentlichen Daten von Serendipity verändert.
Natürlich gibt es mehr als nur diese beiden Events.
Internationalisierung
Alle Texte, de ausgegeben werden sollen, können natürlich nicht einfach so ins Plugin geschrieben werden. Schließlich soll Serendipity nicht nur auf Englisch verfügbar sein. Daher die Nutzung der Variablen wie PLUGIN_EVENT_PLUGINNAME_NAME. Diese werden aus den Sprachdateien wie lang_de.inc.php eingebunden:
// Probe for a language include with constants. Still include defines later on, if some constants were missing $probelang = dirname(__FILE__) . '/' . $serendipity['charset'] . 'lang_' . $serendipity['lang'] . '.inc.php'; if (file_exists($probelang)) { include $probelang; } include dirname(__FILE__) . '/lang_en.inc.php';
In diesen Dateien kann dann der eigentliche Wert angegeben werden:
@define('PLUGIN_EVENT_PLUGINNAME_NAME', 'Spamschutz (Bayes)');
Typische Problemstellungen
Dateien bereitstellen
Viele Plugins haben Dateien, die sie im Laufe ihres Ablaufs benötigen. Das kann die Grafik sein, die neben einem Button angezeigt werden soll. Da wir nicht wissen, wo unser Plugin liegt, wissen wir vorher nicht, wie dieses Bild eingebunden werden kann. Daher fragen wir uns am besten selbst:
$serendipity['baseURL'] . 'index.php?/plugin/spamblock_bayes.spam.png'
Mit diesem Code lösen wir unser eigenes Event aus, das uns dann das Bild zurückgeben kann. Schlüssel hierzu ist das 'external_plugin'-Event:
switch ($event) { case 'external_plugin' : switch ($eventData) { case 'spamblock_bayes.spam.png': header('Content-Type: image/png'); echo file_get_contents(dirname(__FILE__). '/img/spamblock_bayes.spam.png'); break; } }
So kann zuverlässig das Bild ausgegeben werden. Mit dem gleichen Prinzip kann natürlich auch etwas anderes ausgegeben werden, beliebiges - dieses Event ist die Grundlage alle Ajax-Abfragen. Hiermit redet das Javacript, um Daten zu übermitteln oder zu bekommen.
Diese Methode hat jedoch einen Nachteil: Sie ist teuer, da wir ganz Serendipity nur dazu nutzen, eine mickrige Datei auszugeben. Und an sich wird sie nur selten gebraucht, die meisten Plugins liegen ja doch einfach unter /plugins/. Daher sollten Plugins eine 'path'-Option anbieten, die dann so genutzt wird:
$path = $this->get_config('path', ''); if (!empty($path) && $path != 'default' && $path != 'none' && $path != 'empty') { $path_defined = true; $imgpath = $path . 'img/'; } else { $path_defined = false; $imgpath = $serendipity['baseURL'] . 'index.php?/plugin/'; }
Statt dann blind auf den Event-Hook zu verweisen, nutzen wir den vorgegeben Pfad:
<img src="'. $imgpath . 'spamblock_bayes.spam.png" />
Abhängigkeiten
Es sind insbesondere Javascript-Frameworks wie jQuery, auf die andere Plugins aufbauen, wobei natürlich auch oft ein Event- mit einem Seitenleistenplugin gekoppelt wird. jQuery soll natürlich nicht von jedem Plugin, das dies benötigt, selbst eingebunden werden. Es gibt ein jQuery-Plugin, das dann in introspect(&$propbag) als Abhängigkeit eingetragen werden sollte:
$this->dependencies = array('serendipity_event_jquery' => 'remove');
Daten übermitteln und abfragen
Will man von JavaScript aus mit dem Server reden, bastelt man eine Ajax-Anfrage und redet mit 'external_plugin'. Dies sendet z.B. eine id (eines Kommentars) an das Plugin:
function spam(id) { if (window.XMLHttpRequest) { // Mozilla, Safari, Opera, IE7 httpRequest = new XMLHttpRequest(); } else if (window.ActiveXObject) { // IE6, IE5 httpRequest = new ActiveXObject("Microsoft.XMLHTTP"); } httpRequest.onreadystatechange = setMessage; lastID = id; httpRequest.open('POST', '<?php echo $serendipity ['baseURL'] . 'index.php?/plugin/learncomment'; ?>', true); httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=<?php echo LANG_CHARSET; ?>'); httpRequest.send('id='+id+'&category=spam'); // Start request setLoadIndicator(id); }
Um dann die id zu holen greift man auf die üblichen PHP-Mittel zurück:
switch ($event) { case 'external_plugin' : switch ($eventData) { case 'learncomment': if (!serendipity_checkPermission('adminComments')) { break; } $category = $_REQUEST ['category']; $id = $_REQUEST ['id']; ....
Falls etwas dem Javascript zurückgegeben wird, kann das mit einem echo ausgegeben werden und in setMessage() gefangen werden:
function setMessage() { if (httpRequest.readyState == 4 && httpRequest.status == 200) { var response = httpRequest.responseText; } }
Schlusswort
Natürlich gibt es noch viel mehr zu zeigen und zu erklären. Genauso kann es Dinge geben, die hier falsch oder nicht optimal gezeigt wurden oder Dinge, die einfach fehlen. Dann freue ich mich über einen Hinweis.
Generell gilt der Ratschlag von s9y.org: Wenn etwas unklar ist, einfach mal in ein anderes Plugin schauen und gucken, wie es dort gemacht wurde.
Rohform eines Serendipity-Plugins
Tuesday, 22. December 2009
Die Rohform eines Serendipity-Event-Plugins benötigt man immer wieder. Natürlich kann man dafür eines der kleineren Plugins wie s9ymarkup nehmen und dann rauslöschen, was man nicht braucht. Aber im Grund läuft es auf diesen Code hinaus:
<?php if (IN_serendipity !== true) { die ("Don't hack!"); } @serendipity_plugin_api::load_language(dirname(__FILE__)); class serendipity_event_pluginname extends serendipity_event { var $title = PLUGIN_EVENT_PLUGINNAME_NAME; function introspect(&$propbag) { global $serendipity; $propbag->add('name', PLUGIN_EVENT_PLUGINNAME_NAME); $propbag->add('description', PLUGIN_EVENT_PLUGINNAME_DESC); $propbag->add('stackable', false); $propbag->add('author', 'Your Name'); $propbag->add('version', '0.1'); $propbag->add('requirements', array( 'serendipity' => '0.8' )); $propbag->add('event_hooks', array('frontend_display' => true)); $propbag->add('groups', array('MARKUP')); } function generate_content(&$title) { $title = $this->title; } /*function introspect_config_item($name, &$propbag) { }*/ function event_hook($event, &$bag, &$eventData, $addData = null) { global $serendipity; $hooks = &$bag->get('event_hooks'); if (isset($hooks[$event])) { switch($event) { case 'frontend_display': //do something return true; break; default: return false; } } else { return false; } } function debugMsg($msg) { global $serendipity; $this->debug_fp = @fopen ( $serendipity ['serendipityPath'] . 'templates_c/pluginname.log', 'a' ); if (! $this->debug_fp) { return false; } if (empty ( $msg )) { fwrite ( $this->debug_fp, "failure \ " ); } else { fwrite ( $this->debug_fp, print_r ( $msg, true ) ); } fclose ( $this->debug_fp ); } } /* vim: set sts=4 ts=4 expandtab : */ ?>
Performanceprinzip: Gemeinsam statt einzeln
Monday, 21. December 2009
Häufig baut man bei einem neuen Programm erst eine Funktion, die eine Aktion durchführt. Klappt das mit dem Beispiel, ruft man diese jeweils mit jedem Eingabewert auf. Die Funktion getRating(id) z.B. würde einen Wert abfragen und weiterleiten, und klappt das mit der Testid, kommt ein getAllRatings() dazu, das erstmal alle ids sammelt und dann getRating aufruft:
function getAllRatings() { ... for(var i=0; i<ids.length;i++) { getRating(ids[i]); } }
Ein an sich sinnvoller Weg, um die Teilprobleme Schritt für Schritt zu lösen. Nur: Es kann der unperformante Ansatz sein.
getRating sei nun eine Funktion, die per httpRequest mit einem Server redet, den zugehörigen Kommentar zur id aus der Datenbank holt und schließlich die Bewertung (den Spamfaktor) des Kommentars berechnen lässt. Für jede id, also schlimmstenfalls für jeden Kommentar, der je im Blog abgespeichert wurde, wird bisher jede dieser Aktionen einzeln durchgeführt. Bei der Datenbankabfrage und auch bei der Serverkommunikation ist das äußerst ungünstig, da sich so die Wartezeiten addieren bzw. die Datenbank die Abfrage nicht optimieren kann.
Deshalb besser:
function getAllRatings() { ... getRating(ids); }
Wobei getRating nun erkennen muss, dass der Parameter id ein Array ist und es dieses in einem Schwung zum Server geben kann:
function getRating(id) { ... if (id.constructor == Array) { id = id.join(';'); } httpRequest.send('id='+id); }
Auf der Serverseite die ids gesammelt der Datenbank zu geben ist dann kein Problem mehr. Überhaupt ist hier die Umsetzung nicht das Problem und den meisten Programmierern die Mechanismen dahinter auch klar: Ajax-Aufrufe und die Datenbankabfragen minimieren. Trotzdem ist es für einen von "divide and conquer" geprägten Programmierer ein Schritt zu erkennen, dass der an sich logisch aufgebaute Code hier nochmal umgebaut werden sollte.