<?php
if (!defined('_GNUBOARD_')) exit;

// ★ 현재 실행 스크립트가 "댓글 처리"인지 여부를 미리 판단
$GLOBALS['XLOG_COMMENT_CTX'] = in_array(
    basename(isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : ''),
    ['write_comment_update.php', 'delete_comment.php', 'comment_delete.php'], // 환경에 따라 추가 가능
    true
);

if (!defined('XLOG_VERSION')) define('XLOG_VERSION','1.3.5');

/*--------------------------------------------------------------
  DB helpers
--------------------------------------------------------------*/
if (!function_exists('xlog_db_engine_sql')) {
    function xlog_db_engine_sql() {
        $eng = '';
        if (function_exists('sql_fetch')) {
            $row = sql_fetch("SELECT @@default_storage_engine AS e", false);
            if ($row && !empty($row['e'])) $eng = $row['e'];
            if (!$eng) {
                $row = sql_fetch("SELECT @@storage_engine AS e", false);
                if ($row && !empty($row['e'])) $eng = $row['e'];
            }
        }
        return $eng ? " ENGINE=".$eng : "";
    }
}
if (!function_exists('xlog_db_charset')) {
    function xlog_db_charset() {
        if (defined('G5_DB_CHARSET') && G5_DB_CHARSET) return G5_DB_CHARSET;
        if (function_exists('sql_fetch')) {
            $row = sql_fetch("SELECT @@character_set_server AS cs", false);
            if ($row && !empty($row['cs'])) return $row['cs'];
        }
        return 'utf8mb4';
    }
}

/*--------------------------------------------------------------
  Schema bootstrap
--------------------------------------------------------------*/
if (!function_exists('xlog_install')) {
    function xlog_install() {
        if (!function_exists('sql_query')) return;

        sql_query("CREATE TABLE IF NOT EXISTS g5_activity_log (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            mb_id VARCHAR(20) NOT NULL,
            act_type VARCHAR(32) NOT NULL,
            bo_table VARCHAR(50) DEFAULT NULL,
            wr_id INT DEFAULT NULL,
            cm_id INT DEFAULT NULL,
            uri VARCHAR(255) DEFAULT NULL,
            method VARCHAR(10) DEFAULT NULL,
            ip VARBINARY(16) NOT NULL,
            user_agent VARCHAR(255) DEFAULT NULL,
            referer VARCHAR(255) DEFAULT NULL,
            result VARCHAR(50) DEFAULT NULL,
            is_deleted TINYINT(1) NOT NULL DEFAULT 0,
            extra LONGTEXT NULL
        ) DEFAULT CHARSET=".xlog_db_charset()."".xlog_db_engine_sql()."", false);

        sql_query("CREATE TABLE IF NOT EXISTS g5_activity_audit (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            admin_id VARCHAR(20) NOT NULL,
            action VARCHAR(16) NOT NULL,
            target_menu_key VARCHAR(40) NOT NULL,
            filter LONGTEXT NULL,
            affected_count INT DEFAULT 0,
            ip VARBINARY(16) NOT NULL,
            user_agent VARCHAR(255) DEFAULT NULL
        ) DEFAULT CHARSET=".xlog_db_charset()."".xlog_db_engine_sql()."", false);

        sql_query("CREATE TABLE IF NOT EXISTS g5_activity_conf (
            k VARCHAR(64) PRIMARY KEY,
            v TEXT NOT NULL
        ) DEFAULT CHARSET=".xlog_db_charset()."".xlog_db_engine_sql()."", false);

        sql_query("CREATE TABLE IF NOT EXISTS g5_activity_member_diff (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            mb_id VARCHAR(20) NOT NULL,
            admin_id VARCHAR(20) DEFAULT NULL
        ) DEFAULT CHARSET=".xlog_db_charset()."".xlog_db_engine_sql()."", false);

        sql_query("CREATE TABLE IF NOT EXISTS g5_activity_member_diff_row (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            diff_id BIGINT UNSIGNED NOT NULL,
            col_name VARCHAR(64) NOT NULL,
            old_value TEXT NULL,
            new_value TEXT NULL,
            INDEX (diff_id),
            CONSTRAINT fk_member_diff FOREIGN KEY (diff_id) REFERENCES g5_activity_member_diff(id) ON DELETE CASCADE
        ) DEFAULT CHARSET=".xlog_db_charset()."".xlog_db_engine_sql()."", false);

        // indices
        $idx = sql_fetch("SHOW INDEX FROM g5_activity_log WHERE Key_name='idx_act_created'");
        if (!$idx) sql_query("CREATE INDEX idx_act_created ON g5_activity_log (created_at)");
        $idx = sql_fetch("SHOW INDEX FROM g5_activity_log WHERE Key_name='idx_act_mb'");
        if (!$idx) sql_query("CREATE INDEX idx_act_mb ON g5_activity_log (mb_id, created_at)");
        $idx = sql_fetch("SHOW INDEX FROM g5_activity_log WHERE Key_name='idx_act_type'");
        if (!$idx) sql_query("CREATE INDEX idx_act_type ON g5_activity_log (act_type, created_at)");
        $idx = sql_fetch("SHOW INDEX FROM g5_activity_log WHERE Key_name='idx_act_target'");
        if (!$idx) sql_query("CREATE INDEX idx_act_target ON g5_activity_log (bo_table, wr_id)");
        $idx = sql_fetch("SHOW INDEX FROM g5_activity_log WHERE Key_name='idx_act_deleted'");
        if (!$idx) sql_query("CREATE INDEX idx_act_deleted ON g5_activity_log (is_deleted, created_at)");

        // defaults
        $defaults = array(
            'retain_days' => '90',
            'max_rows' => '500000',
            'log_admin_pages' => '0',
            'enable_login'=>'1','enable_login_fail'=>'1','enable_logout'=>'1','enable_member_update'=>'1',
            'enable_post_write'=>'1','enable_post_edit'=>'1','enable_post_delete'=>'1',
            'enable_comment_write'=>'1','enable_comment_edit'=>'1','enable_comment_delete'=>'1',
            'enable_page_view'=>'1',
            'installed_version' => XLOG_VERSION
        );
        foreach ($defaults as $k=>$v) {
            sql_query("INSERT IGNORE INTO g5_activity_conf (k,v) VALUES ('".sql_escape_string($k)."','".sql_escape_string($v)."')");
        }
    }
}
xlog_install();

