<?php
/**
 * MJ Marketplace v3.0.4.2.7 Form Security + Duplicate Guard
 * Safe-by-default helper. It only blocks submissions that include _mj_form_guard=1,
 * which is automatically added by assets/js/mj-v30427-form-ux.js.
 */
if (!function_exists('mj427_table_exists')) {
function mj427_table_exists(string $table): bool {
    try { $r = one('SELECT COUNT(*) c FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=?', [$table]); return (int)($r['c'] ?? 0) > 0; }
    catch (Throwable $e) { return false; }
}}
if (!function_exists('mj427_column_exists')) {
function mj427_column_exists(string $table, string $column): bool {
    try { $r = one('SELECT COUNT(*) c FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=? AND COLUMN_NAME=?', [$table,$column]); return (int)($r['c'] ?? 0) > 0; }
    catch (Throwable $e) { return false; }
}}
if (!function_exists('mj427_ensure_security_schema')) {
function mj427_ensure_security_schema(): void {
    static $done = false; if ($done) return; $done = true;
    try { q("CREATE TABLE IF NOT EXISTS mj_form_tokens (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        session_id VARCHAR(128) NOT NULL,
        token_hash CHAR(64) NOT NULL,
        form_key VARCHAR(160) NULL,
        expires_at DATETIME NOT NULL,
        used_at DATETIME NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_session_token(session_id, token_hash), INDEX idx_expires(expires_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } catch (Throwable $e) {}
    try { q("CREATE TABLE IF NOT EXISTS mj_form_duplicate_guard (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        guard_hash CHAR(64) NOT NULL UNIQUE,
        form_key VARCHAR(160) NULL,
        user_type VARCHAR(30) NULL,
        user_id BIGINT UNSIGNED NULL,
        ip_address VARCHAR(80) NULL,
        user_agent VARCHAR(255) NULL,
        first_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        hit_count INT NOT NULL DEFAULT 1,
        INDEX idx_form_user(form_key,user_type,user_id), INDEX idx_last(last_seen_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } catch (Throwable $e) {}
    try { q("CREATE TABLE IF NOT EXISTS mj_form_rate_limits (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        rate_key CHAR(64) NOT NULL UNIQUE,
        form_key VARCHAR(160) NULL,
        ip_address VARCHAR(80) NULL,
        user_id BIGINT UNSIGNED NULL,
        request_count INT NOT NULL DEFAULT 1,
        window_start DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        last_request_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_window(window_start), INDEX idx_ip(ip_address)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } catch (Throwable $e) {}
    try { q("CREATE TABLE IF NOT EXISTS mj_form_submission_log (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        form_key VARCHAR(160) NULL,
        user_type VARCHAR(30) NULL,
        user_id BIGINT UNSIGNED NULL,
        ip_address VARCHAR(80) NULL,
        status VARCHAR(40) NOT NULL DEFAULT 'received',
        message VARCHAR(255) NULL,
        guard_hash CHAR(64) NULL,
        request_uri VARCHAR(500) NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_form_created(form_key,created_at), INDEX idx_status(status), INDEX idx_user(user_type,user_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } catch (Throwable $e) {}
    try { q("CREATE TABLE IF NOT EXISTS mj_security_events (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        event_type VARCHAR(80) NOT NULL,
        severity VARCHAR(30) NOT NULL DEFAULT 'info',
        message VARCHAR(255) NULL,
        user_type VARCHAR(30) NULL,
        user_id BIGINT UNSIGNED NULL,
        ip_address VARCHAR(80) NULL,
        user_agent VARCHAR(255) NULL,
        request_uri VARCHAR(500) NULL,
        meta_json LONGTEXT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_type_created(event_type,created_at), INDEX idx_severity(severity)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); } catch (Throwable $e) {}
}}
if (!function_exists('mj427_form_token')) {
function mj427_form_token(string $formKey='global'): string {
    if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); }
    if (empty($_SESSION['mj_form_token'])) { $_SESSION['mj_form_token'] = bin2hex(random_bytes(32)); }
    return (string)$_SESSION['mj_form_token'];
}}
if (!function_exists('mj427_token_meta')) {
function mj427_token_meta(): array { return ['name'=>'mj_form_token','value'=>mj427_form_token('global')]; }
}
if (!function_exists('mj427_is_ajax')) {
function mj427_is_ajax(): bool {
    $accept = (string)($_SERVER['HTTP_ACCEPT'] ?? '');
    return strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest' || stripos($accept, 'application/json') !== false || !empty($_POST['_mj_ajax']);
}}
if (!function_exists('mj427_user_context')) {
function mj427_user_context(): array {
    if (!empty($_SESSION['admin_id'])) return ['admin',(int)$_SESSION['admin_id']];
    if (!empty($_SESSION['user_id'])) return ['user',(int)$_SESSION['user_id']];
    return ['guest',0];
}}
if (!function_exists('mj427_security_event')) {
function mj427_security_event(string $type, string $message, string $severity='info', array $meta=[]): void {
    try {
        mj427_ensure_security_schema(); [$ut,$uid]=mj427_user_context();
        q('INSERT INTO mj_security_events(event_type,severity,message,user_type,user_id,ip_address,user_agent,request_uri,meta_json,created_at) VALUES(?,?,?,?,?,?,?,?,?,NOW())', [
            $type,$severity,substr($message,0,255),$ut,$uid,(string)($_SERVER['REMOTE_ADDR']??''),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,255),substr((string)($_SERVER['REQUEST_URI']??''),0,500),json_encode($meta,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)
        ]);
    } catch (Throwable $e) {}
}}
if (!function_exists('mj427_response_block')) {
function mj427_response_block(string $message, int $status=429): void {
    http_response_code($status);
    if (mj427_is_ajax()) {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['ok'=>false,'error'=>$message,'blocked'=>true], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        exit;
    }
    if (function_exists('flash')) { flash('error', $message); }
    $back = (string)($_SERVER['HTTP_REFERER'] ?? '');
    if ($back === '' || stripos($back, (string)($_SERVER['HTTP_HOST'] ?? '')) === false) { $back = function_exists('base_url') ? base_url('/') : '/'; }
    if (!headers_sent()) { header('Location: '.$back); exit; }
    echo '<div style="font-family:Arial;padding:20px;color:#b91c1c">'.htmlspecialchars($message,ENT_QUOTES,'UTF-8').'</div>'; exit;
}}
if (!function_exists('mj427_post_int_any')) {
function mj427_post_int_any(array $names): int {
    foreach ($names as $name) {
        if (isset($_POST[$name]) && is_scalar($_POST[$name])) {
            $v = (int)$_POST[$name];
            if ($v > 0) return $v;
        }
    }
    return 0;
}}
if (!function_exists('mj427_is_edit_submission')) {
function mj427_is_edit_submission(string $formKey=''): bool {
    // Edit forms must not be blocked by the generic duplicate-submission guard.
    // Real duplicate business records are checked by the save page using table data
    // and must exclude the current record ID.
    $id = mj427_post_int_any(['id','listing_id','company_id','deal_id','item_id','record_id','edit_id']);
    if ($id > 0) return true;
    $script = strtolower((string)($_SERVER['SCRIPT_NAME'] ?? ''));
    $uri = strtolower((string)($_SERVER['REQUEST_URI'] ?? ''));
    $key = strtolower($formKey);
    if (preg_match('~/(edit-[^/]+|[^/]+-edit|admin/[^/]+-edit)\.php~', $script.' '.$uri.' '.$key)) return true;
    if (preg_match('~(^|/)(edit|update)(-|_|/)~', $script.' '.$uri.' '.$key)) return true;
    return false;
}}
if (!function_exists('mj427_normalize_payload')) {
function mj427_normalize_payload(array $data): array {
    $skip = ['csrf_token','mj_form_token','_mj_form_guard','_mj_form_uid','_mj_form_started_at','_mj_ajax','submit','password','password_hash','new_password','confirm_password'];
    $out=[];
    foreach ($data as $k=>$v) {
        if (in_array((string)$k, $skip, true)) continue;
        if (is_array($v)) { $v = json_encode($v, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); }
        $v = trim((string)$v);
        if ($v === '') continue;
        $out[(string)$k] = mb_substr($v,0,500);
    }
    ksort($out);
    return $out;
}}
if (!function_exists('mj427_validate_guarded_post')) {
function mj427_validate_guarded_post(): void {
    if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') return;
    if (empty($_POST['_mj_form_guard'])) return; // safe compatibility: only guard enhanced forms.
    if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); }
    mj427_ensure_security_schema();
    $postedToken = (string)($_POST['mj_form_token'] ?? $_POST['csrf_token'] ?? '');
    $sessionToken = (string)($_SESSION['mj_form_token'] ?? $_SESSION['csrf_token'] ?? '');
    if ($sessionToken !== '' && $postedToken !== '' && !hash_equals($sessionToken, $postedToken)) {
        mj427_security_event('invalid_form_token','Invalid form token','warning');
        mj427_response_block('This form expired. Please refresh and try again.', 419);
    }
    $honeypot = trim((string)($_POST['website_url'] ?? $_POST['company_website_confirm'] ?? ''));
    if ($honeypot !== '') {
        mj427_security_event('honeypot_block','Bot honeypot field filled','warning');
        mj427_response_block('Unable to submit this form. Please try again.', 429);
    }
    [$ut,$uid] = mj427_user_context();
    $formKey = preg_replace('/[^a-zA-Z0-9_\.\-\/]/','', (string)($_POST['_mj_form_key'] ?? basename((string)($_SERVER['SCRIPT_NAME'] ?? 'form'))));
    $uri = substr((string)($_SERVER['REQUEST_URI'] ?? ''), 0, 500);
    $payload = mj427_normalize_payload($_POST);
    $hashBase = $formKey.'|'.$ut.'|'.$uid.'|'.($uid?'' : (string)($_SERVER['REMOTE_ADDR'] ?? '')).'|'.json_encode($payload, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
    $guardHash = hash('sha256', $hashBase);
    $isEditSubmission = mj427_is_edit_submission($formKey);
    try {
        if ($isEditSubmission) {
            // Allow saving/editing the current record repeatedly. This prevents edit pages
            // from showing: "This looks like the same submission again".
            // Duplicate record checks, when required, must be page-level checks that compare
            // against another record owned by the same user and exclude the current ID.
            q('INSERT INTO mj_form_submission_log(form_key,user_type,user_id,ip_address,status,message,guard_hash,request_uri,created_at) VALUES(?,?,?,?,?,?,?,?,NOW())', [$formKey,$ut,$uid,(string)($_SERVER['REMOTE_ADDR']??''),'edit_accepted','Edit form accepted - duplicate guard skipped for current record',$guardHash,$uri]);
            return;
        }
        $recent = one('SELECT id, hit_count, last_seen_at FROM mj_form_duplicate_guard WHERE guard_hash=? AND last_seen_at >= (NOW() - INTERVAL 10 MINUTE) LIMIT 1', [$guardHash]);
        if ($recent) {
            q('UPDATE mj_form_duplicate_guard SET hit_count=hit_count+1,last_seen_at=NOW() WHERE id=?', [(int)$recent['id']]);
            q('INSERT INTO mj_form_submission_log(form_key,user_type,user_id,ip_address,status,message,guard_hash,request_uri,created_at) VALUES(?,?,?,?,?,?,?,?,NOW())', [$formKey,$ut,$uid,(string)($_SERVER['REMOTE_ADDR']??''),'duplicate','Duplicate form submission blocked',$guardHash,$uri]);
            mj427_response_block('This looks like the same submission again. We already received it. Please wait before submitting again.', 409);
        }
        q('INSERT INTO mj_form_duplicate_guard(guard_hash,form_key,user_type,user_id,ip_address,user_agent,first_seen_at,last_seen_at) VALUES(?,?,?,?,?,?,NOW(),NOW()) ON DUPLICATE KEY UPDATE hit_count=hit_count+1,last_seen_at=NOW()', [$guardHash,$formKey,$ut,$uid,(string)($_SERVER['REMOTE_ADDR']??''),substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,255)]);
        q('INSERT INTO mj_form_submission_log(form_key,user_type,user_id,ip_address,status,message,guard_hash,request_uri,created_at) VALUES(?,?,?,?,?,?,?,?,NOW())', [$formKey,$ut,$uid,(string)($_SERVER['REMOTE_ADDR']??''),'received','Form accepted',$guardHash,$uri]);
    } catch (Throwable $e) { /* Do not break the website if guard table has a problem. */ }
}}
if (!function_exists('mj427_validate_upload_file')) {
function mj427_validate_upload_file(array $file, array $allowedExt=['jpg','jpeg','png','webp','pdf'], int $maxBytes=5242880): array {
    if (empty($file['name']) || (int)($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) return [true,''];
    if ((int)$file['error'] !== UPLOAD_ERR_OK) return [false,'Upload failed. Please try again.'];
    if ((int)($file['size'] ?? 0) > $maxBytes) return [false,'File is too large. Maximum size is '.round($maxBytes/1048576).'MB.'];
    $name = (string)$file['name']; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    if (!in_array($ext, $allowedExt, true)) return [false,'File type is not allowed.'];
    if (preg_match('/\.(php|phtml|phar|js|html?|svg|exe|sh|bat|cmd)$/i', $name)) return [false,'Unsafe file type blocked.'];
    return [true,''];
}}
try { mj427_validate_guarded_post(); } catch (Throwable $e) { /* never show fatal from security helper */ }
?>
