Fliegenklatsche (Linux-Magazin, September 2006)

Spammer senden nicht nur Emails, sondern nisten sich auch in Diskussionsforen und Blogs ein, um mit linkreichen Pseudopostings die großen Suchmaschinen hinters Licht zu führen. Ein Perlskript macht sauber.

Mein kleines Diskussionsforum auf usarundbrief.com wird in letzter Zeit immer mehr von Link-Spammern heimgesucht. Diese Parasiten des Internets setzen ihre Bots gezielt auf bekannte Forumsoftware wie phpBB oder Blogapplikationen wie Wordpress an, um sie mit mit Beiträgen zuzumüllen, die fast nichts als Links zu Poker- und Sexseiten enthalten. So versuchen sie, Diskussionsteilnehmer zum Klicken auf die Webseiten ihrer Auftraggeber zu animieren und die großen Suchmaschinen hinters Licht führen, die die Relevanz einer Seite anhand der auf sie zeigenden Links berechnen.

Abbildung 1: Ein Spammer hat sich ins Forum eingenistet

Sand im Spambot-Getriebe

Der sogenannte ``Comment Spam'' ([2]) lässt sich eindämmen, wenn nur registrierte User posten dürfen. Doch diese Hürde schreckt auch legitime Nutzer wegen Datenschutzbedenken ab. Wird jedes Posting zuerst von einem Moderator geprüft, bevor es auf der Website erscheint, bleiben zwar die Spammer draußen, aber manuelles Prüfen kostet Arbeitszeit und verursacht diskussionslähmende Verzögerungen.

So genannte ``Captchas'' (``Tippen Sie diese Nummer ab'') sollen sicherstellen, dass am anderen Ende der Leitung tatsächlich ein Mensch und kein Computer sitzt. Sie müssen gar nicht einmal so schwer zu knacken sein wie die der großen Registrierungssites. Ein verblüffend einfaches Beispiel zeigt Jeremy Zawodnys Blog, auf dem der User einfach ``Jeremy'' in ein zusätzliches Feld eintragen muss. Die meisten Bots scheitern daran, da sie sich auf den Massenmarkt konzentrieren und nicht auf individuelle Anpassungen eingehen können, die eh nur Prozentbruchteile des Marktes ausmachen. Auch verstehen manche Bots die Interaktionen von JavaScript mit dem DOM des Browsers nur unvollständig, sodass manchmal schon eine triviale lokale Anpassung der Forumsoftware mit etwas verschleierndem JavaScript die Bots zum Aufgeben zwingt.

Die Abbildungen 2 und 3 zeigt eine triviale Erweiterung von phpBB, die einen Schalter einfügt, der den Poster zunächst als Spammer klassifiziert. Ein Spam-Bot wird den Schalter einfach ignorieren oder den eingestellten Defaultwert übernehmen, was ihn anhand des beim Posten übermittelten Wertes des Radiobuttons als Spammer entlarvt. Falls ein menschlicher User den zusätzlichen Button übersieht, teilt ihm eine Fehlermeldung (Abbildung 3) mit, dass auf der vorhergehenden Seite noch ein Knopf umzustellen ist, damit das Posting durchgeht.

Abbildung 2: Ein neuer Schalter legt die Spammer aufs Kreuz.

Abbildung 3: Die Fehlermeldung, falls der User vergessen hat, von "Spammer" auf "User" umzustellen.

Eine weitere Möglichkeit ist das Erkennen von Comment-Spam anhand des Nachrichtentextes. Steht dort nur wenig Text, aber 15 Links, handelt es sich mit hoher Wahrscheinlichkeit um einen Spammer. Ein Problem stellen freilich die ``Fehler zweiter Art'' (falsche Positive) dar, also der Fall, dass eine als Spam eingestufte Nachricht tatsächlich von einem legitimen Benutzer stammt. Wandern solche Nachrichten unprüft in die Mülltonne, verärgert dies die User, die dann zu anderen Foren abwandern.

Gehirnschmalz zur Abwehr

Hält sich der Forumsverkehr in Grenzen, eignet sich die heute vorgestellte Moderation per Email zur Spam-Eindämmung. Das per Cronjob gestartete Skript posting-watcher fragt auf dem Forumsrechner regelmässig die Mysql-Datenbank des Forumssystems phpBB ab, entdeckt noch nicht gesichtete Einträge in der phpBB-Tabelle phpbb_posts und speichert deren IDs mitsamt einem generierten Zufallsschlüssel in einem lokalen Cache auf der Platte ab. Es schickt anschließend eine Email nach Abbildung 4 zum Moderator, die die wichtigsten Daten des Postings enthält.

