HEX
Server: Apache/2.4.37 (CentOS Stream) OpenSSL/1.1.1k
System: Linux ysnet.com.tw 4.18.0-553.5.1.el8.x86_64 #1 SMP Tue May 21 05:46:01 UTC 2024 x86_64
User: test (521)
PHP: 7.4.33
Disabled: NONE
Upload Files
File: /var/www/test/Installationlist/fm_test.php
<?php
// ============================================================
// fm_test.php  ─  FileMaker Data API 串接測試工具
// ------------------------------------------------------------
// 修正版(v2):
//  * 修正容器上傳 multipart Content-Type 自動 boundary 問題
//  * URL 中的中文容器欄位名做 rawurlencode
//  * 加上 verbose log 方便除錯
//  * 使用結構較完整的測試 PDF
// ============================================================

session_start();
ini_set('display_errors', 1);
error_reporting(E_ALL);

// ── FileMaker 連線設定 ───────────────────────────────────
define('FM_HOST',   'https://61.216.173.217:8443');
define('FM_DB',     'YSNET_MIS');
define('FM_LAYOUT', 'api-申請人資料寫入');
define('FM_USER',   'webapi');
define('FM_PASS',   '@Andrejiao9527');


// 測試專用客戶編號
define('TEST_USER_ID', '2025022410');

// ── 共用 cURL 函式 ───────────────────────────────────────
function fmCurl($method, $url, $token = null, $bodyArr = null, $isMultipart = false, $multipartFile = null, $multipartFieldName = 'upload') {
    $ch = curl_init($url);
    $headers = [];
    if ($token) $headers[] = 'Authorization: Bearer '.$token;

    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_TIMEOUT, 60);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

    if ($isMultipart) {
        // 重要:不要手動設 Content-Type!讓 cURL 自動產生帶 boundary 的 header
        // CURLFile 第二參數 = MIME type,第三參數 = 顯示檔名
        curl_setopt($ch, CURLOPT_POSTFIELDS, [
            $multipartFieldName => new CURLFile($multipartFile, 'application/pdf', basename($multipartFile))
        ]);
    } elseif ($bodyArr !== null) {
        $headers[] = 'Content-Type: application/json';
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($bodyArr, JSON_UNESCAPED_UNICODE));
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $resp = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr  = curl_error($ch);
    $curlErrno = curl_errno($ch);
    curl_close($ch);

    if ($resp === false) {
        return ['ok'=>false, 'http'=>0, 'error'=>"cURL錯誤(#{$curlErrno}): {$curlErr}", 'raw'=>null, 'data'=>null];
    }
    $json = json_decode($resp, true);
    $fmCode = $json['messages'][0]['code'] ?? null;
    $fmMsg  = $json['messages'][0]['message'] ?? '';
    $ok = ($httpCode >= 200 && $httpCode < 300) && ($fmCode === '0');
    return ['ok'=>$ok, 'http'=>$httpCode, 'fmCode'=>$fmCode, 'fmMsg'=>$fmMsg, 'raw'=>$resp, 'data'=>$json];
}