/*--------------------------------------------------------------
  Trigger auto-install (no DECLARE; matches 5.6 schema; excludes mb_icon)
--------------------------------------------------------------*/
if (!function_exists('xlog_install_member_trigger')) {
    function xlog_install_member_trigger() {
        if (!function_exists('sql_fetch') || !function_exists('sql_query')) return true;
        $member_table = isset($GLOBALS['g5']['member_table']) ? $GLOBALS['g5']['member_table'] : 'g5_member';
        $tg_name = 'trg_xlog_before_update_'.preg_replace('/[^a-zA-Z0-9_]/','_', $member_table);
        $has = sql_fetch("SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = DATABASE() AND TRIGGER_NAME = '".sql_escape_string($tg_name)."'", false);
        if ($has && !empty($has['TRIGGER_NAME'])) return true;

        $sql = "
CREATE TRIGGER `{$tg_name}`
BEFORE UPDATE ON `{$member_table}`
FOR EACH ROW
BEGIN
  SET @xlog_last_diff_id := NULL;

  IF NOT (OLD.mb_nick <=> NEW.mb_nick) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_nick',OLD.mb_nick,NEW.mb_nick);
  END IF;

  IF NOT (OLD.mb_email <=> NEW.mb_email) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_email',OLD.mb_email,NEW.mb_email);
  END IF;

  IF NOT (OLD.mb_level <=> NEW.mb_level) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_level',OLD.mb_level,NEW.mb_level);
  END IF;

  IF NOT (OLD.mb_hp <=> NEW.mb_hp) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_hp',OLD.mb_hp,NEW.mb_hp);
  END IF;

  IF NOT (OLD.mb_tel <=> NEW.mb_tel) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_tel',OLD.mb_tel,NEW.mb_tel);
  END IF;

  IF NOT (OLD.mb_zip1 <=> NEW.mb_zip1) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_zip1',OLD.mb_zip1,NEW.mb_zip1);
  END IF;

  IF NOT (OLD.mb_zip2 <=> NEW.mb_zip2) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_zip2',OLD.mb_zip2,NEW.mb_zip2);
  END IF;

  IF NOT (OLD.mb_addr1 <=> NEW.mb_addr1) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_addr1',OLD.mb_addr1,NEW.mb_addr1);
  END IF;

  IF NOT (OLD.mb_addr2 <=> NEW.mb_addr2) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_addr2',OLD.mb_addr2,NEW.mb_addr2);
  END IF;

  IF NOT (OLD.mb_addr3 <=> NEW.mb_addr3) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_addr3',OLD.mb_addr3,NEW.mb_addr3);
  END IF;

  IF NOT (OLD.mb_addr_jibeon <=> NEW.mb_addr_jibeon) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_addr_jibeon',OLD.mb_addr_jibeon,NEW.mb_addr_jibeon);
  END IF;

  IF NOT (OLD.mb_homepage <=> NEW.mb_homepage) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_homepage',OLD.mb_homepage,NEW.mb_homepage);
  END IF;

  IF NOT (OLD.mb_open <=> NEW.mb_open) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_open',OLD.mb_open,NEW.mb_open);
  END IF;

  IF NOT (OLD.mb_mailling <=> NEW.mb_mailling) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_mailling',OLD.mb_mailling,NEW.mb_mailling);
  END IF;

  IF NOT (OLD.mb_sms <=> NEW.mb_sms) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_sms',OLD.mb_sms,NEW.mb_sms);
  END IF;

  IF NOT (OLD.mb_certify <=> NEW.mb_certify) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_certify',OLD.mb_certify,NEW.mb_certify);
  END IF;

  IF NOT (OLD.mb_birth <=> NEW.mb_birth) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_birth',OLD.mb_birth,NEW.mb_birth);
  END IF;

  IF NOT (OLD.mb_sex <=> NEW.mb_sex) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_sex',OLD.mb_sex,NEW.mb_sex);
  END IF;

  IF NOT (OLD.mb_signature <=> NEW.mb_signature) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_signature',OLD.mb_signature,NEW.mb_signature);
  END IF;

  IF NOT (OLD.mb_profile <=> NEW.mb_profile) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row VALUES (NULL,@xlog_last_diff_id,'mb_profile',OLD.mb_profile,NEW.mb_profile);
  END IF;

  -- sensitive: do not store actual values
  IF NOT (OLD.mb_password <=> NEW.mb_password) THEN
    IF @xlog_last_diff_id IS NULL THEN
      INSERT INTO g5_activity_member_diff (created_at, mb_id, admin_id)
      VALUES (NOW(), NEW.mb_id, IFNULL(@xlog_admin_id, '')); SET @xlog_last_diff_id := LAST_INSERT_ID();
    END IF;
    INSERT INTO g5_activity_member_diff_row (diff_id,col_name,old_value,new_value)
    VALUES (@xlog_last_diff_id,'mb_password',NULL,'[CHANGED]');
  END IF;

  -- if nothing changed, clear linkage
  IF @xlog_last_diff_id IS NULL THEN
    SET @xlog_last_diff_id := NULL;
  END IF;
END";
        $ok = @sql_query($sql, false);
        if (!$ok && defined('G5_DATA_PATH')) {
            @file_put_contents(G5_DATA_PATH.'/xlog_trigger_error.log', date('c')." CREATE TRIGGER FAIL\n".$sql."\n", FILE_APPEND);
        }
        return (bool)$ok;
    }
}