Abbildung 4: Anhand dieser Email kann der Forumverwalter entscheiden, ob das Posting behalten oder gelöscht wird.

Abbildung 5: Der Inhalt des File-Cache, der die Zufallsschlüssel und die zugehörigen Post-IDs speichert.

Mails mit legitimen Postings ignoriert der Moderator einfach. Falls der Moderator ein Posting hingegen als Spam klassifiziert, betätigt er lediglich die ``Reply''-Funktion des Mail-Clients, worauf die Mail zurück zum ursprünglichen Skript geschickt wird. Dieses greift sich den in der Mail gespeicherten Zufallsschlüssel, ermittelt über den File-Cache das zugehörige Posting in der Datenbank, und setzt einen Scraper auf die Website an, um das Posting mittels der Web-Administrations-Schnittstelle der phpBB-Forumssoftware zu löschen.

Anschließend schickt es eine Bestätigung zurück an den Moderator. Abbildung 6 zeigt den gesamten Ablauf. Nach diesem Verfahren werden alle Postings (einschließich Spam) zunächst einmal von phpBB dargestellt, aber später ohne großen Aufwand gelöscht, falls sich ein Posting als Spam entpuppt. Da die Moderation über eine Schnittstelle angesprochen wird, die viele sowieso dauernd nutzen (Email), hält sich der Zusatzaufwand bei geringer Forumsaktivität in Grenzen.

Abbildung 6: Der Content-Spam-Killer kommuniziert per Email mit dem Moderator.

Sowohl der vom Cronjob regelmäßig aufgerufene Datenbank-Überwacher als auch der als Auftragskiller agierende Scraper werden vom gleichen Skript posting-watcher implementiert. Mit dem Parameter -c (check) aufgerufen, durchsucht es die Datenbank nach neuen Postings und schickt für jedes eine Email an den Moderator. Mit dem Kommandozeilenparameter -k (kill) hingegen erwartet das Skript eine Email auf STDIN, die einen Kill-Schlüssel enthält.

Warum enthält die Email nicht einfach die ID des Postings? Damit posting-watcher weiss, dass die Auftragskiller-Email vom Moderator (und nicht etwa von einem Scherzbold) kommt, legt es beim Absenden einen schwer zu erratenden Zufallsschlüssel an und speichert diesen sowohl in einer lokalen Tabelle (Abbildung 5) zusammen mit der ID des zu untersuchenden Postings. Kommt die Email vom Moderator zurück, extrahiert das Skript den Zufallsschlüssel und prüft anhand der Tabelle, ob dieser tatsächlich einem gerade moderierten Posting zugeordnet ist und leitet nur in diesem Fall die Löschung ein.

Der Name der Rose

Die Forumssoftware phpBB legt die Forumsdaten verteilt in 30 Mysql- Tabellen ab. Die Postings finden sich in der Tabelle phpbb_posts. Dort steht allerdings nicht direkt der Wortlaut des Nachrichtentextes. Vielmehr nutzt phpBB die Spalte post_id in der Tabelle phpbb_posts, um über diese einen Zusammenhang zwischen phpbb_posts und einer weiteren Tabelle phpbb_posts_text herzustellen und so dem Posting seinen Volltext zuzuordnen (Abb. 8).

Abbildung 7: Die Tabellen in der Datenbank der Forumssoftware phpBB

Abfragen, die zwei Tabellen auf diese Art verknüpfen, lassen sich mit etwas SQL leicht in den Griff bekommen. Allerdings sieht mit Perl gemixtes SQL nicht gerade schön aus und deswegen hat es sich in letzter Zeit immer mehr eingebürgert, objektorientierte Perl-Wrapper auf relationalen Datenbanken aufzusetzen.

Bei diesem Verfahren fragen Objektmethoden die Datenbankdaten ab führen auch Manipulationen durch. SQL bleibt draußen. In [5] kam im Snapshot schon einmal Class::DBI zum Einsatz, aber kürzlich kam mit Rose ein neues aufregendes Framework heraus, das nicht nur flexibler sondern sogar performanter arbeitet.

Das vom CPAN erhältliche Framework spricht Datenbanken über die Abstraktion Rose::DB an und greift mit Rose::DB::Object auf Tabellenzeilen zu.

