[Anmerkung für die Preiss'n: ``Einmerkerl'' ist bayrisch für ``Lesezeichen''.] Egal ob der Browserbenutzer im Büro sitzt, zuhause surft oder vom Hotelzimmer aus mit dem Laptop das Web abgrast: Ein global zugängliches CGI-Skript gewährleistet, dass immer dieselbe Bookmark-Liste verfügbar ist.
Das heute vorgestellte CGI-Skript nutze ich seit einiger Zeit, um meine wichtigsten URLs überall griffbereit zu haben. Um eine einmal in der Liste gespeicherte Website anzuwählen, klicke ich einfach auf meinen neuen Eintrag ``Bookmarks'' im Toolbar, der auf das Skript zeigt, und eine Seite nach Abbildung 1 anzeigt. Ein Mausklick auf einen Eintrag verzweigt sofort auf die entsprechende Webseite.
Hinter jedem Eintrag in Abbildung 1
steht eine Reihe von klickbaren Operatoren: + (nach oben), - (nach unten)
und x
(löschen), mit denen sich sowohl Ordner als auch Links
herumschieben lassen, um die Bookmark-Liste für den Benutzer schön
strukturiert und übersichtlich zu halten.
Abbildung 1: Die globale Bookmark-Liste in Aktion. Als neues "Einmerkerl" wird ein Link auf die "Perlmonks"-Seite im "Perl"-Ordner angelegt. |
Mittels des unter der Liste sichtbaren Web-Formulars kann man neue URLs in Ordner einfügen und permanent speichern. Um eine gerade vom Browser dargestellte Webseite neu in die Bookmarkliste aufzunehmen, will aber natürlich niemand URLs von Hand abtippen. Vielmehr soll der Titel der gerade dargestellten Webseite und ihr URL einfach per Mausklick in die Bookmark-Liste wandern.
Hierzu greifen wir in die JavaScript-Trickkiste: Moderne Browser erlauben in den definierbaren Bookmark-Einträgen der Toolbar-Leiste nicht nur URLs, sondern auch JavaScript-Code. Klickt der Benutzer auf einen Toolbar-Eintrag, der folgenden Code enthält, extrahiert der Browser, Titel und URL der gerade dargestellten Webseite, öffnet ein neues Fenster, ruft dort das Bookmark-CGI-Skript auf und füllt Titel und URL gleich ins Webformular ein:
javascript:void(win=window.open('http://myserver.com/cgi/bm?a='+location.href+'&t='+document.title))
Der Benutzer wählt dann nur noch einen Ordner aus der angebotenen Liste aus (oder gibt den Namen eines neuen ein) und klickt Submit, um den Eintrag einzuspeichern.
Der obige JavaScript-URL gelangt
einfach über den Bookmarks->Manage Bookmarks
-Dialog
in die Toolbar-Leiste, wie Abbildung 2 zeigt.
Außer diesem Toolbar-Eintrag,
den wir ``Add'' nennen, sollte auch noch der eingangs erwähnte
``Bookmarks''-Eintrag in die Toolbar-Leiste,
der einfach zum Skript zeigt, das die bisher definierten Bookmarks
zur Auswahl anbietet. Dies sollte bei jedem neuen Browser, den man längere
Zeit benutzt, durchgeführt werden -- ab dann ist die weltweit verfügbare
Bookmarkliste nur einen Mausklick entfernt.
Damit man den etwas komplexen Java-Script-Eintrag nicht auswendig lernen
oder abtippen muss, stellt das Skript ihn praktischerweise am unteren Ende
für ein einfaches Cut-and-Paste dar.
Abbildung 2: Mit JavaScript lässt sich ein Toolbar-Shortcut definieren, der Titel und URL der gegenwärtig dargestellten Browserseite an das Bookmark-Skript schickt. |
Die Implementierung ist auf zwei Dateien verteilt: Ein Modul Bookmarks.pm
,
das die Funktionalität der Bookmark-Liste implementiert, und
das Skript bm
, das sich um die
Darstellung im Web-Browser kümmert und Benutzereingaben verarbeitet.
Die Bookmark-Hierarchie legt Bookmarks.pm
in einer Baumstruktur ab. Das
Modul Tree::DAG_Node
von Sean Burke erzeugt und manipuliert gerichtete
azyklischen Graphen und eignet sich hervorragend, um die in Ordnern
liegenden Bookmarks zu implementieren. Sowohl Ordner als auch
Bookmarks sind Knoten (Nodes) im Graphen, der an der Wurzel (root)
beginnt. Der @ISA
-Array in Zeile 11 von Bookmarks.pm
bestimmt, dass Bookmarks
eine von Tree::DAG_Node
abgeleitete Klasse
ist. Ein Objekt vom Typ Bookmarks
repräsentiert einfach den
Wurzelknoten des Baums, der wiederum alle Ordner als Unterknoten
enthält, die wiederum die Bookmarks mit URL und Text, wiederum als
Unterknoten, enthalten.
Bookmarks.pm
definiert keinen Konstruktor new()
. Deshalb
wird Bookmarks->new()
einfach an Tree::DAG_Node
weitergeleitet.
Objekte vom Typ Tree::DAG_Node
führen neben Knoten-typischen Instanzvariablen auch ein Attribut attributes
,
hinter dem ein Hash hängt, in den applikationsspezifische Attribute passen,
ohne mit den Knotenattributen zu kollidieren. Einen neuen Bookmark-Order
erzeugt so
Bookmarks->new({ attributes => { type => "folder", path => "Perl", } });
und einen neuen Knoten, der einen Bookmark-Eintrag führt, kreiert dieses Konstrukt:
Bookmarks->new({ attributes => { type => "entry", text => $text, link => $link, } });
Abbildung 3 zeigt, wie die Ordner unter der Baumwurzel hängen und jeweils ein oder mehrere Bookmarks enthalten.
Abbildung 3: Der Baum, in dem die Bookmarks in den dazugehörigen Ordnern hängen. |
[Abbildung 3 ist auch als fig/tree.svg vorhanden].
Das Attribut type
dient der Applikation dazu, zwischen Ordnern und
Bookmark-Einträgen zu unterscheiden.
Beide Konstruktor-Aufrufe führt Bookmarks.pm
nicht direkt aus, sondern
über die Methode new_daughter()
, die aber hinter den Kulissen
ein new()
der Applikationsklasse aufruft. Mehr davon später.
Die ab Zeile 14 in Bookmarks.pm
definierte Methode insert()
nimmt als Parameter den Text und URL eines neuen Bookmark-Eintrags
sowie den Namen des Ordners entgegen, in dem dieser zu liegen kommt.
Als erstes Argument kommt der Wurzelknoten herein, da der Aufruf
über
$bm->insert(...)
erfolgt und $bm
das Wurzelobjekt des Baums ist. Dessen Kinder,
also die Ordner, fördert in Zeile 22 die daughters()
-Methode zutage.
In matriarchaischen Tree::DAG_Node
gibt es nur Mütter mit Töchtern,
Väter und Söhne hat Autor Sean Burke wohl augenzwinkernd ausgespart.
Ist der angegebene Ordner nicht darunter, erzeugt Zeile 32 einen neuen als Kind der Wurzel. Und Zeile 41 erzeugt anschließend den Bookmark-Eintrag als Kind des Ordners.
Die ab Zeile 51 in Bookmarks.pm
definierte
Methode folders
gibt eine Liste der Namen aller Ordner zurück,
die nutzt später das CGI-Skript, um die bestehenden Ordner in einer
Auswahlliste anzubieten.
Um einen Knoten innerhalb des Baums zu identifizieren, bietet
Tree::DAG_Node
die Methode address()
an, die den Weg von der Wurzel
zum jeweiligen
Knoten als Folge von Indizes beschreibt. Der zweite Eintrag (Index 1)
des dritten Ordners (Index 2) hört so auf den Namen "0:2:1"
.
Umgekehrt kommt man von dieser Hausnummer auf das damit referenzierte
Knotenobjekt, indem
man sie irgendeinem Baumobjekt (z. B. der Wurzel) als Parameter der
address()
-Methode übergibt:
my $node = $bm->address("0:2:1");
Die Hausnummer nutzt das CGI-Skript später, um herauszufinden, von welchem
Knoten der Benutzer den Navigationslink (rauf, runter, löschen)
angeklickt hat.
Die Methode as_html()
ab Zeile 60 in Bookmarks.pm
gibt eine
HTML-Darstellung des Bookmark-Baumes zurück und ruft sowohl für Ordner
als auch Bookmark-Einträge eine als Referenz $nav
hereingegebene Funktion
auf, der es die Hausnummer des jeweiligen Knotens übergibt. So kann
das aufrufende Skript bestimmen, wie die Navigationslinks jedes Eintrags
aussehen. as_html()
nutzt die praktischen Funktionen aus dem
CGI
-Modul, um HTML-Sequenzen zu erzeugen.
Die Methoden move_up
und move_down
nehmen jeweils eine Hausnummer
entgegen und befördern das damit referenzierte Knotenobjekt nach oben
oder unten. Sowohl Ordner als auch Bookmark-Einträge können so innerhalb
ihres Eltern-Containers umherwandern.
Tree::DAG_Node
malt die Kinder eines Elternknotens von links
nach rechts, nicht wie in der
Bookmarkliste von oben nach unten. Die in einer Zeile aufgereihten
Einträge eines Ordners starten also im Baum links mit dem ersten Eintrag
und setzen sich nach rechts bis zum letzten fort.
Tree::DAG_Node
bietet zwar keinen direkten Weg, um einen
Knoten nach links oder rechts zu verschieben, aber man kann den
Nachbarsknoten bestimmen (left_sister()
oder right_sister()
),
den gegenwärtigen Knoten aus dem Eltern-Container entfernen
($node->unlink_from_mother()
und ihn dann entweder links oder
rechts vom linken bzw. rechten Nachbarn wieder einfügen. Genau dies
tun move_up
und move_down
mit einem als Hausnummer referenzierten
Knoten.
Die delete()
-Methode entfernt einen Knoten vom Eltern-Container. Handelt
es sich um einen Ordner, verschwinden auch die in ihm enthaltenen
Bookmark-Einträge auf Nimmerwiedersehen.
Als Datenbank, die den Zustand der Bookmarkliste zwischen den Aufrufen
des CGI-Skripts speichert, nutzt Bookmarks.pm
das Modul Storable
,
das komplizierte und verschachtelte Datenstrukturen einfach mit
store
speichert und mit restore
wiederholt.
Die Methoden save
und restore
aus Bookmarks.pm
tun dies
jeweils mit dem Wurzelobjekt des Baums und schleifen damit indirekt
den ganzen Baum mit. Zu beachten ist, dass store()
auf einer
Instanzvariablen wie in
$bm->store($file);
aufgerufen wird, während restore
eine Klassenmethode ist, die
mit
my $bm = Bookmarks->restore($file);
einen in der angegebenen Datei abgelegten Baum ausgräbt und die
Instanz eines Bookmarks
-Objekts zurückgibt.
Das CGI-Skript bm
(Listing 2) übernimmt die Benutzerführung im
Browser. Es zieht das Modul CGI
für die HTML-Sequenzen herein
und spezifiziert fatalsToBrowser
für CGI::Carp
, um im Fehlerfall
schön formatierte Fehlermeldungen im Browser anzuzeigen, anstatt sich
mit Internal Server Error davonzuschleichen.
Außerdem kommt natürlich das vorher erläuterte Bookmarks
-Modul
zum Einsatz.
In der Variablen $DB_FILE
steht in Zeile 9 der Name der
Datei, in der Bookmarks.pm
den Baum permanent per
Storable::store
sichert.
Zeile 20 prüft, ob Parameterwerte für URL und Text vorliegen (u
und t
) und ob der Submit-Knopf gedrückt wurde, was über den Parameter
s
angezeigt wird, ein versteckter (hidden) Parameter im
weiter unten angezeigten Web-Formular. Dies ist notwendig, damit das
CGI-Skript unterscheiden kann, ob nur der JavaScript-Toolbareintrag
Titel und URL der gerade angezeigten Webseite sandte oder ob der Benutzer
schon einen Ordner ausgewählt und den Submit-Knopf gedrückt hat.
Im letzeren Fall holt Zeile 23 den Namen des Ordners und Zeile 25 prüft,
ob der Benutzer nicht den Namen eines neu anzulegenden Ordners
(angezeigt im Parameter fnew
) in das Textfeld eingetragen hat und
damit diesen favorisiert. Liegt kein Ordner vor, bricht Zeile 26 mit
einem Fehler ab. Sonst fügt Zeile 28 mit der insert()
-Methode den
neuen Eintrag in die Datenbank ein.
Hat der Benutzer auf einen Navigationslink gedrückt, sind entweder
del
, mvu
(move up), oder mvd
(move down) gesetzt und
die Zeilen 31 bis 33 rufen die passende Methode aus Bookmarks.pm
auf,
um die Baumstruktur zu manipulieren.
Anschließend folgt die HTML-Ausgabe, eingeleitet vom HTTP-Header in Zeile 36 und gefolgt von der HTML-Repräsentation des Bookmark-Baumes in Zeile 39. Zeile 40 sichert eine eventuell modifizierte Baumstruktur permanent auf Platte.
Die print
-Anweisung ab Zeile 42 malt das Webformular, das Modul
CGI
sorgt dafür, dass die Felder entsprechend den vorliegenden
CGI-Parametern vorbesetzt werden. Das ab Zeile 48 erzeugte Popup-Menü
mit den Namen aller existierenden Ordner entsteht mit Hilfe der
in Bookmarks.pm
definierten folders()
-Methode.
Zeile 60 gibt den an den örtlichen URL angepassten JavaScript-Eintrag aus, den der Benutzer im Toolbar eintragen muss, damit neue Einträge einfach per Mausklick im Baum landen.
Die in Zeile 39 aufgerufene as_html()
-Methode erhielt eine Referenz
auf die Funktion nav()
mit, die ab Zeile 66 definiert ist. Sie
gibt das HTML für die hinter jedem Eintrag angezeigte Navigationsliste
zurück.
Wie oben ausgeführt, ruft as_html()
die Funktion nav()
für
jeden angezeigten Eintrag auf, und übergibt jeweils die Hausnummer.
Die ist dort als $n
verfügbar und wird an die Links angehängt, die
auf das CGI-Skript selbst zurückdeuten und
Navigationsanweisungen wie mvu
, mvd
oder del
enthalten.
Bookmarks.pm
nutzt Tree::DAG_Node
und Storable
vom CPAN. Sind sie
installiert, muss bm
ausführbar
ins cgi-bin
-Verzeichnis des Webservers und
Bookmarks.pm
entweder ins selbe Verzeichnis oder an eine Stelle,
an der bm
danach sucht.
Damit nicht die ganze Welt die Bookmarkliste manipuliert, schützt
der Webserver sie auf einem Shared-System mit einer .htaccess
-Datei a la
AuthType Basic AuthName "Mike's Bookmarks" AuthUserFile /var/www/htpasswd Require valid-user
zumindest per Basic Auth mit einem Passwort, das der Admin einmal per
htpasswd username /var/www/htpasswd
setzt und das dann vom Webserver beim ersten Zugriff eingefordert wird. Sicher ist das nicht, da das Passwort quasi im Klartext über die Leitung geht, aber für meine Zwecke reicht's.
Das Skript geht davon aus, dass jeweils nur ein Benutzer es nutzt und trifft keine Vorkehrungen um Zugriffe auf die permanenten Daten zu synchronisieren.
Und es handelt nach der Unix-Philosophie, dass ein fortgeschrittener Benutzer immer weiß, was er tut: Einmal auf das ``x'' eines Ordners geklickt, und schon verschwindet dieser mitsamt der in ihm enthaltenen Links auf Nimmerwiedersehen im Orkus.
Wer weitere Manipulationsmöglichkeiten wünscht, wie zum Beispiel das
Umbenennen von Ordnern oder Unter-Ordner, kann das Skript
erweitern.
Wen die in der Datenbankdatei abgelegte Datenstruktur
interessiert, kann mittels des dumpsto
-Skripts [2] einen Dump
der Storable-Datei erzeugen, mit einem Editor darin herumfuhrwerken
und anschließend die Daten
via dumpsto -u
wieder in eine Storable-Datei überführen.
001 ########################################### 002 package Bookmarks; 003 ########################################### 004 # Administer browser bookmarks 005 # Mike Schilli, 2004, m@perlmeister.com 006 ########################################### 007 008 use Storable; 009 use CGI qw(:all *dl *dt); 010 use Tree::DAG_Node; 011 our @ISA = qw(Tree::DAG_Node); 012 013 ########################################### 014 sub insert { 015 ########################################### 016 my($self, $text, 017 $link, $folder_name) = @_; 018 019 my $folder; 020 021 # Search folder node 022 for($self->daughters()) { 023 if($_->attributes()->{path} eq 024 $folder_name) { 025 $folder = $_; 026 last; 027 } 028 } 029 030 # Not found? Create it. 031 unless(defined $folder) { 032 $folder = $self->new_daughter( 033 { attributes => { 034 type => "folder", 035 path => $folder_name, 036 }, 037 }); 038 } 039 040 # Add it 041 return $folder->new_daughter( 042 { attributes => { 043 type => "entry", 044 text => $text, 045 link => $link, 046 }, 047 }); 048 } 049 050 ########################################### 051 sub folders { 052 ########################################### 053 my($self) = @_; 054 055 return map { $_->attributes()->{path} } 056 $self->daughters(); 057 } 058 059 ########################################### 060 sub as_html { 061 ########################################### 062 my($self, $nav) = @_; 063 064 my $html = start_dl(); 065 066 for my $folder ($self->daughters()) { 067 068 $html .= dt( 069 b($folder->attributes()->{path}), 070 $nav->($folder->SUPER::address())); 071 072 for my $bm ($folder->daughters()) { 073 my $bma = $bm->SUPER::address(); 074 075 my($link, $text) = 076 map { $bm->attributes()->{$_} } 077 qw(link text); 078 079 my $a = $bm->attributes(); 080 081 $html .= dd(a({href => $link}, 082 $text), $nav->($bma)); 083 } 084 } 085 086 $html .= end_dl(); 087 088 return $html; 089 } 090 091 ########################################### 092 sub move_up { 093 ########################################### 094 my($self, $address) = @_; 095 096 my $node = 097 $self->SUPER::address($address); 098 if(my $left = $node->left_sister()) { 099 $node->unlink_from_mother(); 100 $left->add_left_sister($node); 101 } 102 } 103 104 ########################################### 105 sub move_down { 106 ########################################### 107 my($self, $address) = @_; 108 109 my $node = 110 $self->SUPER::address($address); 111 if(my $right = $node->right_sister()) { 112 $node->unlink_from_mother(); 113 $right->add_right_sister($node); 114 } 115 } 116 117 ########################################### 118 sub delete { 119 ########################################### 120 my($self, $address) = @_; 121 122 my $node = 123 $self->SUPER::address($address); 124 $node->unlink_from_mother(); 125 } 126 127 ########################################### 128 sub restore { 129 ########################################### 130 my($class, $filename) = @_; 131 my $self = retrieve($filename) or 132 die "Cannot retrieve $filename ($!)"; 133 } 134 135 ########################################### 136 sub save { 137 ########################################### 138 my($self, $filename) = @_; 139 store $self, $filename or 140 die "Cannot save $filename ($!)"; 141 } 142 143 1;
01 #!/usr/bin/perl 02 ########################################### 03 # bm -- Administer bookmarks CGI 04 # Mike Schilli, 2004 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 my $DB_FILE = "/tmp/bm.sto"; 10 11 use CGI qw(:all *table); 12 use CGI::Carp qw(fatalsToBrowser); 13 use Bookmarks; 14 15 my $bm = Bookmarks->new(); 16 17 $bm = Bookmarks->restore($DB_FILE) if 18 -f $DB_FILE; 19 20 if(param('t') and param('a') and 21 param('s')) { 22 my $f = param('f'); 23 24 # String overrides box selection 25 $f = param('fnew') if param('fnew'); 26 die "No folder defined" unless length($f); 27 28 $bm->insert(param('t'), param('a'), $f); 29 } 30 31 $bm->delete(param('del')) if param('del'); 32 $bm->move_up(param('mvu')) if param('mvu'); 33 $bm->move_down( 34 param('mvd')) if param('mvd'); 35 36 print header(), 37 start_html(-title => "Bookmarks"); 38 39 print $bm->as_html(\&nav); 40 $bm->save($DB_FILE); 41 42 print start_form(), 43 start_table(), 44 TR(td("Title"), td(textfield( 45 -name => 't', -size => 80))), 46 TR(td("URL"), td(textfield( 47 -name => 'a', -size => 80))), 48 TR(td("Folder"), td(popup_menu( 49 -name => 'f', -values => 50 [$bm->folders()]))), 51 TR(td("New Folder"), td(textfield( 52 -name => 'fnew', -size => 80))), 53 end_table(), 54 hidden(s => 1), 55 submit(), 56 end_form(), 57 end_html(), 58 ; 59 60 print "Use this in your toolbar: ", 61 pre("javascript:void(win=window.open('" . 62 url(-path_info => 1) . "?a='+location." . 63 "href+'&t='+document.title))"); 64 65 ########################################### 66 sub nav { 67 ########################################### 68 my($n) = @_; 69 70 return " [" . 71 a({href => url() . "?mvu=$n"}, 72 "+") . " " . 73 a({href => url() . "?mvd=$n"}, 74 "-") . " " . 75 a({href => url() . "?del=$n"}, 76 "x") . "]"; 77 }
dumpsto
und andere Skripts in Mike's Script Archive:
http://perlmeister.com/scripts
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |