SHS Textpatter Plugins

Webmention

Permalink

Aller guten Dinge sind drei. Drei mal habe ich von Webmention gehört oder gelesen, jedes mal dachte ich: “Cool, einfach, praktikabel – git’s das auch für Textpattern

Die Antwort lautete: Nein. Damit habe ich mich beim ersten mal abgefunden, mich beim zweiten mal etwas geärgert und es beim dritten mal geändert.

Webmention?

Bevor ich aber weiter ins Detail gehe, erst mal die Grundlagen. Was ist eigentlich Webmention?

Es handelt sich dabei, ähnlich zu Trackbacks und Pingbacks, um eine Technik die den Verfasser einer Webseite darüber informiert das diese verlinkt wurde. Ein Autor kann also erfahren wenn er referenziert wurde.

Im Gegensatz zu Pingbacks wird auf den Einsatz von XML-RPC zur Kommunikation verzichtet. Stattdessen findet der Informationsaustausch über simple HTTP-Post-Requests statt.

Eine weitere Unterscheidung zwischen beiden Techniken liegt in der vorgesehenen Verarbeitung der verlinkenden Seite. Diese kann nämlich, durch Mikroformate angereichert, Informationen über die Art der Referenzierung (Antwort, Like oder inhaltsgleiche Wiederholung) den Autor und den Inhalt bereitstellen.

Textpattern?

Textpattern ist das CMS-System mit dem ich diesen Blog betreibe. Ich weiß, es ist nicht das verbreitetste System aber ich mag es.

Textmansion

Textmansion habe ich das mein Plugin getauft das Webmention für Textpattern realisiert. Gut, im Moment ist nur der Receiver und das Discovery realisiert. Das versenden von Webmentions muss noch gänzlich manuell erfolgen.

Discovery

Die URL des Empfängers, auch Endpoint genannt, muss dem Sender einer Webmention irgendwie bekannt gemacht werden. Dazu wird in jede Seite die das Ziel eines Webmentions sein könnte ein Link-Element eingebaut das auf diese URL verweist

<link rel="webmention" href="http://human-injection.de/webmention.php" />

Um diesen Schritt so einfach wie möglich zu machen definiert das Plugin ein Tag <txp:shs_webmention_discovery />. Diese Tag erzeugt, im HEAD-Bereich des Templates eingesetzt, das gewünschte Element.