Dabei braucht man, wie Listing PhpbbDB.pm zeigt, keineswegs die einzelnen Tabellenspalten manuell durchzugehen, um zur Klassendarstellung zu gelangen. Mit auto_initialize() durchforstet Rose Tabellen gängiger Datenbanksysteme (MySQL, Postgres, ...) selbständig und legt automatisch die notwendige Methoden für den Abstraktionslayer an.

Normalerweise verlinkt man Tabellen in relationalen Datenbanken über einen Fremdschlüssel in einer extra Spalte, aber phpBB implementiert dies recht eigenwillig mit zwei Primärschlüsseln, die jeweils post_id heißen (Abb. 8). Das ist schade, denn Rose verfügt über einen sogenannten Convention-Manager, der (ähnlich wie in Ruby on Rails) Tabellenbeziehungen anhand von Konventionen errät. Ist alles nach Standard angelegt, genügt es, Rose::DB::Object::Loader aufzurufen, das alles geradezu magisch erledigt.

Abbildung 8: Die ungewöhnliche Verlinkung zweier Tabellen in Forumsoftware phpBB.

Findet Rose eine Tabelle namens phpbb_posts (Mehrzahl), legt es eine Klasse PhpbbPost (Einzahl) an. Aus Tabellennamen mit Unterstrichen werden Klassennamen im sogenannten CamelCase. Den Aufruf von

   __PACKAGE__->meta->table('phpbb_topics');

kann sich PhpbbDB.pm so zum Beispiel sparen, da Rose den Tabellennamen 'phpbb_topics' automatisch aus dem Klassenamen PhpbbTopic per Konvention errät.

Fände es einen Fremdschlüssel post_text, würde es nach einer weiteren Tabelle post_texts suchen. Existiert diese, verlinkt Rose die beiden Klassen PhpbbPost und PostText mit einer ``many to one''-(N:1)-Beziehung. Zu beachten ist, dass Tabellennamen immer in der Mehrzahl sind (post_texts), während Spaltennamen für Fremdschlüssel in 1:1 und N:1-Beziehungen in der Einzahl (post_text) geschrieben werden.

Listing 1: PhpbbDB.pm

    01 package Phpbb::DB;
    02 ###########################################
    03 use base qw(Rose::DB);
    04 __PACKAGE__->use_private_registry();
    05 __PACKAGE__->register_db(
    06     driver =>   'mysql',
    07     database => 'forum_db',
    08     host     => 'forum.db.host.com',
    09     username => 'db_user',
    10     password => 'XXXXXX',
    11 );
    12 
    13 ###########################################
    14 package Phpbb::DB::Object;
    15 ###########################################
    16 use base qw(Rose::DB::Object);
    17 sub init_db { Phpbb::DB->new(); }
    18 
    19 ###########################################
    20 package PhpbbTopic;
    21 ###########################################
    22 use base "Phpbb::DB::Object";
    23 __PACKAGE__->meta->auto_initialize();
    24 
    25 ###########################################
    26 package PhpbbPostsText;
    27 ###########################################
    28 use base "Phpbb::DB::Object";
    29 __PACKAGE__->meta->table(
    30                        'phpbb_posts_text');
    31 __PACKAGE__->meta->auto_initialize();
    32 
    33 ###########################################
    34 package PhpbbPost;
    35 ###########################################
    36 use base "Phpbb::DB::Object";
    37 
    38 __PACKAGE__->meta->table('phpbb_posts');
    39 __PACKAGE__->meta->add_relationships(
    40   text => { 
    41     type       => "one to one",
    42     class      => "PhpbbPostsText",
    43     column_map => { post_id => 'post_id' },
    44   },
    45   topic => { 
    46     type       => "one to one",
    47     class      => "PhpbbTopic",
    48     column_map => { 
    49                   topic_id => 'topic_id' },
    50   }
    51 );
    52 
    53 __PACKAGE__->meta->auto_initialize();
    54 __PACKAGE__->meta->make_manager_class(
    55                    'phpbb_posts');
    56 
    57 1;

