2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

XML + XSLのファイルをモダンブラウザで表示するバッチファイル

Posted at

今時XMLファイルを触る人は多くないかもしれませんが、eGovなどから発行される公文書はXML形式です。
Internet Explorerを前提で作られているため、セキュリティが強固になったモダンブラウザでは開けません。
EdgeのIEモードを有効にする手順が複雑化したこともあり、簡単にXMLを閲覧するプログラムを作ろうと思い立って実現したので共有します。

成果物

XMLファイルをダブルクリックしてブラウザで開くことが可能になります。

使っている様子.gif

ソースコード

必ずShift-JISで保存してください。 (メモ帳ならANSIかも)

XMLディスペンサー.bat
@echo off
rem ブラウザのセキュリティ制限により、ローカルのXSLファイルは読み込めません。
rem このバッチファイルは、簡易HTTPサーバーを立ててファイルを提供することで、その制限を回避します。

setlocal
set "TARGET_FILE=%~1"
set "WAIT_MILLISECONDS=1000"

rem --- 設定項目 ---
rem 1: 1つのXSL送信後に自動で閉じる(デフォルト) / 0: 手動で閉じるまで待機
set "AUTO_CLOSE=1"
rem 1: XMLと違う階層にあるXSLも許可する / 0: 同じ階層のみ許可(デフォルト)
set "ALLOW_EXTERNAL_XSL=0"
rem ---------------

if "%TARGET_FILE%"=="" (
    echo XMLファイルをこのアイコンにドラッグ&ドロップしてください。
    pause
    exit /b
)

powershell -NoProfile -ExecutionPolicy Bypass -Command ^
    "$targetFull = [System.IO.Path]::GetFullPath('%TARGET_FILE%');" ^
    "$targetDir = [System.IO.Path]::GetDirectoryName($targetFull);" ^
    "$port = 8000;" ^
    "while ($true) {" ^
    "    $connection = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port);" ^
    "    try { $connection.Start(); $connection.Stop(); break; } catch { $port++ }" ^
    "    if ($port -gt 9000) { Write-Host '空きポートが見つかりませんでした。'; pause; exit }" ^
    "}" ^
    "$url = \"http://localhost:$port/$($targetFull.Replace('\','/'))\";" ^
    "Start-Process $url;" ^
    "$listener = New-Object System.Net.HttpListener;" ^
    "$listener.Prefixes.Add(\"http://localhost:$port/\");" ^
    "$listener.Start();" ^
    "Write-Host '--------------------------------------------------' -ForegroundColor Gray;" ^
    "Write-Host \"サーバー起動中 (Port: $port)\";" ^
    "Write-Host \"URL: $url\";" ^
    "if ('%AUTO_CLOSE%' -eq '0') { Write-Host '※ 全てのXSLが読み込まれたら、この画面を閉じてください。' -ForegroundColor Yellow };" ^
    "if ('%ALLOW_EXTERNAL_XSL%' -eq '1') { Write-Host '※ 別階層XSL許可モード:全階層のXSLへのアクセスを許可しています。' -ForegroundColor Magenta };" ^
    "Write-Host '--------------------------------------------------' -ForegroundColor Gray;" ^
    "$xmlServed = $false;" ^
    "$xslServed = $false;" ^
    "while ($listener.IsListening) {" ^
    "    try {" ^
    "        $context = $listener.GetContext();" ^
    "        $request = $context.Request;" ^
    "        $response = $context.Response;" ^
    "        $reqPath = $request.Url.LocalPath;" ^
    "        if ($reqPath.StartsWith('/')) { $reqPath = $reqPath.Substring(1) }" ^
    "        if ($reqPath -match '^[a-zA-Z]:') { } elseif ($reqPath.StartsWith('/')) { $reqPath = $reqPath.Substring(1) }" ^
    "        $localPath = [System.IO.Path]::GetFullPath($reqPath.Replace('/', '\'));" ^
    "        $isTargetXml = ($localPath -eq $targetFull);" ^
    "        $isSameDir = ([System.IO.Path]::GetDirectoryName($localPath) -eq $targetDir);" ^
    "        $isXsl = ([System.IO.Path]::GetExtension($localPath).ToLower() -eq '.xsl');" ^
    "        $allowXsl = if ('%ALLOW_EXTERNAL_XSL%' -eq '1') { $isXsl } else { $isSameDir -and $isXsl };" ^
    "        if ($isTargetXml) {" ^
    "            $content = [System.IO.File]::ReadAllBytes($localPath);" ^
    "            $response.ContentType = 'text/xml';" ^
    "            $response.OutputStream.Write($content, 0, $content.Length);" ^
    "            $xmlServed = $true;" ^
    "            Write-Host \"XMLレスポンス: $reqPath\" -ForegroundColor Cyan;" ^
    "        } elseif ($allowXsl) {" ^
    "            if (Test-Path $localPath -PathType Leaf) {" ^
    "                $content = [System.IO.File]::ReadAllBytes($localPath);" ^
    "                $response.ContentType = 'text/xml';" ^
    "                $response.OutputStream.Write($content, 0, $content.Length);" ^
    "                $xslServed = $true;" ^
    "                Write-Host \"XSLレスポンス: $reqPath\" -ForegroundColor Green;" ^
    "            } else { $response.StatusCode = 404 }" ^
    "        } else {" ^
    "            $response.StatusCode = 403;" ^
    "            Write-Host \"[警告] ブロックされたアクセス: $reqPath\" -ForegroundColor Red;" ^
    "        }" ^
    "        $response.Close();" ^
    "    } catch { break }" ^
    "    if ('%AUTO_CLOSE%' -eq '1' -and $xmlServed -and $xslServed) {" ^
    "        Write-Host '読み込み完了。自動終了します。';" ^
    "        Start-Sleep -Milliseconds %WAIT_MILLISECONDS%;" ^
    "        $listener.Stop();" ^
    "    }" ^
    "}" ^