// ============================================================
// AJAX 動作處理
// ============================================================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
    header('Content-Type: application/json; charset=utf-8');
    $action = $_POST['action'];
    $result = ['ok'=>false, 'message'=>'未知動作'];

    switch ($action) {

        // ── Step 1:登入 ──────────────────────────────
        case 'login':
            $loginUrl = FM_HOST.'/fmi/data/v1/databases/'.rawurlencode(FM_DB).'/sessions';

            // 手動組 Basic Auth header,避開 CURLOPT_USERPWD 在某些情境下的字元編碼問題
            $basicAuth = 'Basic ' . base64_encode(FM_USER . ':' . FM_PASS);

            $ch = curl_init($loginUrl);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode((object)[]));
            curl_setopt($ch, CURLOPT_HTTPHEADER, [
                'Content-Type: application/json',
                'Authorization: '.$basicAuth,
            ]);
            curl_setopt($ch, CURLOPT_TIMEOUT, 30);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            $resp = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $curlErr = curl_error($ch);
            $curlErrno = curl_errno($ch);
            curl_close($ch);

            // 額外 debug 資訊(檢查帳密是否混到全形字元)
            $debug = [
                'url' => $loginUrl,
                'user_len' => strlen(FM_USER),
                'pass_len' => strlen(FM_PASS),
                'user_hex' => bin2hex(FM_USER),
                'pass_hex' => bin2hex(FM_PASS),
                'basic_auth_preview' => substr($basicAuth, 0, 20).'...(len='.strlen($basicAuth).')',
            ];

            if ($resp === false) {
                $result = ['ok'=>false, 'http'=>0, 'message'=>"cURL連線錯誤(#{$curlErrno}): {$curlErr}", 'debug'=>$debug, 'raw'=>null];
                break;
            }
            $json = json_decode($resp, true);
            $token = $json['response']['token'] ?? null;
            if ($token) {
                $_SESSION['fm_test_token'] = $token;
                $result = ['ok'=>true, 'http'=>$httpCode, 'message'=>'登入成功,Token 已取得並存入 PHP Session', 'token_preview'=>substr($token,0,12).'...', 'debug'=>$debug, 'raw'=>$json];
            } else {
                $msg = $json['messages'][0]['message'] ?? '未知錯誤';
                $result = ['ok'=>false, 'http'=>$httpCode, 'message'=>"登入失敗: {$msg}", 'debug'=>$debug, 'raw'=>$json];
            }
            break;

        // ── Step 2:查詢(依測試客戶編號 _find)─────────
        case 'find':
            $token = $_SESSION['fm_test_token'] ?? null;
            if (!$token) { $result = ['ok'=>false, 'message'=>'尚未登入,請先執行 Step 1'];break; }

            $baseUrl = FM_HOST.'/fmi/data/v1/databases/'.rawurlencode(FM_DB).'/layouts/'.rawurlencode(FM_LAYOUT);
            $r = fmCurl('POST', $baseUrl.'/_find', $token, [
                'query' => [[ '客戶編號' => '=='.TEST_USER_ID ]]
            ]);

            if ($r['ok']) {
                $records = $r['data']['response']['data'] ?? [];
                $_SESSION['fm_test_recordId'] = $records[0]['recordId'] ?? null;
                $result = [
                    'ok'=>true, 'http'=>$r['http'],
                    'message'=>'查詢成功,找到 '.count($records).' 筆紀錄',
                    'recordId'=>$_SESSION['fm_test_recordId'],
                    'fieldData'=>$records[0]['fieldData'] ?? null,
                    'raw'=>$r['data']
                ];
            } elseif ($r['fmCode'] === '401') {
                $_SESSION['fm_test_recordId'] = null;
                $result = ['ok'=>true, 'http'=>$r['http'], 'message'=>'查無此客戶編號紀錄(FM code 401,這代表測試編號尚未建立,請先在 FileMaker 手動建立一筆空白紀錄,或讓 Step 3 自動新增)', 'raw'=>$r['data']];
            } else {
                $result = ['ok'=>false, 'http'=>$r['http'], 'message'=>"查詢失敗: {$r['fmMsg']} (FM code {$r['fmCode']})", 'raw'=>$r['data']];
            }
            break;

        // ── Step 3:寫入測試資料(PATCH 或 POST)────────
        case 'write':
            $token = $_SESSION['fm_test_token'] ?? null;
            if (!$token) { $result = ['ok'=>false, 'message'=>'尚未登入,請先執行 Step 1'];break; }

            $baseUrl = FM_HOST.'/fmi/data/v1/databases/'.rawurlencode(FM_DB).'/layouts/'.rawurlencode(FM_LAYOUT);
            $recordId = $_SESSION['fm_test_recordId'] ?? null;

            $stamp = date('Y-m-d H:i:s');
            $fieldData = [
                '客戶編號'     => TEST_USER_ID,
                '用戶名稱'     => '測試用戶_'.$stamp,
                '身分證字號'   => 'A123456789',
                '住家電話'     => '03-1234567',
                '行動電話'     => '0912345678',
                '外部郵址'     => 'test_'.time().'@example.com',
                '資訊箱內設備' => '光貓 ×1、1000M無線分享器 ×1',
                '家中設備情況' => '1000M無線分享器 ×1(測試寫入於 '.$stamp.')',
            ];

            if ($recordId) {
                $r = fmCurl('PATCH', $baseUrl.'/records/'.$recordId, $token, ['fieldData'=>$fieldData]);
                $mode = 'PATCH(更新既有紀錄 recordId='.$recordId.')';
            } else {
                $r = fmCurl('POST', $baseUrl.'/records', $token, ['fieldData'=>$fieldData]);
                $mode = 'POST(新增紀錄,因為 Step 2 查無資料)';
                if ($r['ok']) {
                    $_SESSION['fm_test_recordId'] = $r['data']['response']['recordId'] ?? null;
                }
            }

            if ($r['ok']) {
                $result = ['ok'=>true, 'http'=>$r['http'], 'message'=>"寫入成功!模式:{$mode}", 'fieldData_sent'=>$fieldData, 'raw'=>$r['data']];
            } else {
                $result = ['ok'=>false, 'http'=>$r['http'], 'message'=>"寫入失敗: {$r['fmMsg']} (FM code {$r['fmCode']})。模式:{$mode}", 'fieldData_sent'=>$fieldData, 'raw'=>$r['data']];
            }
            break;

        // ── Step 4:上傳測試 PDF 到容器欄位(v2 修正版)──
        case 'upload':
            $token = $_SESSION['fm_test_token'] ?? null;
            $recordId = $_SESSION['fm_test_recordId'] ?? null;
            if (!$token) { $result = ['ok'=>false, 'message'=>'尚未登入,請先執行 Step 1'];break; }
            if (!$recordId) { $result = ['ok'=>false, 'message'=>'尚無 recordId,請先執行 Step 2(查詢)或 Step 3(寫入)'];break; }

            // 產生結構較完整的測試 PDF(含二進位標記、xref 偏移表)
            $testPdfPath = sys_get_temp_dir().'/fm_test_'.time().'.pdf';
            $pdfContent = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"
                ."1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
                ."2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
                ."3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj\n"
                ."4 0 obj\n<< /Length 55 >>\nstream\nBT /F1 18 Tf 50 750 Td (FileMaker Container Upload Test) Tj ET\nendstream\nendobj\n"
                ."5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"
                ."xref\n0 6\n0000000000 65535 f \n0000000018 00000 n \n0000000068 00000 n \n0000000119 00000 n \n0000000236 00000 n \n0000000337 00000 n \n"
                ."trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n405\n%%EOF\n";
            file_put_contents($testPdfPath, $pdfContent);

            // 重要:容器欄位名稱含中文,URL 必須做 rawurlencode
            $baseUrl = FM_HOST.'/fmi/data/v1/databases/'.rawurlencode(FM_DB).'/layouts/'.rawurlencode(FM_LAYOUT);
            $uploadUrl = $baseUrl.'/records/'.$recordId.'/containers/'.rawurlencode('裝機單掃描').'/1';

            // 直接寫 cURL 並開啟 verbose 取得詳細通訊紀錄
            $verbose = fopen('php://temp', 'w+');
            $ch = curl_init($uploadUrl);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, 60);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($ch, CURLOPT_VERBOSE, true);
            curl_setopt($ch, CURLOPT_STDERR, $verbose);
            // 重要:只設 Authorization,不要手動設 Content-Type,讓 cURL 自動生成帶 boundary 的 header
            curl_setopt($ch, CURLOPT_HTTPHEADER, [
                'Authorization: Bearer '.$token
            ]);
            curl_setopt($ch, CURLOPT_POSTFIELDS, [
                'upload' => new CURLFile($testPdfPath, 'application/pdf', basename($testPdfPath))
            ]);

            $resp = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $curlErr = curl_error($ch);
            curl_close($ch);

            rewind($verbose);
            $verboseLog = stream_get_contents($verbose);
            fclose($verbose);

            @unlink($testPdfPath);

            $json = $resp ? json_decode($resp, true) : null;
            $fmCode = $json['messages'][0]['code'] ?? null;
            $fmMsg  = $json['messages'][0]['message'] ?? '';
            $ok = ($httpCode >= 200 && $httpCode < 300) && ($fmCode === '0');

            if ($ok) {
                $result = [
                    'ok'=>true,
                    'http'=>$httpCode,
                    'message'=>'容器上傳成功!請到 FileMaker 後台檢查「裝機單掃描」欄位是否出現測試 PDF',
                    'url'=>$uploadUrl,
                    'raw'=>$json
                ];
            } else {
                $result = [
                    'ok'=>false,
                    'http'=>$httpCode,
                    'message'=>"容器上傳失敗 HTTP {$httpCode}".($fmMsg ? " (FM code {$fmCode}: {$fmMsg})" : ' (無 FM 回應,可能 IIS Reverse Proxy 直接擋掉)'),
                    'url'=>$uploadUrl,
                    'curl_error'=>$curlErr,
                    'raw_response'=>$resp,
                    'verbose_log'=>$verboseLog,
                ];
            }
            break;

        // ── Step 5:登出 ──────────────────────────────
        case 'logout':
            $token = $_SESSION['fm_test_token'] ?? null;
            if (!$token) { $result = ['ok'=>false, 'message'=>'尚未登入,無需登出'];break; }
            $r = fmCurl('DELETE', FM_HOST.'/fmi/data/v1/databases/'.rawurlencode(FM_DB).'/sessions/'.$token);
            unset($_SESSION['fm_test_token'], $_SESSION['fm_test_recordId']);
            $result = ['ok'=>$r['ok'], 'http'=>$r['http'], 'message'=>$r['ok'] ? '登出成功,Session 已清除' : "登出時發生錯誤(仍會清除本地Token): {$r['fmMsg']}", 'raw'=>$r['data']];
            break;

        // ── 額外:顯示目前測試狀態 ────────────────────
        case 'status':
            $result = [
                'ok'=>true,
                'message'=>'目前測試狀態',
                'has_token'=> !empty($_SESSION['fm_test_token']),
                'token_preview'=> !empty($_SESSION['fm_test_token']) ? substr($_SESSION['fm_test_token'],0,12).'...' : null,
                'recordId'=> $_SESSION['fm_test_recordId'] ?? null,
            ];
            break;
    }

    echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit;
}
?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FileMaker Data API 串接測試工具 v2</title>
<style>
    * { box-sizing: border-box; }
    body { font-family: "Microsoft JhengHei", "PingFang TC", sans-serif; background:#f3f4f6; color:#1a1a1a; padding:20px; max-width:920px; margin:0 auto; }
    h1 { font-size:20px; margin-bottom:4px; }
    .sub { color:#666; font-size:13px; margin-bottom:20px; }
    .config-box { background:#fff; border:1px solid #ddd; border-radius:8px; padding:14px 18px; margin-bottom:20px; font-size:13px; line-height:1.8; }
    .config-box b { color:#0F6E56; }
    .warn { background:#FFF3CD; border:1px solid #F0C36D; border-radius:8px; padding:10px 14px; font-size:13px; color:#7A5C00; margin-bottom:20px; }
    .step { background:#fff; border:1px solid #ddd; border-radius:10px; margin-bottom:14px; overflow:hidden; }
    .step-hdr { display:flex; align-items:center; gap:10px; padding:14px 18px; border-bottom:1px solid #eee; }
    .step-num { width:28px; height:28px; border-radius:50%; background:#1D9E75; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:bold; font-size:13px; flex-shrink:0; }
    .step-title { font-size:15px; font-weight:500; flex:1; }
    .step-desc { font-size:12px; color:#888; }
    .step-body { padding:14px 18px; }
    button { background:#1D9E75; color:#fff; border:none; padding:9px 18px; border-radius:6px; font-size:14px; cursor:pointer; font-family:inherit; }
    button:hover { background:#0F6E56; }
    button:disabled { background:#ccc; cursor:not-allowed; }
    .result-box { margin-top:10px; padding:10px 14px; border-radius:6px; font-size:12px; white-space:pre-wrap; word-break:break-all; max-height:420px; overflow:auto; font-family:monospace; display:none; }
    .result-box.show { display:block; }
    .result-box.ok { background:#E1F5EE; border:1px solid #9FE1CB; color:#085041; }
    .result-box.fail { background:#FCEBEB; border:1px solid #F0A8A8; color:#A32D2D; }
    .status-bar { display:flex; gap:16px; font-size:12px; color:#666; background:#fff; border:1px solid #ddd; border-radius:8px; padding:10px 16px; margin-bottom:20px; }
    .status-bar span b { color:#1D9E75; }
    .ver { display:inline-block; font-size:11px; padding:2px 8px; border-radius:10px; background:#1D9E75; color:#fff; margin-left:6px; vertical-align:middle; }
</style>
</head>
<body>

<h1>🔧 FileMaker Data API 串接測試工具<span class="ver">v2</span></h1>
<div class="sub">修正容器上傳的 Content-Type boundary 與 URL 中文編碼問題</div>

<div class="config-box">
    主機:<b><?= htmlspecialchars(FM_HOST) ?></b> 
    資料庫:<b><?= htmlspecialchars(FM_DB) ?></b> 
    布局:<b><?= htmlspecialchars(FM_LAYOUT) ?></b><br>
    測試客戶編號:<b><?= htmlspecialchars(TEST_USER_ID) ?></b>(請確認此編號已在 FileMaker 手動建立,或讓 Step 3 自動新增)
</div>

<div class="warn">
    ⚠️ 此工具內含 API 帳密,僅供測試期間使用。測試完成並確認 submit.php 可正常運作後,請刪除此檔案或將其搬出公開存取的網頁目錄。
</div>

<div class="status-bar" id="status-bar">
    目前狀態:<span id="status-text">尚未查詢</span>
    <button onclick="checkStatus()" style="padding:4px 12px;font-size:12px">重新整理狀態</button>
</div>

<!-- Step 1 -->
<div class="step">
    <div class="step-hdr">
        <div class="step-num">1</div>
        <div class="step-title">登入 FileMaker(取得 Token)</div>
        <div class="step-desc">POST /sessions</div>
    </div>
    <div class="step-body">
        <button onclick="runStep('login', this)">執行登入</button>
        <div class="result-box" id="result-login"></div>
    </div>
</div>

<!-- Step 2 -->
<div class="step">
    <div class="step-hdr">
        <div class="step-num">2</div>
        <div class="step-title">查詢測試客戶編號是否已有紀錄</div>
        <div class="step-desc">POST /_find(依客戶編號)</div>
    </div>
    <div class="step-body">
        <button onclick="runStep('find', this)">執行查詢</button>
        <div class="result-box" id="result-find"></div>
    </div>
</div>

<!-- Step 3 -->
<div class="step">
    <div class="step-hdr">
        <div class="step-num">3</div>
        <div class="step-title">寫入測試資料(自動 PATCH 或 POST)</div>
        <div class="step-desc">若 Step 2 找到紀錄則 PATCH 更新;找不到則 POST 新增</div>
    </div>
    <div class="step-body">
        <button onclick="runStep('write', this)">執行寫入</button>
        <div class="result-box" id="result-write"></div>
    </div>
</div>

<!-- Step 4 -->
<div class="step">
    <div class="step-hdr">
        <div class="step-num">4</div>
        <div class="step-title">上傳測試 PDF 到「裝機單掃描」容器欄位 <span class="ver">已修正</span></div>
        <div class="step-desc">POST /records/{id}/containers/裝機單掃描/1</div>
    </div>
    <div class="step-body">
        <button onclick="runStep('upload', this)">執行上傳</button>
        <div class="result-box" id="result-upload"></div>
    </div>
</div>

<!-- Step 5 -->
<div class="step">
    <div class="step-hdr">
        <div class="step-num">5</div>
        <div class="step-title">登出(釋放 Session)</div>
        <div class="step-desc">DELETE /sessions/{token}</div>
    </div>
    <div class="step-body">
        <button onclick="runStep('logout', this)">執行登出</button>
        <div class="result-box" id="result-logout"></div>
    </div>
</div>

<script>
function runStep(action, btn) {
    btn.disabled = true;
    const origText = btn.textContent;
    btn.textContent = '執行中…';

    const box = document.getElementById('result-' + action);
    box.className = 'result-box show';
    box.textContent = '請求送出中…';

    const fd = new FormData();
    fd.append('action', action);

    fetch(window.location.href, { method: 'POST', body: fd })
        .then(r => r.text())
        .then(text => {
            let data;
            try { data = JSON.parse(text); }
            catch(e) {
                box.className = 'result-box show fail';
                box.textContent = '回應非 JSON,可能是 PHP 錯誤:\n\n' + text;
                return;
            }
            box.className = 'result-box show ' + (data.ok ? 'ok' : 'fail');
            box.textContent = (data.ok ? '✅ ' : '❌ ') + data.message + '\n\n' + JSON.stringify(data, null, 2);
            checkStatus();
        })
        .catch(err => {
            box.className = 'result-box show fail';
            box.textContent = '請求失敗:' + err.message;
        })
        .finally(() => {
            btn.disabled = false;
            btn.textContent = origText;
        });
}

function checkStatus() {
    const fd = new FormData();
    fd.append('action', 'status');
    fetch(window.location.href, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(data => {
            const txt = (data.has_token ? 'Token: ' + data.token_preview : '未登入')
                      + ' | recordId: ' + (data.recordId || '尚未取得');
            document.getElementById('status-text').textContent = txt;
        });
}

// 頁面載入時先查一次狀態
checkStatus();
</script>

</body>
</html>