Entspricht das Datenbankschema einer Applikation allerdings nicht diesem Standard, kann man entweder den Convention-Manager anpassen oder Rose manuell unter die Arme greifen. So legt Listing PhpbbDB.pm ab Zeile 39 mit add_relationships die Beziehungen der Tabelle phpbb_posts zu den Tabellen phpbb_posts_text und phpbb_topics fest. Die Beziehung text (ab Zeile 40) stellt zum Beispiel eine ``one to one''-(1:1)-Verbindung von der Tabelle phpbb_posts zur Tabelle phpbb_posts_text her. ``One to one'' heisst hier, dass jede Reihe in phpbb_posts einer Reihe in phpbb_posts_text zugeordnet ist und umgekehrt. Entsprechend gibt es auch die Beziehungstypen "one to many" (1:N), "many to one" (N:1) und "many to many" (N:N). Wichtig ist, dass add_relationships vor dem Aufruf von auto_initialize() erfolgt, sonst ignoriert Rose die Beziehungen einfach kommentarlos (ein Bugfix ist hoffentlich bald verfügbar).

Mit dem so weitgehend automatisch angelegten Abstraktionswrapper lässt sich nun zum Beispiel in einem Objekt der Klasse PhpbbPost mit der Methode post_id() der Wert der Spalte id abfragen. Und wegen der vorher angelegten Beziehung genügt es, die Methodenkette text()->post_text() aufzurufen, um von einem Objekt der Klasse PhpbbPost zum zugehörigen Textstring des Postings zu gelangen, der in der Tabelle phpbb_posts_text liegt!

Eine ausführliche Beschreibung des objektorientierten Datenbankwrappers Rose und praktische Beispiele finden sich in der CPAN-Distribution des Moduls, das Tutorial ist unter [4] erhältlich.

Um SELECT-ähnliche Sammelabfragen auf eine abstrahierte Tabelle zuzulassen, wird die Klasse PhpbbPost in Zeile 54 von PhpbbDB.pm instruiert, die Klasse PhpbbPost::Manager zu erzeugen, deren Methode query() die Abfrage ausführt. Die Syntax für die Abfrage ist ein erstaunlich sauberes Konstrukt in reinem Perl (Zeile 73 in posting-watcher):

    [ post_id => { gt => $latest } ]

entspricht einem ``SELECT ... WHERE post_id > $latest''. Bei einer erfolgreichen Suche liefert die Methode get_phpbb_posts() eine Referenz auf einen Array zurück, der Treffer-Objekte vom Typ PhpbbPost enthält. Diese wiederum geben mittels den Methoden post_id(), text()->post_text() und topic()->topic_title() wegen der vorher definierten Tabellenbeziehungen die ID des Postings und dessen Titel und Text als Strings zurück. Das Skript posting-watcher nutzt sie, um die Email an den Moderator mit Inhalt zu füllen.

Den persistenten Cache, der die Posting-IDs den voher erwähnten Zufallsschlüsseln für die Emails zuordnet, implementiert das CPAN-Modul Cache::FileCache. Wie in Zeile 21 festgelegt, verfallen Einträge nach 14 Tagen und werden als akzeptiert gehandelt. Die Methode purge() (Zeile 28) räumt verfallene Cache-Einträge auf.

Die ID der zuletzt moderierten Nachricht speichert Zeile 92 einfach unter dem Schlüssel _latest im Cache, der eigentlich Post-IDs den voher erwähnten Zufallsschlüsseln für die Emails zuordnet. Die Funktion check() in posting-watcher holt die Nummer des zuletzt untersuchten Postings aus dem Cache und setzt eine SQL-Abfrage ab, die alle Postings mit neueren IDs liefert.

Die 32 Bytes langen Schlüsselstrings im Hexformat erzeugt die Funktion genkey() ab Zeile 96. Sie verwendet ein aus dem CPAN-Modul Apache::Session kopiertes Verfahren, das als Zufallsparameter die aktuelle Uhrzeit, eine Speicheradresse, eine Zufallszahl und die ID des laufenden Prozesses zweimal durch ein MD5-Hashing laufen lässt. So entsteht eine fast 100%ig eindeutiger und sehr schwer zu erratender Schlüssel. Wer ihn kennt, darf das ihm im Cache zugeordnete Posting aus dem Forum löschen. Das Skript posting-watcher schickt den Schlüssel an den Moderator, und wenn dieser ihn wieder zurück an das Skript schickt, startet dieses den Agenten, der das Posting durch simulierte Mausklicks auf der Admin-GUI des Forums aus diesem verschwinden lässt.

