ArcWOC/admin/services/class-hesabix-v2-log-service.php
morrning 8aedcf2500 Release 4.9.0: invoice profit in WP + marketplace license banner
Show Hesabix invoice profit on ArcWOC orders list and order meta box,
refresh after sync, and surface WooCommerce marketplace license upsell
(hidden when active). Also includes 4.8.2 bank/cash take≤100 fixes.
2026-08-04 12:56:06 +00:00

414 lines
11 KiB
PHP
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Logging Service
*
* با دیباگ روشن: فایل + دیتابیس برای همه سطوح (شامل جزئیات تبادل API).
* با دیباگ خاموش: فقط خطاها در فایل و جدول لاگ.
*
* @since 2.0.0
* @package Hesabix_V2
* @subpackage Hesabix_V2/admin/services
*/
class Hesabix_V2_Log_Service
{
/** @var string */
private static $log_dir;
/** @var int حداکثر طول متن برای ذخیره پس از json_encode (~۵۱۲KiB پیش‌فرض) */
const MAX_JSON_CHARS = 524288;
/**
* تشخیص حالت لاگ پیش‌پیکربندی‌شده افزونه
*
* @return bool
*/
private static function is_debug_mode_on()
{
return (bool) get_option('hesabix_v2_debug_mode');
}
private static function init()
{
if (!self::$log_dir) {
$upload_dir = wp_upload_dir();
self::$log_dir = $upload_dir['basedir'] . '/hesabix-v2-logs';
if (!file_exists(self::$log_dir)) {
wp_mkdir_p(self::$log_dir);
file_put_contents(self::$log_dir . '/.htaccess', 'Deny from all');
}
}
}
/**
* حذف / جایگزینی فیلدهای حساس در آبجکت آرایه‌ای پیش از لاگ.
*
* @param mixed $data
* @return mixed
*/
public static function sanitize_log_recursive($data)
{
if (is_array($data)) {
$out = array();
foreach ($data as $k => $v) {
$lk = is_string($k) ? strtolower($k) : '';
if (in_array($lk, array('password', 'passwd', 'api_key', 'apikey', 'secret', 'token', 'refresh_token', 'credit_card'), true)) {
$out[$k] = '[redacted]';
continue;
}
if ($lk === 'authorization' && is_string($v)) {
$out[$k] = preg_match('#^apitoken\s|^apikey\s#i', $v)
? preg_replace('#^(\S+)\s+\S(.*)$#', '$1 ********', $v, 1)
: '[redacted]';
continue;
}
if (is_array($v)) {
$out[$k] = self::sanitize_log_recursive($v);
} elseif (is_object($v)) {
$out[$k] = self::sanitize_log_recursive((array) $v);
} else {
$out[$k] = $v;
}
}
return $out;
}
return $data;
}
/**
* ساده‌سازی فیلدهای حساس (برای هر دو فایل و DB)
*
* @param mixed $payload
* @return mixed
*/
public static function prepare_payload_for_storage($payload)
{
return self::sanitize_log_recursive($payload);
}
/**
* json برای ستون با محدود طول و UTF-8
*
* @param mixed $payload
* @return string|null
*/
private static function json_column($payload)
{
if (null === $payload) {
return null;
}
$prepared = self::sanitize_log_recursive($payload);
$max = (int) apply_filters('hesabix_v2_log_payload_max_json_chars', self::MAX_JSON_CHARS);
if ($max < 4096) {
$max = 4096;
}
$json = wp_json_encode($prepared, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if ($json !== false && strlen($json) > $max) {
return substr($json, 0, $max) . "\n…[truncated]";
}
return is_string($json) ? $json : wp_json_encode(array('_encode_error' => true), JSON_UNESCAPED_UNICODE);
}
/**
* پیام آمادهٔ فایل (با trim طول تقریبی)
*
* @param mixed $payload
* @return string
*/
private static function json_for_file_preview($payload)
{
$prepared = self::sanitize_log_recursive($payload);
$json = wp_json_encode($prepared, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE);
if ($json === false) {
return '';
}
$max = (int) apply_filters('hesabix_v2_log_file_preview_max_chars', self::MAX_JSON_CHARS);
if ($max < 8192) {
$max = 8192;
}
if (strlen($json) > $max) {
return substr($json, 0, $max) . "\n…[truncated]";
}
return $json;
}
/**
* نگاشت context به ستون‌های request_data / response_data
*
* @param string $level
* @param array<string,mixed> $context
* @return array{request:?string,response:?string}
*/
private static function derive_request_response_columns($level, $context)
{
$req_col = isset($context['request']) ? self::json_column($context['request']) : null;
$res_col = isset($context['response']) ? self::json_column($context['response']) : null;
if ($level === 'error' && !$req_col && !$res_col) {
$extra = array();
foreach ($context as $k => $v) {
if (!in_array($k, array('entity_type', 'entity_id', 'execution_time'), true)) {
$extra[$k] = $v;
}
}
if (!empty($extra)) {
$res_col = self::json_column($extra);
}
} elseif ($level === 'error' && !$res_col && !empty($context['error'])) {
$res_col = self::json_column(array('error_detail' => $context['error']));
}
return array(
'request' => $req_col,
'response' => $res_col,
);
}
/**
* متن قابل‌نمایش ستون پیام برای خطا
*
* @param string $title
* @param array<string,mixed> $context
*/
private static function format_error_detail_line($title, $context)
{
$detail = '';
if (!empty($context['error']) && is_string($context['error'])) {
$detail = trim($context['error']);
}
if ($detail === '') {
return wp_strip_all_tags((string) $title);
}
return wp_strip_all_tags(trim($title . ' — ' . $detail));
}
/**
* Write log to file and database
*
* @since 2.0.0
* @param string $level Log level (info, warning, error, debug)
* @param string $message Log message
* @param array $context Additional context
*/
private static function write($level, $message, $context = array())
{
self::init();
$debug_on = self::is_debug_mode_on();
if ($level === 'debug' && !$debug_on) {
return;
}
$file_write = ($level === 'error') || $debug_on;
if ($file_write) {
$timestamp = current_time('Y-m-d H:i:s');
$log_entry = sprintf(
"[%s] [%s] %s\n",
$timestamp,
strtoupper($level),
$message
);
if (!empty($context)) {
$log_entry .= "Context:\n" . self::json_for_file_preview($context) . "\n";
}
$log_entry .= str_repeat('-', 80) . "\n";
file_put_contents(self::$log_dir . '/' . date('Y-m-d') . '.log', $log_entry, FILE_APPEND);
}
$db_insert = ($level === 'error') || $debug_on;
if (!$db_insert) {
return;
}
global $wpdb;
$table = $wpdb->prefix . 'hesabix_v2_sync_log';
$cols = self::derive_request_response_columns($level, $context);
if ($level === 'error') {
$status_val = 'error';
} elseif ($level === 'warning') {
$status_val = 'warning';
} elseif ($level === 'debug') {
$status_val = 'debug';
} else {
$status_val = 'success';
}
$row = array(
'entity_type' => isset($context['entity_type']) ? (string) $context['entity_type'] : 'system',
'entity_id' => isset($context['entity_id']) ? (int) $context['entity_id'] : 0,
'action' => $level === 'debug' ? 'debug' : $level,
'status' => $status_val,
'request_data' => $cols['request'],
'response_data' => $cols['response'],
'error_message' => ('error' === $level ? self::format_error_detail_line($message, $context) : null),
'created_at' => current_time('mysql'),
);
$formats_map = array(
'entity_type' => '%s',
'entity_id' => '%d',
'action' => '%s',
'status' => '%s',
'request_data' => '%s',
'response_data' => '%s',
'error_message' => '%s',
'created_at' => '%s',
);
if (isset($context['execution_time'])) {
$row['execution_time'] = (float) $context['execution_time'];
$formats_map['execution_time'] = '%f';
}
$wpdb->insert($table, $row, $formats_map);
}
/**
* @param string $message
* @param array<string,mixed> $context
*/
public static function info($message, $context = array())
{
self::write('info', $message, $context);
}
/**
* @param string $message
* @param array<string,mixed> $context
*/
public static function warning($message, $context = array())
{
self::write('warning', $message, $context);
}
/**
* @param string $message
* @param array<string,mixed> $context
*/
public static function error($message, $context = array())
{
self::write('error', $message, $context);
}
/**
* @param string $message
* @param array<string,mixed> $context
*/
public static function debug($message, $context = array())
{
self::write('debug', $message, $context);
}
/**
* Get recent logs from database
*
* @since 2.0.0
* @param int $limit
* @param string $level action ستون؛ برای فیلتر مثلاً error یا debug
* @return array<int,array<string,mixed>>
*/
public static function get_recent_logs($limit = 100, $level = null)
{
global $wpdb;
$table = $wpdb->prefix . 'hesabix_v2_sync_log';
if ($level) {
$query = $wpdb->prepare(
"SELECT * FROM $table WHERE action = %s ORDER BY created_at DESC LIMIT %d",
$level,
(int) $limit
);
return $wpdb->get_results($query, ARRAY_A) ?: array();
}
return $wpdb->get_results($wpdb->prepare("SELECT * FROM $table ORDER BY created_at DESC LIMIT %d", (int) $limit), ARRAY_A) ?: array();
}
/**
* پاک کردن تمام لاگ‌ها (جدول دیتابیس + فایل‌های .log)
*
* @since 2.0.0
*/
public static function clear_all_logs()
{
global $wpdb;
$table = $wpdb->prefix . 'hesabix_v2_sync_log';
$wpdb->query("TRUNCATE TABLE $table");
self::init();
$files = glob(self::$log_dir . '/*.log');
foreach ($files as $file) {
if (is_file($file)) {
@unlink($file);
}
}
}
/**
* Clean old logs
*
* @param int $days Days to keep
*/
public static function clean_old_logs($days = 30)
{
global $wpdb;
$table = $wpdb->prefix . 'hesabix_v2_sync_log';
$date = date('Y-m-d H:i:s', strtotime('-' . absint($days) . ' days'));
$wpdb->query($wpdb->prepare("DELETE FROM $table WHERE created_at < %s", $date));
self::init();
$files = glob(self::$log_dir . '/*.log');
foreach ($files as $file) {
if (is_file($file) && filemtime($file) < strtotime('-' . absint($days) . ' days')) {
@unlink($file);
}
}
}
/**
* Cron: پاکسازی لاگ‌های قدیمی (فیلتر روزها: hesabix_v2_log_retention_days، حداقل ۷).
*
* @return void
*/
public static function cron_clean_old_logs()
{
$days = (int) apply_filters('hesabix_v2_log_retention_days', 30);
if ($days < 7) {
$days = 7;
}
self::clean_old_logs($days);
}
/**
* Get log file path for download
*
* @since 2.0.0
* @param string|null $date Date in Y-m-d format
* @return string|false
*/
public static function get_log_file($date = null)
{
self::init();
if (!$date) {
$date = date('Y-m-d');
}
$file = self::$log_dir . '/' . $date . '.log';
if (file_exists($file)) {
return $file;
}
return false;
}
}