| 1 |
<?php |
| 2 |
/** Local-only HTTP fixture; mounted exclusively by the webserver test stack. */ |
| 3 |
defined('ABSPATH') || exit; |
| 4 |
if (wp_get_environment_type() !== 'local' || !getenv('MATRIX_TOKEN')) return; |
| 5 |
|
| 6 |
// Keep loopbacks real; refuse remote APIs, email and update checks. |
| 7 |
add_filter('pre_http_request', static function ($preempt, $args, $url) { |
| 8 |
if (wp_parse_url($url, PHP_URL_HOST) === wp_parse_url(home_url(), PHP_URL_HOST)) return $preempt; |
| 9 |
return new WP_Error('matrix_external_http_blocked', 'Only local matrix loopbacks are allowed.'); |
| 10 |
}, PHP_INT_MAX, 3); |
| 11 |
add_filter('pre_wp_mail', '__return_false'); |
| 12 |
add_filter('sync_basalam_jobs_runner_async_time_limit', static function () { return 1.0; }); |
| 13 |
|
| 14 |
add_action('plugins_loaded', static function () { |
| 15 |
if (!function_exists('syncBasalamContainer')) return; |
| 16 |
|
| 17 |
$fixture = new class implements \SyncBasalam\Jobs\JobType { |
| 18 |
public function getType(): string { return 'sync_basalam_matrix'; } |
| 19 |
public function getPriority(): int { return -100; } |
| 20 |
public function canRun(): bool { return true; } |
| 21 |
public function execute(array $payload) { |
| 22 |
$key = $payload['key']; |
| 23 |
$events = get_option('sync_basalam_matrix_events', []); |
| 24 |
$event = $events[$key] ?? ['starts' => 0, 'returns' => 0]; |
| 25 |
$event['starts']++; |
| 26 |
$event['pid'] = getmypid(); |
| 27 |
$event['sapi'] = PHP_SAPI; |
| 28 |
$event['noabort'] = $_SERVER['noabort'] ?? $_SERVER['REDIRECT_noabort'] ?? getenv('noabort') ?: null; |
| 29 |
$event['started_at'] = microtime(true); |
| 30 |
$job = syncBasalamContainer()->get(\SyncBasalam\JobManager::class)->getJob(['job_type' => 'sync_basalam_matrix', 'status' => 'processing']); |
| 31 |
$event['history'][] = ['pid' => getmypid(), 'time' => $event['started_at'], 'job_id' => (int) $job->id, 'attempts' => (int) $job->attempts]; |
| 32 |
$events[$key] = $event; |
| 33 |
update_option('sync_basalam_matrix_events', $events, false); |
| 34 |
usleep((int) ($payload['seconds'] * 1000000)); |
| 35 |
if ($payload['mode'] === 'fatal') trigger_error('Intentional matrix worker fatal', E_USER_ERROR); |
| 36 |
$events = get_option('sync_basalam_matrix_events', []); |
| 37 |
$events[$key]['returns']++; |
| 38 |
$events[$key]['returned_at'] = microtime(true); |
| 39 |
update_option('sync_basalam_matrix_events', $events, false); |
| 40 |
if ($payload['mode'] === 'reject') { |
| 41 |
return \SyncBasalam\Jobs\JobResult::failed(new \SyncBasalam\Jobs\Exceptions\NonRetryableException('matrix API rejection (422)', 422)); |
| 42 |
} |
| 43 |
return \SyncBasalam\Jobs\JobResult::success(); |
| 44 |
} |
| 45 |
}; |
| 46 |
syncBasalamContainer()->get(\SyncBasalam\Jobs\JobRegistry::class)->register($fixture); |
| 47 |
|
| 48 |
// Control requests inspect/prepare evidence without accidentally restarting a worker. |
| 49 |
if (($_REQUEST['action'] ?? '') === 'sync_basalam_matrix_control' || defined('WP_INSTALLING')) { |
| 50 |
$runner = syncBasalamContainer()->get(\SyncBasalam\JobsRunner::class); |
| 51 |
remove_action('shutdown', [$runner, 'maybeDispatchAsyncRequest'], 2); |
| 52 |
remove_action('sync_basalam_job_created', [$runner, 'maybeDispatchAsyncRequest'], 10); |
| 53 |
} |
| 54 |
}); |
| 55 |
|
| 56 |
$control = static function () { |
| 57 |
if (!hash_equals(getenv('MATRIX_TOKEN'), $_SERVER['HTTP_X_MATRIX_TOKEN'] ?? '')) { |
| 58 |
wp_send_json_error(['message' => 'Invalid matrix token'], 403); |
| 59 |
} |
| 60 |
global $wpdb; |
| 61 |
$queue = syncBasalamContainer()->get(\SyncBasalam\JobManager::class); |
| 62 |
$runner = syncBasalamContainer()->get(\SyncBasalam\JobsRunner::class); |
| 63 |
$lock = syncBasalamContainer()->get(\SyncBasalam\Jobs\LockManager::class); |
| 64 |
$table = $wpdb->prefix . 'sync_basalam_job_manager'; |
| 65 |
$op = $_POST['op'] ?? 'state'; |
| 66 |
if ($op === 'reset') { |
| 67 |
if (!$lock->acquireGlobalJobsLock()) wp_send_json_error(['message' => 'Worker still active'], 409); |
| 68 |
$wpdb->delete($table, ['job_type' => 'sync_basalam_matrix']); |
| 69 |
foreach (['sync_basalam_async_worker_health', 'sync_basalam_matrix_events', 'sync_basalam_jobs_runner_last_run', 'sync_basalam_circuit_breaker'] as $name) delete_option($name); |
| 70 |
foreach (['sync_basalam_jobs_runner_async_dispatch_lock', 'sync_basalam_jobs_runner_idle_probe_lock'] as $name) delete_transient($name); |
| 71 |
$lock->releaseGlobalJobsLock(); |
| 72 |
} elseif ($op === 'enqueue') { |
| 73 |
$key = sanitize_key($_POST['key'] ?? 'job'); |
| 74 |
$mode = $_POST['mode'] ?? 'success'; |
| 75 |
if (!in_array($mode, ['success', 'reject', 'fatal'], true)) wp_send_json_error([], 400); |
| 76 |
$seconds = max(0.0, min(60.0, (float) ($_POST['seconds'] ?? 0))); |
| 77 |
$queue->createJob('sync_basalam_matrix', 'pending', wp_json_encode(compact('key', 'mode', 'seconds'))); |
| 78 |
$id = (int) $wpdb->insert_id; |
| 79 |
update_option('sync_basalam_jobs_runner_last_run', 0, false); |
| 80 |
if (!empty($_POST['dispatch'])) $runner->maybeDispatchAsyncRequest(); |
| 81 |
wp_send_json_success(['id' => $id]); |
| 82 |
} elseif (in_array($op, ['age', 'retry'], true)) { |
| 83 |
$id = (int) ($_POST['id'] ?? 0); |
| 84 |
$job = $queue->getJob(['id' => $id, 'job_type' => 'sync_basalam_matrix']); |
| 85 |
if (!$job) wp_send_json_error([], 404); |
| 86 |
$values = ['started_at' => time() - 121]; |
| 87 |
if ($op === 'retry') { |
| 88 |
$payload = json_decode($job->payload, true); |
| 89 |
$payload['mode'] = 'success'; |
| 90 |
$payload['seconds'] = 0; |
| 91 |
$values['payload'] = wp_json_encode($payload); |
| 92 |
$values['retry_after'] = 0; |
| 93 |
update_option('sync_basalam_jobs_runner_last_run', 0, false); |
| 94 |
} |
| 95 |
$queue->updateJob($values, ['id' => $id]); |
| 96 |
} elseif ($op === 'lease') { |
| 97 |
$name = 'sync_basalam_jobs_runner_async_dispatch_lock'; |
| 98 |
delete_transient($name); |
| 99 |
add_option('_transient_' . $name, 1, '', false); |
| 100 |
delete_option('_transient_timeout_' . $name); |
| 101 |
} elseif ($op === 'dispatch') { |
| 102 |
$runner->maybeDispatchAsyncRequest(); |
| 103 |
} elseif ($op === 'notice') { |
| 104 |
$_GET['page'] = 'sync_basalam'; |
| 105 |
if (($_POST['role'] ?? 'administrator') === 'anonymous') wp_set_current_user(0); |
| 106 |
else { |
| 107 |
$users = get_users(['role' => sanitize_key($_POST['role'] ?? 'administrator'), 'number' => 1]); |
| 108 |
if (!$users) wp_send_json_error([], 404); |
| 109 |
wp_set_current_user($users[0]->ID); |
| 110 |
} |
| 111 |
ob_start(); |
| 112 |
$runner->renderWorkerHealthNotice(); |
| 113 |
wp_send_json_success(['html' => ob_get_clean()]); |
| 114 |
} elseif ($op !== 'state') { |
| 115 |
wp_send_json_error([], 400); |
| 116 |
} |
| 117 |
$free = $lock->acquireGlobalJobsLock(); |
| 118 |
if ($free) $lock->releaseGlobalJobsLock(); |
| 119 |
$health = new \SyncBasalam\Services\AsyncWorkerHealth(); |
| 120 |
wp_send_json_success([ |
| 121 |
'nonce' => wp_create_nonce('sync_basalam_run_jobs_async'), |
| 122 |
'sapi' => PHP_SAPI, |
| 123 |
'php' => PHP_VERSION, |
| 124 |
'server' => $_SERVER['SERVER_SOFTWARE'] ?? '', |
| 125 |
'jobs' => $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE job_type = %s ORDER BY id", 'sync_basalam_matrix')), |
| 126 |
'events' => (object) get_option('sync_basalam_matrix_events', []), |
| 127 |
'health' => get_option(\SyncBasalam\Services\AsyncWorkerHealth::OPTION_KEY, []), |
| 128 |
'notice' => $health->getNotice(), |
| 129 |
'lock_free' => $free, |
| 130 |
'autoload' => $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name = %s", \SyncBasalam\Services\AsyncWorkerHealth::OPTION_KEY)), |
| 131 |
]); |
| 132 |
}; |
| 133 |
add_action('wp_ajax_sync_basalam_matrix_control', $control); |
| 134 |
add_action('wp_ajax_nopriv_sync_basalam_matrix_control', $control); |
| 135 |
|