AlkantarClanX12
| Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/classes/ |
| Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/classes/lead-dispatch-log.php |
<?php if (!defined('ABSPATH')) exit;
/**
* LBT-1468 — Dedicated text log for lead-dispatch outcomes.
*
* Writes one human-readable, grep-friendly line per dispatch attempt to a file
* that is INDEPENDENT of the PSR-3 logger (wp-content/leadbox-debug.log) and is
* written ALWAYS — it does not depend on WP_DEBUG_LOG or the logger's min level.
*
* Purpose: during the `both` parity window (and any `leadbox_os` testing) you can
* see, per lead, whether the dispatch to Leadbox Platform succeeded and WHY
* (HTTP status + error_code + message). PII (email/phone) is partially masked.
*
* The router only calls record() when target !== 'legacy', so default-legacy
* sites stay completely silent — a plugin update never produces log noise there.
*
* Line format (OK / FAIL prefix is intentional for `grep`):
* [2026-06-05 14:23:01] OK both/leadbox_os form=12 sub=4567 http=200 lat=312ms lead_id=cnv_abc contact=j***@gmail.com phone=***1234
* [2026-06-05 14:23:05] FAIL both/leadbox_os form=12 http=422 err=validation_failed lat=240ms contact=j***@gmail.com phone=***1234 msg="Email or Phone required"
*
* Master switch: define('LEADBOX_LEAD_DISPATCH_LOG', false) in wp-config.php to
* disable entirely. File path is filterable via 'leadbox_dispatch_log_path'.
*/
final class LB_Lead_Dispatch_Log
{
const FILENAME = 'leadbox-lead-dispatch.log';
/**
* Persist one dispatch outcome. Uses WordPress functions; not unit-tested.
*
* @param array $outcome Keys: target, backend, form_id, nf_submission_id,
* success(bool), http_status, error_code, message,
* latency_ms, lead_id, is_retry(bool), email, phone, note.
*/
public static function record(array $outcome)
{
if (defined('LEADBOX_LEAD_DISPATCH_LOG') && !LEADBOX_LEAD_DISPATCH_LOG) {
return;
}
$timestamp = function_exists('current_time') ? current_time('mysql') : date('Y-m-d H:i:s');
$line = self::format_line($outcome, $timestamp);
$default_path = (defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : __DIR__) . '/' . self::FILENAME;
$path = function_exists('apply_filters')
? apply_filters('leadbox_dispatch_log_path', $default_path)
: $default_path;
// Best-effort; logging must never disturb the request.
@file_put_contents($path, $line . "\n", FILE_APPEND | LOCK_EX);
}
/**
* Build the log line from an outcome array. Pure PHP (no WordPress) so it is
* unit-testable. The timestamp is supplied by the caller.
*
* @param array $o
* @param string $timestamp
* @return string
*/
public static function format_line(array $o, $timestamp = '')
{
$status = !empty($o['success']) ? 'OK ' : 'FAIL';
$parts = array();
$parts[] = self::val($o, 'target', '?') . '/' . self::val($o, 'backend', '?');
$parts[] = 'form=' . self::val($o, 'form_id', '0');
if (self::has($o, 'nf_submission_id')) $parts[] = 'sub=' . $o['nf_submission_id'];
if (self::has($o, 'http_status')) $parts[] = 'http=' . $o['http_status'];
if (empty($o['success']) && self::has($o, 'error_code')) $parts[] = 'err=' . $o['error_code'];
if (self::has($o, 'latency_ms')) $parts[] = 'lat=' . $o['latency_ms'] . 'ms';
if (self::has($o, 'lead_id')) $parts[] = 'lead_id=' . $o['lead_id'];
if (!empty($o['is_retry'])) $parts[] = 'retry=1';
$parts[] = 'contact=' . self::mask_email(self::val($o, 'email', ''));
$parts[] = 'phone=' . self::mask_phone(self::val($o, 'phone', ''));
if (empty($o['success']) && self::has($o, 'message')) {
$parts[] = 'msg="' . str_replace(array('"', "\n", "\r"), array("'", ' ', ' '), (string) $o['message']) . '"';
}
if (self::has($o, 'note')) $parts[] = 'note=' . $o['note'];
$prefix = $timestamp !== '' ? '[' . $timestamp . '] ' : '';
return $prefix . $status . ' ' . implode(' ', $parts);
}
/**
* Mask an email: first char of the local part + ***@ + domain.
* 'john@gmail.com' => 'j***@gmail.com'. Empty => '(none)'. Malformed => '***'.
*
* @param string $email
* @return string
*/
public static function mask_email($email)
{
$email = trim((string) $email);
if ($email === '') {
return '(none)';
}
$at = strpos($email, '@');
if ($at === false) {
return '***';
}
$local = substr($email, 0, $at);
$domain = substr($email, $at + 1);
$first = $local !== '' ? substr($local, 0, 1) : '';
return $first . '***@' . $domain;
}
/**
* Mask a phone: *** + last 4 digits. '+1 (613) 555-1234' => '***1234'.
* Empty/no-digits => '(none)'.
*
* @param string $phone
* @return string
*/
public static function mask_phone($phone)
{
$digits = preg_replace('/\D+/', '', (string) $phone);
if ($digits === '') {
return '(none)';
}
return '***' . substr($digits, -4);
}
private static function has($o, $k)
{
return isset($o[$k]) && $o[$k] !== null && $o[$k] !== '';
}
private static function val($o, $k, $default)
{
return self::has($o, $k) ? $o[$k] : $default;
}
}