WHMCS Callback File 中 json_decode() 返回 null
结论
问题通常不在 json_decode(),而是 WHMCS 加载 init.php 时可能会过滤或转义请求变量,改变 $_GET['resp'] 中的双引号等字符,使其不再是合法的 JSON。
比较稳妥的做法是:先保存原始参数,再加载 init.php,最后解码 JSON。 PHP 已经自动对查询参数进行了 URL 解码,通常不需要再次调用 urldecode()。
推荐写法
<?php
// 必须在加载 WHMCS init.php 之前读取。
// 此时 $_GET['resp'] 已由 PHP 完成 URL 解码。
$rawResponse = isset($_GET['resp']) && is_string($_GET['resp'])
? $_GET['resp']
: '';
require_once __DIR__ . '/../../../init.php';
if ($rawResponse === '') {
exit('Missing payment gateway response');
}
$response = json_decode($rawResponse);
if (json_last_error() !== JSON_ERROR_NONE) {
$error = function_exists('json_last_error_msg')
? json_last_error_msg()
: 'JSON error code: ' . json_last_error();
// 实际项目中应写入网关日志,不建议直接向请求方输出完整响应。
logTransaction(
'gateway',
[
'response' => $rawResponse,
'json_error' => $error,
],
'Invalid JSON response'
);
exit('Invalid payment gateway response');
}
if (
!isset($response->tx) ||
!is_object($response->tx) ||
!isset($response->tx->amount)
) {
exit('Payment response is missing transaction data');
}
$amount = $response->tx->amount;
如果需要关联数组,可以将 json_decode() 的第二个参数设为 true:
$response = json_decode($rawResponse, true);
if (json_last_error() !== JSON_ERROR_NONE) {
exit('Invalid JSON response');
}
$amount = $response['tx']['amount'] ?? null;
为什么不应再次直接调用 urldecode()
访问 PHP 脚本时,查询字符串中的:
%7B%22name%22...
PHP 已经会将其解析为类似下面的内容:
{"name":"vbvcomplete", ...}
因此,正常情况下:
$_GET['resp']
已经是 URL 解码后的字符串。如果再次执行:
urldecode($_GET['resp']);
可能会破坏 JSON 中原本合法的 %xx、+ 或嵌套 URL。应先检查实际字符串,再判断是否需要额外处理。
如果只能在加载 init.php 后读取
在某些 WHMCS 环境中,引号可能已经被转换成 HTML 实体,例如:
{"name":"vbvcomplete"}
确认收到的内容确实是这种形式后,可以先还原 HTML 实体:
require_once __DIR__ . '/../../../init.php';
$rawResponse = isset($_GET['resp']) && is_string($_GET['resp'])
? $_GET['resp']
: '';
$json = html_entity_decode($rawResponse, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$response = json_decode($json);
if (json_last_error() !== JSON_ERROR_NONE) {
exit('JSON decode failed: ' . json_last_error_msg());
}
不要在未检查输入的情况下同时使用 urldecode()、stripslashes() 和 html_entity_decode()。这些函数处理的问题不同,重复转换可能会损坏有效数据。可以先记录转换前的值和 json_last_error_msg(),查清数据究竟发生了什么变化。
更可靠地定位错误
不要只检查返回值是否为 null,因为 JSON 字面量 null 也会被合法解码成 PHP 的 null。应该检查 JSON 错误状态:
$response = json_decode($rawResponse);
if (json_last_error() !== JSON_ERROR_NONE) {
$message = json_last_error_msg();
}
还可以临时记录字符串的可见形式:
logTransaction(
'gateway',
[
'response' => $rawResponse,
'response_debug' => var_export($rawResponse, true),
'json_error' => json_last_error_msg(),
],
'Callback debug'
);
生产环境的日志应避免记录银行卡信息、令牌、完整邮箱或其他敏感字段。
支付回调还需要注意
JSON 解码成功并不表示付款信息可信。处理回调时,至少还要完成以下检查:
- 验证支付网关提供的签名、哈希或 Webhook 密钥。
- 必要时通过网关的服务端 API 再次查询交易状态。
- 校验
invoiceid、交易编号、金额和币种。 - 不要仅凭客户端重定向参数中的
status判断付款成功。 - 使用 WHMCS 的
checkCbInvoiceID()、checkCbTransID()和addInvoicePayment()等回调函数。 - 对同一交易编号进行幂等处理,避免因刷新回调地址而重复入账。
处理这个问题的重点仍然是:在 init.php 改写请求数据前保存 $_GET['resp'],再对保存的字符串调用 json_decode()。只有确认内容已被转换成 HTML 实体时,才使用 html_entity_decode() 还原。
备注:内容仅供参考。
