libxml2.7.0以降で、実態参照が消失する不具合があり、これを使ってコンパイルされたPHPでは、XMLをパースする際に問題になる。

■実態参照とは

XML文書内で直接記述できない文字や記号(たとえば、 “<” や “>” など)を表記する際に用いられる記述方法です。以下のようなものがある。

  • 「&」 ・・・ 「&amp;」
  • 「< 」・・・ 「&lt;」
  • 「> 」・・・ 「&gt;」
  • 「” 」・・・ 「&quot;」
  • 「’ 」・・・ 「&apos;」

XML内の、<description>タグ、<content>タグなどの中にHTMLを記述する場合、多くの場合以下のような記述を用いる。

<description>
<![CDATA[
<h1>タイトル</h1>
<p>文章がここにはいる</p>
]]>
</description>

このようにしてCDATAセクションを使うことで、実態参照を用いずに記述することが可能である。しかし、CDATAを使わずに実態参照を用いて記述されているXMLも数多く存在する。

<description>
&lt;h1&gt;タイトル&lt;/h1&gt;
&lt;p&gt;文章がここにはいる&lt;/p&gt;
</description>

このようになる。

■実態参照が使われたXMLをパースする際の問題

こういったXML文書をlibxml2.7.0以降を使ったPHP関数でパースすると、実態参照だけが消失し、以下のような見るに堪えない結果となる。

h1タイトル/h1
p文章がここにはいる/p

さくらインターネットのレンタルサーバでは、各種ミドルウェアのバージョンアップが早く、libxml2.7.2などがすでに導入されており、そのサーバを利用している場合、RSSやXMLから情報収集しているプログラムで不具合が生じることになるだろう。

■対処方法

専用サーバであれば、libxmlのバージョンを2.6.30に落とすなどして対処できるが、そうでない場合には別の対処が必要となるだろう。考えられるひとつの方法は、XML内の実態参照をデコードし、CDATAを使った記述に書き換えた上でパースすることだ。以下にPHPでそれを行うための関数を示す。

function insertCDATA( $xml_str )
{
$target_tags = array( ‘description’, ‘content’, ‘content:encoded’ );
foreach ( $target_tags as $tag ) {
$ent = false;
if ( preg_match_all( ‘/<‘.$tag.'[^>]*>(.*?)<\/’.$tag.’>/ims’, $xml_str, $matches ) ) {
if ( is_array( $matches[1] ) && count( $matches[1] ) ) {
foreach ( $matches[1] as $m ) {
if ( !preg_match( ‘/^<!\[CDATA\[/im’, $m ) && preg_match( ‘/&gt;|&lt;|&amp;|&quot;|&apos;/ims’, $m ) ) {
$ent = true;
break;
}
}
}
if ( !$ent ) {
continue;
}
$encode = mb_detect_encoding( $xml_str );
$xml_str = preg_replace( ‘/(<‘.$tag.'[^>]*>)(?!\s*<!\[CDATA\[)(.*)(<\/’.$tag.’>)/Ueims’,
‘”\\1<![CDATA[“.html_entity_decode(“\\2″,ENT_COMPAT,”$encode”).”]]>\\3″‘, $xml_str );
}
}
return $xml_str;
}

function insertCDATA( $xml_str )
{
$target_tags = array( ‘description’, ‘content’, ‘content:encoded’ );
foreach ( $target_tags as $tag ) {
$ent = false;
if ( preg_match_all( ‘/<‘.$tag.'[^>]*>(.*?)<\/’.$tag.’>/ims’, $xml_str, $matches ) ) {
if ( is_array( $matches[1] ) && count( $matches[1] ) ) {
foreach ( $matches[1] as $m ) {
if ( !preg_match( ‘/^<!\[CDATA\[/im’, $m ) &&
preg_match( ‘/&gt;|&lt;|&amp;|&quot;|&apos;/ims’, $m ) ) {
$ent = true;
break;
}
}
}
if ( !$ent ) { continue; }
$encode = mb_detect_encoding( $xml_str );
$xml_str = preg_replace(
‘/(<‘.$tag.'[^>]*>)(?!\s*<!\[CDATA\[)(.*)(<\/’.$tag.’>)/Ueims’,
‘”\\1<![CDATA[“.html_entity_decode(“\\2″,ENT_COMPAT,”$encode”).”]]>\\3″‘, $xml_str );
}
}
return $xml_str;
}

この関数にXML文字列を渡すと、<description>タグ、<content>タグ、<content:enceded>タグを調べて、そこに実態参照があれば、CDATAによる記述に書き換えて返してくれます。

RSSをパースする際に、MagpieRSS(http://magpierss.sourceforge.net/)をご利用の方も多いだろう。このライブラリもlibxml2.7.xの影響を受ける。その場合、rss_fetch.incを開き、上記の関数を書き加えたうえで、_response_to_rss関数のはじめの部分を以下のように書き換えてやるといい。

/*===============================================*\
Function:   _response_to_rss
Purpose:    parse an HTTP response object into an RSS object
Input:      an HTTP response object (see Snoopy)
Output:     parsed RSS object (see rss_parse)
\*===============================================*/

function _response_to_rss ($resp) {
/* ++++++++++++++++++++ Simple Eye changed +++++++++++++++++++ */
//    $rss = new MagpieRSS( insertCDATA($resp->results), MAGPIE_OUTPUT_ENCODING, MAGPIE_INPUT_ENCODING, MAGPIE_DETECT_ENCODING );
/* ++++++++++++++++++++ Simple Eye changed +++++++++++++++++++ */
$rss = new MagpieRSS( $resp->results, MAGPIE_OUTPUT_ENCODING, MAGPIE_INPUT_ENCODING, MAGPIE_DETECT_ENCODING );

・・・・・・・・つづく・・・・・・・・・