// common_header 훅이 없거나 add_event 미정의면 즉시 종료
if (!function_exists('add_event')) return;

// 설치 훅

/* XLOG: Named callback wrappers to avoid closure incompat with old hook engine */

if (!function_exists('xlog_common_header_install_cb')) {
    function xlog_common_header_install_cb() {

    xlog_install_member_trigger(); // check & create if missing

    }
}

if (!function_exists('xlog_common_header_set_admin_cb')) {
    function xlog_common_header_set_admin_cb() {

    global $member;
    if (!empty($member['mb_id'])) {{
        @sql_query("SET @xlog_admin_id = '".sql_escape_string($member['mb_id'])."'", false);
    }}

    }
}

if (!function_exists('xlog_common_header_pageview_cb')) {
    function xlog_common_header_pageview_cb() {

    global $member;
    xlog_rotate();
    if (!$member['mb_id']) return;
    $conf = xlog_conf();
    $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
    if (!$conf['log_admin_pages'] && strpos($uri, '/adm/') !== false) return;
    if (!xlog_can('page_view')) return;
    if (preg_match('~\\.(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2?)($|\\?)~i', $uri)) return;
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id' => $member['mb_id'],
        'act_type' => 'page_view',
        'uri' => substr($uri, 0, 255),
        'method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET',
        'ip' => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
        'referer' => substr(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 0, 255),
    ));

    }
}