Listing 2: posting-watcher

    001 #!/usr/bin/perl -w
    002 use strict;
    003 use PhpbbDB;
    004 use Cache::FileCache;
    005 use Digest::MD5;
    006 use Mail::Mailer;
    007 use Mail::Internet;
    008 use Text::ASCIITable;
    009 use Text::Wrap qw(wrap);
    010 use Getopt::Std;
    011 use WWW::Mechanize::Pluggable;
    012 
    013 getopts("kc", \my %opts);
    014 
    015 my $FORUM_URL  = "http://foo.com/forum";
    016 my $FORUM_USER = "forum_user_id";
    017 my $FORUM_PASS = "XXXXXXXX";
    018 
    019 my $TO     = 'moderator@foo.com';
    020 my $REPL   = 'forumcleaner@foo.com';
    021 my $EXPIRE = 14*24*3600;
    022 
    023 my $cache = Cache::FileCache->new({
    024   cache_root => "$ENV{HOME}/phpbb-cache",
    025   namespace  => "phpbb-watcher",
    026 });
    027 
    028 $cache->purge();
    029 
    030 if($opts{k}) {
    031   my @data = <>;
    032   my $body = join '', @data;
    033   if($body =~ /\[delete-key (.*?)\]/) {
    034     my $id = kill_by_key($1);
    035     my $mail = Mail::Internet->new(\@data);
    036     if($mail) {
    037     my $reply = $mail->reply();
    038     $reply->body(
    039       ["Deleted posting $id.\n\n", @data]);
    040     $reply->send() or 
    041                    die "Reply mail failed";
    042   } 
    043     } 
    044 } elsif($opts{c}) {
    045   check();
    046 } else {
    047   die "Use -c or -k";
    048 }
    049 
    050 ###########################################
    051 sub kill_by_key {
    052 ###########################################
    053   my($key) = @_;
    054   my $id = $cache->get("key$key");
    055   if(defined $id) {
    056       msg_remove($id);
    057   } else {
    058       die "Invalid key $key";
    059   }
    060 
    061   return $id;
    062 }
    063 
    064 ###########################################
    065 sub check {
    066 ###########################################
    067   my $latest = $cache->get("_latest");
    068   $latest = -1 unless defined $latest;
    069 
    070   my $new_posts = 
    071     PhpbbPost::Manager->get_phpbb_posts(
    072       query => 
    073         [ post_id => { gt => $latest } ]
    074   );
    075     
    076   foreach my $p (@$new_posts) {
    077     my $id = $p->post_id();
    078     
    079     my $key = genkey();
    080     
    081     mail($id, format_post($id, 
    082          $p->text()->post_text(),
    083          $p->topic()->topic_title(),
    084          $key), $key
    085     );
    086     
    087       $cache->set("key$key", $id, $EXPIRE);
    088     
    089       $latest = $id;
    090     }
    091     
    092     $cache->set("_latest", $latest);
    093 }
    094 
    095 ###########################################
    096 sub genkey {
    097 ###########################################
    098   return Digest::MD5::md5_hex(
    099         Digest::MD5::md5_hex(
    100             time(). {}. rand(). $$));
    101 }
    102 
    103 ###########################################
    104 sub mail {
    105 ###########################################
    106   my($id, $body, $key) = @_;
    107 
    108   my $m = Mail::Mailer->new('sendmail');
    109 
    110   $m->open({
    111     To      => $TO,
    112     Subject => "Forum News (#$id) [delete-key $key]",
    113     From    => $REPL });
    114 
    115   print $m $body;
    116 }
    117 
    118 ###########################################
    119 sub format_post {
    120 ###########################################
    121   my($id, $text, $topic, $key) = @_;
    122 
    123   my $t = Text::ASCIITable->new(
    124                        {drawRowLine => 1});
    125 
    126   $t->setCols('Header', 'Content');
    127   $t->setColWidth("Header", 6);
    128 
    129   $Text::Wrap::columns=60;
    130 
    131   $text =~ s/[^[:print:]]/./g;
    132 
    133   $t->addRow('post', "#$id");
    134   $t->addRow('topic', $topic);
    135   $t->addRow('text', wrap("", "", $text));
    136   $t->addRow('key', "[delete-key $key]");
    137 
    138   return $t->draw();
    139 }
    140 
    141 ###########################################
    142 sub msg_remove {
    143 ###########################################
    144   my($post_id) = @_;
    145 
    146   my $mech = 
    147       WWW::Mechanize::Pluggable->new();
    148   $mech->get($FORUM_URL);
    149 
    150   $mech->phpbb_login($FORUM_USER, 
    151                      $FORUM_PASS);
    152   $mech->get(
    153     "$FORUM_URL/viewtopic.php?p=$post_id");
    154   $mech->phpbb_post_remove($post_id);
    155 }

ASCII-Art per Email

