File manager - Edit - /home/proidvn/site.proid.vn/wp-content/plugins/novamira/includes/abilities/run-wp-cli.php
Back
<?php // SPDX-FileCopyrightText: 2026 Ovation S.r.l. <dev@novamira.ai> // SPDX-License-Identifier: AGPL-3.0-or-later declare(strict_types=1); /** * Abilities: Execute WP-CLI commands and query job statuses. */ if (!defined('ABSPATH')) { exit(); } wp_register_ability('novamira/run-wp-cli', [ 'label' => __('Run WP-CLI Command', domain: 'novamira'), 'description' => __( 'Runs a WP-CLI command on the server. Can be run synchronously (default) or asynchronously in the background.', domain: 'novamira', ), 'category' => 'code-execution', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'args' => [ 'type' => 'array', 'description' => 'Arguments to pass to wp (e.g. ["plugin", "list", "--format=json"]).', 'items' => [ 'type' => 'string', ], 'minItems' => 1, ], 'async' => [ 'type' => 'boolean', 'description' => 'Whether to run the command asynchronously in the background.', 'default' => false, ], ], 'required' => ['args'], 'additionalProperties' => false, ], 'output_schema' => [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean', 'description' => 'Whether the command started successfully (or completed successfully for synchronous run).', ], 'exit_code' => [ 'type' => 'integer', 'description' => 'Exit status code of the process (null if async).', ], 'stdout' => [ 'type' => 'string', 'description' => 'Captured standard output (empty if async).', ], 'stderr' => [ 'type' => 'string', 'description' => 'Captured standard error (empty if async).', ], 'job_id' => [ 'type' => 'string', 'description' => 'The generated background job ID (null if synchronous).', ], 'pid' => [ 'type' => 'integer', 'description' => 'PID of the background process (null if synchronous).', ], ], 'required' => ['success'], ], 'execute_callback' => 'novamira_run_wp_cli', 'permission_callback' => 'novamira_permission_callback', 'meta' => [ 'show_in_rest' => true, 'mcp' => ['public' => true], 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => false, ], ], ]); wp_register_ability('novamira/get-wp-cli-job', [ 'label' => __('Get WP-CLI Job Status', domain: 'novamira'), 'description' => __('Checks the status of an asynchronous background WP-CLI job.', domain: 'novamira'), 'category' => 'code-execution', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'job_id' => [ 'type' => 'string', 'description' => 'The ID of the background job to check.', 'minLength' => 1, ], 'offset' => [ 'type' => 'integer', 'description' => 'Byte offset to start reading from the output log.', 'default' => 0, 'minimum' => 0, ], 'limit' => [ 'type' => 'integer', 'description' => 'Maximum bytes of the log output to return. Use -1 for the entire file.', 'default' => 1_048_576, ], ], 'required' => ['job_id'], 'additionalProperties' => false, ], 'output_schema' => [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean', 'description' => 'Whether the job was found.', ], 'job_id' => [ 'type' => 'string', ], 'status' => [ 'type' => 'string', 'description' => 'Job status: "running", "completed", or "not_found".', ], 'exit_code' => [ 'type' => 'integer', 'description' => 'Exit code of the process (null if running or not found).', ], 'stdout' => [ 'type' => 'string', 'description' => 'Captured stdout and stderr output log.', ], 'bytes_read' => [ 'type' => 'integer', 'description' => 'Number of bytes read from log file.', ], 'truncated' => [ 'type' => 'boolean', 'description' => 'Whether the log output was truncated due to limit.', ], ], 'required' => ['success', 'job_id', 'status'], ], 'execute_callback' => 'novamira_get_wp_cli_job', 'permission_callback' => 'novamira_permission_callback', 'meta' => [ 'show_in_rest' => true, 'mcp' => ['public' => true], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true, ], ], ]); /** * Execute WP-CLI command. * * @param array $input Input parameters. * @return array|WP_Error */ function novamira_run_wp_cli(array $input) { $raw_args = is_array($input['args'] ?? null) ? $input['args'] : []; $args = []; /** @var mixed $arg */ foreach ($raw_args as $arg) { if (!is_string($arg)) { return new WP_Error('invalid_wp_cli_arg', __('WP-CLI arguments must be strings.', domain: 'novamira')); } $args[] = $arg; } $async = ($input['async'] ?? null) === true; if (!function_exists('proc_open') || !function_exists('exec')) { return new WP_Error('process_execution_disabled', __( 'Process execution (proc_open or exec) is disabled in PHP configuration.', domain: 'novamira', )); } $wp_path = novamira_find_wp_cli_path(); if ($wp_path === null) { return new WP_Error('wp_cli_not_found', __( 'WP-CLI is not installed or not executable on this server.', domain: 'novamira', )); } // Automatically append --allow-root if executing as root. if (novamira_is_current_user_root() && !in_array(needle: '--allow-root', haystack: $args, strict: true)) { array_unshift($args, '--allow-root'); } if ($async) { return novamira_run_wp_cli_async($wp_path, $args); } return novamira_run_wp_cli_sync($wp_path, $args); } /** * Query the status of an asynchronous background WP-CLI job. * * @param array $input Input parameters. * @return array|WP_Error */ function novamira_get_wp_cli_job(array $input) { $job_id = (string) ($input['job_id'] ?? ''); if (!preg_match('/^[a-f0-9]{16}$/i', $job_id)) { return new WP_Error('invalid_job_id', __('Invalid job ID format.', domain: 'novamira')); } $job_dir = novamira_get_sandbox_dir(ensure_exists: false) . 'wp-cli-jobs/'; $log_file = $job_dir . 'job_' . $job_id . '.log'; $status_file = $job_dir . 'job_' . $job_id . '.status'; if (!is_file($log_file)) { return novamira_get_missing_wp_cli_job_result($job_id, $status_file); } $status = 'running'; $exit_code = null; if (is_file($status_file)) { $status = 'completed'; $exit_content = trim((string) file_get_contents($status_file)); if ($exit_content !== '') { $exit_code = (int) $exit_content; } } $offset = (int) ($input['offset'] ?? 0); $limit = (int) ($input['limit'] ?? 1_048_576); $slice = novamira_read_log_slice($log_file, $offset, $limit); if (is_wp_error($slice)) { return $slice; } $res = [ 'success' => true, 'job_id' => $job_id, 'status' => $status, 'stdout' => $slice['content'], 'bytes_read' => $slice['bytes_read'], 'truncated' => $slice['truncated'], ]; if ($exit_code !== null) { $res['exit_code'] = $exit_code; } return $res; } /** * Build a job response when the log file is not present. * * @param string $job_id Job ID. * @param string $status_file Status file path. * @return array */ function novamira_get_missing_wp_cli_job_result(string $job_id, string $status_file): array { if (!is_file($status_file)) { return [ 'success' => false, 'job_id' => $job_id, 'status' => 'not_found', 'stdout' => '', 'bytes_read' => 0, 'truncated' => false, ]; } $exit_content = trim((string) file_get_contents($status_file)); $res = [ 'success' => true, 'job_id' => $job_id, 'status' => 'completed', 'stdout' => '', 'bytes_read' => 0, 'truncated' => false, ]; if ($exit_content !== '') { $res['exit_code'] = (int) $exit_content; } return $res; } /** * Helper to read a slice of log file. * * @param string $log_file File path. * @param int $offset Byte offset. * @param int $limit Max bytes to read. * @return array{content: string, bytes_read: int, truncated: bool}|WP_Error */ function novamira_read_log_slice(string $log_file, int $offset, int $limit): array|WP_Error { $size = (int) filesize($log_file); if ($offset >= $size) { return [ 'content' => '', 'bytes_read' => 0, 'truncated' => false, ]; } $handle = fopen(filename: $log_file, mode: 'rb'); if ($handle === false) { return new WP_Error('read_failed', sprintf(__('Could not open log file: %s', domain: 'novamira'), $log_file)); } if ($offset > 0) { fseek($handle, $offset); } $read_length = $limit === -1 ? $size - $offset : $limit; $content = fread($handle, max(1, $read_length)); fclose($handle); if ($content === false) { return new WP_Error('read_failed', sprintf(__('Could not read log file: %s', domain: 'novamira'), $log_file)); } $bytes_read = strlen($content); $truncated = $limit !== -1 && ($offset + $bytes_read) < $size; return [ 'content' => $content, 'bytes_read' => $bytes_read, 'truncated' => $truncated, ]; } /** * Find the WP-CLI executable on the server. * * @return string|null */ function novamira_find_wp_cli_path(): ?string { if (function_exists('exec')) { $output = []; $return_var = 0; exec('which wp 2>/dev/null', $output, $return_var); $first_out = $output[0] ?? null; if ($return_var === 0 && is_string($first_out) && trim($first_out) !== '') { return trim($first_out); } $output = []; $return_var = 0; exec('command -v wp 2>/dev/null', $output, $return_var); $first_out = $output[0] ?? null; if ($return_var === 0 && is_string($first_out) && trim($first_out) !== '') { return trim($first_out); } } $common_paths = [ '/usr/local/bin/wp', '/usr/bin/wp', '/bin/wp', '/usr/local/sbin/wp', '/usr/sbin/wp', ]; foreach ($common_paths as $path) { if (is_file($path) && is_executable($path)) { return $path; } } return null; } /** * Check if the PHP process is executing as root user. * * @return bool */ function novamira_is_current_user_root(): bool { if (function_exists('posix_getuid')) { return posix_getuid() === 0; } if (getenv('USER') === 'root' || getenv('USERNAME') === 'root') { return true; } if (function_exists('exec')) { $whoami = []; $rc = 0; exec('whoami 2>/dev/null', $whoami, $rc); $first_out = $whoami[0] ?? null; if ($rc === 0 && is_string($first_out) && trim($first_out) === 'root') { return true; } } return false; } /** * Execute WP-CLI synchronously. * * @param string $wp_path Path to the wp command. * @param list<string> $args Arguments array. * @return array */ function novamira_run_wp_cli_sync(string $wp_path, array $args): array { $cmd = array_merge([$wp_path], $args); $descriptorspec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; /** @var array<int, resource> $pipes */ $pipes = []; $process = proc_open($cmd, $descriptorspec, $pipes, ABSPATH); if (!is_resource($process)) { return [ 'success' => false, 'exit_code' => -1, 'stdout' => '', 'stderr' => __('Failed to start process.', domain: 'novamira'), 'job_id' => null, 'pid' => null, ]; } $stdin_pipe = $pipes[0] ?? null; if (is_resource($stdin_pipe)) { fclose($stdin_pipe); } [$stdout, $stderr] = novamira_collect_process_output( stdout_pipe: $pipes[1] ?? null, stderr_pipe: $pipes[2] ?? null, ); $exit_code = proc_close($process); return [ 'success' => $exit_code === 0, 'exit_code' => $exit_code, 'stdout' => $stdout, 'stderr' => $stderr, ]; } /** * Read process stdout and stderr concurrently. * * @param mixed $stdout_pipe Stdout pipe resource. * @param mixed $stderr_pipe Stderr pipe resource. * @return array{string, string} */ function novamira_collect_process_output(mixed $stdout_pipe, mixed $stderr_pipe): array { if (is_resource($stdout_pipe)) { stream_set_blocking(stream: $stdout_pipe, enable: false); } if (is_resource($stderr_pipe)) { stream_set_blocking(stream: $stderr_pipe, enable: false); } $stdout = ''; $stderr = ''; while (is_resource($stdout_pipe) || is_resource($stderr_pipe)) { /** @var list<resource> $read */ $read = []; if (is_resource($stdout_pipe)) { if (feof($stdout_pipe)) { fclose($stdout_pipe); $stdout_pipe = null; } if (is_resource($stdout_pipe)) { $read[] = $stdout_pipe; } } if (is_resource($stderr_pipe)) { if (feof($stderr_pipe)) { fclose($stderr_pipe); $stderr_pipe = null; } if (is_resource($stderr_pipe)) { $read[] = $stderr_pipe; } } if ($read === []) { break; } $write = null; $except = null; $ready = stream_select($read, $write, $except, seconds: 0, microseconds: 200_000); if ($ready === false) { break; } if ($ready === 0) { continue; } foreach ($read as $pipe) { novamira_append_process_pipe_output($pipe, $stdout_pipe, $stderr_pipe, $stdout, $stderr); } } return [$stdout, $stderr]; } /** * Append one readable process pipe chunk to the correct output buffer. * * @param mixed $pipe Readable pipe resource. * @param mixed $stdout_pipe Stdout pipe resource. * @param mixed $stderr_pipe Stderr pipe resource. * @param string $stdout Stdout buffer. * @param string $stderr Stderr buffer. */ function novamira_append_process_pipe_output( mixed $pipe, mixed $stdout_pipe, mixed $stderr_pipe, string &$stdout, string &$stderr, ): void { if (!is_resource($pipe)) { return; } $chunk = stream_get_contents($pipe); if ($chunk === false || $chunk === '') { return; } if (is_resource($stdout_pipe) && $pipe === $stdout_pipe) { $stdout .= $chunk; return; } if (is_resource($stderr_pipe) && $pipe === $stderr_pipe) { $stderr .= $chunk; } } /** * Execute WP-CLI asynchronously. * * @param string $wp_path Path to the wp command. * @param list<string> $args Arguments array. * @return array */ function novamira_run_wp_cli_async(string $wp_path, array $args): array { $job_id = bin2hex(random_bytes(8)); $job_dir = novamira_get_sandbox_dir(ensure_exists: true) . 'wp-cli-jobs/'; if (!is_dir($job_dir)) { wp_mkdir_p($job_dir); } $log_file = $job_dir . 'job_' . $job_id . '.log'; $status_file = $job_dir . 'job_' . $job_id . '.status'; if (file_put_contents(filename: $log_file, data: '') === false) { return [ 'success' => false, 'stderr' => sprintf(__('Failed to create log file: %s', domain: 'novamira'), $log_file), ]; } $cmd_args = array_map('escapeshellarg', $args); $wp_cmd = escapeshellarg($wp_path) . ' ' . implode(' ', $cmd_args); $background_cmd = sprintf( 'cd %s && (%s > %s 2>&1; echo $? > %s)', escapeshellarg(ABSPATH), $wp_cmd, escapeshellarg($log_file), escapeshellarg($status_file), ); $cmd = 'nohup sh -c ' . escapeshellarg($background_cmd) . ' > /dev/null 2>&1 & echo $!'; $pid_output = []; $rc = 0; exec($cmd, $pid_output, $rc); $pid = null; $first_out = $pid_output[0] ?? null; if ($rc === 0 && is_string($first_out) && trim($first_out) !== '') { $pid = (int) trim($first_out); } if ($rc !== 0 || $pid === null) { file_put_contents(filename: $status_file, data: '127'); return [ 'success' => false, 'job_id' => $job_id, 'exit_code' => $rc, 'stdout' => '', 'stderr' => __('Failed to start background WP-CLI process.', domain: 'novamira'), ]; } return [ 'success' => true, 'job_id' => $job_id, 'pid' => $pid, ]; }
| ver. 1.4 |
Github
|
.
| PHP 8.0.30 | Generation time: 0.14 |
proxy
|
phpinfo
|
Settings