if (!function_exists('xlog_member_login_check_cb')) {
    function xlog_member_login_check_cb($mb, $link, $is_social_login) {

    if (!xlog_can('login')) return;
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id' => $mb['mb_id'],
        'act_type' => 'login',
        'result' => $is_social_login ? 'social' : 'local',
        'ip' => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
    ));

    }
}

if (!function_exists('xlog_password_is_wrong_cb')) {
    function xlog_password_is_wrong_cb($type, $data, $qstr='') {

    if (!xlog_can('login_fail')) return;
    $mb_id = '';
    if ($type === 'login' && is_array($data) && isset($data['mb_id'])) $mb_id = $data['mb_id'];
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id' => $mb_id ?: 'unknown',
        'act_type' => 'login_fail',
        'result' => $type,
        'ip' => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
        'extra' => array('qstr'=>$qstr),
    ));

    }
}

if (!function_exists('xlog_member_logout_cb')) {
    function xlog_member_logout_cb($link) {

    global $member;
    if (!xlog_can('logout')) return;
    if (!$member['mb_id']) return;
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id' => $member['mb_id'],
        'act_type' => 'logout',
        'ip' => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
    ));

    }
}

if (!function_exists('xlog_sql_after_member_update_cb')) {
    function xlog_sql_after_member_update_cb($result, $sql) {

    static $logged_member_update = false;
    if ($logged_member_update) return;

    $member_table = isset($GLOBALS['g5']['member_table']) ? $GLOBALS['g5']['member_table'] : 'g5_member';
    $re = '~^\\s*update\\s+`?'.preg_quote($member_table, '~').'`?\\s+set~i';
    if (!preg_match($re, $sql)) return;

    $did_row = sql_fetch("SELECT @xlog_last_diff_id AS did", false);
    $did = isset($did_row['did']) ? (int)$did_row['did'] : 0;
    if ($did <= 0) return; // only record when trigger produced a diff

    $logged_member_update = true;
    if (!xlog_can('member_update')) return;
        $diff = sql_fetch("SELECT mb_id, admin_id FROM g5_activity_member_diff WHERE id=".(int)$did, false);
    $target_mb_id = isset($diff['mb_id']) ? $diff['mb_id'] : (isset($_POST['mb_id']) ? $_POST['mb_id'] : (isset($GLOBALS['member']['mb_id']) ? $GLOBALS['member']['mb_id'] : ''));
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id'      => $target_mb_id,
        'act_type'   => 'member_update',
        'ip'         => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
        'extra'      => array('diff_id'=>$did)
    ));

    }
}

if (!function_exists('xlog_write_update_after_cb')) {
    function xlog_write_update_after_cb($board, $wr_id, $w, $qstr=null, $redirect_url=null) {

    global $member;
    $type = (in_array($w, array('u','update','modify')) ? 'post_edit' : 'post_write');
    if (!xlog_can($type)) return;
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id' => isset($member['mb_id']) ? $member['mb_id'] : '',
        'act_type' => $type,
        'bo_table' => isset($board['bo_table']) ? $board['bo_table'] : '',
        'wr_id' => (int)$wr_id,
        'ip' => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
    ));

    }
}

