<?xml version="1.0" encoding="utf-8"?>

<rdf:RDF 
  xmlns="http://purl.org/rss/1.0/"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:admin="http://webns.net/mvcb/"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/"
> 

  <channel rdf:about="http://blog.bulknews.net/cookbook/blosxom">
    <title>Blog Developer's Cookbook</title>
    <link>http://blog.bulknews.net/cookbook/blosxom</link>
    <description>Cookbook About Programming Weblog Technologies</description>
    <language>ja</language>
    <dc:creator>Tatsuhiko Miyagawa (miyagawa@bulknews.net)</dc:creator>
    <dc:rights>Copyright Tatsuhiko Miyagawa</dc:rights>
    <admin:generatorAgent rdf:resource="http://www.raelity.org/apps/blosxom/?v=2.0rc5" />
    <admin:errorReportsTo rdf:resource="mailto:miyagawa@bulknews.net"/>

    <items>
      <rdf:Seq>
        <rdf:li rdf:resource="http://blog.bulknews.net/cookbook/blosxom/trackback/tb_rss.html" />

      </rdf:Seq>
    </items>


  </channel>
  <item rdf:about="http://blog.bulknews.net/cookbook/blosxom/trackback/tb_rss.html">
    <title>Trackback Ping 一覧を RSS 出力する</title>
    <link>http://blog.bulknews.net/cookbook/blosxom/trackback/tb_rss.html</link>
    <description>