Damit der Moderator die Nachricht über ein neu eingeganges Posting schön formatiert erhält, presst die ab Zeile 119 definierte Funktion format_post ID, Titel und Text in die in Abbildung 4 gezeigten ASCII- Kästen. Diese erzeugt das CPAN-Modul Text::ASCIITable ohne großen Heckmeck. Da der Text auch Tabs und andere schwer druckbare Sonderzeichen enthalten kann, filtert sie der reguläre Ausdruck in Zeile 131 aus und ersetzt sie durch Punkte. Vor dem Einpressen des Texts wird dieser vom Modul Text::Wrap auf eine Zeilenbreite von 60 Zeichen formatiert. In der letzten ASCII-Tabellenzeile schließlich hängt format_post den geheimen Schlüssel an. Außerdem wird er der Subject:-Zeile der Email angehängt, so dass der Moderator nur den Reply-Knopf seines Mailclients drücken muss, um die Antwort mitsamt dem Schlüssel im Subject (Diesmal mit "Re: ...") an den Spammörder zu schicken.

Abbildung 9: Bestätigung der Löschung.

Auftragskiller kam vom CPAN

Die Funktion msg_remove ab Zeile 142 startet den Screen-Scraper WWW::Mechanize über den von meinem Yahoo-Kollegen Joe McMahon entworfenen Plugin- Mechanismus im CPAN-Modul WWW::Mechanize::Pluggable. Mit diesem Modul lassen sich einfach Plugin-Module bauen und aufs CPAN stellen, die WWW::Mechanize mit praktischen Befehlen erweitern. So fügt zum Beispiel WWW::Mechanize::Pluggable::Phpbb die Methoden phpbb_login und phpbb_post_remove in WWW::Mechanize ein, die den virtuellen Browserbenutzer als Administrator in ein Phpbb-Forum einloggen und die nötigen Knöpfe drücken, um ein mit seiner ID identifiziertes Posting zu tilgen. Anschließend schickt msg_remove eine Bestätigungs-Email an den Auftraggeber (Abbildung 9).

Installation

Damit die vom Moderator an den Unix-Account des Spammörders zurückgeschickten Emails an posting-watcher weitergeleitet werden, legt man eine .forward-Datei im Home-Verzeichnis mit folgendem Inhalt an:

    | /vollständiger/pfad/posting-watcher-kill.sh

Im ausführbaren Shellskript posting-watcher-kill.sh steht dann die vollständige Kommandozeile

    #!/bin/sh
    /home/mschilli/bin/posting-watcher -k

Grund für diesen Umstand: Die .forward-Datei kann keine Kommandozeilenoptionen aufzurufender Skripts verarbeiten.

Weiter sind die Zeilen 15 bis 21 von posting-watcher mit gültigen Emails, einer Forum-URL sowie mit dem Benutzernamen und dem Passwort des Forumsadministrators auf die individuellen Bedürfnisse anzupassen.

Der Cronjob, der die Forumsdatenbank alle 15 Minuten lang auf neue Einträge hin untersucht, wird mit crontab -e und dem Eintrag

    */15 * * * * /vollständiger/pfad/posting-watcher -c

aufgerufen. Wichtig ist, dass die Moduldatei PhpbbDB.pm vom Skript gefunden wird. Falls PhpbbDB.pm nicht bei den anderen Perl-Modulen steht, kann man posting-watcher mit

    use lib '/pfad/zum/modulverzeichnis';

auf das Modulverzeichnis hinweisen. Wer möchte, kann das Skript erweitern und es Postings, die bestimmten Kriterien genügen (zum Beispiel mehr als eine bestimmte Anzahl Links oder eindeutige Schlüsselwörter enthalten) auch ohne Rückfrage automatisch zu löschen. Kampf den Spammern!

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/09/Perl

[2]
``Comment Spam'', Benjamin Trott, http://www.sixapart.com/about/news/2003/10/comment_spam.html

[3]
Forumssoftware phpBB, http://phpbb.com

[4]
Das exzellente Tutorial zum Programmieren mit dem objekt-relationalen Datenbankwrapper Rose: http://search.cpan.org/dist/Rose-DB-Object/lib/Rose/DB/Object/Tutorial.pod

[5]
``Musik aus dem Keller'', Michael Schilli, http://www.linux-magazin.de/Artikel/ausgabe/2003/03/perl/perl.html, enthält ein Beispiel zum Thema Class::DBI.

Michael Schilli

arbeitet 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.