if (!function_exists('xlog_bbs_delete_cb')) {
    function xlog_bbs_delete_cb($write, $board) {

    global $member;
    if (!xlog_can('post_delete')) return;
    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id' => isset($member['mb_id']) ? $member['mb_id'] : '',
        'act_type' => 'post_delete',
        'bo_table' => isset($board['bo_table']) ? $board['bo_table'] : '',
        'wr_id' => (int)(isset($write['wr_id']) ? $write['wr_id'] : 0),
        'ip' => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
    ));

    }
}

if (!function_exists('xlog_sql_after_comment_crud_cb')) {
    function xlog_sql_after_comment_crud_cb($result, $sql) {

    if (!preg_match('~\\bg5_write_([a-z0-9_]+)\\b~i', $sql, $m)) return;
    $bo_table = $m[1];
    $is_comment = (stripos($sql,'wr_is_comment') !== false) || (stripos($sql,' where ') !== false && stripos($sql,'wr_parent') !== false);
    if (!$is_comment) return;

    $act = null;
    if (stripos($sql,'insert ')===0) $act='comment_write';
    elseif (stripos($sql,'update ')===0) $act='comment_edit';
    elseif (stripos($sql,'delete ')===0) $act='comment_delete';
    if (!$act || !xlog_can($act)) return;

    $wr_id = 0; $cm_id = 0;
    $write_table = $GLOBALS['g5']['write_prefix'].$bo_table;

    if (preg_match('~\\bwr_id\\s*=\\s*([0-9]+)~i', $sql, $mm)) {{
        $cm_id = (int)$mm[1];
        $parent = sql_fetch("SELECT wr_parent FROM {{$write_table}} WHERE wr_id={{$cm_id}}");
        if ($parent && $parent['wr_parent']) $wr_id = (int)$parent['wr_parent'];
    }} else {{
        global $member;
        $mbid = isset($member['mb_id']) ? $member['mb_id'] : '';
        $guess = sql_fetch("
          SELECT wr_id, wr_parent FROM {{$write_table}}
          WHERE wr_is_comment=1
            AND mb_id='".sql_escape_string($mbid)."'
            AND wr_datetime >= DATE_SUB(NOW(), INTERVAL 3 SECOND)
          ORDER BY wr_id DESC LIMIT 1
        ");
        if ($guess) {{ $cm_id = (int)$guess['wr_id']; $wr_id = (int)$guess['wr_parent']; }}
    }}

    xlog_insert(array(
        'created_at' => xlog_now(),
        'mb_id'      => isset($GLOBALS['member']['mb_id']) ? $GLOBALS['member']['mb_id'] : '',
        'act_type'   => $act,
        'bo_table'   => $bo_table,
        'wr_id'      => $wr_id ?: 0,
        'cm_id'      => $cm_id ?: 0,
        'ip'         => xlog_ip_bin(),
        'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255),
        'extra'      => array('sql_hint'=>substr($sql,0,120))
    ));

    }
}

/* End of XLOG callback wrappers */
add_event('common_header', 'xlog_common_header_install_cb', 10, 1);

/*--------------------------------------------------------------
  Utilities
--------------------------------------------------------------*/
if (!function_exists('xlog_ip_bin')) {
    function xlog_ip_bin($ip=null) { $ip = $ip ?: ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); return @inet_pton($ip); }
}
if (!function_exists('xlog_now')) {
    function xlog_now() { return date('Y-m-d H:i:s'); }
}
if (!function_exists('xlog_insert')) {
    function xlog_insert($row) {
        if (!function_exists('sql_query')) return;
        // ★ 댓글 이벤트는 "댓글 컨텍스트"에서만 허용 (게시글 수정 중 오탐 방지)
        if (isset($row['act_type']) && strpos($row['act_type'], 'comment_') === 0) {
            if (empty($GLOBALS['XLOG_COMMENT_CTX'])) {
                return; // 댓글 컨텍스트가 아니면 SQL 훅에서 올라온 comment_*는 차단
            }
        }
        $tbl = 'g5_activity_log'; $keys = array(); $vals = array();
        foreach ($row as $k=>$v) {
            $keys[] = $k;
            if (is_null($v)) $vals[] = 'NULL';
            else if ($k === 'ip') $vals[] = "UNHEX('".bin2hex($v)."')";
            else $vals[] = "'".sql_escape_string(is_array($v)? json_encode($v, JSON_UNESCAPED_UNICODE) : $v)."'";
        }
        sql_query("INSERT INTO {$tbl} (".implode(',',$keys).") VALUES (".implode(',',$vals).")", false);
    }
}
if (!function_exists('xlog_opt_get')) {
    function xlog_opt_get($k, $default=null) {
        $row = sql_fetch("SELECT v FROM g5_activity_conf WHERE k='".sql_escape_string($k)."'");
        return ($row && isset($row['v'])) ? $row['v'] : $default;
    }
}
if (!function_exists('xlog_conf')) {
    function xlog_conf() {
        $en = array('login'=>1,'login_fail'=>1,'logout'=>1,'member_update'=>1,'post_write'=>1,'post_edit'=>1,'post_delete'=>1,'comment_write'=>1,'comment_edit'=>1,'comment_delete'=>1,'page_view'=>1);
        foreach ($en as $k=>$v) $en[$k] = (int) xlog_opt_get('enable_'.$k, $v);
        return array('enable'=>$en, 'retain_days'=>(int)xlog_opt_get('retain_days',90),'max_rows'=>(int)xlog_opt_get('max_rows',500000),'log_admin_pages'=>(int)xlog_opt_get('log_admin_pages',0));
    }
}
if (!function_exists('xlog_can')) { function xlog_can($t){ $c=xlog_conf(); return !empty($c['enable'][$t]); } }
if (!function_exists('xlog_rotate')) {
    function xlog_rotate() {
        static $done = false; if ($done) return; $done = true;
        $c = xlog_conf();
        if ($c['retain_days'] > 0) sql_query("DELETE FROM g5_activity_log WHERE created_at < DATE_SUB(NOW(), INTERVAL ".(int)$c['retain_days']." DAY)", false);
        if ($c['max_rows'] > 0) {
            $row = sql_fetch("SELECT COUNT(*) AS cnt FROM g5_activity_log");
            $over = (int)$row['cnt'] - (int)$c['max_rows'];
            if ($over > 0) sql_query("DELETE FROM g5_activity_log ORDER BY created_at ASC LIMIT ".(int)$over, false);
        }
    }
}