必ずShift-JISで保存してください。
大事なことなので2度言いました。

解説もどき

.NETの機能を使い、Webサーバーを立ち上げます。8000~9000番の空いているポートを順番に探し、最初に見つかったものを使用します。その後、ブラウザを立ち上げ、XMLとXSLのリクエストを1件ずつ確認したら自動で閉じるという流れです。

セキュリティ対策のためデフォルトでは「返せるXMLは引数に指定されたものだけ。返せるXSLはXMLと同階層にあるものだけ。」という仕様になっていますが、設定可能なオプションが2つあります。
1つ目は、複数のXSLを読み込むタイプを想定して、手動で閉じるまでXSLのリクエストを待機する設定です。2つ目は、別階層にXSLがある場合を想定して、別階層のXSLを許可する設定です。

PowerShellで何をやっているか気になる人は次のコードを読んでください。

PowerShell部分の抜き出し
整形済み.ps1
# バッチファイルで定義した環境変数のシミュレーション
$TARGET_FILE = "C:\Users\path\to\your-file.xml"
$AUTO_CLOSE = "1"
$ALLOW_EXTERNAL_XSL = "0"
$WAIT_MILLISECONDS = 1000

# フルパスとターゲットディレクトリの取得
$targetFull = [System.IO.Path]::GetFullPath($TARGET_FILE)
$targetDir = [System.IO.Path]::GetDirectoryName($targetFull)
$port = 8000

# 空きポートの探索
while ($true) {
    $connection = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $port)
    try {
        $connection.Start()
        $connection.Stop()
        break
    } catch {
        $port++
    }
    if ($port -gt 9000) {
        Write-Host '空きポートが見つかりませんでした。'
        pause
        exit
    }
}

# 既定のブラウザでURLを開く
$url = "http://localhost:$port/$($targetFull.Replace('\','/'))"
Start-Process $url

# HTTPリスナーの設定
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://localhost:$port/")
$listener.Start()

Write-Host '--------------------------------------------------' -ForegroundColor Gray
Write-Host "サーバー起動中 (Port: $port)"
Write-Host "URL: $url"
if ($AUTO_CLOSE -eq '0') {
    Write-Host '※ 全てのXSLが読み込まれたら、この画面を閉じてください。' -ForegroundColor Yellow 
}
if ($ALLOW_EXTERNAL_XSL -eq '1') {
    Write-Host '※ 別階層XSL許可モード:全階層のXSLへのアクセスを許可しています。' -ForegroundColor Magenta
}
Write-Host '--------------------------------------------------' -ForegroundColor Gray

$xmlServed = $false
$xslServed = $false

# メインループ
while ($listener.IsListening) {
    try {
        $context = $listener.GetContext()
        $request = $context.Request
        $response = $context.Response
        $reqPath = $request.Url.LocalPath

        # パスの正規化
        if ($reqPath.StartsWith('/')) { $reqPath = $reqPath.Substring(1) }
        if ($reqPath -match '^[a-zA-Z]:') { } elseif ($reqPath.StartsWith('/')) { $reqPath = $reqPath.Substring(1) }

        $localPath = [System.IO.Path]::GetFullPath($reqPath.Replace('/', '\'))

        # 判定ロジック
        $isTargetXml = ($localPath -eq $targetFull)
        $isSameDir = ([System.IO.Path]::GetDirectoryName($localPath) -eq $targetDir)
        $isXsl = ([System.IO.Path]::GetExtension($localPath).ToLower() -eq '.xsl')
        $allowXsl = if ($ALLOW_EXTERNAL_XSL -eq '1') { $isXsl } else { $isSameDir -and $isXsl }

        if ($isTargetXml) {
            # XMLの配信
            $content = [System.IO.File]::ReadAllBytes($localPath)
            $response.ContentType = 'text/xml'
            $response.OutputStream.Write($content, 0, $content.Length)
            $xmlServed = $true
            Write-Host "XMLレスポンス: $reqPath" -ForegroundColor Cyan
        } elseif ($allowXsl) {
            # XSLの配信
            if (Test-Path $localPath -PathType Leaf) {
                $content = [System.IO.File]::ReadAllBytes($localPath)
                $response.ContentType = 'text/xml'
                $response.OutputStream.Write($content, 0, $content.Length)
                $xslServed = $true
                Write-Host "XSLレスポンス: $reqPath" -ForegroundColor Green
            } else {
                $response.StatusCode = 404
            }
        } else {
            # ブロック
            $response.StatusCode = 403
            Write-Host "[警告] ブロックされたアクセス: $reqPath" -ForegroundColor Red
        }
        $response.Close()
    } catch {
        break
    }

    # 自動終了判定
    if ($AUTO_CLOSE -eq '1' -and $xmlServed -and $xslServed) {
        Write-Host '読み込み完了。自動終了します。'
        Start-Sleep -Milliseconds $WAIT_MILLISECONDS
        $listener.Stop()
    }
}

XMLをダブルクリックして開く

このバッチファイルを既定のアプリにしておけば、ダブルクリックでXMLをデフォルトのブラウザで表示できます。

.xmlを開くアプリの選択ダイアログ

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?