*/ public static function ui_i18n() { $strings = [ 'Input' => __( 'Input', 'wteam-executor' ), 'Output' => __( 'Output', 'wteam-executor' ), 'Context' => __( 'Context', 'wteam-executor' ), 'Connected' => __( 'Connected', 'wteam-executor' ), 'Sources' => __( 'Sources', 'wteam-executor' ), 'Password' => __( 'Password', 'wteam-executor' ), 'Remember Me' => __( 'Remember Me', 'wteam-executor' ), 'Log In' => __( 'Log In', 'wteam-executor' ), 'Open wp-admin' => __( 'Open wp-admin', 'wteam-executor' ), 'Username or Email Address' => __( 'Username or Email Address', 'wteam-executor' ), ' (inactive)' => __( ' (inactive)', 'wteam-executor' ), ' (verified)' => __( ' (verified)', 'wteam-executor' ), ' but the live control is still ' => __( ' but the live control is still ', 'wteam-executor' ), ' Created: ' => __( ' Created: ', 'wteam-executor' ), ' in / ' => __( ' in / ', 'wteam-executor' ), ' is absent after delete.' => __( ' is absent after delete.', 'wteam-executor' ), ' is no longer present in the current document.' => __( ' is no longer present in the current document.', 'wteam-executor' ), ' Loop detector marked this no-effect strategy as repeat-forbidden.' => __( ' Loop detector marked this no-effect strategy as repeat-forbidden.', 'wteam-executor' ), ' ms' => __( ' ms', 'wteam-executor' ), ' on ' => __( ' on ', 'wteam-executor' ), ' out' => __( ' out', 'wteam-executor' ), ' Re-identify all source/target ids from the next fresh visible_elements snapshot; do not reuse old snapshot ids.' => __( ' Re-identify all source/target ids from the next fresh visible_elements snapshot; do not reuse old snapshot ids.', 'wteam-executor' ), ' Re-identify all source/target ids from the next fresh visible_elements snapshot; do not reuse stale ids.' => __( ' Re-identify all source/target ids from the next fresh visible_elements snapshot; do not reuse stale ids.', 'wteam-executor' ), ' rounds' => __( ' rounds', 'wteam-executor' ), ' to ' => __( ' to ', 'wteam-executor' ), ' tokens' => __( ' tokens', 'wteam-executor' ), ' v' => __( ' v', 'wteam-executor' ), ' · auto: ' => __( ' · auto: ', 'wteam-executor' ), '" in ' => __( '" in ', 'wteam-executor' ), '). You can continue typing the command.' => __( '). You can continue typing the command.', 'wteam-executor' ), ', clientHeight=' => __( ', clientHeight=', 'wteam-executor' ), ', scrollHeight=' => __( ', scrollHeight=', 'wteam-executor' ), /* translators: 1: limit label, 2: percentage used, 3: percentage remaining. */ '%1$s: 100% · %2$s% used · %3$s% left' => __( '%1$s: 100% · %2$s% used · %3$s% left', 'wteam-executor' ), /* translators: 1: limit label, 2: percentage used, 3: percentage remaining, 4: reset time. */ '%1$s: 100% · %2$s% used · %3$s% left · resets %4$s' => __( '%1$s: 100% · %2$s% used · %3$s% left · resets %4$s', 'wteam-executor' ), /* translators: %1$s: number of days in the usage limit. */ '%1$sd limit' => __( '%1$sd limit', 'wteam-executor' ), /* translators: %1$s: number of hours in the usage limit. */ '%1$sh limit' => __( '%1$sh limit', 'wteam-executor' ), '1M text tokens' => __( '1M text tokens', 'wteam-executor' ), '5h limit' => __( '5h limit', 'wteam-executor' ), 'A UI target became stale.' => __( 'A UI target became stale.', 'wteam-executor' ), 'Action required · the target is highlighted. Enable Auto Actions to execute it automatically.' => __( 'Action required · the target is highlighted. Enable Auto Actions to execute it automatically.', 'wteam-executor' ), 'Affected plugin' => __( 'Affected plugin', 'wteam-executor' ), 'AI' => __( 'AI', 'wteam-executor' ), 'AI connection is not configured.' => __( 'AI connection is not configured.', 'wteam-executor' ), 'AI connection is not configured. Open Executor > Settings.' => __( 'AI connection is not configured. Open Executor > Settings.', 'wteam-executor' ), 'Allow supported automatic click, scroll, type, select, toggle, drag/drop and navigation actions.' => __( 'Allow supported automatic click, scroll, type, select, toggle, drag/drop and navigation actions.', 'wteam-executor' ), 'An unknown error occurred.' => __( 'An unknown error occurred.', 'wteam-executor' ), 'Analyze screen' => __( 'Analyze screen', 'wteam-executor' ), 'API text price' => __( 'API text price', 'wteam-executor' ), 'auto' => __( 'auto', 'wteam-executor' ), 'Auto Action blocked: the requested admin URL is not a safe same-site wp-admin destination.' => __( 'Auto Action blocked: the requested admin URL is not a safe same-site wp-admin destination.', 'wteam-executor' ), 'Auto Action failed: ' => __( 'Auto Action failed: ', 'wteam-executor' ), 'Auto Action will not type model-provided text into a secret/password field.' => __( 'Auto Action will not type model-provided text into a secret/password field.', 'wteam-executor' ), 'Auto Action: clicked the requested control.' => __( 'Auto Action: clicked the requested control.', 'wteam-executor' ), 'Auto Action: drag/drop dispatched; verifying the live canvas next…' => __( 'Auto Action: drag/drop dispatched; verifying the live canvas next…', 'wteam-executor' ), 'Auto Action: Elementor structure changed and was verified.' => __( 'Auto Action: Elementor structure changed and was verified.', 'wteam-executor' ), 'Auto Action: entered and verified the requested value.' => __( 'Auto Action: entered and verified the requested value.', 'wteam-executor' ), 'Auto Action: executing Elementor structural command…' => __( 'Auto Action: executing Elementor structural command…', 'wteam-executor' ), 'Auto Action: opening the requested admin page…' => __( 'Auto Action: opening the requested admin page…', 'wteam-executor' ), 'Auto Action: scrolling…' => __( 'Auto Action: scrolling…', 'wteam-executor' ), 'Auto Action: selected the requested option.' => __( 'Auto Action: selected the requested option.', 'wteam-executor' ), 'Auto Action: toggle changed and verified.' => __( 'Auto Action: toggle changed and verified.', 'wteam-executor' ), 'Auto Action: waiting for the interface…' => __( 'Auto Action: waiting for the interface…', 'wteam-executor' ), 'Auto Actions' => __( 'Auto Actions', 'wteam-executor' ), 'Auto Actions disabled.' => __( 'Auto Actions disabled.', 'wteam-executor' ), 'Auto Actions enabled. Executor can click, scroll, type, select, drag/drop and navigate automatically on supported interfaces.' => __( 'Auto Actions enabled. Executor can click, scroll, type, select, drag/drop and navigate automatically on supported interfaces.', 'wteam-executor' ), 'Auto Actions: executing ' => __( 'Auto Actions: executing ', 'wteam-executor' ), 'Auto-clicked ' => __( 'Auto-clicked ', 'wteam-executor' ), 'Auto-detect' => __( 'Auto-detect', 'wteam-executor' ), 'Auto-dragged ' => __( 'Auto-dragged ', 'wteam-executor' ), 'Auto-navigated to: ' => __( 'Auto-navigated to: ', 'wteam-executor' ), 'Auto-scrolled element ' => __( 'Auto-scrolled element ', 'wteam-executor' ), 'Auto-scrolled page ' => __( 'Auto-scrolled page ', 'wteam-executor' ), 'Auto-scrolled to element ' => __( 'Auto-scrolled to element ', 'wteam-executor' ), 'Auto-selected "' => __( 'Auto-selected "', 'wteam-executor' ), 'Auto-toggled ' => __( 'Auto-toggled ', 'wteam-executor' ), 'Auto-typed into ' => __( 'Auto-typed into ', 'wteam-executor' ), 'Auto-waited ' => __( 'Auto-waited ', 'wteam-executor' ), 'Cached input' => __( 'Cached input', 'wteam-executor' ), 'ChatGPT account' => __( 'ChatGPT account', 'wteam-executor' ), 'ChatGPT connection request failed.' => __( 'ChatGPT connection request failed.', 'wteam-executor' ), 'Checking connection…' => __( 'Checking connection…', 'wteam-executor' ), 'checked' => __( 'checked', 'wteam-executor' ), 'unchecked' => __( 'unchecked', 'wteam-executor' ), 'unknown' => __( 'unknown', 'wteam-executor' ), 'Click event dispatched; verify the requested effect from the next live screen.' => __( 'Click event dispatched; verify the requested effect from the next live screen.', 'wteam-executor' ), 'Close and end session' => __( 'Close and end session', 'wteam-executor' ), 'Close panel and end session' => __( 'Close panel and end session', 'wteam-executor' ), 'Closing the panel ends the current session.' => __( 'Closing the panel ends the current session.', 'wteam-executor' ), 'Connecting ChatGPT…' => __( 'Connecting ChatGPT…', 'wteam-executor' ), 'Connection error' => __( 'Connection error', 'wteam-executor' ), 'Continuing with another execution batch…' => __( 'Continuing with another execution batch…', 'wteam-executor' ), 'Created Elementor element(s): ' => __( 'Created Elementor element(s): ', 'wteam-executor' ), /* translators: %1$s: formatted credit amount or credit status. */ 'Credits: %1$s' => __( 'Credits: %1$s', 'wteam-executor' ), 'Describe the task below. The first message starts the visual debug session.' => __( 'Describe the task below. The first message starts the visual debug session.', 'wteam-executor' ), 'Describe the task first.' => __( 'Describe the task first.', 'wteam-executor' ), 'Describe what you want Executor to do.' => __( 'Describe what you want Executor to do.', 'wteam-executor' ), 'Done · review the completed process and close it when ready.' => __( 'Done · review the completed process and close it when ready.', 'wteam-executor' ), 'Drag/drop event sequence dispatched across the live editor DOM; verify the resulting canvas state on the next AI decision.' => __( 'Drag/drop event sequence dispatched across the live editor DOM; verify the resulting canvas state on the next AI decision.', 'wteam-executor' ), 'Elementor $e command API is not available in this editor.' => __( 'Elementor $e command API is not available in this editor.', 'wteam-executor' ), 'Elementor create requires a model object.' => __( 'Elementor create requires a model object.', 'wteam-executor' ), 'Elementor create requires model.elType.' => __( 'Elementor create requires model.elType.', 'wteam-executor' ), 'Elementor element ' => __( 'Elementor element ', 'wteam-executor' ), 'Elementor model settings changed for ' => __( 'Elementor model settings changed for ', 'wteam-executor' ), 'Elementor model settings did not produce a detectable model change.' => __( 'Elementor model settings did not produce a detectable model change.', 'wteam-executor' ), 'Elementor preview container is not available.' => __( 'Elementor preview container is not available.', 'wteam-executor' ), 'Elementor save command resolved; verify saved/published state from the live editor before declaring completion.' => __( 'Elementor save command resolved; verify saved/published state from the live editor before declaring completion.', 'wteam-executor' ), 'Elementor structural action ' => __( 'Elementor structural action ', 'wteam-executor' ), 'Elementor structural action suggested · enable Auto Actions to execute it automatically.' => __( 'Elementor structural action suggested · enable Auto Actions to execute it automatically.', 'wteam-executor' ), 'Elementor structural action value must be a JSON object.' => __( 'Elementor structural action value must be a JSON object.', 'wteam-executor' ), 'Elementor structural action value must be valid JSON.' => __( 'Elementor structural action value must be valid JSON.', 'wteam-executor' ), 'Elementor structural command completed.' => __( 'Elementor structural command completed.', 'wteam-executor' ), 'Elementor structural state changed.' => __( 'Elementor structural state changed.', 'wteam-executor' ), 'Embedded frame' => __( 'Embedded frame', 'wteam-executor' ), 'Enter a message first.' => __( 'Enter a message first.', 'wteam-executor' ), 'Follow the AI instruction.' => __( 'Follow the AI instruction.', 'wteam-executor' ), 'High-impact action requires manual confirmation.' => __( 'High-impact action requires manual confirmation.', 'wteam-executor' ), 'in' => __( 'in', 'wteam-executor' ), 'inactive' => __( 'inactive', 'wteam-executor' ), 'input, textarea' => __( 'input, textarea', 'wteam-executor' ), 'Listening… speak your command.' => __( 'Listening… speak your command.', 'wteam-executor' ), 'Live field value verified after execution.' => __( 'Live field value verified after execution.', 'wteam-executor' ), 'Live select value verified after execution.' => __( 'Live select value verified after execution.', 'wteam-executor' ), 'Live toggle state verified after execution.' => __( 'Live toggle state verified after execution.', 'wteam-executor' ), 'Loop detected · forcing a different strategy…' => __( 'Loop detected · forcing a different strategy…', 'wteam-executor' ), 'Loop detector: this same semantic action already produced no meaningful DOM/Elementor change twice. Repeating this strategy is forbidden; choose a different action or use the Elementor structural bridge.' => __( 'Loop detector: this same semantic action already produced no meaningful DOM/Elementor change twice. Repeating this strategy is forbidden; choose a different action or use the Elementor structural bridge.', 'wteam-executor' ), 'Max output' => __( 'Max output', 'wteam-executor' ), 'Microphone access was blocked. Allow microphone permission in Chrome/Chromium and try again.' => __( 'Microphone access was blocked. Allow microphone permission in Chrome/Chromium and try again.', 'wteam-executor' ), 'Microphone input requires a secure HTTPS page (localhost is also allowed by Chromium). Open wp-admin over HTTPS and try again.' => __( 'Microphone input requires a secure HTTPS page (localhost is also allowed by Chromium). Open wp-admin over HTTPS and try again.', 'wteam-executor' ), 'Minimize' => __( 'Minimize', 'wteam-executor' ), 'Minimize panel' => __( 'Minimize panel', 'wteam-executor' ), 'Minimize to launcher' => __( 'Minimize to launcher', 'wteam-executor' ), 'model ?' => __( 'model ?', 'wteam-executor' ), 'Navigation requested; verify the destination on the next screen.' => __( 'Navigation requested; verify the destination on the next screen.', 'wteam-executor' ), 'Navigation suggested · enable Auto Actions to execute it automatically.' => __( 'Navigation suggested · enable Auto Actions to execute it automatically.', 'wteam-executor' ), 'No meaningful DOM or Elementor structural change was detected after the action.' => __( 'No meaningful DOM or Elementor structural change was detected after the action.', 'wteam-executor' ), 'No administrators found.' => __( 'No administrators found.', 'wteam-executor' ), 'No navigation instruction was returned.' => __( 'No navigation instruction was returned.', 'wteam-executor' ), 'No speech detected. Try the microphone again.' => __( 'No speech detected. Try the microphone again.', 'wteam-executor' ), 'No, stop' => __( 'No, stop', 'wteam-executor' ), 'No, stop.' => __( 'No, stop.', 'wteam-executor' ), 'Not connected' => __( 'Not connected', 'wteam-executor' ), 'Refresh' => __( 'Refresh', 'wteam-executor' ), 'Back to Assistant' => __( 'Back to Assistant', 'wteam-executor' ), 'Files' => __( 'Files', 'wteam-executor' ), 'Remove administrator' => __( 'Remove administrator', 'wteam-executor' ), 'Search and select administrators…' => __( 'Search and select administrators…', 'wteam-executor' ), 'Open Settings' => __( 'Open Settings', 'wteam-executor' ), 'Open Executor Assistant' => __( 'Open Executor Assistant', 'wteam-executor' ), 'Open Executor Assistant — session active' => __( 'Open Executor Assistant — session active', 'wteam-executor' ), 'Open Executor Assistant — task running' => __( 'Open Executor Assistant — task running', 'wteam-executor' ), 'OpenAI API' => __( 'OpenAI API', 'wteam-executor' ), 'out' => __( 'out', 'wteam-executor' ), 'Preparing the current screen…' => __( 'Preparing the current screen…', 'wteam-executor' ), 'Pricing is unavailable for this catalog entry.' => __( 'Pricing is unavailable for this catalog entry.', 'wteam-executor' ), 'Ready' => __( 'Ready', 'wteam-executor' ), 'Round limit reached · choose Yes to continue or No to stop.' => __( 'Round limit reached · choose Yes to continue or No to stop.', 'wteam-executor' ), 'rounds' => __( 'rounds', 'wteam-executor' ), 'Screenshot module unavailable: ' => __( 'Screenshot module unavailable: ', 'wteam-executor' ), 'Scroll suggested · enable Auto Actions to scroll automatically.' => __( 'Scroll suggested · enable Auto Actions to scroll automatically.', 'wteam-executor' ), 'Scrollable region' => __( 'Scrollable region', 'wteam-executor' ), 'Select verification failed after the value change.' => __( 'Select verification failed after the value change.', 'wteam-executor' ), 'Secondary limit' => __( 'Secondary limit', 'wteam-executor' ), 'Send' => __( 'Send', 'wteam-executor' ), 'Session ended' => __( 'Session ended', 'wteam-executor' ), 'Settings' => __( 'Settings', 'wteam-executor' ), 'Start voice input' => __( 'Start voice input', 'wteam-executor' ), 'Starting ChatGPT device authorization…' => __( 'Starting ChatGPT device authorization…', 'wteam-executor' ), 'Starting secure ChatGPT authorization…' => __( 'Starting secure ChatGPT authorization…', 'wteam-executor' ), 'Starting Executor assistant session…' => __( 'Starting Executor assistant session…', 'wteam-executor' ), 'Stop voice input' => __( 'Stop voice input', 'wteam-executor' ), 'Stopped at the configured round limit.' => __( 'Stopped at the configured round limit.', 'wteam-executor' ), 'Text-entry verification failed after the value change.' => __( 'Text-entry verification failed after the value change.', 'wteam-executor' ), 'The AI needs more information.' => __( 'The AI needs more information.', 'wteam-executor' ), 'The AI request failed.' => __( 'The AI request failed.', 'wteam-executor' ), 'The server returned an invalid response instead of JSON.' => __( 'The server returned an invalid response instead of JSON.', 'wteam-executor' ), 'Plugin files changed · refreshing the admin page…' => __( 'Plugin files changed · refreshing the admin page…', 'wteam-executor' ), 'Primary limit' => __( 'Primary limit', 'wteam-executor' ), 'The AI returned an unsupported automatic action.' => __( 'The AI returned an unsupported automatic action.', 'wteam-executor' ), 'The automatic UI action could not be completed.' => __( 'The automatic UI action could not be completed.', 'wteam-executor' ), 'The drag source or destination changed before execution.' => __( 'The drag source or destination changed before execution.', 'wteam-executor' ), 'The editor changed during the UI action: ' => __( 'The editor changed during the UI action: ', 'wteam-executor' ), 'The editor changed · re-identifying the live target (' => __( 'The editor changed · re-identifying the live target (', 'wteam-executor' ), 'The editor changed · refreshing the live DOM (' => __( 'The editor changed · refreshing the live DOM (', 'wteam-executor' ), 'The editor target changed repeatedly. Automatic stale-target recovery stopped after 2 attempts.' => __( 'The editor target changed repeatedly. Automatic stale-target recovery stopped after 2 attempts.', 'wteam-executor' ), 'The Elementor structural bridge is not available on the current editor screen.' => __( 'The Elementor structural bridge is not available on the current editor screen.', 'wteam-executor' ), 'The Elementor structural bridge is not available on the current screen.' => __( 'The Elementor structural bridge is not available on the current screen.', 'wteam-executor' ), 'The Elementor structural command did not produce a verifiable result.' => __( 'The Elementor structural command did not produce a verifiable result.', 'wteam-executor' ), 'The Elementor structural target id is missing.' => __( 'The Elementor structural target id is missing.', 'wteam-executor' ), 'The live editor DOM changed before the requested UI action could be completed.' => __( 'The live editor DOM changed before the requested UI action could be completed.', 'wteam-executor' ), 'The live page/editor state changed after the action.' => __( 'The live page/editor state changed after the action.', 'wteam-executor' ), 'The next action is blocked or requires user input.' => __( 'The next action is blocked or requires user input.', 'wteam-executor' ), 'The requested drag/drop destination is disabled.' => __( 'The requested drag/drop destination is disabled.', 'wteam-executor' ), 'The requested drag/drop destination is no longer available.' => __( 'The requested drag/drop destination is no longer available.', 'wteam-executor' ), 'The requested Elementor element is still present after delete.' => __( 'The requested Elementor element is still present after delete.', 'wteam-executor' ), 'The requested select option is not available anymore.' => __( 'The requested select option is not available anymore.', 'wteam-executor' ), 'The requested select target is no longer a select control.' => __( 'The requested select target is no longer a select control.', 'wteam-executor' ), 'The requested type target is not editable.' => __( 'The requested type target is not editable.', 'wteam-executor' ), 'The requested UI source/target changed before it could be used.' => __( 'The requested UI source/target changed before it could be used.', 'wteam-executor' ), 'The requested UI target is currently disabled; re-evaluate the fresh editor state before retrying.' => __( 'The requested UI target is currently disabled; re-evaluate the fresh editor state before retrying.', 'wteam-executor' ), 'The screenshot module could not be loaded. Check the browser and Content Security Policy network rules.' => __( 'The screenshot module could not be loaded. Check the browser and Content Security Policy network rules.', 'wteam-executor' ), 'The screenshot module loaded but could not be started.' => __( 'The screenshot module loaded but could not be started.', 'wteam-executor' ), 'The structural create/duplicate command returned without a verifiable new element.' => __( 'The structural create/duplicate command returned without a verifiable new element.', 'wteam-executor' ), 'The target is no longer available. Analyze the screen again.' => __( 'The target is no longer available. Analyze the screen again.', 'wteam-executor' ), 'The task appears complete.' => __( 'The task appears complete.', 'wteam-executor' ), 'The UI target changed repeatedly. Please analyze the screen or adjust the task.' => __( 'The UI target changed repeatedly. Please analyze the screen or adjust the task.', 'wteam-executor' ), 'The UI target remains unavailable after automatic recovery.' => __( 'The UI target remains unavailable after automatic recovery.', 'wteam-executor' ), 'The user approved another execution batch after the configured round limit.' => __( 'The user approved another execution batch after the configured round limit.', 'wteam-executor' ), 'The user chose not to continue after the configured round limit.' => __( 'The user chose not to continue after the configured round limit.', 'wteam-executor' ), 'Toggle verification failed: expected ' => __( 'Toggle verification failed: expected ', 'wteam-executor' ), /* translators: %1$s: expected toggle state; %2$s: actual toggle state. */ 'Toggle verification failed: expected %1$s but the live control is still %2$s.' => __( 'Toggle verification failed: expected %1$s but the live control is still %2$s.', 'wteam-executor' ), 'tokens' => __( 'tokens', 'wteam-executor' ), 'Tokens: ' => __( 'Tokens: ', 'wteam-executor' ), 'Top document' => __( 'Top document', 'wteam-executor' ), 'Unsupported Elementor structural action: ' => __( 'Unsupported Elementor structural action: ', 'wteam-executor' ), 'Unlimited' => __( 'Unlimited', 'wteam-executor' ), 'Voice input' => __( 'Voice input', 'wteam-executor' ), 'Voice input added to the prompt.' => __( 'Voice input added to the prompt.', 'wteam-executor' ), 'Voice input could not start. Check microphone permission and try again.' => __( 'Voice input could not start. Check microphone permission and try again.', 'wteam-executor' ), 'Voice input failed (' => __( 'Voice input failed (', 'wteam-executor' ), 'Voice input is not available in this Chromium browser profile. Enable the browser speech-recognition service or update the browser, then try again.' => __( 'Voice input is not available in this Chromium browser profile. Enable the browser speech-recognition service or update the browser, then try again.', 'wteam-executor' ), 'Voice input ready to send.' => __( 'Voice input ready to send.', 'wteam-executor' ), 'Weekly limit' => __( 'Weekly limit', 'wteam-executor' ), 'WordPress ' => __( 'WordPress ', 'wteam-executor' ), 'Executor Assistant' => __( 'Executor Assistant', 'wteam-executor' ), 'Executor Assistant (Ctrl/Cmd + Shift + K)' => __( 'Executor Assistant (Ctrl/Cmd + Shift + K)', 'wteam-executor' ), 'Executor is analyzing the current screen…' => __( 'Executor is analyzing the current screen…', 'wteam-executor' ), 'Executor is analyzing the live page structure…' => __( 'Executor is analyzing the live page structure…', 'wteam-executor' ), 'Executor is working… Click to open' => __( 'Executor is working… Click to open', 'wteam-executor' ), 'Executor navigation completed · resuming the task once…' => __( 'Executor navigation completed · resuming the task once…', 'wteam-executor' ), 'Executor session active — click to open' => __( 'Executor session active — click to open', 'wteam-executor' ), 'Yes, continue' => __( 'Yes, continue', 'wteam-executor' ), 'Yes, continue.' => __( 'Yes, continue.', 'wteam-executor' ), 'You' => __( 'You', 'wteam-executor' ), ]; return apply_filters( 'wteam_executor_ui_i18n', $strings ); } /** @return void */ public function register_hooks() { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_bridge' ], 99 ); add_action( 'elementor/editor/after_enqueue_scripts', [ $this, 'enqueue_elementor_editor_assets' ], 99 ); add_action( 'elementor/frontend/after_enqueue_scripts', [ $this, 'enqueue_frontend_bridge' ], 99 ); add_action( 'wp_ajax_' . self::AJAX_ACTION, [ $this, 'ajax_visual_step' ] ); } /** * @param string $hook_suffix * @return void */ public function enqueue_assets( $hook_suffix = '' ) { $this->enqueue_assistant_assets( 'wp-admin', (string) $hook_suffix ); } /** @return void */ public function enqueue_elementor_editor_assets() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Elementor preview detection; no state is changed. if ( isset( $_GET['elementor-preview'] ) && absint( wp_unslash( $_GET['elementor-preview'] ) ) > 0 ) { return; } $this->enqueue_assistant_assets( 'elementor-editor', 'elementor-editor' ); } /** @return void */ public function enqueue_frontend_bridge() { if ( is_admin() || ! WTeam_Executor_Access_Control::can_current_user() || ! $this->is_elementor_preview_context() ) { return; } wp_enqueue_script( 'wteam-executor-frontend-bridge', plugins_url( 'assets/js/frontend-bridge.js', WTEAM_EXECUTOR_FILE ), [], WTEAM_EXECUTOR_VERSION, false ); } /** * @param string $editor_context * @param string $hook_suffix * @return void */ private function enqueue_assistant_assets( $editor_context, $hook_suffix ) { if ( ! WTeam_Executor_Access_Control::can_current_user() ) { return; } $elementor_bridge_url = plugins_url( 'assets/js/elementor-bridge.js', WTEAM_EXECUTOR_FILE ); wp_enqueue_script( 'wteam-executor-elementor-bridge', $elementor_bridge_url, [], WTEAM_EXECUTOR_VERSION, false ); $assistant_script_url = (string) apply_filters( 'wteam_executor_assistant_script_url', plugins_url( 'assets/js/assistant.js', WTEAM_EXECUTOR_FILE ) ); wp_enqueue_script( 'wteam-executor-visual-navigator', $assistant_script_url, [ 'wteam-executor-elementor-bridge' ], WTEAM_EXECUTOR_VERSION, false ); wp_enqueue_style( 'wteam-executor-visual-navigator', plugins_url( 'assets/css/assistant.css', WTEAM_EXECUTOR_FILE ), [], WTEAM_EXECUTOR_VERSION ); $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $page_context = [ 'screen_id' => $screen && isset( $screen->id ) ? (string) $screen->id : '', 'screen_base' => $screen && isset( $screen->base ) ? (string) $screen->base : '', 'hook_suffix' => (string) $hook_suffix, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin page context used to describe the current screen. 'page_slug' => isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '', 'editor_context' => sanitize_key( (string) $editor_context ), ]; $detected = self::detect_affected_plugin( $page_context ); $plugins = self::get_plugin_choices(); $settings = WTeam_Executor_Settings::get_settings(); $codex_available = class_exists( WTeam_Executor_Codex_Runtime::class ); $auto_actions_available = (bool) apply_filters( 'wteam_executor_auto_actions_available', true ); $assistant_config = [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'ajaxAction' => self::AJAX_ACTION, 'nonce' => wp_create_nonce( self::NONCE_ACTION ), 'pluginVersion' => WTEAM_EXECUTOR_VERSION, 'wordpressVersion' => get_bloginfo( 'version' ), 'provider' => isset( $settings['ai_provider'] ) ? (string) $settings['ai_provider'] : 'openai_api', 'model' => ( isset( $settings['ai_provider'] ) && 'chatgpt_codex' === $settings['ai_provider'] ) ? ( ! empty( $settings['codex_model'] ) ? (string) $settings['codex_model'] : __( 'Auto (ChatGPT)', 'wteam-executor' ) ) : ( isset( $settings['model'] ) ? (string) $settings['model'] : '' ), 'maxRounds' => isset( $settings['max_rounds'] ) ? (int) $settings['max_rounds'] : WTeam_Executor_Settings::DEFAULT_MAX_ROUNDS, 'askToContinue' => ! empty( $settings['ask_to_continue'] ), 'launcherPosition' => ( isset( $settings['assistant_launcher_position'] ) && 'left' === $settings['assistant_launcher_position'] ) ? 'left' : 'right', 'hasApiKey' => ! empty( $settings['api_key'] ), 'hasConnection' => ( $codex_available && isset( $settings['ai_provider'] ) && 'chatgpt_codex' === $settings['ai_provider'] ) ? WTeam_Executor_Codex_Runtime::has_auth() : ! empty( $settings['api_key'] ), 'connectionMessage' => ( isset( $settings['ai_provider'] ) && 'chatgpt_codex' === $settings['ai_provider'] ) ? __( 'Connect your ChatGPT account under Executor > Settings.', 'wteam-executor' ) : __( 'The OpenAI API key is missing. Add it under Executor > Settings.', 'wteam-executor' ), 'html2canvasUrl' => plugins_url( 'assets/vendor/html2canvas/html2canvas.min.js', WTEAM_EXECUTOR_FILE ), 'adminUrl' => admin_url(), 'autoActionsAvailable' => $auto_actions_available, 'settingsUrl' => admin_url( 'admin.php?page=' . WTeam_Executor_Settings::PAGE_SLUG ), 'editorContext' => sanitize_key( (string) $editor_context ), 'pageContext' => $page_context, 'plugins' => $plugins, 'autoDetectedPlugin' => $detected, 'i18n' => self::ui_i18n(), ]; $assistant_config = apply_filters( 'wteam_executor_assistant_config', $assistant_config, $settings, $page_context, $editor_context, $hook_suffix ); wp_localize_script( 'wteam-executor-visual-navigator', 'WTeam_ExecutorAssistantConfig', $assistant_config ); } /** @return bool */ private function is_elementor_preview_context() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Elementor preview detection; no state is changed. if ( isset( $_GET['elementor-preview'] ) && absint( wp_unslash( $_GET['elementor-preview'] ) ) > 0 ) { return true; } if ( class_exists( '\\Elementor\\Plugin' ) ) { try { $instance = \Elementor\Plugin::$instance; if ( $instance && isset( $instance->preview ) && is_object( $instance->preview ) && method_exists( $instance->preview, 'is_preview_mode' ) ) { return (bool) $instance->preview->is_preview_mode(); } if ( $instance && isset( $instance->editor ) && is_object( $instance->editor ) && method_exists( $instance->editor, 'is_edit_mode' ) ) { return (bool) $instance->editor->is_edit_mode(); } } catch ( \Throwable $e ) { return false; } } return false; } /** @return void */ public function ajax_visual_step() { check_ajax_referer( self::NONCE_ACTION, 'nonce' ); if ( ! WTeam_Executor_Access_Control::can_current_user() ) { wp_send_json_error( [ 'message' => __( 'You do not have permission to use the Executor assistant.', 'wteam-executor' ) ], 403 ); } $settings = WTeam_Executor_Settings::get_settings(); $provider = isset( $settings['ai_provider'] ) ? (string) $settings['ai_provider'] : 'openai_api'; if ( 'chatgpt_codex' === $provider ) { if ( ! WTeam_Executor_Codex_Runtime::has_auth() ) { wp_send_json_error( [ 'message' => __( 'No ChatGPT account is connected for this WordPress user. Open Executor > Settings and connect ChatGPT first.', 'wteam-executor' ) ], 400 ); } } elseif ( empty( $settings['api_key'] ) ) { wp_send_json_error( [ 'message' => __( 'The OpenAI API key is missing. Add it under Executor > Settings.', 'wteam-executor' ) ], 400 ); } $configured_max_rounds = WTeam_Executor_Settings::normalize_max_rounds( isset( $settings['max_rounds'] ) ? $settings['max_rounds'] : WTeam_Executor_Settings::DEFAULT_MAX_ROUNDS ); $task_rounds_used = isset( $_POST['task_rounds_used'] ) ? absint( wp_unslash( $_POST['task_rounds_used'] ) ) : 0; $task_rounds_used = min( 100000, $task_rounds_used ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_prompt_text() validates UTF-8/control bytes while preserving intentional prompt formatting. $goal = isset( $_POST['goal'] ) ? self::sanitize_prompt_text( wp_unslash( $_POST['goal'] ), 12000 ) : ''; if ( '' === trim( $goal ) ) { wp_send_json_error( [ 'message' => __( 'The task description is empty.', 'wteam-executor' ) ], 400 ); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Base64 image data cannot be text-sanitized; it is size-limited and strictly validated by the data-URL regex below. $screenshot = isset( $_POST['screenshot'] ) ? wp_unslash( $_POST['screenshot'] ) : ''; if ( ! is_string( $screenshot ) ) { $screenshot = ''; } if ( '' !== $screenshot && ( strlen( $screenshot ) > self::MAX_IMAGE_CHARS || ! preg_match( '#^data:image/(?:jpeg|jpg|png|webp);base64,[A-Za-z0-9+/=\r\n]+$#', $screenshot ) ) ) { wp_send_json_error( [ 'message' => __( 'The screenshot is invalid or too large.', 'wteam-executor' ) ], 400 ); } $capture_mode = isset( $_POST['capture_mode'] ) ? sanitize_key( wp_unslash( $_POST['capture_mode'] ) ) : ( $screenshot ? 'image' : 'dom-only' ); $capture_warning = isset( $_POST['capture_warning'] ) ? sanitize_text_field( wp_unslash( $_POST['capture_warning'] ) ) : ''; $capture_warning = self::limit_text( $capture_warning, 500 ); $trigger = isset( $_POST['trigger'] ) ? self::limit_text( sanitize_key( wp_unslash( (string) $_POST['trigger'] ) ), 60 ) : 'manual'; $elements = self::decode_json_post( 'elements' ); $elements = self::sanitize_elements( is_array( $elements ) ? $elements : [] ); $transcript = self::decode_json_post( 'transcript' ); $transcript = self::sanitize_transcript( is_array( $transcript ) ? $transcript : [], $goal ); $page_context = self::decode_json_post( 'page_context' ); $page_context = self::sanitize_page_context( is_array( $page_context ) ? $page_context : [] ); $state_summary = isset( $_POST['state_summary'] ) ? sanitize_textarea_field( wp_unslash( $_POST['state_summary'] ) ) : ''; $state_summary = self::limit_text( $state_summary, 1800 ); $last_execution = self::decode_json_post( 'last_execution' ); $last_execution = self::sanitize_last_execution( is_array( $last_execution ) ? $last_execution : [] ); $action_effects = self::decode_json_post( 'action_effects' ); $action_effects = self::sanitize_action_effects( is_array( $action_effects ) ? $action_effects : [] ); $dom_search = self::decode_json_post( 'dom_search' ); $dom_search = is_array( $dom_search ) ? [ 'query' => isset( $dom_search['query'] ) ? self::limit_text( sanitize_text_field( (string) $dom_search['query'] ), 240 ) : '', 'mode' => isset( $dom_search['mode'] ) ? self::limit_text( sanitize_key( (string) $dom_search['mode'] ), 40 ) : '', 'status' => isset( $dom_search['status'] ) ? self::limit_text( sanitize_key( (string) $dom_search['status'] ), 40 ) : '', 'match_count' => isset( $dom_search['match_count'] ) ? absint( $dom_search['match_count'] ) : 0, 'result_count' => isset( $dom_search['result_count'] ) ? absint( $dom_search['result_count'] ) : 0, 'text_match_count' => isset( $dom_search['text_match_count'] ) ? absint( $dom_search['text_match_count'] ) : 0, 'text_found' => ! empty( $dom_search['text_found'] ), ] : []; $elementor_structure = self::decode_json_post( 'elementor_structure' ); $elementor_structure = self::sanitize_elementor_structure( is_array( $elementor_structure ) ? $elementor_structure : [] ); $auto_actions = isset( $_POST['auto_actions'] ) && '1' === sanitize_key( wp_unslash( (string) $_POST['auto_actions'] ) ); if ( ! (bool) apply_filters( 'wteam_executor_auto_actions_available', true ) ) { $auto_actions = false; } $selected_plugin = isset( $_POST['plugin'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin'] ) ) : ''; if ( '' === $selected_plugin ) { $selected_plugin = self::detect_affected_plugin( $page_context, $goal ); } $environment = self::build_environment_context( $selected_plugin, $page_context ); $payload = [ 'interface' => (string) apply_filters( 'wteam_executor_assistant_interface', 'wp-admin', $page_context ), 'goal' => $goal, 'state_summary' => $state_summary, 'recent_transcript'=> $transcript, 'environment' => $environment, 'current_page' => $page_context, 'viewport' => [ 'width' => isset( $_POST['viewport_width'] ) ? absint( wp_unslash( $_POST['viewport_width'] ) ) : 0, 'height' => isset( $_POST['viewport_height'] ) ? absint( wp_unslash( $_POST['viewport_height'] ) ) : 0, 'scroll_y' => isset( $_POST['scroll_y'] ) ? absint( wp_unslash( $_POST['scroll_y'] ) ) : 0, 'document_height' => isset( $_POST['document_height'] ) ? absint( wp_unslash( $_POST['document_height'] ) ) : 0, ], 'capture' => [ 'image_available' => '' !== $screenshot, 'mode' => $capture_mode, 'warning' => $capture_warning, ], 'visible_elements' => $elements, 'automation' => [ 'auto_actions' => $auto_actions, 'admin_base_path' => (string) wp_parse_url( admin_url(), PHP_URL_PATH ), 'trigger' => $trigger, 'last_execution' => $last_execution, 'action_effects' => $action_effects, 'dom_search' => $dom_search, ], 'builder' => [ 'elementor' => $elementor_structure, ], ]; $payload = apply_filters( 'wteam_executor_visual_payload', $payload, $page_context, $goal ); $remaining_task_rounds = max( 0, $configured_max_rounds - $task_rounds_used ); $payload['task_round_budget'] = [ 'used' => $task_rounds_used, 'max_rounds' => $configured_max_rounds, 'remaining' => $remaining_task_rounds, ]; if ( $remaining_task_rounds <= 0 ) { $ask_to_continue = ! empty( $settings['ask_to_continue'] ); $limit_navigation = [ 'status' => $ask_to_continue ? 'ask_user' : 'blocked', 'message' => $ask_to_continue /* translators: %d: runtime value inserted into this message. */ ? sprintf( __( 'The configured limit of %d AI rounds for the current task was reached before the task was verified as complete. Continue? (Yes) (No)', 'wteam-executor' ), $configured_max_rounds ) /* translators: %d: runtime value inserted into this message. */ : sprintf( __( 'The configured limit of %d AI rounds for the current task was reached before the task was verified as complete.', 'wteam-executor' ), $configured_max_rounds ), 'state_summary' => $state_summary, 'requires_confirmation' => false, 'actions' => [], 'action' => [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ], 'sources' => [], 'continue_prompt' => $ask_to_continue, 'round_limit' => $configured_max_rounds, ]; wp_send_json_success( [ 'navigation' => $limit_navigation, 'usage' => [], 'rounds_used' => 0, 'rounds_remaining' => 0, 'environment' => [ 'wordpress_version' => isset( $environment['wordpress']['version'] ) ? $environment['wordpress']['version'] : '', 'plugin' => isset( $environment['affected_plugin'] ) ? $environment['affected_plugin'] : null, ], ] ); } $ai_payload = $payload; $ai_payload['visible_elements'] = self::compact_elements_for_ai( isset( $payload['visible_elements'] ) && is_array( $payload['visible_elements'] ) ? $payload['visible_elements'] : [] ); $service = new WTeam_Executor_Assistant_Service(); $buffer_level = ob_get_level(); ob_start(); try { $result = $service->run( WTeam_Executor_AI_Client_Factory::get_model_for_current_provider(), $ai_payload, $screenshot, $remaining_task_rounds ); } finally { while ( ob_get_level() > $buffer_level ) { ob_end_clean(); } } if ( is_wp_error( $result ) ) { $rounds_used_on_error = $service->get_round_count(); wp_send_json_error( [ 'message' => $result->get_error_message(), 'error_code' => sanitize_key( (string) $result->get_error_code() ), 'rounds_used' => $rounds_used_on_error, 'rounds_remaining' => max( 0, $configured_max_rounds - $task_rounds_used - $rounds_used_on_error ), 'usage' => $service->get_usage(), ], 502 ); } $result = self::validate_navigation_result( $result, $elements, $elementor_structure ); $result = apply_filters( 'wteam_executor_navigation_result', $result, $payload, $goal ); $response_data = [ 'navigation' => $result, 'usage' => $service->get_usage(), 'rounds_used' => $service->get_round_count(), 'rounds_remaining' => max( 0, $configured_max_rounds - $task_rounds_used - $service->get_round_count() ), 'page_refresh_required' => false, 'environment' => [ 'wordpress_version' => isset( $environment['wordpress']['version'] ) ? $environment['wordpress']['version'] : '', 'plugin' => isset( $environment['affected_plugin'] ) ? $environment['affected_plugin'] : null, ], ]; $response_data = apply_filters( 'wteam_executor_visual_response_data', $response_data, $result, $payload, $goal ); wp_send_json_success( $response_data ); } /** @return array> */ private static function get_plugin_choices() { self::ensure_plugin_functions(); $plugins = function_exists( 'get_plugins' ) ? get_plugins() : []; $choices = []; foreach ( $plugins as $file => $data ) { $choices[] = [ 'file' => (string) $file, 'name' => isset( $data['Name'] ) ? wp_strip_all_tags( (string) $data['Name'] ) : (string) $file, 'version' => isset( $data['Version'] ) ? sanitize_text_field( (string) $data['Version'] ) : '', 'active' => function_exists( 'is_plugin_active' ) ? is_plugin_active( $file ) : false, ]; } usort( $choices, static function ( $a, $b ) { return strcasecmp( $a['name'], $b['name'] ); } ); return $choices; } /** * @param array $page_context * @param string $extra_hint Optional user goal/follow-up text used when the current screen is generic. * @return string */ private static function detect_affected_plugin( array $page_context, $extra_hint = '' ) { self::ensure_plugin_functions(); $plugins = function_exists( 'get_plugins' ) ? get_plugins() : []; if ( empty( $plugins ) ) { return ''; } $screen_clues = strtolower( implode( ' ', [ isset( $page_context['screen_id'] ) ? (string) $page_context['screen_id'] : '', isset( $page_context['screen_base'] ) ? (string) $page_context['screen_base'] : '', isset( $page_context['hook_suffix'] ) ? (string) $page_context['hook_suffix'] : '', isset( $page_context['page_slug'] ) ? (string) $page_context['page_slug'] : '', isset( $page_context['url_path'] ) ? (string) $page_context['url_path'] : '', ] ) ); $goal_clues = strtolower( self::limit_text( wp_strip_all_tags( (string) $extra_hint ), 2500 ) ); $best_file = ''; $best_score = 0; foreach ( $plugins as $file => $data ) { $parts = explode( '/', (string) $file ); $folder = count( $parts ) > 1 ? $parts[0] : pathinfo( (string) $file, PATHINFO_FILENAME ); $textdomain = isset( $data['TextDomain'] ) ? (string) $data['TextDomain'] : ''; $name_slug = isset( $data['Name'] ) ? sanitize_title( wp_strip_all_tags( (string) $data['Name'] ) ) : ''; $plain_name = isset( $data['Name'] ) ? strtolower( trim( wp_strip_all_tags( (string) $data['Name'] ) ) ) : ''; $candidates = array_unique( array_filter( [ strtolower( $folder ), strtolower( $textdomain ), strtolower( $name_slug ) ] ) ); $score = 0; foreach ( $candidates as $candidate ) { if ( strlen( $candidate ) < 4 ) { continue; } if ( false !== strpos( $screen_clues, $candidate ) ) { $score += 4; } if ( false !== strpos( $goal_clues, $candidate ) ) { $score += 5; } $short = preg_replace( '/[^a-z0-9]+/', '', $candidate ); if ( strlen( $short ) >= 5 && false !== strpos( preg_replace( '/[^a-z0-9]+/', '', $screen_clues ), $short ) ) { $score += 2; } if ( strlen( $short ) >= 5 && false !== strpos( preg_replace( '/[^a-z0-9]+/', '', $goal_clues ), $short ) ) { $score += 2; } } if ( strlen( $plain_name ) >= 4 && false !== strpos( $goal_clues, $plain_name ) ) { $score += 8; } if ( $score > $best_score ) { $best_score = $score; $best_file = (string) $file; } } return $best_score >= 4 ? $best_file : ''; } /** * @param string $plugin_file * @param array $page_context * @return array */ private static function build_environment_context( $plugin_file, array $page_context ) { global $wp_version; self::ensure_plugin_functions(); $theme = wp_get_theme(); $context = [ 'wordpress' => [ 'version' => isset( $wp_version ) ? (string) $wp_version : get_bloginfo( 'version' ), 'locale' => function_exists( 'determine_locale' ) ? determine_locale() : get_locale(), 'multisite' => is_multisite(), ], 'php' => [ 'version' => PHP_VERSION, ], 'theme' => [ 'name' => $theme->get( 'Name' ), 'version' => $theme->get( 'Version' ), 'stylesheet' => $theme->get_stylesheet(), 'template' => $theme->get_template(), 'is_child_theme' => $theme->get_stylesheet() !== $theme->get_template(), 'block_theme' => function_exists( 'wp_is_block_theme' ) ? wp_is_block_theme() : false, 'functions_file' => file_exists( get_stylesheet_directory() . '/functions.php' ) ? WTeam_Executor_File_Security_Policy::to_portable_path( get_stylesheet_directory() . '/functions.php' ) : '', ], 'affected_plugin' => null, ]; $plugins = function_exists( 'get_plugins' ) ? get_plugins() : []; if ( $plugin_file && isset( $plugins[ $plugin_file ] ) ) { $absolute = WP_PLUGIN_DIR . '/' . $plugin_file; $header = function_exists( 'get_plugin_data' ) && is_readable( $absolute ) ? get_plugin_data( $absolute, false, false ) : $plugins[ $plugin_file ]; if ( function_exists( 'get_file_data' ) && is_readable( $absolute ) ) { $explicit_header = get_file_data( $absolute, [ 'Name' => 'Plugin Name', 'PluginURI' => 'Plugin URI', 'Version' => 'Version', 'Description' => 'Description', 'Author' => 'Author', 'AuthorURI' => 'Author URI', 'TextDomain' => 'Text Domain', 'DomainPath' => 'Domain Path', 'Network' => 'Network', 'RequiresWP' => 'Requires at least', 'RequiresPHP' => 'Requires PHP', 'UpdateURI' => 'Update URI', 'RequiresPlugins' => 'Requires Plugins', 'License' => 'License', 'LicenseURI' => 'License URI', ], 'plugin' ); $header = array_merge( $header, array_filter( $explicit_header, 'strlen' ) ); } $fields = [ 'Name', 'PluginURI', 'Version', 'Description', 'Author', 'AuthorURI', 'TextDomain', 'DomainPath', 'Network', 'RequiresWP', 'RequiresPHP', 'UpdateURI', 'RequiresPlugins', 'License', 'LicenseURI', ]; $clean_header = []; foreach ( $fields as $field ) { if ( isset( $header[ $field ] ) && '' !== (string) $header[ $field ] ) { $value = wp_strip_all_tags( (string) $header[ $field ] ); $clean_header[ $field ] = self::limit_text( $value, 'Description' === $field ? 800 : 300 ); } } $context['affected_plugin'] = [ 'plugin_file' => $plugin_file, 'active' => function_exists( 'is_plugin_active' ) ? is_plugin_active( $plugin_file ) : false, 'network_active'=> is_multisite() && function_exists( 'is_plugin_active_for_network' ) ? is_plugin_active_for_network( $plugin_file ) : false, 'header' => $clean_header, ]; } else { $active_inventory = []; foreach ( $plugins as $file => $data ) { if ( function_exists( 'is_plugin_active' ) && ! is_plugin_active( $file ) ) { continue; } $active_inventory[] = [ 'file' => (string) $file, 'name' => isset( $data['Name'] ) ? wp_strip_all_tags( (string) $data['Name'] ) : (string) $file, 'version' => isset( $data['Version'] ) ? sanitize_text_field( (string) $data['Version'] ) : '', ]; if ( count( $active_inventory ) >= 30 ) { break; } } $context['active_plugins_if_affected_unknown'] = $active_inventory; } return $context; } /** * @param string $key * @return mixed */ private static function decode_json_post( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This helper is called only from ajax_visual_step(), which verifies the AJAX nonce before reading payload data. if ( ! isset( $_POST[ $key ] ) ) { return []; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce is verified by the caller; JSON is decoded and each supported structure is sanitized immediately afterwards. $raw = wp_unslash( $_POST[ $key ] ); if ( ! is_string( $raw ) || strlen( $raw ) > 250000 ) { return []; } $decoded = json_decode( $raw, true ); return null === $decoded ? [] : $decoded; } /** * @param array $elements * @return array> */ private static function sanitize_elements( array $elements ) { $clean = []; foreach ( array_slice( $elements, 0, self::MAX_ELEMENTS ) as $item ) { if ( ! is_array( $item ) ) { continue; } $id = isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : ''; if ( ! preg_match( '/^e\d{1,4}$/', $id ) ) { continue; } $type = isset( $item['type'] ) ? self::limit_text( sanitize_text_field( (string) $item['type'] ), 40 ) : ''; $text = isset( $item['text'] ) ? self::limit_text( sanitize_text_field( (string) $item['text'] ), 220 ) : ''; $state = isset( $item['state'] ) ? self::limit_text( sanitize_text_field( (string) $item['state'] ), 120 ) : ''; $href = isset( $item['href'] ) ? self::limit_text( sanitize_text_field( (string) $item['href'] ), 240 ) : ''; $frame = isset( $item['frame'] ) ? self::limit_text( sanitize_text_field( (string) $item['frame'] ), 100 ) : 'top'; $element = [ 'id' => $id, 'type' => $type, 'text' => $text, ]; if ( '' !== $state ) { $element['state'] = $state; } if ( '' !== $href ) { $element['href'] = $href; } if ( '' !== $frame && 'top' !== $frame ) { $element['frame'] = $frame; } $clean[] = $element; } return $clean; } /** * @param array $elements * @return array> */ private static function compact_elements_for_ai( array $elements ) { $compact = []; foreach ( $elements as $item ) { if ( ! is_array( $item ) || empty( $item['id'] ) ) { continue; } $state = isset( $item['state'] ) ? (string) $item['state'] : ''; $href = isset( $item['href'] ) ? (string) $item['href'] : ''; $frame = isset( $item['frame'] ) ? (string) $item['frame'] : ''; $element = [ (string) $item['id'], isset( $item['type'] ) ? (string) $item['type'] : '', isset( $item['text'] ) ? (string) $item['text'] : '', ]; if ( '' !== $state || '' !== $href || '' !== $frame ) { $element[] = $state; } if ( '' !== $href || '' !== $frame ) { $element[] = $href; } if ( '' !== $frame ) { $element[] = $frame; } $compact[] = $element; } return $compact; } /** * @param mixed $text * @param int $max_chars * @return string */ private static function sanitize_prompt_text( $text, $max_chars ) { if ( ! is_string( $text ) ) { return ''; } $text = wp_check_invalid_utf8( $text ); $text = str_replace( "\0", '', $text ); $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text ); if ( ! is_string( $text ) ) { return ''; } return self::limit_text( $text, max( 1, (int) $max_chars ) ); } /** * @param array $receipt * @return array */ private static function sanitize_last_execution( array $receipt ) { $status = isset( $receipt['status'] ) ? sanitize_key( (string) $receipt['status'] ) : ''; if ( ! in_array( $status, [ 'verified', 'dispatched', 'failed', 'stale', 'strategy_failed' ], true ) ) { $status = ''; } return [ 'type' => isset( $receipt['type'] ) ? self::limit_text( sanitize_key( (string) $receipt['type'] ), 40 ) : '', 'element_id' => isset( $receipt['element_id'] ) ? self::limit_text( sanitize_key( (string) $receipt['element_id'] ), 20 ) : '', 'target_element_id' => isset( $receipt['target_element_id'] ) ? self::limit_text( sanitize_key( (string) $receipt['target_element_id'] ), 20 ) : '', 'target' => isset( $receipt['target'] ) ? self::limit_text( sanitize_text_field( (string) $receipt['target'] ), 180 ) : '', 'requested_value' => isset( $receipt['requested_value'] ) ? self::limit_text( sanitize_text_field( (string) $receipt['requested_value'] ), 300 ) : '', 'before_state' => isset( $receipt['before_state'] ) ? self::limit_text( sanitize_text_field( (string) $receipt['before_state'] ), 120 ) : '', 'after_state' => isset( $receipt['after_state'] ) ? self::limit_text( sanitize_text_field( (string) $receipt['after_state'] ), 120 ) : '', 'status' => $status, 'note' => isset( $receipt['note'] ) ? self::limit_text( sanitize_text_field( (string) $receipt['note'] ), 400 ) : '', ]; } /** * @param array $effects * @return array> */ private static function sanitize_action_effects( array $effects ) { $clean = []; foreach ( array_slice( $effects, -12 ) as $effect ) { if ( ! is_array( $effect ) ) { continue; } $clean[] = [ 'signature' => isset( $effect['signature'] ) ? self::limit_text( sanitize_text_field( (string) $effect['signature'] ), 360 ) : '', 'type' => isset( $effect['type'] ) ? self::limit_text( sanitize_key( (string) $effect['type'] ), 50 ) : '', 'status' => isset( $effect['status'] ) ? self::limit_text( sanitize_key( (string) $effect['status'] ), 40 ) : '', 'changed' => ! empty( $effect['changed'] ), 'repeat_forbidden' => ! empty( $effect['repeat_forbidden'] ), 'note' => isset( $effect['note'] ) ? self::limit_text( sanitize_text_field( (string) $effect['note'] ), 320 ) : '', 'before_structure_hash' => isset( $effect['before_structure_hash'] ) ? self::limit_text( sanitize_key( (string) $effect['before_structure_hash'] ), 20 ) : '', 'after_structure_hash' => isset( $effect['after_structure_hash'] ) ? self::limit_text( sanitize_key( (string) $effect['after_structure_hash'] ), 20 ) : '', 'before_dom_hash' => isset( $effect['before_dom_hash'] ) ? self::limit_text( sanitize_key( (string) $effect['before_dom_hash'] ), 20 ) : '', 'after_dom_hash' => isset( $effect['after_dom_hash'] ) ? self::limit_text( sanitize_key( (string) $effect['after_dom_hash'] ), 20 ) : '', ]; } return $clean; } /** * @param array $snapshot * @return array */ private static function sanitize_elementor_structure( array $snapshot ) { $capabilities = []; $allowed_commands = [ 'document/elements/create', 'document/elements/duplicate', 'document/elements/delete', 'document/elements/settings', 'document/save/update', 'document/save/publish', ]; if ( isset( $snapshot['capabilities'] ) && is_array( $snapshot['capabilities'] ) ) { foreach ( $allowed_commands as $command ) { $capabilities[ $command ] = ! empty( $snapshot['capabilities'][ $command ] ); } } $elements = []; if ( isset( $snapshot['elements'] ) && is_array( $snapshot['elements'] ) ) { foreach ( array_slice( $snapshot['elements'], 0, 140 ) as $item ) { if ( ! is_array( $item ) ) { continue; } $id = isset( $item['id'] ) ? sanitize_key( (string) $item['id'] ) : ''; $parent_id = isset( $item['parent_id'] ) ? sanitize_key( (string) $item['parent_id'] ) : ''; if ( '' !== $id && ! preg_match( '/^[a-zA-Z0-9_-]{1,80}$/', $id ) ) { continue; } if ( '' !== $parent_id && ! preg_match( '/^[a-zA-Z0-9_-]{1,80}$/', $parent_id ) ) { $parent_id = ''; } $element = [ 'id' => $id, 'parent_id' => $parent_id, 'index' => isset( $item['index'] ) ? max( 0, min( 500, (int) $item['index'] ) ) : 0, 'depth' => isset( $item['depth'] ) ? max( 0, min( 20, (int) $item['depth'] ) ) : 0, 'el_type' => isset( $item['el_type'] ) ? self::limit_text( sanitize_text_field( (string) $item['el_type'] ), 60 ) : '', 'widget_type' => isset( $item['widget_type'] ) ? self::limit_text( sanitize_text_field( (string) $item['widget_type'] ), 100 ) : '', 'child_count' => isset( $item['child_count'] ) ? max( 0, min( 1000, (int) $item['child_count'] ) ) : 0, 'model_hash' => isset( $item['model_hash'] ) ? self::limit_text( sanitize_key( (string) $item['model_hash'] ), 20 ) : '', ]; $settings_preview = isset( $item['settings_preview'] ) ? self::sanitize_prompt_text( (string) $item['settings_preview'], 360 ) : ''; if ( '' !== $settings_preview ) { $element['settings_preview'] = $settings_preview; } $elements[] = $element; } } return [ 'available' => ! empty( $snapshot['available'] ), 'version' => isset( $snapshot['version'] ) ? self::limit_text( sanitize_text_field( (string) $snapshot['version'] ), 40 ) : '', 'capabilities' => $capabilities, 'structure_hash' => isset( $snapshot['structure_hash'] ) ? self::limit_text( sanitize_key( (string) $snapshot['structure_hash'] ), 20 ) : '', 'element_count' => count( $elements ), 'elements' => $elements, ]; } /** * @param array $transcript * @param string $goal * @return array> */ private static function sanitize_transcript( array $transcript, $goal = '' ) { $clean = []; $goal = trim( self::sanitize_prompt_text( (string) $goal, 12000 ) ); foreach ( array_slice( $transcript, -5 ) as $item ) { if ( ! is_array( $item ) ) { continue; } $role = isset( $item['role'] ) && 'assistant' === $item['role'] ? 'assistant' : 'user'; $text = isset( $item['text'] ) ? self::sanitize_prompt_text( (string) $item['text'], 1800 ) : ''; if ( '' === trim( $text ) ) { continue; } $clean[] = [ 'role' => $role, 'text' => $text ]; } if ( ! empty( $clean ) ) { $last_index = count( $clean ) - 1; if ( 'user' === $clean[ $last_index ]['role'] && '' !== $goal && trim( $clean[ $last_index ]['text'] ) === $goal ) { array_pop( $clean ); } } return $clean; } /** * @param array $page_context * @return array */ private static function sanitize_page_context( array $page_context ) { $headings = []; if ( isset( $page_context['visible_headings'] ) && is_array( $page_context['visible_headings'] ) ) { foreach ( array_slice( $page_context['visible_headings'], 0, 16 ) as $heading ) { $heading = self::limit_text( sanitize_text_field( (string) $heading ), 180 ); if ( '' !== trim( $heading ) ) { $headings[] = $heading; } } } $notices = []; if ( isset( $page_context['admin_notices'] ) && is_array( $page_context['admin_notices'] ) ) { foreach ( array_slice( $page_context['admin_notices'], 0, 6 ) as $notice ) { if ( ! is_array( $notice ) ) { continue; } $text = isset( $notice['text'] ) ? self::limit_text( sanitize_text_field( (string) $notice['text'] ), 320 ) : ''; if ( '' === trim( $text ) ) { continue; } $type = isset( $notice['type'] ) ? sanitize_key( (string) $notice['type'] ) : 'info'; if ( ! in_array( $type, [ 'success', 'error', 'warning', 'info' ], true ) ) { $type = 'info'; } $notices[] = [ 'type' => $type, 'text' => $text ]; } } $frames = []; if ( isset( $page_context['frames'] ) && is_array( $page_context['frames'] ) ) { foreach ( array_slice( $page_context['frames'], 0, 8 ) as $frame ) { if ( ! is_array( $frame ) ) { continue; } $frames[] = [ 'frame' => isset( $frame['frame'] ) ? self::limit_text( sanitize_text_field( (string) $frame['frame'] ), 100 ) : '', 'title' => isset( $frame['title'] ) ? self::limit_text( sanitize_text_field( (string) $frame['title'] ), 180 ) : '', 'url_path' => isset( $frame['url_path'] ) ? self::limit_text( sanitize_text_field( (string) $frame['url_path'] ), 260 ) : '', 'accessible' => ! empty( $frame['accessible'] ), 'bridge' => ! empty( $frame['bridge'] ), ]; } } return [ 'screen_id' => isset( $page_context['screen_id'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['screen_id'] ), 120 ) : '', 'screen_base' => isset( $page_context['screen_base'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['screen_base'] ), 120 ) : '', 'hook_suffix' => isset( $page_context['hook_suffix'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['hook_suffix'] ), 120 ) : '', 'page_slug' => isset( $page_context['page_slug'] ) ? self::limit_text( sanitize_key( (string) $page_context['page_slug'] ), 120 ) : '', 'url_path' => isset( $page_context['url_path'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['url_path'] ), 300 ) : '', 'body_classes' => isset( $page_context['body_classes'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['body_classes'] ), 500 ) : '', 'page_title' => isset( $page_context['page_title'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['page_title'] ), 240 ) : '', 'primary_heading' => isset( $page_context['primary_heading'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['primary_heading'] ), 220 ) : '', 'visible_headings' => $headings, 'active_admin_menu' => isset( $page_context['active_admin_menu'] ) ? self::limit_text( sanitize_text_field( (string) $page_context['active_admin_menu'] ), 240 ) : '', 'editor_context' => isset( $page_context['editor_context'] ) ? self::limit_text( sanitize_key( (string) $page_context['editor_context'] ), 80 ) : '', 'admin_notices' => $notices, 'frames' => $frames, ]; } /** * @param array $result * @param array> $elements * @return array */ private static function validate_navigation_result( array $result, array $elements, array $elementor_structure = [] ) { $valid_ids = []; foreach ( $elements as $element ) { if ( isset( $element['id'] ) && is_string( $element['id'] ) && '' !== $element['id'] ) { $valid_ids[ $element['id'] ] = true; } } $valid_elementor_ids = []; if ( isset( $elementor_structure['elements'] ) && is_array( $elementor_structure['elements'] ) ) { foreach ( $elementor_structure['elements'] as $elementor_element ) { if ( is_array( $elementor_element ) && ! empty( $elementor_element['id'] ) ) { $valid_elementor_ids[ (string) $elementor_element['id'] ] = true; } } } if ( isset( $result['actions'] ) && is_array( $result['actions'] ) ) { $validated_actions = []; $standalone_types = [ 'navigate_url', 'wait', 'find_elements', 'scan_page', 'drag_drop', 'elementor_create', 'elementor_duplicate', 'elementor_delete', 'elementor_update_settings', 'elementor_save' ]; foreach ( array_slice( $result['actions'], 0, 3 ) as $candidate_action ) { if ( ! is_array( $candidate_action ) ) { continue; } $single = $result; unset( $single['actions'] ); $single['action'] = $candidate_action; $single = self::validate_navigation_result( $single, $elements, $elementor_structure ); if ( isset( $single['status'] ) && in_array( $single['status'], [ 'not_found', 'blocked' ], true ) ) { $single['actions'] = []; return $single; } $validated = isset( $single['action'] ) && is_array( $single['action'] ) ? $single['action'] : []; $type = isset( $validated['type'] ) ? sanitize_key( (string) $validated['type'] ) : 'none'; if ( 'none' === $type ) { continue; } // Actions that can invalidate the current document/snapshot always run alone. if ( in_array( $type, $standalone_types, true ) ) { if ( empty( $validated_actions ) ) { $validated_actions[] = $validated; } break; } $validated_actions[] = $validated; // A click may navigate, submit, or asynchronously re-render the document, so it is always last. if ( 'click' === $type ) { break; } } if ( ! empty( $result['requires_confirmation'] ) && count( $validated_actions ) > 1 ) { $validated_actions = [ $validated_actions[0] ]; } if ( 'action' === ( isset( $result['status'] ) ? $result['status'] : '' ) && empty( $validated_actions ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'No safe current-screen action could be validated. Analyze the live screen again.', 'wteam-executor' ); } $result['actions'] = $validated_actions; $result['action'] = ! empty( $validated_actions ) ? $validated_actions[0] : [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } if ( ! isset( $result['action'] ) || ! is_array( $result['action'] ) ) { return $result; } $type = isset( $result['action']['type'] ) ? sanitize_key( (string) $result['action']['type'] ) : 'none'; $id = isset( $result['action']['element_id'] ) ? sanitize_text_field( (string) $result['action']['element_id'] ) : ''; $target_id = isset( $result['action']['target_element_id'] ) ? sanitize_text_field( (string) $result['action']['target_element_id'] ) : ''; $value = isset( $result['action']['value'] ) ? self::sanitize_prompt_text( (string) $result['action']['value'], 4000 ) : ''; $result['action']['type'] = $type; $result['action']['element_id'] = $id; $result['action']['target_element_id'] = $target_id; $result['action']['value'] = $value; if ( 'navigate_url' === $type && '' === trim( $value ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'The suggested admin URL is empty. Analyze the screen again.', 'wteam-executor' ); $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '', ]; return $result; } $elementor_action_types = [ 'elementor_create', 'elementor_duplicate', 'elementor_delete', 'elementor_update_settings', 'elementor_save' ]; if ( in_array( $type, $elementor_action_types, true ) ) { if ( empty( $elementor_structure['available'] ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'The Elementor structural editor bridge is not available on the current screen. Re-analyze the live editor and use another safe action.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'elementor_bridge_unavailable'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } $required_commands = [ 'elementor_create' => 'document/elements/create', 'elementor_duplicate' => 'document/elements/duplicate', 'elementor_delete' => 'document/elements/delete', 'elementor_update_settings' => 'document/elements/settings', ]; if ( isset( $required_commands[ $type ] ) ) { $required_command = $required_commands[ $type ]; if ( isset( $elementor_structure['capabilities'][ $required_command ] ) && ! $elementor_structure['capabilities'][ $required_command ] ) { $result['status'] = 'not_found'; $result['message'] = __( 'That Elementor structural command is not available in the current editor build. Choose a different structural or visible-UI strategy from the current evidence.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'elementor_command_unavailable'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } } if ( 'elementor_save' === $type ) { $save_mode = strtolower( trim( $value ) ); $save_command = 'publish' === $save_mode ? 'document/save/publish' : 'document/save/update'; if ( isset( $elementor_structure['capabilities'][ $save_command ] ) && ! $elementor_structure['capabilities'][ $save_command ] ) { $result['status'] = 'not_found'; $result['message'] = __( 'The requested Elementor save mode is not available through the current editor command API. Use the visible editor save/publish control instead.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'elementor_save_command_unavailable'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } } if ( in_array( $type, [ 'elementor_duplicate', 'elementor_delete', 'elementor_update_settings' ], true ) && ( '' === $id || ! isset( $valid_elementor_ids[ $id ] ) ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'The requested Elementor element is no longer present in the current structural tree. Re-read builder.elementor.elements and choose a current stable Elementor id.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'stale_elementor_element'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } if ( 'elementor_create' === $type && '' !== $target_id && ! isset( $valid_elementor_ids[ $target_id ] ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'The requested Elementor parent is no longer present in the current structural tree. Re-read builder.elementor.elements and choose a current parent id.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'stale_elementor_parent'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } if ( in_array( $type, [ 'elementor_create', 'elementor_update_settings' ], true ) ) { $decoded_value = json_decode( $value, true ); if ( ! is_array( $decoded_value ) ) { $result['status'] = 'blocked'; $result['message'] = __( 'The requested Elementor structural payload was not valid JSON. Rebuild the action from the current structural tree instead of retrying the same payload.', 'wteam-executor' ); $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '' ]; return $result; } } if ( 'elementor_save' === $type ) { $mode = strtolower( trim( $value ) ); if ( ! in_array( $mode, [ 'update', 'publish' ], true ) ) { $result['action']['value'] = 'update'; } } return $result; } if ( in_array( $type, [ 'click', 'type', 'select', 'toggle', 'drag_drop' ], true ) && '' === $id ) { $result['status'] = 'not_found'; $result['message'] = __( 'The live editor DOM changed before the suggested UI action could be validated. I will re-identify the current target from a fresh DOM scan.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'stale_ui_source'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '', ]; return $result; } if ( 'drag_drop' === $type && ( '' === $target_id || ! isset( $valid_ids[ $target_id ] ) ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'The drag/drop destination changed before execution. I will re-identify the current source and destination from a fresh DOM scan.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'stale_drag_drop_destination'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '', ]; return $result; } if ( ! in_array( $type, [ 'none', 'navigate_url' ], true ) && '' !== $id && ! isset( $valid_ids[ $id ] ) ) { $result['status'] = 'not_found'; $result['message'] = __( 'The suggested UI element changed before execution. I will re-identify the current target from a fresh DOM scan.', 'wteam-executor' ); $result['auto_recoverable'] = true; $result['recovery_reason'] = 'stale_ui_target'; $result['action'] = [ 'type' => 'none', 'element_id' => '', 'target_element_id' => '', 'value' => '', ]; } return $result; } /** @return void */ private static function ensure_plugin_functions() { if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) || ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } } /** * @param string $text * @param int $max * @return string */ private static function limit_text( $text, $max ) { $text = (string) $text; if ( function_exists( 'mb_substr' ) ) { return mb_substr( $text, 0, $max ); } return substr( $text, 0, $max ); } }