#!/usr/bin/env php params = $jsonParams; foreach ($headers as $k => $v) { $this->headers[strtolower((string) $k)] = (string) $v; } } public function get_method(): string { return $this->method; } public function get_route(): string { return $this->route; } public function get_json_params(): array { return $this->jsonParams; } public function set_param(string $key, mixed $value): void { $this->params[$key] = $value; } public function get_param(string $key): mixed { return $this->params[$key] ?? null; } public function get_header(string $name): string { return $this->headers[strtolower($name)] ?? ''; } } final class WPDB_Compat { public string $prefix = 'wp_'; public int $insert_id = 0; /** @var array>> */ private array $tables = []; /** @var array */ private array $autoIds = []; public function __construct(string $dbPath) { // In-memory emulation; path retained for compatibility. } public function get_charset_collate(): string { return ''; } public function prepare(string $query, mixed ...$args): string { $out = $query; foreach ($args as $arg) { $replacement = is_int($arg) ? (string) $arg : ("'" . str_replace("'", "''", (string) $arg) . "'"); $out = preg_replace('/%[dsf]/', $replacement, $out, 1) ?? $out; } return $out; } public function query(string $query): int|false { $trimmed = trim($query); if (preg_match('/^CREATE TABLE IF NOT EXISTS\s+([a-zA-Z0-9_]+)/i', $trimmed, $m)) { $table = $m[1]; $this->tables[$table] = $this->tables[$table] ?? []; $this->autoIds[$table] = $this->autoIds[$table] ?? 0; return 0; } return 0; } public function get_results(string $query): array { $q = trim($query); if (preg_match('/^SELECT \* FROM\s+([a-zA-Z0-9_]+)\s+ORDER BY\s+id\s+ASC$/i', $q, $m)) { $rows = $this->tables[$m[1]] ?? []; usort($rows, static fn(array $a, array $b): int => ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0))); return array_map(static fn(array $r): object => (object) $r, $rows); } if (preg_match('/^SELECT\s+\*\s+FROM\s+([a-zA-Z0-9_]+)\s+WHERE\s+id\s*=\s*(\d+)$/i', $q, $m)) { $row = $this->findById($m[1], (int) $m[2]); return $row ? [(object) $row] : []; } if (preg_match('/^SELECT\s+\*\s+FROM\s+([a-zA-Z0-9_]+)\s+WHERE\s+caldav_resource\s*=\s*\'([^\']*)\'$/i', $q, $m)) { $table = $m[1]; $needle = str_replace("''", "'", $m[2]); $rows = $this->tables[$table] ?? []; foreach ($rows as $row) { if ((string) ($row['caldav_resource'] ?? '') === $needle) { return [(object) $row]; } } return []; } if (preg_match('/^SELECT\s+occurrence_key\s+FROM\s+([a-zA-Z0-9_]+)\s+WHERE\s+event_id\s*=\s*(\d+)\s+AND\s+exception_type\s*=\s*\'([^\']+)\'$/i', $q, $m)) { $table = $m[1]; $eventId = (int) $m[2]; $type = $m[3]; $rows = $this->tables[$table] ?? []; $filtered = array_values(array_filter( $rows, static fn(array $r): bool => ((int) ($r['event_id'] ?? 0) === $eventId) && ((string) ($r['exception_type'] ?? '') === $type) )); return array_map( static fn(array $r): object => (object) ['occurrence_key' => (string) ($r['occurrence_key'] ?? '')], $filtered ); } return []; } public function get_row(string $query): ?object { $rows = $this->get_results($query); return $rows[0] ?? null; } public function get_var(string $query): mixed { $q = trim($query); if (preg_match('/^SHOW TABLES LIKE\s*\'([^\']+)\'$/i', $q, $m)) { $table = str_replace("''", "'", $m[1]); return array_key_exists($table, $this->tables) ? $table : null; } $row = $this->get_row($query); if (!$row) { return null; } $vars = get_object_vars($row); foreach ($vars as $value) { return $value; } return null; } public function insert(string $table, array $data, array $formats = []): int|false { $this->tables[$table] = $this->tables[$table] ?? []; $this->autoIds[$table] = $this->autoIds[$table] ?? 0; $id = ++$this->autoIds[$table]; $row = ['id' => $id] + $data; $this->tables[$table][] = $row; $this->insert_id = $id; return 1; } public function update(string $table, array $data, array $where, array $formats = [], array $whereFormats = []): int|false { $rows = $this->tables[$table] ?? []; $updated = 0; foreach ($rows as $i => $row) { if (!$this->matchesWhere($row, $where)) { continue; } $rows[$i] = array_merge($row, $data); $updated++; } $this->tables[$table] = $rows; return $updated; } public function delete(string $table, array $where, array $whereFormats = []): int|false { $rows = $this->tables[$table] ?? []; $kept = []; $deleted = 0; foreach ($rows as $row) { if ($this->matchesWhere($row, $where)) { $deleted++; continue; } $kept[] = $row; } $this->tables[$table] = $kept; return $deleted; } private function findById(string $table, int $id): ?array { $rows = $this->tables[$table] ?? []; foreach ($rows as $row) { if ((int) ($row['id'] ?? 0) === $id) { return $row; } } return null; } private function matchesWhere(array $row, array $where): bool { foreach ($where as $k => $v) { if (!array_key_exists($k, $row)) { return false; } if ((string) $row[$k] !== (string) $v) { return false; } } return true; } } $GLOBALS['wp_actions'] = []; $GLOBALS['wp_shortcodes'] = []; $GLOBALS['wp_routes'] = []; $GLOBALS['wp_options'] = []; $GLOBALS['wp_mail_outbox'] = []; $GLOBALS['wp_user'] = [ 'id' => 1, 'email' => 'admin@example.test', 'caps' => ['manage_options', 'edit_posts'], ]; $GLOBALS['wp_activation_hooks'] = []; $GLOBALS['wp_admin_menu'] = []; $GLOBALS['wp_options']['admin_email'] = 'admin@example.test'; function add_action(string $hook, callable $callback, int $priority = 10, int $acceptedArgs = 1): void { $GLOBALS['wp_actions'][$hook][] = $callback; } function do_action(string $hook, mixed ...$args): void { foreach (($GLOBALS['wp_actions'][$hook] ?? []) as $cb) { $cb(...$args); } } function add_shortcode(string $tag, callable $callback): void { $GLOBALS['wp_shortcodes'][$tag] = $callback; } function do_shortcode(string $content): string { if (preg_match('/\[([a-z0-9_\-]+)\]/i', $content, $m)) { $tag = $m[1]; if (isset($GLOBALS['wp_shortcodes'][$tag])) { return (string) call_user_func($GLOBALS['wp_shortcodes'][$tag]); } } return $content; } function register_rest_route(string $namespace, string $route, array $args): void { $GLOBALS['wp_routes'][] = [ 'namespace' => '/' . trim($namespace, '/'), 'route' => $route, 'args' => $args, ]; } function rest_do_request(WP_REST_Request $request): array|WP_Error { $method = strtoupper($request->get_method()); $path = '/' . trim($request->get_route(), '/'); foreach ($GLOBALS['wp_routes'] as $entry) { $namespace = $entry['namespace']; $routePattern = $entry['route']; $args = $entry['args']; $registeredMethods = strtoupper((string) ($args['methods'] ?? 'GET')); if ($registeredMethods !== $method) { continue; } $regex = '#^' . preg_quote($namespace, '#') . preg_replace('#/#', '\\/', $routePattern) . '$#'; if (!preg_match($regex, $path, $matches)) { continue; } foreach ($matches as $k => $v) { if (is_string($k)) { $request->set_param($k, $v); } } $perm = $args['permission_callback'] ?? '__return_true'; $allowed = is_callable($perm) ? (bool) call_user_func($perm, $request) : false; if (!$allowed) { return new WP_Error('forbidden', 'Forbidden', ['status' => 403]); } $cb = $args['callback'] ?? null; if (!is_callable($cb)) { return new WP_Error('server_error', 'Route callback missing', ['status' => 500]); } return call_user_func($cb, $request); } return new WP_Error('not_found', 'Route not found', ['status' => 404]); } function register_activation_hook(string $pluginFile, callable $callback): void { $GLOBALS['wp_activation_hooks'][$pluginFile] = $callback; } function register_deactivation_hook(string $pluginFile, callable $callback): void { $GLOBALS['wp_activation_hooks'][$pluginFile . ':deactivate'] = $callback; } function get_option(string $key, mixed $default = false): mixed { return array_key_exists($key, $GLOBALS['wp_options']) ? $GLOBALS['wp_options'][$key] : $default; } function update_option(string $key, mixed $value, bool $autoload = true): bool { $GLOBALS['wp_options'][$key] = $value; return true; } function delete_option(string $key): bool { unset($GLOBALS['wp_options'][$key]); return true; } function get_current_user_id(): int { return (int) ($GLOBALS['wp_user']['id'] ?? 0); } function current_user_can(string $capability): bool { return in_array($capability, (array) ($GLOBALS['wp_user']['caps'] ?? []), true); } function wp_verify_nonce(string $nonce, string $action): bool { return $nonce === 'ok:' . $action; } function wp_get_current_user(): object { return (object) ['user_email' => (string) ($GLOBALS['wp_user']['email'] ?? '')]; } function esc_html(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function __return_true(): bool { return true; } if (!function_exists('dbDelta')) { function dbDelta(string $sql): array { return []; } } function add_menu_page(string $pageTitle, string $menuTitle, string $capability, string $menuSlug, callable $callback): void { $GLOBALS['wp_admin_menu'][$menuSlug] = ['title' => $menuTitle, 'callback' => $callback]; } function add_submenu_page(string $parentSlug, string $pageTitle, string $menuTitle, string $capability, string $menuSlug, callable $callback): void { $GLOBALS['wp_admin_menu'][$menuSlug] = ['title' => $menuTitle, 'parent' => $parentSlug, 'callback' => $callback]; } function admin_url(string $path = ''): string { return '/wp-admin/' . ltrim($path, '/'); } function home_url(string $path = ''): string { return 'http://localhost' . (str_starts_with($path, '/') ? $path : ('/' . $path)); } function wp_mail(string|array $to, string $subject, string $message): bool { $GLOBALS['wp_mail_outbox'][] = ['to' => $to, 'subject' => $subject, 'message' => $message]; return true; } function wp_generate_password(int $length = 12, bool $specialChars = true): string { $bytes = random_bytes(max(1, intdiv($length + 1, 2))); return substr(bin2hex($bytes), 0, $length); } if (!defined('ABSPATH')) { define('ABSPATH', __DIR__ . '/wp-emu-root/'); define('CALENDAR_PLUGIN_ALLOW_DEBUG_TOKENS', true); } $upgradePath = ABSPATH . 'wp-admin/includes/upgrade.php'; @mkdir(dirname($upgradePath), 0775, true); file_put_contents( $upgradePath, "query('CREATE TABLE IF NOT EXISTS wp_calendar_events (id INTEGER PRIMARY KEY AUTOINCREMENT)'); $wpdb->query('CREATE TABLE IF NOT EXISTS wp_calendar_recurrence_exceptions (id INTEGER PRIMARY KEY AUTOINCREMENT)'); require_once dirname(__DIR__) . '/code/calendar-plugin.php'; do_action('init'); do_action('rest_api_init'); if (PHP_SAPI === 'cli' && basename((string) ($_SERVER['SCRIPT_FILENAME'] ?? '')) === 'wp_emulation.php') { echo "wp-emulation booted\n"; echo 'routes=' . count($GLOBALS['wp_routes']) . "\n"; }