[PHP]xml_set_processing_instruction_handlerとは?XML処理命令を検知する方法を徹底解説

PHP

はじめに

これまでの記事で、要素・テキスト・名前空間宣言・DTD関連など、XML文書のさまざまな構成要素を検知するハンドラ関数を数多く解説してきました。今回取り上げる xml_set_processing_instruction_handler() は、**処理命令(Processing Instruction、略してPI)**と呼ばれる特殊な構文を検知するための関数です。

処理命令とは、<?xml-stylesheet type="text/xsl" href="style.xsl"?> のような <? ... ?> の形式で記述される、XML文書の解析対象アプリケーションに向けた「指示」です。最も身近な例は、XML文書の先頭にある <?xml version="1.0" encoding="UTF-8"?> というXML宣言(ただし、これは技術的には処理命令とは別の特別な構文として扱われます)と混同されがちですが、それ以外にも xml-stylesheet のように、CSSやXSLTスタイルシートの適用を指示するために使われることがあります。本記事では、この関数の基本的な使い方から、実践的な活用例まで詳しく解説します。


関数概要

項目内容
関数名xml_set_processing_instruction_handler()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_set_processing_instruction_handler(XMLParser $parser, ?callable $handler): true
引数1$parserxml_parser_create()等で生成したパーサーインスタンス
引数2$handler — 処理命令検知時に呼び出すコールバック(nullで解除)
コールバックの引数(XMLParser $parser, string $target, string $data)
戻り値常に true
対応バージョンPHP 4以降
検知対象<?target data?> 形式の処理命令(先頭のXML宣言自体は対象外)

処理命令の全体像(イメージ図)

  XML文書
  <?xml version="1.0" encoding="UTF-8"?>  ← これはXML宣言(特別扱い、対象外)
  <?xml-stylesheet type="text/xsl" href="style.xsl"?>  ← これが処理命令
  <root>
    <?custom-instruction key="value"?>    ← これも処理命令
    <item>value</item>
  </root>
              │
              ▼
        xml_parse()による解析
              │
              │ "<?xml-stylesheet ...?>" を検知
              ▼
  ┌───────────────────────────────┐
  │ xml_set_processing_instruction_handler() │ ★この記事の対象
  │  → target="xml-stylesheet"                │
  │    data='type="text/xsl" href="style.xsl"'│
  └───────────────────────────────┘

ポイントは、処理命令が target(命令の対象・種類)と data(命令の内容、自由形式のテキスト)という2つの部分から構成されているという点です。data 部分は属性のような構造化された形式に見えることもありますが、パーサーは単なる文字列として渡すため、必要であれば呼び出し側で独自に解析する必要があります。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicPiHandlerDemo
{
    public function detectInstructions(string $xml): array
    {
        $instructions = [];
        $parser = xml_parser_create();

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target, $data) use (&$instructions) {
                $instructions[] = ['target' => $target, 'data' => $data];
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $instructions;
    }
}

$demo = new BasicPiHandlerDemo();
print_r($demo->detectInstructions('<?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>'));

例2:xml-stylesheet処理命令からスタイルシート情報を抽出するクラス

<?php

class StylesheetReferenceExtractor
{
    /**
     * xml-stylesheet処理命令のdata部分(属性のような形式)を
     * 独自に解析して構造化データとして取り出す
     */
    public function extractStylesheets(string $xml): array
    {
        $stylesheets = [];
        $parser = xml_parser_create();

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target, $data) use (&$stylesheets) {
                if ($target !== 'xml-stylesheet') {
                    return;
                }

                // data部分から属性風の値を正規表現で抽出する簡易パーサー
                preg_match_all('/(\w+)="([^"]*)"/', $data, $matches, PREG_SET_ORDER);
                $attrs = [];
                foreach ($matches as $match) {
                    $attrs[$match[1]] = $match[2];
                }
                $stylesheets[] = $attrs;
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $stylesheets;
    }
}

$extractor = new StylesheetReferenceExtractor();
print_r($extractor->extractStylesheets('<?xml-stylesheet type="text/css" href="main.css"?><root/>'));

例3:独自の処理命令を使ったテンプレートエンジンの簡易実装

<?php

class CustomPiTemplateEngine
{
    /**
     * "<?include file="header.xml"?>" のような
     * 独自の処理命令をアプリケーション固有の指示として解釈する
     */
    public function findIncludeDirectives(string $xml): array
    {
        $includes = [];
        $parser = xml_parser_create();

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target, $data) use (&$includes) {
                if ($target === 'include' && preg_match('/file="([^"]+)"/', $data, $m)) {
                    $includes[] = $m[1];
                }
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $includes;
    }
}

$engine = new CustomPiTemplateEngine();
$xml = '<root><?include file="header.xml"?><content/><?include file="footer.xml"?></root>';
print_r($engine->findIncludeDirectives($xml));

例4:処理命令の出現位置(行番号)を記録するデバッグツール

<?php

class ProcessingInstructionLocator
{
    /**
     * 処理命令が文書のどの行に存在するかを、
     * xml_get_current_line_number()と組み合わせて記録する
     */
    public function locateAll(string $xml): array
    {
        $locations = [];
        $parser = xml_parser_create();

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target, $data) use (&$locations) {
                $locations[] = [
                    'target' => $target,
                    'line'   => xml_get_current_line_number($p),
                ];
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $locations;
    }
}

$locator = new ProcessingInstructionLocator();
$xml = "<root>\n  <?custom-pi data?>\n  <item/>\n</root>";
print_r($locator->locateAll($xml));

