<?php
/**
 * Security Verifier Trait — denylist that blocks commands violating
 * WordPress.org Plugin Guidelines or destroying block-editor content.
 *
 * Extracted from RunWpCli so the policy lives in one focused file. The
 * trait is a pure function on a parsed args array — no side effects, no
 * dependency on RunWpCli state.
 *
 * @package zip-ai
 */

namespace ZipAI\MCP\Classes\Abilities\Core;

defined( 'ABSPATH' ) || exit;

/**
 * Trait holding `verify_command_security`.
 */
trait Security_Verifier_Trait {

	/**
	 * Block commands that violate WordPress.org Plugin Guidelines or would
	 * destroy block-editor content.
	 *
	 * @param string[] $args Parsed command tokens.
	 * @return true|\WP_Error
	 */
	private function verify_command_security( array $args ) {
		// Shell operators are not interpreted by the native dispatcher — the
		// tokeniser treats them as ordinary tokens, so a chained command like
		// `option update foo bar && option update baz qux` would silently dispatch
		// only the first sub-command and discard the rest. Reject upfront so the
		// model learns to issue one command per call.
		$shell_operators = array( '&&', '||', ';' );
		foreach ( $args as $arg ) {
			if ( in_array( $arg, $shell_operators, true ) ) {
				return new \WP_Error(
					'security_blocked',
					sprintf(
						'Security policy: shell operator "%s" is not supported — run one command per call.',
						$arg
					)
				);
			}
		}

		// Strip leading flags so we can locate the positional base/subcommand.
		$positional = array();
		foreach ( $args as $arg ) {
			if ( ! str_starts_with( $arg, '-' ) ) {
				$positional[] = strtolower( $arg );
			}
		}

		$base_command = $positional[0] ?? '';
		$sub_command  = $positional[1] ?? '';

		// 1. Arbitrary code execution & shell access are forbidden.
		$forbidden_commands = array( 'eval', 'eval-file', 'shell', 'package', 'server' );
		if ( in_array( $base_command, $forbidden_commands, true ) ) {
			return new \WP_Error( 'security_blocked', sprintf( 'Security policy: WP-CLI "%s" command is blocked.', $base_command ) );
		}

		// 2. Direct SQL is blocked (SQLi prevention).
		if ( 'db' === $base_command && in_array( $sub_command, array( 'query', 'import', 'drop', 'reset' ), true ) ) {
			return new \WP_Error( 'security_blocked', sprintf( 'Security policy: WP-CLI "db %s" is blocked.', $sub_command ) );
		}

		// 2a. `option add` and `option patch` remain blocked.
		// `add` is rarely useful — `update` is idempotent (creates the option
		// if missing). `patch` performs array-key surgery on serialized
		// options, which is a sharp edge that's almost never the right path
		// for an AI agent. `update` + `delete` cover the legitimate write
		// surface; brick-risk keys (siteurl / home / admin_email /
		// default_role / db_version / blog_charset / users_can_register) are
		// protected at the per-key `pre_update_option_<key>` filter layer
		// (Protected_Options_Filter) AND with an explicit upfront check in
		// handle_option_update / handle_option_delete so callers get a
		// precise error instead of a silent filter no-op.
		if ( 'option' === $base_command && in_array( $sub_command, array( 'add', 'patch' ), true ) ) {
			return new \WP_Error(
				'security_blocked',
				sprintf( 'Security policy: WP-CLI "option %s" is blocked. Use "option update" instead (it creates the option when missing).', $sub_command )
			);
		}

		// 2b. User creation/deletion and account mutation are blocked.
		// WordPress account management has reauth, email-confirmation,
		// password, and security-plugin hooks that generic automation must
		// not bypass. Read-only user discovery remains available.
		if ( 'user' === $base_command && in_array( $sub_command, array( 'create', 'update', 'delete' ), true ) ) {
			return new \WP_Error(
				'security_blocked',
				sprintf( 'Security policy: WP-CLI "user %s" is blocked. Manage users from WordPress admin or a dedicated, reviewed flow.', $sub_command )
			);
		}

		if ( 'user' === $base_command && 'meta' === $sub_command ) {
			$meta_verb = $positional[2] ?? '';
			if ( in_array( $meta_verb, array( 'add', 'update', 'set', 'delete' ), true ) ) {
				return new \WP_Error(
					'security_blocked',
					sprintf( 'Security policy: WP-CLI "user meta %s" is blocked. User metadata writes can affect authentication and capabilities.', $meta_verb )
				);
			}
		}

		// 2b-bis. Block administrator-promotion / lockout via role and cap
		// commands. Non-admin role changes (e.g. add-role <id> editor) are
		// allowed through to dispatch_native; only grants/removals of admin-
		// class roles and capabilities are refused here.
		if ( 'user' === $base_command
			&& in_array( $sub_command, array( 'add-role', 'remove-role', 'set-role' ), true ) ) {
			$role_arg        = strtolower( $positional[3] ?? '' );
			$high_risk_roles = array( 'administrator', 'super-admin' );
			if ( '' !== $role_arg && in_array( $role_arg, $high_risk_roles, true ) ) {
				return new \WP_Error(
					'security_blocked',
					sprintf(
						'Security policy: `wp user %s <id> %s` is blocked. Promoting or demoting administrators must be done from wp-admin → Users.',
						$sub_command,
						$role_arg
					)
				);
			}
		}

		if ( 'user' === $base_command
			&& in_array( $sub_command, array( 'add-cap', 'remove-cap' ), true ) ) {
			$cap_arg = strtolower( $positional[3] ?? '' );
			// Admin-class capabilities — granting any of these elevates a
			// user to admin in practice; removing manage_options from the
			// only admin can lock out site access.
			$admin_caps = array(
				'manage_options',
				'install_plugins',
				'activate_plugins',
				'delete_plugins',
				'edit_plugins',
				'install_themes',
				'switch_themes',
				'edit_themes',
				'delete_themes',
				'unfiltered_html',
				'create_users',
				'delete_users',
				'edit_users',
				'promote_users',
				'manage_network',
				'manage_sites',
			);
			if ( '' !== $cap_arg && in_array( $cap_arg, $admin_caps, true ) ) {
				return new \WP_Error(
					'security_blocked',
					sprintf(
						'Security policy: `wp user %s <id> %s` is blocked. "%s" is an admin-class capability — manage it from wp-admin → Users.',
						$sub_command,
						$cap_arg,
						$cap_arg
					)
				);
			}
		}

		// 2c. `wp post|comment meta add/update/set/delete/patch` is blocked.
		// Caller-supplied object id + caller-supplied meta key is a generic
		// write primitive equivalent to `option update` — it can target
		// `_edit_lock`, `_thumbnail_id`, theme/plugin private meta, or any
		// custom field. Specific edits route through dedicated abilities.
		if ( in_array( $base_command, array( 'post', 'comment' ), true ) && 'meta' === $sub_command ) {
			$meta_verb = $positional[2] ?? '';
			if ( in_array( $meta_verb, array( 'add', 'update', 'set', 'delete', 'patch' ), true ) ) {
				return new \WP_Error(
					'security_blocked',
					sprintf( 'Security policy: WP-CLI "%s meta %s" is blocked. Use a dedicated ability for the specific meta key you need to modify.', $base_command, $meta_verb )
				);
			}
		}

		// 2d. `wp transient set` and `transient patch` are always blocked —
		// caller-supplied key + value is a generic write primitive (same
		// shape as `option update`) that can clobber internal WP transients
		// like `update_plugins`, `update_themes`, session caches, etc.
		if ( 'transient' === $base_command && in_array( $sub_command, array( 'set', 'patch' ), true ) ) {
			return new \WP_Error(
				'security_blocked',
				sprintf( 'Security policy: WP-CLI "transient %s" is blocked.', $sub_command )
			);
		}
		// `wp transient delete` is conditional:
		// - `wp transient delete --expired`  ALLOW (safe bulk-cleanup)
		// - `wp transient delete --all`      ALLOW (destructive cache flush, approval-gated)
		// - `wp transient delete <key>`      BLOCK (specific-key deletion can nuke
		// `update_plugins` and break the WP update system)
		if ( 'transient' === $base_command && 'delete' === $sub_command ) {
			$has_expired_flag = false;
			$has_all_flag     = false;
			$has_positional   = false;
			foreach ( $args as $arg ) {
				$arg_lower = strtolower( $arg );
				if ( '--expired' === $arg_lower || str_starts_with( $arg_lower, '--expired=' ) ) {
					$has_expired_flag = true;
					continue;
				}
				if ( '--all' === $arg_lower || str_starts_with( $arg_lower, '--all=' ) ) {
					$has_all_flag = true;
					continue;
				}
				if ( ! str_starts_with( $arg, '-' )
					&& 'transient' !== strtolower( $arg )
					&& 'delete' !== strtolower( $arg ) ) {
					$has_positional = true;
				}
			}
			if ( $has_positional ) {
				return new \WP_Error(
					'security_blocked',
					'Security policy: WP-CLI "transient delete <key>" is blocked. Use "transient delete --expired" (safe bulk cleanup) or "transient delete --all" (full cache flush, approval required).'
				);
			}
			if ( ! $has_expired_flag && ! $has_all_flag ) {
				return new \WP_Error(
					'security_blocked',
					'Security policy: WP-CLI "transient delete" requires either --expired or --all. Specific-key deletion is blocked.'
				);
			}
		}

		// 3. Honor DISALLOW_FILE_MODS for install/update/delete on plugin/theme/core.
		$modifying_commands = array( 'plugin', 'theme', 'core' );
		$modifying_subs     = array( 'install', 'update', 'delete' );
		if ( in_array( $base_command, $modifying_commands, true ) && in_array( $sub_command, $modifying_subs, true ) ) {
			if ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ) {
				return new \WP_Error( 'security_blocked', 'Security policy: file modifications are disabled on this site (DISALLOW_FILE_MODS).' );
			}
		}

