403 ] ); } } public static function origin(): string { $origin = defined( 'SMOOTH_SANDBOX_PLUGINPEER_ORIGIN' ) ? SMOOTH_SANDBOX_PLUGINPEER_ORIGIN : 'https://pluginpeer.com'; $local = 'local' === wp_get_environment_type() || 'development' === wp_get_environment_type(); if ( 'https://pluginpeer.com' !== $origin && ! ( $local && preg_match( '#^http://(localhost|127\.0\.0\.1):[0-9]{2,5}$#D', $origin ) ) ) { throw new \RuntimeException( __( 'The PluginPeer origin is invalid. Use the official service or a loopback development server.', 'smooth-sandbox' ) ); } return $origin; } public static function url( string $tab = '' ): string { return admin_url( 'admin.php?page=' . self::SLUG . ( $tab ? '--' . $tab : '' ) ); } public function menu(): void { add_menu_page( 'Smooth Sandbox', 'Smooth Sandbox', 'manage_options', self::SLUG, [ $this, 'sandbox' ], 'dashicons-welcome-view-site', 82 ); add_submenu_page( self::SLUG, __( 'Sandbox', 'smooth-sandbox' ), __( 'Sandbox', 'smooth-sandbox' ), 'manage_options', self::SLUG, [ $this, 'sandbox' ] ); add_submenu_page( self::SLUG, __( 'Recordings', 'smooth-sandbox' ), __( 'Recordings', 'smooth-sandbox' ), 'manage_options', self::SLUG . '--recordings', [ $this, 'recordings' ] ); add_submenu_page( self::SLUG, __( 'Settings', 'smooth-sandbox' ), __( 'Settings', 'smooth-sandbox' ), 'manage_options', self::SLUG . '--settings', [ $this, 'settings' ] ); } public function assets( string $hook ): void { if ( false === strpos( $hook, self::SLUG ) ) { return; } // Refresh changed assets without bumping the unpublished plugin version. $asset_version = static fn( string $file ): string => SMOOTH_SANDBOX_VERSION . '.' . filemtime( dirname( SMOOTH_SANDBOX_FILE ) . '/assets/' . $file ); wp_enqueue_style( self::SLUG, plugins_url( 'assets/admin.css', SMOOTH_SANDBOX_FILE ), [], $asset_version( 'admin.css' ) ); wp_register_style( self::SLUG . '-preparing', false, [], $asset_version( 'preparing.css' ) ); wp_enqueue_style( self::SLUG . '-preparing' ); // The initial about:blank tab reuses this WordPress-generated style without a request. wp_add_inline_style( self::SLUG . '-preparing', file_get_contents( dirname( SMOOTH_SANDBOX_FILE ) . '/assets/preparing.css' ) ); wp_enqueue_script( self::SLUG, plugins_url( 'assets/admin.js', SMOOTH_SANDBOX_FILE ), [], $asset_version( 'admin.js' ), true ); wp_enqueue_script( self::SLUG . '-environment', plugins_url( 'assets/environment.js', SMOOTH_SANDBOX_FILE ), [ self::SLUG, self::SLUG . '-snapshot' ], $asset_version( 'environment.js' ), true ); wp_enqueue_script( self::SLUG . '-snapshot', plugins_url( 'assets/snapshot.js', SMOOTH_SANDBOX_FILE ), [ self::SLUG ], $asset_version( 'snapshot.js' ), true ); wp_enqueue_script( self::SLUG . '-settings', plugins_url( 'assets/settings.js', SMOOTH_SANDBOX_FILE ), [ self::SLUG . '-snapshot' ], $asset_version( 'settings.js' ), true ); try { $origin = self::origin(); } catch ( \Throwable $e ) { $origin = ''; } wp_localize_script( self::SLUG, 'smooth_sandbox', [ 'ajax' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'smooth_sandbox' ), 'origin' => $origin, 'recordings' => self::url( 'recordings' ), 'maxBytes' => Recordings::MAX_BYTES, 'chunkBytes' => Recordings::CHUNK, 'strings' => [ 'working' => __( 'Preparing content preview…', 'smooth-sandbox' ), 'review' => __( 'Review the exact data below, then confirm the transfer.', 'smooth-sandbox' ), 'changed' => __( 'Selection changed. Prepare a new preview before launching.', 'smooth-sandbox' ), 'waiting' => __( 'Waiting for PluginPeer… Keep this page open.', 'smooth-sandbox' ), 'recheckTimeout' => __( 'WordPress did not finish checking the reviewed content in time. Nothing was sent. Check your connection and retry.', 'smooth-sandbox' ), 'rechecking' => __( 'Checking your permissions and the reviewed content again…', 'smooth-sandbox' ), 'stale' => __( 'Content changed after your review. Nothing was sent. Prepare a new preview and review it again.', 'smooth-sandbox' ), 'bootTimeout' => __( 'WordPress did not become ready within 3 minutes. Startup was cancelled. Try again with a smaller selection or check your connection.', 'smooth-sandbox' ), 'bootFailed' => __( 'PluginPeer could not finish starting WordPress. Launch a new sandbox to retry.', 'smooth-sandbox' ), 'ready' => __( 'WordPress is ready in PluginPeer. Manage tools are available. Changes stay in this temporary browser copy.', 'smooth-sandbox' ), 'closed' => __( 'The PluginPeer window was closed or its connection was blocked. Allow popups and launch again.', 'smooth-sandbox' ), 'cancelled' => __( 'Startup cancelled. You can launch a new sandbox from the reviewed selection.', 'smooth-sandbox' ), 'stage_files' => __( 'Content transferred. Preparing the WordPress runtime…', 'smooth-sandbox' ), 'stage_runtime' => __( 'Content transferred. Loading WordPress…', 'smooth-sandbox' ), 'stage_setup' => __( 'Content transferred. Importing the selected content…', 'smooth-sandbox' ), 'stage_screen' => __( 'Content transferred. Preparing the WordPress screen…', 'smooth-sandbox' ), /* translators: 1: selected item count, 2: JSON size in KiB. */ 'summary' => __( 'Selected: %1$s · JSON: %2$s KiB / 512 KiB', 'smooth-sandbox' ), /* translators: %s: elapsed seconds measured in this browser. */ 'timing' => __( 'Ready after %s seconds in this browser. This timing is not uploaded.', 'smooth-sandbox' ), 'opened' => __( 'Content transferred. The sandbox is starting in PluginPeer. Keep this page open until WordPress is ready.', 'smooth-sandbox' ), 'timeout' => __( 'PluginPeer did not confirm the private import protocol. No content was sent. The Smooth Sandbox adapter must be deployed in PluginPeer.', 'smooth-sandbox' ), 'popup' => __( 'Allow the PluginPeer window to open, then retry.', 'smooth-sandbox' ), 'originUnavailable' => __( 'The PluginPeer address is invalid. Check the SMOOTH_SANDBOX_PLUGINPEER_ORIGIN configuration before launching.', 'smooth-sandbox' ), 'randomUnavailable' => __( 'This browser cannot create a secure transfer channel. Use a current browser on HTTPS or localhost.', 'smooth-sandbox' ), 'error' => __( 'The request failed. Keep this page open and retry. If your login expired, sign in in another tab first.', 'smooth-sandbox' ), 'unsupported' => __( 'This browser cannot record a tab here. Use a desktop browser with screen capture support on HTTPS or localhost.', 'smooth-sandbox' ), 'recording' => __( 'Recording the surface you selected. Stop when finished. Audio is not recorded.', 'smooth-sandbox' ), 'recorded' => __( 'Review the video below. It is only in this browser until you save it privately.', 'smooth-sandbox' ), 'empty' => __( 'No video was captured. Try recording again.', 'smooth-sandbox' ), 'limit' => __( 'The video reached the 20 MB limit. Record a shorter session.', 'smooth-sandbox' ), 'uploading' => __( 'Saving privately…', 'smooth-sandbox' ), 'saved' => __( 'Saved privately. Open Recordings to review, publish or delete it.', 'smooth-sandbox' ), 'leave' => __( 'An unsaved video will be lost if you leave this page.', 'smooth-sandbox' ), ], ] ); } private function header( string $title ): void { self::authorize(); echo '
'; } } public function sandbox(): void { $this->header( 'Smooth Sandbox' ); try { $inventory = Snapshot::inventory(); $versions = Runtime_Versions::get(); } catch ( \Throwable $error ) { echo '' . esc_html( $error->getMessage() ) . '