例5:許可されていない処理命令をセキュリティ観点で検出するツール

<?php

class UnexpectedPiDetector
{
    private array $allowedTargets;

    public function __construct(array $allowedTargets)
    {
        $this->allowedTargets = $allowedTargets;
    }

    /**
     * 想定していない未知の処理命令が含まれていないかを検証する
     * (外部から受け取るXMLの内容チェックなどに利用できる)
     */
    public function detectUnexpected(string $xml): array
    {
        $unexpected = [];
        $parser = xml_parser_create();

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target) use (&$unexpected) {
                if (!in_array($target, $this->allowedTargets, true)) {
                    $unexpected[] = $target;
                }
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $unexpected;
    }
}

$detector = new UnexpectedPiDetector(['xml-stylesheet']);
print_r($detector->detectUnexpected('<root><?unknown-instruction data?><item/></root>'));

例6:複数の異なるターゲットを持つ処理命令を分類するクラス

<?php

class PiClassifier
{
    /**
     * 複数種類の処理命令が混在するXMLから、
     * target名ごとにグループ化して集計する
     */
    public function classify(string $xml): array
    {
        $grouped = [];
        $parser = xml_parser_create();

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target, $data) use (&$grouped) {
                $grouped[$target][] = $data;
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $grouped;
    }
}

$classifier = new PiClassifier();
$xml = '<root><?css style1?><?css style2?><?script src="a.js"?></root>';
print_r($classifier->classify($xml));

例7:処理命令を保持したまま元のXMLに近い形で再構築するクラス

<?php

class PiPreservingReconstructor
{
    private string $output = '';

    /**
     * 要素ハンドラと組み合わせて、
     * 処理命令部分も含めた形でXMLを再構築する
     */
    public function reconstruct(string $xml): string
    {
        $this->output = '';
        $parser = xml_parser_create();

        xml_set_element_handler(
            $parser,
            function ($p, $name) { $this->output .= "<{$name}>"; },
            function ($p, $name) { $this->output .= "</{$name}>"; }
        );

        xml_set_processing_instruction_handler(
            $parser,
            function ($p, $target, $data) {
                $this->output .= "<?{$target} {$data}?>";
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $this->output;
    }
}

$reconstructor = new PiPreservingReconstructor();
echo $reconstructor->reconstruct('<root><?custom-pi data="1"?><item/></root>') . PHP_EOL;

関連関数との比較

関数役割xml_set_processing_instruction_handlerとの違い
xml_set_processing_instruction_handler()処理命令(<? ... ?>)を検知本記事の対象。target/dataの2要素を扱う
xml_set_comment_handler()XMLコメント(<!-- -->)を検知構文的に似ているが、処理命令とコメントは意味的に異なる
xml_set_element_handler()要素の開始・終了を検知通常の要素タグを対象とし、処理命令とは別のイベント種別
xml_set_default_handler()未処理データ全般を捕捉処理命令用のハンドラが未登録の場合、こちらに流れることがある
DOMDocument::getElementsByTagName()DOM方式でのXML操作DOMProcessingInstructionクラスを介した処理命令へのアクセスが可能

よくある落とし穴(注意点)

  1. XML宣言(<?xml version="1.0"?>)は処理命令として扱われない 文書の先頭にあるXML宣言は、構文上は処理命令に似ていますが、XML仕様上は特別な構文として区別されており、xml_set_processing_instruction_handler() では検知されません。
  2. data 部分は構造化されていない生の文字列である xml-stylesheet のように属性風の記述(type="text/xsl" href="style.xsl")がされていても、パーサーはこれを単なる文字列として渡します。構造化データとして扱いたい場合は、呼び出し側で正規表現などによる独自のパース処理が必要です(例2を参照)。
  3. 独自の処理命令を使う場合、target名の衝突に注意する アプリケーション固有の処理命令(<?include ...?>など)を定義する場合、他のツールやライブラリが使う可能性のある一般的な名前との衝突を避けるため、十分にユニークなtarget名を選ぶことが望ましいです。
  4. 信頼できない外部XMLに含まれる処理命令の扱いに注意する 処理命令自体が直接コード実行を引き起こすことは通常ありませんが、アプリケーション側でその内容(data)を無検証のまま危険な処理(ファイルパスの解決やコマンド実行など)に使うと、間接的な脆弱性につながる可能性があります。外部から受け取ったXMLの処理命令は、常に検証してから利用しましょう(例5を参照)。
  5. PIハンドラを登録しない場合、default_handlerに流れることがある 専用のハンドラを登録していない状態で処理命令に遭遇すると、xml_set_default_handler() が登録されていればそちらにデータが渡されます。両方のハンドラを併用する場合は、この優先順位の関係を意識しましょう。

まとめ

観点まとめ
何をする関数かXML文書中の処理命令(<?target data?>)を検知するコールバックを登録する
主な用途スタイルシート参照の抽出、アプリケーション固有の独自命令の解釈、文書構造の完全な再構築
コールバックの引数target(命令の種類)とdata(自由形式の内容文字列)の2つ
XML宣言との違い文書先頭のXML宣言(<?xml version="1.0"?>)はこのハンドラの対象外
注意点dataが非構造化の生文字列であること、target名の衝突回避、外部XMLの処理命令の検証

xml_set_processing_instruction_handler() は、XML文書に埋め込まれた「アプリケーションへの指示」を検知するための専門的な関数です。標準的な xml-stylesheet の解析から、独自のテンプレートディレクティブの実装まで、処理命令という仕組みを活用したさまざまな応用が可能です。

タイトルとURLをコピーしました