		// 4. Block code-execution and path-traversal flags.
		$forbidden_flags = array( '--require', '--exec', '--ssh', '--config', '--path', '--prompt' );
		foreach ( $args as $arg ) {
			$arg_lower = strtolower( $arg );
			foreach ( $forbidden_flags as $flag ) {
				if ( $arg_lower === $flag || str_starts_with( $arg_lower, $flag . '=' ) ) {
					return new \WP_Error( 'security_blocked', sprintf( 'Security policy: WP-CLI flag "%s" is blocked.', $flag ) );
				}
			}
		}

		// 5. Block post_content replacement — it destroys block markup.
		if ( 'post' === $base_command && in_array( $sub_command, array( 'update', 'create' ), true ) ) {
			foreach ( $args as $arg ) {
				if ( str_starts_with( strtolower( $arg ), '--post_content' ) ) {
					return new \WP_Error(
						'security_blocked',
						sprintf( 'Security policy: "wp post %s --post_content=" is blocked. It replaces the entire post_content with a plain string, destroying all block markup. ', $sub_command )
						. 'For block edits, use editor__apply_change with the page open in the block editor. '
						. 'There is no server-side path for editing page content without the editor — ask the user to open the page in the block editor.'
					);
				}
			}
		}

		return true;
	}
}
