class-avcf-abilities-base.php
3 weeks ago
class-avcf-abilities-block-navigation.php
3 weeks ago
class-avcf-abilities-cache.php
3 weeks ago
class-avcf-abilities-content.php
1 week ago
class-avcf-abilities-core.php
3 days ago
class-avcf-abilities-execute-php.php
2 weeks ago
class-avcf-abilities-global-styles.php
3 weeks ago
class-avcf-abilities-gutenberg.php
2 weeks ago
class-avcf-abilities-media.php
1 week ago
class-avcf-abilities-metadata.php
1 week ago
class-avcf-abilities-navigation.php
3 weeks ago
class-avcf-abilities-patterns.php
3 weeks ago
class-avcf-abilities-plugins.php
3 days ago
class-avcf-abilities-readonly.php
2 weeks ago
class-avcf-abilities-settings.php
3 weeks ago
class-avcf-abilities-taxonomies.php
3 weeks ago
class-avcf-abilities-templates.php
3 weeks ago
class-avcf-abilities-theme-files.php
2 weeks ago
class-avcf-abilities-themes.php
3 days ago
class-avcf-abilities-users.php
3 weeks ago
class-avcf-abilities-wp-cli.php
3 days ago
class-avcf-abilities-wp-cli.php
411 lines
| 1 | <?php |
| 2 | /** |
| 3 | * WP-CLI escape hatch. |
| 4 | * |
| 5 | * Runs the site's `wp` binary for actions that named abilities can't do or that |
| 6 | * are too large / long-running for a single web request (bulk search-replace, |
| 7 | * db export/import, regenerate-thumbnails, cron runs, etc). Two abilities: |
| 8 | * |
| 9 | * - atarim/run-wp-cli : run a `wp` command, synchronously (returns |
| 10 | * stdout/stderr/exit_code) or asynchronously in the |
| 11 | * background (returns a job_id to poll). |
| 12 | * - atarim/get-wp-cli-job : poll a background job's status + output log. |
| 13 | * |
| 14 | * Capability parity with Novamira's run-wp-cli: arbitrary `wp` commands, no |
| 15 | * command allow-list. Guardrails (Atarim conventions): |
| 16 | * - Requires `manage_options` (administrator). |
| 17 | * - Requires PHP process functions (proc_open/exec); refuses cleanly if the |
| 18 | * host disables them or the `wp` binary is absent — so it degrades to |
| 19 | * "unavailable", never a fatal. |
| 20 | * - Every invocation is audit-logged (user, time, command hash + preview) via |
| 21 | * error_log and the `avcf_wp_cli_audit` action. |
| 22 | * - Sync runs are bounded by a timeout (default 58s) and the process is |
| 23 | * terminated if exceeded — use async for anything longer. |
| 24 | * - Background job logs are written to a protected dir under uploads |
| 25 | * (.htaccess deny + index.php), since output can contain sensitive data. |
| 26 | * |
| 27 | * This is the most powerful ability in the catalog (arbitrary `wp`, marked |
| 28 | * destructive). Consent is handled agent-side, as with execute-php. |
| 29 | * |
| 30 | * @package atarim-visual-collaboration |
| 31 | */ |
| 32 | |
| 33 | if ( ! defined( 'ABSPATH' ) ) { |
| 34 | exit; |
| 35 | } |
| 36 | |
| 37 | class AVCF_Abilities_WP_CLI { |
| 38 | |
| 39 | /** Default seconds a synchronous command may run before it is terminated. */ |
| 40 | const SYNC_TIMEOUT = 58; |
| 41 | /** Hard ceiling on the sync timeout a caller can request. */ |
| 42 | const MAX_SYNC_TIMEOUT = 300; |
| 43 | /** Default max bytes of a job log returned by get-wp-cli-job. */ |
| 44 | const DEFAULT_LOG_LIMIT = 1048576; // 1 MB |
| 45 | |
| 46 | public function register() { |
| 47 | if ( ! function_exists( 'wp_register_ability' ) ) { |
| 48 | return; |
| 49 | } |
| 50 | $this->register_run(); |
| 51 | $this->register_get_job(); |
| 52 | } |
| 53 | |
| 54 | private function register_run() { |
| 55 | $self = $this; |
| 56 | wp_register_ability( 'atarim/run-wp-cli', [ |
| 57 | 'label' => 'Run WP-CLI Command', |
| 58 | 'description' => 'Run a WP-CLI command on the server via the site\'s `wp` binary — for actions that no named ability covers, or that are too large/slow for one web request (e.g. wp search-replace, wp db export, wp media regenerate, wp cron event run). Pass args as an array WITHOUT the leading "wp" (e.g. ["plugin","list","--format=json"]). Runs synchronously by default (returns stdout/stderr/exit_code); set async:true for long-running commands (returns a job_id — poll get-wp-cli-job for output). Powerful and DESTRUCTIVE: it can run any wp command, including database and file changes. Requires the wp binary and PHP process execution; if the host disables those it returns a clear "unavailable" message. Every call is audit-logged.', |
| 59 | 'category' => 'atarim', |
| 60 | 'input_schema' => [ |
| 61 | 'type' => 'object', |
| 62 | 'properties' => [ |
| 63 | 'args' => [ 'type' => 'array', 'minItems' => 1, 'items' => [ 'type' => 'string' ], 'description' => 'Arguments passed to `wp`, without the leading "wp". Example: ["option","get","siteurl"].' ], |
| 64 | 'async' => [ 'type' => 'boolean', 'default' => false, 'description' => 'Run in the background and return a job_id to poll (use for long-running commands).' ], |
| 65 | 'timeout' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => self::MAX_SYNC_TIMEOUT, 'description' => 'Synchronous-run timeout in seconds (default ' . self::SYNC_TIMEOUT . ', max ' . self::MAX_SYNC_TIMEOUT . '). Ignored when async is true.' ], |
| 66 | ], |
| 67 | 'required' => [ 'args' ], |
| 68 | 'additionalProperties' => false, |
| 69 | ], |
| 70 | 'output_schema' => [ |
| 71 | 'type' => 'object', |
| 72 | 'properties' => [ |
| 73 | 'success' => [ 'type' => 'boolean' ], |
| 74 | 'exit_code' => [ 'type' => 'integer' ], |
| 75 | 'stdout' => [ 'type' => 'string' ], |
| 76 | 'stderr' => [ 'type' => 'string' ], |
| 77 | 'job_id' => [ 'type' => 'string' ], |
| 78 | 'pid' => [ 'type' => 'integer' ], |
| 79 | 'message' => [ 'type' => 'string' ], |
| 80 | ], |
| 81 | 'required' => [ 'success', 'message' ], |
| 82 | ], |
| 83 | 'execute_callback' => function( $input = [] ) use ( $self ) { return $self->run( (array) $input ); }, |
| 84 | 'permission_callback' => function() { return current_user_can( 'manage_options' ); }, |
| 85 | 'meta' => [ |
| 86 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 87 | 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => false ], |
| 88 | ], |
| 89 | ] ); |
| 90 | } |
| 91 | |
| 92 | private function register_get_job() { |
| 93 | $self = $this; |
| 94 | wp_register_ability( 'atarim/get-wp-cli-job', [ |
| 95 | 'label' => 'Get WP-CLI Job Status', |
| 96 | 'description' => 'Check an asynchronous WP-CLI job started by run-wp-cli with async:true. Returns status ("running" | "completed" | "not_found"), the exit_code once finished, and the captured output log. Use offset/limit to page through a large log (limit -1 returns the whole file).', |
| 97 | 'category' => 'atarim', |
| 98 | 'input_schema' => [ |
| 99 | 'type' => 'object', |
| 100 | 'properties' => [ |
| 101 | 'job_id' => [ 'type' => 'string', 'minLength' => 1, 'description' => 'The job_id returned by run-wp-cli.' ], |
| 102 | 'offset' => [ 'type' => 'integer', 'minimum' => 0, 'default' => 0, 'description' => 'Byte offset to start reading the log from.' ], |
| 103 | 'limit' => [ 'type' => 'integer', 'description' => 'Max bytes of log to return (default 1 MB; -1 for the whole file).' ], |
| 104 | ], |
| 105 | 'required' => [ 'job_id' ], |
| 106 | 'additionalProperties' => false, |
| 107 | ], |
| 108 | 'output_schema' => [ |
| 109 | 'type' => 'object', |
| 110 | 'properties' => [ |
| 111 | 'success' => [ 'type' => 'boolean' ], |
| 112 | 'job_id' => [ 'type' => 'string' ], |
| 113 | 'status' => [ 'type' => 'string' ], |
| 114 | 'exit_code' => [ 'type' => 'integer' ], |
| 115 | 'stdout' => [ 'type' => 'string' ], |
| 116 | 'bytes_read' => [ 'type' => 'integer' ], |
| 117 | 'truncated' => [ 'type' => 'boolean' ], |
| 118 | 'message' => [ 'type' => 'string' ], |
| 119 | ], |
| 120 | 'required' => [ 'success', 'job_id', 'status', 'message' ], |
| 121 | ], |
| 122 | 'execute_callback' => function( $input = [] ) use ( $self ) { return $self->get_job( (array) $input ); }, |
| 123 | 'permission_callback' => function() { return current_user_can( 'manage_options' ); }, |
| 124 | 'meta' => [ |
| 125 | 'mcp' => [ 'public' => true, 'type' => 'tool' ], |
| 126 | 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ], |
| 127 | ], |
| 128 | ] ); |
| 129 | } |
| 130 | |
| 131 | /** Main run entry: validate, guard host, resolve wp, audit, dispatch. */ |
| 132 | public function run( $input ) { |
| 133 | $raw = isset( $input['args'] ) && is_array( $input['args'] ) ? $input['args'] : []; |
| 134 | $args = []; |
| 135 | foreach ( $raw as $arg ) { |
| 136 | if ( ! is_string( $arg ) ) { |
| 137 | return [ 'success' => false, 'message' => 'Every entry in args must be a string.' ]; |
| 138 | } |
| 139 | $args[] = $arg; |
| 140 | } |
| 141 | if ( empty( $args ) ) { |
| 142 | return [ 'success' => false, 'message' => 'args is required (the wp command as an array, e.g. ["plugin","list"]).' ]; |
| 143 | } |
| 144 | if ( ! function_exists( 'proc_open' ) || ! function_exists( 'exec' ) ) { |
| 145 | return [ 'success' => false, 'message' => 'WP-CLI is unavailable: PHP process execution (proc_open/exec) is disabled on this host. This is a server configuration limit — an administrator would need to enable it, or run the command manually.' ]; |
| 146 | } |
| 147 | $wp_path = $this->find_wp_path(); |
| 148 | if ( null === $wp_path ) { |
| 149 | return [ 'success' => false, 'message' => 'WP-CLI is unavailable: the `wp` binary was not found on this server. Install WP-CLI or run the command manually.' ]; |
| 150 | } |
| 151 | // Run as root needs --allow-root, or wp refuses. |
| 152 | if ( $this->is_root() && ! in_array( '--allow-root', $args, true ) ) { |
| 153 | array_unshift( $args, '--allow-root' ); |
| 154 | } |
| 155 | |
| 156 | $async = ! empty( $input['async'] ); |
| 157 | $this->audit( $args, $async ); |
| 158 | |
| 159 | if ( $async ) { |
| 160 | return $this->run_async( $wp_path, $args ); |
| 161 | } |
| 162 | $timeout = isset( $input['timeout'] ) ? (int) $input['timeout'] : self::SYNC_TIMEOUT; |
| 163 | $timeout = max( 1, min( self::MAX_SYNC_TIMEOUT, $timeout ) ); |
| 164 | return $this->run_sync( $wp_path, $args, $timeout ); |
| 165 | } |
| 166 | |
| 167 | /** Synchronous run via proc_open, bounded by $timeout seconds. */ |
| 168 | private function run_sync( $wp_path, $args, $timeout ) { |
| 169 | $descriptors = [ 0 => [ 'pipe', 'r' ], 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ] ]; |
| 170 | $pipes = []; |
| 171 | // Array form (no shell) on PHP 7.4+; escaped string form otherwise. |
| 172 | if ( PHP_VERSION_ID >= 70400 ) { |
| 173 | $cmd = array_merge( [ $wp_path ], $args ); |
| 174 | } else { |
| 175 | $cmd = escapeshellarg( $wp_path ); |
| 176 | foreach ( $args as $a ) { |
| 177 | $cmd .= ' ' . escapeshellarg( $a ); |
| 178 | } |
| 179 | } |
| 180 | $process = proc_open( $cmd, $descriptors, $pipes, ABSPATH ); |
| 181 | if ( ! is_resource( $process ) ) { |
| 182 | return [ 'success' => false, 'exit_code' => -1, 'stdout' => '', 'stderr' => '', 'message' => 'Failed to start the wp process.' ]; |
| 183 | } |
| 184 | if ( isset( $pipes[0] ) && is_resource( $pipes[0] ) ) { |
| 185 | fclose( $pipes[0] ); |
| 186 | } |
| 187 | foreach ( [ 1, 2 ] as $i ) { |
| 188 | if ( isset( $pipes[ $i ] ) && is_resource( $pipes[ $i ] ) ) { |
| 189 | stream_set_blocking( $pipes[ $i ], false ); |
| 190 | } |
| 191 | } |
| 192 | $stdout = ''; |
| 193 | $stderr = ''; |
| 194 | $deadline = time() + $timeout; |
| 195 | $timed_out = false; |
| 196 | do { |
| 197 | $status = proc_get_status( $process ); |
| 198 | if ( isset( $pipes[1] ) && is_resource( $pipes[1] ) ) { |
| 199 | $stdout .= stream_get_contents( $pipes[1] ); |
| 200 | } |
| 201 | if ( isset( $pipes[2] ) && is_resource( $pipes[2] ) ) { |
| 202 | $stderr .= stream_get_contents( $pipes[2] ); |
| 203 | } |
| 204 | if ( ! $status['running'] ) { |
| 205 | break; |
| 206 | } |
| 207 | if ( time() >= $deadline ) { |
| 208 | $timed_out = true; |
| 209 | proc_terminate( $process, 9 ); |
| 210 | break; |
| 211 | } |
| 212 | usleep( 100000 ); // 100ms |
| 213 | } while ( true ); |
| 214 | |
| 215 | foreach ( [ 1, 2 ] as $i ) { |
| 216 | if ( isset( $pipes[ $i ] ) && is_resource( $pipes[ $i ] ) ) { |
| 217 | fclose( $pipes[ $i ] ); |
| 218 | } |
| 219 | } |
| 220 | $exit_code = proc_close( $process ); |
| 221 | |
| 222 | if ( $timed_out ) { |
| 223 | return [ |
| 224 | 'success' => false, |
| 225 | 'exit_code' => -1, |
| 226 | 'stdout' => $stdout, |
| 227 | 'stderr' => $stderr, |
| 228 | 'message' => sprintf( 'Command exceeded the %ds synchronous timeout and was terminated. Re-run with async:true for long-running commands.', $timeout ), |
| 229 | ]; |
| 230 | } |
| 231 | return [ |
| 232 | 'success' => ( 0 === $exit_code ), |
| 233 | 'exit_code' => $exit_code, |
| 234 | 'stdout' => $stdout, |
| 235 | 'stderr' => $stderr, |
| 236 | 'message' => ( 0 === $exit_code ) ? 'Command completed.' : sprintf( 'Command exited with code %d.', $exit_code ), |
| 237 | ]; |
| 238 | } |
| 239 | |
| 240 | /** Background run: nohup the command, capture output+exit to job files. */ |
| 241 | private function run_async( $wp_path, $args ) { |
| 242 | $dir = $this->jobs_dir(); |
| 243 | if ( null === $dir ) { |
| 244 | return [ 'success' => false, 'message' => 'Could not create the job directory for background execution.' ]; |
| 245 | } |
| 246 | $job_id = bin2hex( random_bytes( 8 ) ); |
| 247 | $log_file = $dir . 'job_' . $job_id . '.log'; |
| 248 | $status_file = $dir . 'job_' . $job_id . '.status'; |
| 249 | if ( false === file_put_contents( $log_file, '' ) ) { // phpcs:ignore |
| 250 | return [ 'success' => false, 'message' => 'Failed to create the job log file.' ]; |
| 251 | } |
| 252 | |
| 253 | $cmd_args = array_map( 'escapeshellarg', $args ); |
| 254 | $wp_cmd = escapeshellarg( $wp_path ) . ' ' . implode( ' ', $cmd_args ); |
| 255 | $inner = sprintf( 'cd %s && (%s > %s 2>&1; echo $? > %s)', escapeshellarg( ABSPATH ), $wp_cmd, escapeshellarg( $log_file ), escapeshellarg( $status_file ) ); |
| 256 | $cmd = 'nohup sh -c ' . escapeshellarg( $inner ) . ' > /dev/null 2>&1 & echo $!'; |
| 257 | |
| 258 | $out = []; |
| 259 | $rc = 0; |
| 260 | exec( $cmd, $out, $rc ); // phpcs:ignore |
| 261 | $pid = ( 0 === $rc && isset( $out[0] ) && '' !== trim( $out[0] ) ) ? (int) trim( $out[0] ) : null; |
| 262 | if ( 0 !== $rc || null === $pid ) { |
| 263 | file_put_contents( $status_file, '127' ); // phpcs:ignore |
| 264 | return [ 'success' => false, 'job_id' => $job_id, 'message' => 'Failed to start the background wp process.' ]; |
| 265 | } |
| 266 | return [ |
| 267 | 'success' => true, |
| 268 | 'job_id' => $job_id, |
| 269 | 'pid' => $pid, |
| 270 | 'message' => sprintf( 'Started background job %s (pid %d). Poll get-wp-cli-job for status and output.', $job_id, $pid ), |
| 271 | ]; |
| 272 | } |
| 273 | |
| 274 | /** Poll a background job. */ |
| 275 | public function get_job( $input ) { |
| 276 | $job_id = isset( $input['job_id'] ) ? (string) $input['job_id'] : ''; |
| 277 | if ( ! preg_match( '/^[a-f0-9]{16}$/i', $job_id ) ) { |
| 278 | return [ 'success' => false, 'job_id' => $job_id, 'status' => 'not_found', 'message' => 'Invalid job_id format.' ]; |
| 279 | } |
| 280 | $dir = $this->jobs_dir( false ); |
| 281 | $log_file = $dir . 'job_' . $job_id . '.log'; |
| 282 | $status_file = $dir . 'job_' . $job_id . '.status'; |
| 283 | if ( ! is_file( $log_file ) ) { |
| 284 | return [ 'success' => false, 'job_id' => $job_id, 'status' => 'not_found', 'message' => 'No job with that id (it may have expired or never existed).' ]; |
| 285 | } |
| 286 | $status = 'running'; |
| 287 | $exit_code = null; |
| 288 | if ( is_file( $status_file ) ) { |
| 289 | $status = 'completed'; |
| 290 | $content = trim( (string) file_get_contents( $status_file ) ); |
| 291 | if ( '' !== $content ) { |
| 292 | $exit_code = (int) $content; |
| 293 | } |
| 294 | } |
| 295 | $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0; |
| 296 | $limit = isset( $input['limit'] ) ? (int) $input['limit'] : self::DEFAULT_LOG_LIMIT; |
| 297 | $slice = $this->read_log_slice( $log_file, $offset, $limit ); |
| 298 | |
| 299 | $res = [ |
| 300 | 'success' => true, |
| 301 | 'job_id' => $job_id, |
| 302 | 'status' => $status, |
| 303 | 'stdout' => $slice['content'], |
| 304 | 'bytes_read' => $slice['bytes_read'], |
| 305 | 'truncated' => $slice['truncated'], |
| 306 | 'message' => ( 'completed' === $status ) ? sprintf( 'Job completed (exit %s).', ( null === $exit_code ? '?' : $exit_code ) ) : 'Job still running.', |
| 307 | ]; |
| 308 | if ( null !== $exit_code ) { |
| 309 | $res['exit_code'] = $exit_code; |
| 310 | } |
| 311 | return $res; |
| 312 | } |
| 313 | |
| 314 | /* ------------------------------------------------------------------ */ |
| 315 | |
| 316 | /** Locate the `wp` binary. Returns absolute path or null. */ |
| 317 | private function find_wp_path() { |
| 318 | if ( function_exists( 'exec' ) ) { |
| 319 | foreach ( [ 'which wp 2>/dev/null', 'command -v wp 2>/dev/null' ] as $probe ) { |
| 320 | $out = []; |
| 321 | $rc = 0; |
| 322 | exec( $probe, $out, $rc ); // phpcs:ignore |
| 323 | if ( 0 === $rc && isset( $out[0] ) && '' !== trim( $out[0] ) ) { |
| 324 | return trim( $out[0] ); |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | foreach ( [ '/usr/local/bin/wp', '/usr/bin/wp', '/bin/wp', '/usr/local/sbin/wp', '/usr/sbin/wp' ] as $p ) { |
| 329 | if ( is_file( $p ) && is_executable( $p ) ) { |
| 330 | return $p; |
| 331 | } |
| 332 | } |
| 333 | return null; |
| 334 | } |
| 335 | |
| 336 | /** Whether the PHP process is running as root. */ |
| 337 | private function is_root() { |
| 338 | if ( function_exists( 'posix_geteuid' ) ) { |
| 339 | return 0 === posix_geteuid(); |
| 340 | } |
| 341 | if ( function_exists( 'exec' ) ) { |
| 342 | $out = []; |
| 343 | $rc = 0; |
| 344 | exec( 'id -u 2>/dev/null', $out, $rc ); // phpcs:ignore |
| 345 | if ( 0 === $rc && isset( $out[0] ) ) { |
| 346 | return '0' === trim( $out[0] ); |
| 347 | } |
| 348 | } |
| 349 | return false; |
| 350 | } |
| 351 | |
| 352 | /** Protected jobs directory under uploads. Returns path (with trailing slash) or null. */ |
| 353 | private function jobs_dir( $ensure = true ) { |
| 354 | $up = wp_upload_dir(); |
| 355 | if ( ! empty( $up['error'] ) || empty( $up['basedir'] ) ) { |
| 356 | return null; |
| 357 | } |
| 358 | $dir = trailingslashit( $up['basedir'] ) . 'atarim-wpcli-jobs/'; |
| 359 | if ( $ensure && ! is_dir( $dir ) ) { |
| 360 | wp_mkdir_p( $dir ); |
| 361 | // Harden: job logs can contain sensitive output — block web access. |
| 362 | @file_put_contents( $dir . '.htaccess', "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" ); // phpcs:ignore |
| 363 | @file_put_contents( $dir . 'index.php', "<?php // Silence is golden.\n" ); // phpcs:ignore |
| 364 | } |
| 365 | return $dir; |
| 366 | } |
| 367 | |
| 368 | /** Read a byte slice of a log file. Returns [ content, bytes_read, truncated ]. */ |
| 369 | private function read_log_slice( $log_file, $offset, $limit ) { |
| 370 | $size = (int) @filesize( $log_file ); // phpcs:ignore |
| 371 | if ( $offset >= $size ) { |
| 372 | return [ 'content' => '', 'bytes_read' => 0, 'truncated' => false ]; |
| 373 | } |
| 374 | $handle = fopen( $log_file, 'rb' ); // phpcs:ignore |
| 375 | if ( false === $handle ) { |
| 376 | return [ 'content' => '', 'bytes_read' => 0, 'truncated' => false ]; |
| 377 | } |
| 378 | if ( $offset > 0 ) { |
| 379 | fseek( $handle, $offset ); |
| 380 | } |
| 381 | $read_length = ( -1 === $limit ) ? ( $size - $offset ) : $limit; |
| 382 | $content = fread( $handle, max( 1, (int) $read_length ) ); |
| 383 | fclose( $handle ); |
| 384 | if ( false === $content ) { |
| 385 | return [ 'content' => '', 'bytes_read' => 0, 'truncated' => false ]; |
| 386 | } |
| 387 | $bytes_read = strlen( $content ); |
| 388 | $truncated = ( -1 !== $limit ) && ( ( $offset + $bytes_read ) < $size ); |
| 389 | return [ 'content' => $content, 'bytes_read' => $bytes_read, 'truncated' => $truncated ]; |
| 390 | } |
| 391 | |
| 392 | /** Audit every invocation (user, time, command hash + preview). */ |
| 393 | public function audit( $args, $async ) { |
| 394 | $user = function_exists( 'wp_get_current_user' ) ? wp_get_current_user() : null; |
| 395 | $joined = implode( ' ', $args ); |
| 396 | $entry = [ |
| 397 | 'time' => gmdate( 'c' ), |
| 398 | 'user_id' => ( $user && isset( $user->ID ) ) ? (int) $user->ID : 0, |
| 399 | 'login' => ( $user && isset( $user->user_login ) ) ? (string) $user->user_login : '', |
| 400 | 'async' => (bool) $async, |
| 401 | 'sha1' => sha1( $joined ), |
| 402 | 'preview' => substr( $joined, 0, 200 ), |
| 403 | ]; |
| 404 | error_log( sprintf( // phpcs:ignore |
| 405 | '[atarim/run-wp-cli] user=%d(%s) async=%s sha1=%s cmd=%s', |
| 406 | $entry['user_id'], $entry['login'], $entry['async'] ? '1' : '0', $entry['sha1'], $entry['preview'] |
| 407 | ) ); |
| 408 | do_action( 'avcf_wp_cli_audit', $entry ); |
| 409 | } |
| 410 | } |
| 411 |