calendar-plugin/compatibility-layer/wp_emulation.php

466 lines
13 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Lightweight local WordPress emulation for executing plugin code in code/.
*/
final class WP_Error
{
public function __construct(
public string $code,
public string $message,
public array $data = []
) {
}
}
final class WP_REST_Request
{
private array $params = [];
private array $headers = [];
public function __construct(
private readonly string $method,
private readonly string $route,
private readonly array $jsonParams = [],
array $headers = []
) {
$this->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<string, array<int, array<string, mixed>>> */
private array $tables = [];
/** @var array<string, int> */
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,
"<?php\nif (!function_exists('dbDelta')) { function dbDelta(string \$sql): array { return []; } }\n"
);
$wpdb = new WPDB_Compat(__DIR__ . '/wp_emulation.db');
$GLOBALS['wpdb'] = $wpdb;
$wpdb->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";
}