受信した [[Trackback]] Ping の一覧を取得するために、RSS で Trackback 一覧を出力するという仕様があります.</description>
    <dc:subject>Trackback Pings</dc:subject>
    <dc:creator>Tatsuhiko Miyagawa</dc:creator>
    <dc:date>2003-11-28T01:36+09:00</dc:date>
    <trackback:ping rdf:resource="http://blog.bulknews.net/cookbook/trackback/trackback_tb_rss" />
    <content:encoded><![CDATA[
<p />
受信した <a href="http://blog.bulknews.net/cookbook/kwiki/kwiki.cgi?Trackback">Trackback</a> Ping の一覧を取得するために、RSS で Trackback 一覧を出力するという仕様があります。今回は、<a href="http://blog.bulknews.net/cookbook/blosxom/trackback/tb_receive.rss10">Trackback Ping Server</a> で作成したスクリプトに、RSS 出力機能を実装してみます。

<hr class="seemore">


<p />
<a href="http://www.movabletype.org/docs/mttrackback.html">Trackback 規格仕様書</a> によると、ある Trackback Ping URL に対して送信された Ping のリストは、Ping URL にクエリパラメータ <code>__mode=rss</code> を付加することによって、RSS データをレスポンスとして取得することができます。

<p />
例えば、Trackback Ping URL が
<p><blockquote class="inlineRaw">http://example.com/tb/1234<br />
</blockquote></p>
であれば、該当する RSS 取得 URL は
<p><blockquote class="inlineRaw">http://example.com/tb/1234?__mode=rss<br />
</blockquote></p>
となり、この URL に HTTP GET でリクエストを送信すると、

<p><pre class="inlineRaw">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;response&gt;
&lt;error&gt;0&lt;/error&gt;
&lt;rss version=&quot;0.91&quot;&gt;&lt;channel&gt;
&lt;title&gt;TrackBack Test&lt;/title&gt;
&lt;link&gt;http://example.com/tb/1234&lt;/link&gt;
&lt;description&gt;Description of the TrackBack item&lt;/description&gt;
&lt;item&gt;
&lt;title&gt;TrackBack Demo&lt;/title&gt;
&lt;link&gt;http://blog.example.com/permalink/&lt;/link&gt;
&lt;description&gt;Excerpt&lt;/description&gt;
&lt;/item&gt;
&lt;/channel&gt;
&lt;/rss&gt;&lt;/response&gt;
</pre></p>

のような XML が取得できます。よく見ると気付くと思いますが、この XML は純粋な RSS ではなく、Trackback レスポンスの中に RSS ノードを埋めこむ形になっています<a href="#note-1">(*1)</a>。


<h2>サンプルコード</h2>

<a href="http://blog.bulknews.net/cookbook/blosxom/trackback/tb_receive.rss10">Trackback Ping Server</a> に __mode=rss を実装したスクリプトは <a href="#listing-1">List 1</a> のようになります。前回との差分についてのみ解説します。

<p />
<pre class="scriptsSnippet">our $tb_prefix = &quot;Blog Developer's Trackback&quot;;
our $tb_link   = &quot;http://example.com/tb&quot;;
</pre>

RSS の channel 要素の title, link に使用する文字列や URL を指定しています。<code>$tb_link</code> の方は、このスクリプト 自体の URL にしておけばよいでしょう。

<p />
<pre class="scriptsSnippet">my $mode  = $query-&gt;param('__mode');
if ($mode &amp;&amp; $mode eq 'rss') {
    show_rss($query, $tb_id);
} else {
    receive_ping($query, $tb_id);
}
</pre>

CGI パラメータの <code>__mode</code> を取得し、 rss であれば RSS 出力、そうでなければ受信モードに移行します。

<p />
<pre class="scriptsSnippet">sub connect_db {
    my $tb_id = shift;
    my $is_existent = -e &quot;$dbdir/$tb_id.db&quot;;
    my @dsn = (&quot;dbi:SQLite:dbname=$dbdir/$tb_id.db&quot;, &quot;&quot;, &quot;&quot;);
    my $dbh = DBI-&gt;connect(@dsn, { RaiseError =&gt; 1,
                                   AutoCommit =&gt; 1, });

    # Create table &quot;tb&quot; if it's first time
    init_table($dbh) unless $is_existent;
    return $dbh;
}
</pre>

データベース接続部分を再利用できるよう、<code>connect_db</code> としてまとめてあります。

<p />
<pre class="scriptsSnippet">sub show_rss {
    my($query, $tb_id) = @_;
    my $dbh = connect_db($tb_id);
    my $sql = sprintf &quot;SELECT %s FROM tb ORDER BY timestamp DESC&quot;,
        join(&quot;,&quot;, @cols);
    my $sth = $dbh-&gt;prepare($sql);
    $sth-&gt;execute();
</pre>

まずはデータベースに接続し、SELECT 文を発行します。ここでもカラムの指定はパッケージ変数 <code>@cols</code> から生成しています。Trackback Ping は timestamp の新しい順にソートして取得します。

<p />
<pre class="scriptsSnippet">    my $rss = XML::RSS-&gt;new(version =&gt; 0.91);
    $rss-&gt;channel(
        title =&gt; &quot;$tb_prefix: $tb_id&quot;,
        link  =&gt; &quot;$tb_link/$tb_id&quot;,
        description =&gt; &quot;Trackback Discussion on $tb_id&quot;,
    );

    while (my $data = $sth-&gt;fetchrow_hashref) {
        $rss-&gt;add_item(
            title =&gt; $data-&gt;{title},
            link  =&gt; $data-&gt;{url},
        );
    }
    $sth-&gt;finish();
</pre>

XML::RSS オブジェクトを version 0.91 <a href="#note-2">(*2)</a> で new し、channel や item をセットします。出来上がった XML::RSS オブジェクトを <code>send_rss</code> に渡し、レスポンスを出力します。

<p />
<pre class="scriptsSnippet">sub send_rss {
    my($query, $rss) = @_;
    my $rss_node = hack_rss_node($rss-&gt;as_string);
    print $query-&gt;header('text/xml'), &lt;&lt;XML;
&lt;?xml encoding=&quot;utf-8&quot;?&gt;
&lt;response&gt;
  &lt;error&gt;0&lt;/error&gt;
  $rss_node
&lt;/response&gt;
XML
    ;
}
</pre>

RSS オブジェクトを <code>as_string</code> で文字列化して Trackback の response ノード内に埋め込みます。ここで XML::RSS を 0.91 で実行すると、
<p><blockquote class="inlineRaw">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;<br />
<br />
&lt;!DOCTYPE rss PUBLIC &quot;-//Netscape Communications//DTD RSS 0.91//EN&quot;<br />
            &quot;http://my.netscape.com/publish/formats/rss-0.91.dtd&quot;&gt;<br />
<br />
&lt;rss version=&quot;0.91&quot;&gt;<br />
...<br />
</blockquote></p>

のような XML 宣言や DTD 宣言が入ってしまい、邪魔になるため、<code>hack_rss_node</code> というサブルーチンでその部分を除去します<a href="#note-3">(*3)</a>。

<p />
<pre class="scriptsSnippet">sub hack_rss_node {
    my $rss = shift;
    $rss =~ s@&lt;\?xml .*?&gt;\n*@@s;
    $rss =~ s@&lt;!DOCTYPE rss .*?&gt;\n*@@s;
    return $rss;
}
</pre>

XML や DTD 宣言を strip し、余計な空行もとります。

<h2>実行例</h2>

いくつか Ping を送信した URL に対し、__mode=rss を付加してアクセスすると、

<p><pre class="inlineRaw">&lt;?xml encoding=&quot;utf-8&quot;?&gt;
&lt;response&gt;
  &lt;error&gt;0&lt;/error&gt;
  &lt;rss version=&quot;0.91&quot;&gt;

&lt;channel&gt;
&lt;title&gt;Blog Hacks Trackback: 1234&lt;/title&gt;
&lt;link&gt;http://example.com/tb/1234&lt;/link&gt;
&lt;description&gt;Trackback Discussion on 1234&lt;/description&gt;

&lt;item&gt;
&lt;title&gt;TEST&lt;/title&gt;
&lt;link&gt;http://blog.example.com/&lt;/link&gt;
&lt;/item&gt;

&lt;/channel&gt;
&lt;/rss&gt;
&lt;/response&gt;
</pre></p>

といった XML レスポンスが取得できます。

<h2>Hack the Hacks</h2>

このサンプルでは、存在しない Trackback ID をいれた場合でも、とりあえずつなぎにいって <code>init_table</code> でテーブルを生成してしまいます<a href="#note-4">(*4)</a>。ファイルが存在するかどうか最初にチェックして、なければつくらないようにした方がよりよいでしょう。

<p />

<a href="http://www.movabletype.org/docs/mttrackback.html">Trackback 仕様書</a>には、
<p><blockquote class="inlineRaw">In future revisions of the specification--once the grace period for switching to GET from POST has passed--this may be simplified such that sending a GET request to the TrackBack Ping URL will return the list of pings.<br />
</blockquote></p>

と記載されており、将来的には Ping の送信は POST, RSS の受信は GET という風にしようと考えているようです。ただ実際には、執筆時点ではこうした <a href="http://blog.bulknews.net/cookbook/kwiki/kwiki.cgi?Trackback">Trackback</a> 自体の拡張より、<a href="http://blog.bulknews.net/cookbook/kwiki/kwiki.cgi?Atom">Atom</a> API への統合の方が現実的なのかもしれません。


<h2>Listings</h2>
<div class="scriptsListings"><a name="listing-1"><span class="scripsListingHeader">List 1: tb_rss.pl</span></a>
<pre class="scriptsListing">#!/usr/local/bin/perl -w
# tb_rss - implements __mode=rss

use strict;
use CGI;
use DBI;
use HTML::Entities;
use Time::Piece;
use XML::RSS;

our $dbdir = &quot;/tmp&quot;;
our @cols  = qw(url title blog_name excerpt timestamp);
our $tb_prefix = &quot;Blog Developer's Trackback&quot;;
our $tb_link   = &quot;http://example.com/tb&quot;;

my $query = CGI-&gt;new();
   $query-&gt;charset('utf-8');

my $tb_id = munge_tb_id($query) or do {
    send_response(1 =&gt; &quot;Invalid Trackback ID&quot;);
    exit;
};

my $mode  = $query-&gt;param('__mode');
if ($mode &amp;&amp; $mode eq 'rss') {
    show_rss($query, $tb_id);
} else {
    receive_ping($query, $tb_id);
}

sub receive_ping {
    my($query, $tb_id) = @_;
    my $vars = $query-&gt;Vars();

    if (!$vars-&gt;{url}) {
        send_response(1 =&gt; &quot;No url parameter (required)&quot;);
        return;
    }

    fix_encoding($vars) if $vars-&gt;{charset};

    # Default values
    $vars-&gt;{title}     ||= $vars-&gt;{url};
    $vars-&gt;{blog_name} ||= do {
        require URI;
        URI-&gt;new($vars-&gt;{url})-&gt;host;
    };
    $vars-&gt;{excerpt} ||= &quot;No excerpt.&quot;;
    $vars-&gt;{timestamp} = Time::Piece-&gt;new-&gt;datetime;

    eval {
        store_ping($tb_id, $vars);
        send_response(0 =&gt; &quot;Thanks for your Ping to $tb_id&quot;);
    };

    if ($@) {
        send_response(1 =&gt; &quot;Error while storing ping: $@&quot;);
    }
}

sub fix_encoding {
    my $vars = shift;
    require Encode;
    Encode::from_to($vars-&gt;{$_},
                    $vars-&gt;{charset} =&gt; &quot;utf-8&quot;) for @cols;
}

sub store_ping {
    my($tb_id, $vars) = @_;
    my $dbh = connect_db($tb_id);

    # Generate SQL statements
    my $sql = sprintf &quot;INSERT INTO tb (%s) VALUES (%s)&quot;,
        join(&quot;,&quot;, @cols), join(&quot;,&quot;, ('?') x @cols);
    my $sth = $dbh-&gt;prepare($sql);
    $sth-&gt;execute(@{$vars}{@cols});
    $sth-&gt;finish();

    $dbh-&gt;disconnect();
}

sub connect_db {
    my $tb_id = shift;
    my $is_existent = -e &quot;$dbdir/$tb_id.db&quot;;
    my @dsn = (&quot;dbi:SQLite:dbname=$dbdir/$tb_id.db&quot;, &quot;&quot;, &quot;&quot;);
    my $dbh = DBI-&gt;connect(@dsn, { RaiseError =&gt; 1,
                                   AutoCommit =&gt; 1, });

    # Create table &quot;tb&quot; if it's first time
    init_table($dbh) unless $is_existent;
    return $dbh;
}

sub init_table {
    my $dbh = shift;
    $dbh-&gt;do(&lt;&lt;SQL);
CREATE TABLE tb (
  url    VARCHAR(255) NOT NULL,
  title  VARCHAR(255) NOT NULL,
  blog_name VARCHAR(255) NOT NULL,
  excerpt TEXT,
  timestamp DATETIME
)
SQL
    ;
}

sub show_rss {
    my($query, $tb_id) = @_;
    my $dbh = connect_db($tb_id);
    my $sql = sprintf &quot;SELECT %s FROM tb ORDER BY timestamp DESC&quot;,
        join(&quot;,&quot;, @cols);
    my $sth = $dbh-&gt;prepare($sql);
    $sth-&gt;execute();

    my $rss = XML::RSS-&gt;new(version =&gt; 0.91);
    $rss-&gt;channel(
        title =&gt; &quot;$tb_prefix: $tb_id&quot;,
        link  =&gt; &quot;$tb_link/$tb_id&quot;,
        description =&gt; &quot;Trackback Discussion on $tb_id&quot;,
    );

    while (my $data = $sth-&gt;fetchrow_hashref) {
        $rss-&gt;add_item(
            title =&gt; $data-&gt;{title},
            link  =&gt; $data-&gt;{url},
        );
    }
    $sth-&gt;finish();
    send_rss($query, $rss);
}

sub munge_tb_id {
    my $query = shift;
    my $path_info = $query-&gt;path_info();
    $path_info &amp;&amp; $path_info =~ m!(\w+)! &amp;&amp; return $1;
    return;
}

sub send_response {
    my($error, $message, $no_escape) = @_;
    my $msg = $no_escape ? $message : encode_entities($message);
    print $query-&gt;header('text/xml'), &lt;&lt;XML;
&lt;?xml encoding=&quot;utf-8&quot;?&gt;
&lt;response&gt;
  &lt;error&gt;$error&lt;/error&gt;
  &lt;message&gt;$msg&lt;/message&gt;
&lt;/response&gt;
XML
    ;
}

sub send_rss {
    my($query, $rss) = @_;
    my $rss_node = hack_rss_node($rss-&gt;as_string);
    print $query-&gt;header('text/xml'), &lt;&lt;XML;
&lt;?xml encoding=&quot;utf-8&quot;?&gt;
&lt;response&gt;
  &lt;error&gt;0&lt;/error&gt;
  $rss_node
&lt;/response&gt;
XML
    ;
}

sub hack_rss_node {
    my $rss = shift;
    $rss =~ s@&lt;\?xml .*?&gt;\n*@@s;
    $rss =~ s@&lt;!DOCTYPE rss .*?&gt;\n*@@s;
    return $rss;
}
</pre>
</div>
<div class="footnote"><a href="#note-1" name="note-1">*1)</a> REST の対称性からこういう仕様にしたのだとは思いますが、はっきり言って不便です...<br />
<a href="#note-2" name="note-2">*2)</a> Trackback 仕様書や <a href="http://blog.bulknews.net/cookbook/kwiki/kwiki.cgi?MovableType">MovableType</a> での実装での RSS version が 0.91 となっているため、ここでも 0.91 としました。明確な規定はないようです。<br />
<a href="#note-3" name="note-3">*3)</a> 名前からわかるように非常にハック的なので、あまり参考にしない方がよいでしょう ;-)<br />
<a href="#note-4" name="note-4">*4)</a> どういったエントリが存在するかがわからないので、Blog ツールに依存しないスタンドアロン実装としてはしょうがないところもありますが。<br />
</div>
]]></content:encoded>
  </item>


</rdf:RDF>