/*--------------------------------------------------------------
  Pass current admin mb_id to trigger context
--------------------------------------------------------------*/
add_event('common_header', 'xlog_common_header_set_admin_cb', 10, 1);

/*--------------------------------------------------------------
  Page view (members only, optional)
--------------------------------------------------------------*/
add_event('common_header', 'xlog_common_header_pageview_cb', 10, 1);

/*--------------------------------------------------------------
  Login / Logout / Fail
--------------------------------------------------------------*/
add_event('member_login_check', 'xlog_member_login_check_cb', 10, 3);

add_event('password_is_wrong', 'xlog_password_is_wrong_cb', 10, 3);

add_event('member_logout', 'xlog_member_logout_cb', 10, 1);

/*--------------------------------------------------------------
  Member update log (only if trigger produced diff_id)
--------------------------------------------------------------*/
add_event('sql_query_after', 'xlog_sql_after_member_update_cb', 10, 2);

/*--------------------------------------------------------------
  Post / Comment CRUD
--------------------------------------------------------------*/
add_event('write_update_after', 'xlog_write_update_after_cb', 10, 5);

add_event('bbs_delete', 'xlog_bbs_delete_cb', 10, 2);

add_event('sql_query_after', 'xlog_sql_after_comment_crud_cb', 10, 2);