function shs_webmention_discovery($atts) {
  global $prefs;
  header('Link:<http://'.$prefs['siteurl'].'/webmention.php>; 
          rel="webmention"', false);
  $returnvalue .= '<link rel="webmention" href="http://'.$prefs['siteurl'].'/webmention.php" />';
  return $returnvalue;
}

Receiver

Der Empfänger ist etwas umfangreicher. Der erste Schritt besteht darin eine URL für den Receiver zu reservieren.

Dazu klinke ich mich in den Verarbeitungsprozess von Textpattern ein:

register_callback('shs_webmention_receive','textpattern');

Jedes mal wen ein HTTP-Request Textpattern erreicht wird das Event textpattern ausgelöst. Durch die obige Zeile wird dann auch jedes Mal meine Funktion shs_webmention ausgeführt. In dieser Methode wird zu erst geprüft ob das Plugin zuständig ist. Dies geschieht in der Funktion…

function shs_webmention_receiver_called() {
  global $pretext;

  $uri = $pretext['request_uri'];
  $uri = explode('?',$uri);
  $uri = explode('/',$uri[0]);
  $uri = array_reverse($uri);
  # check if url is relevant for the receiver
  if(in_array($uri[0],array('webmention.php'))) {
    return true;
  }
  # else
  return false;
}

Hierdurch wird jede URL die auf webmention.php endet durch mein Plugin verarbeitet.

Im zweiten Schritt werden die Parameter mit Ziel- und Sender-URL auf Existenz geprüft…

$values['targetUrl'] = gps('target');
$values['sourceUrl'] = gps('source');
if ($values['sourceUrl'] == '' || $values['targetUrl'] == '') {
  header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
  exit();
}

…und ein paar Informationen zusammengetragen. Das Markup der Quelle wird geladen (shs_getSource()), die ID des Artikels auf die sich die Webmention bezieht wird bestimmt (shs_getArticleId()), ein eventuell vorhandener Kommentar mit gleicher Quelle und gleichem Ziel wird gesucht (shs_getCommentId()) und der neue Kommentar zusammengesetzt.

Um Spam zu vermeiden wird untersucht ob die Quelle auch wirklich einen Link enthält der zum Ziel führt. Dazu verwende ich einen regulären Ausdruck in den ich die Target-URL einsetze:

preg_match_all(
  '/<a[^>]*href=["|\']'
  .str_replace(
    array('.', '/')
    , array('\.', '\/')
    , $values['targetUrl']
  )
  .'["|\'][^>]*>/'
  , $sourceContent
  , $links
)

Hierdurch erhalte ich auch gleich das A-Tag in der Variable $links und kann so die Art der Verlinkung bestimmen. Hierzu wende ich wiederum reguläre Ausdrücke an um die Werte der Attribute rel und class zu überprüfen. Hier zum Beispiel für Antworten, angegeben durch die Attributwerte u-in-reply-to oder in-reply-to:

preg_match_all(
  '/(rel|class)=["|\']((\w|-)*\s?)*(u-)?in-reply-to(\s?(\w|-)*)*["|\']/'
  , $links[0][$i]
  , $rel
)

Basierend auf dem Ergebnis dieser Überprüfung wird dann ein Text für den Kommentar erzeugt der die Webmention repräsentieren soll.

Zum Schluss wird dieser Kommentar durch die Funktion shs_saveComment() in der Datenbank gespeichert.

Bestimmen des Ziels

Um herauszufinden welcher Artikel von der Webmention betroffen ist muss die übergebene URL untersucht werden. Dazu extrahiere ich den letzten Teil des Pfades aus der URL und suche einen Artikel mit passender “benutzerdefinierter URL” (Datenbankspalte url_title) aus der Datenbank:

function shs_getArticleId($targetUrl, $default) {
	$articleUrlTitle = explode('?', $targetUrl);
	$articleUrlTitle = explode('/', $articleUrlTitle[0]);
	$articleUrlTitle = array_reverse($articleUrlTitle);
	$detectedParent = safe_row( "ID", "textpattern", "url_title='".$articleUrlTitle[0]."'", false );
	if ($detectedParent != false) {
		return $detectedParent["ID"];
	} else {
		return $default;
	}
}

Wenn auf diese Weise kein passender Artikel gefunden wurde verwende ich eine festgelegte ID ($default) um keine Informationen zu verlieren. Auf diese weise kann ich auch Mentions erfassen die sich auf eine Artikelliste beziehen. Diesen können nämlich in Textpattern keine Kommentare zugeordnet werden.

Vermeiden von Dubletten

weiter oben bin ich recht schnell über die Funktion shs_getCommendId() hinweggegangen. Die Funktion überprüft anhand der Quelle und dem Ziel ob bereits ein entsprechender Kommentar vorliegt.

function shs_getCommentId($sourceUrl, $parentId) {
  $detectedComment = safe_row( "discussid", "txp_discuss", "parentid='".$parentId."' AND web='".$sourceUrl."' AND message LIKE '<div class=\"webmention\">%'", false );
  if ($detectedComment != false) {
    return $detectedComment["discussid"];
  } else {
    return -1;
  }
}

Dies geschieht um doppelte Einträge zu verhindern und gleichzeitig das Aktualisieren von Webmentions zu ermöglichen. Wenn diese Funktion die ID eines bestehenden Kommentars zurückliefert wird beim Speichern ein Update durchgeführt anstelle eines Insert-Befehls.

Schlussbemerkung

Den vollständigen Sourcecode habe ich auf GitHub veröffentlicht. Wer also Interesse hat oder Verbesserungen beisteuern möchte ist herzlich eingeladen sich zu beteiligen.

Funktionen die ich selbst noch angedacht habe sind ein Sender von Webmentions, die Auswertung von Mikroformaten in der Verlinkenden Seite und eine Konfigurationsoberfläche für verschiedene Parameter.