AlkantarClanX12

Your IP : 216.73.217.24


Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/classes/
Upload File :
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;
  }
}