/* === XLOG: script-based comment edit/delete detection (additive, safe) === */
(function(){
    $base = basename(isset($_SERVER['SCRIPT_NAME'])?$_SERVER['SCRIPT_NAME']:'');

    // helper to read request vars
    $req = function($k,$d=''){ return isset($_POST[$k]) ? $_POST[$k] : (isset($_GET[$k])? $_GET[$k] : $d); };

    if ($base === 'write_comment_update.php') {
        // treat presence of comment_id as EDIT; inserts are already handled by SQL hook
        $bo = $req('bo_table','');
        $cm = (int)$req('comment_id', (int)$req('cm_id', 0));
        if ($bo && $cm>0 && xlog_can('comment_edit')) {
            $write = $GLOBALS['g5']['write_prefix'].$bo;
            $row = sql_fetch("SELECT wr_parent FROM `{$write}` WHERE wr_id={$cm}");
            $wr = $row && isset($row['wr_parent']) ? (int)$row['wr_parent'] : 0;
            register_shutdown_function(function() use ($bo,$wr,$cm){
                xlog_insert(array(
                    'created_at' => xlog_now(),
                    'mb_id'      => isset($GLOBALS['member']['mb_id']) ? $GLOBALS['member']['mb_id'] : '',
                    'act_type'   => 'comment_edit',
                    'bo_table'   => $bo,
                    'wr_id'      => $wr,
                    'cm_id'      => $cm,
                    'ip'         => xlog_ip_bin(),
                    'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255)
                ));
            });
        }
    }
    else if ($base === 'delete_comment.php') {
        $bo = $req('bo_table','');
        $cm = (int)$req('comment_id', (int)$req('cm_id', 0));
        if ($bo && $cm>0 && xlog_can('comment_delete')) {
            $write = $GLOBALS['g5']['write_prefix'].$bo;
            // prefetch parent BEFORE deletion
            $row = sql_fetch("SELECT wr_parent FROM `{$write}` WHERE wr_id={$cm}");
            $wr = $row && isset($row['wr_parent']) ? (int)$row['wr_parent'] : 0;
            register_shutdown_function(function() use ($bo,$wr,$cm){
                xlog_insert(array(
                    'created_at' => xlog_now(),
                    'mb_id'      => isset($GLOBALS['member']['mb_id']) ? $GLOBALS['member']['mb_id'] : '',
                    'act_type'   => 'comment_delete',
                    'bo_table'   => $bo,
                    'wr_id'      => $wr,
                    'cm_id'      => $cm,
                    'ip'         => xlog_ip_bin(),
                    'user_agent' => substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255)
                ));
            });
        }
    }
})();
/* === /XLOG: script-based comment edit/delete detection =================== */


/* === XLOG: robust logout detection (session snapshot + shutdown) ========= */
(function(){
    $base = basename(isset($_SERVER['SCRIPT_NAME'])?$_SERVER['SCRIPT_NAME']:'');
    if ($base !== 'logout.php') return;

    // 1) capture mb_id BEFORE session is cleared
    $xlog_logout_id = null;
    if (function_exists('get_session')) {
        $xlog_logout_id = get_session('ss_mb_id');
    }
    if (!$xlog_logout_id && isset($GLOBALS['member']['mb_id']) && $GLOBALS['member']['mb_id'] !== '') {
        $xlog_logout_id = $GLOBALS['member']['mb_id'];
    }
    if (!$xlog_logout_id && isset($_SESSION['ss_mb_id']) && $_SESSION['ss_mb_id'] !== '') {
        $xlog_logout_id = $_SESSION['ss_mb_id'];
    }

    // 2) after logout completes, write one record (dedupe 3s)
    if ($xlog_logout_id) {
        register_shutdown_function(function() use ($xlog_logout_id){
            if (!function_exists('sql_query')) return;
            if (function_exists('xlog_can') && !xlog_can('logout')) return;
            $mb = sql_escape_string($xlog_logout_id);
            $dup = sql_fetch("SELECT id FROM g5_activity_log
                               WHERE act_type='logout' AND mb_id='{$mb}'
                                 AND created_at >= (NOW() - INTERVAL 3 SECOND)
                               ORDER BY id DESC LIMIT 1");
            if ($dup && isset($dup['id'])) return;
            $ua = substr(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 0, 255);
            xlog_insert(array(
                'created_at' => function_exists('xlog_now') ? xlog_now() : date('Y-m-d H:i:s'),
                'mb_id'      => $xlog_logout_id,
                'act_type'   => 'logout',
                'bo_table'   => '',
                'wr_id'      => 0,
                'cm_id'      => 0,
                'ip'         => function_exists('xlog_ip_bin') ? xlog_ip_bin() : null,
                'user_agent' => $ua
            ));
        });
    }
})();
/* === /XLOG: robust logout detection ===================================== */

?>