false, 'message' => 'aismart-core directory is missing.']; } $target_exists = file_exists($env_file); if ($target_exists && !is_writable($env_file)) { return ['ok' => false, 'message' => '.env file is not writable.']; } if (!$target_exists && !is_writable($env_dir)) { return ['ok' => false, 'message' => 'aismart-core directory is not writable.']; } $written = @file_put_contents($env_file, (string) $content, LOCK_EX); if ($written === false) { return ['ok' => false, 'message' => 'Failed to write .env file.']; } return ['ok' => true, 'message' => '.env file synchronized from WordPress config.']; } /** Keys the plugin always sets from WordPress. */ function aismart_embedded_env_managed_keys() { return [ 'APP_NAME', 'APP_ENV', 'APP_KEY', 'APP_DEBUG', 'APP_URL', 'DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'DB_SOCKET', 'SESSION_DRIVER', 'QUEUE_CONNECTION', 'QUEUE_DRIVER', 'CACHE_STORE', 'CACHE_DRIVER', 'FILESYSTEM_DISK', ]; } /** The core's settings for this site, from wp-config.php and the site address. */ function aismart_embedded_env_values($app_key) { [$host, $port, $socket] = defined('DB_HOST') ? aismart_parse_wp_db_host((string) DB_HOST) : ['127.0.0.1', '3306', '']; return [ 'APP_NAME' => 'AISmart', 'APP_ENV' => aismart_get_wp_app_env(), 'APP_KEY' => (string) $app_key, 'APP_DEBUG' => (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false', 'APP_URL' => aismart_resolve_wp_app_url(), 'DB_CONNECTION' => 'mysql', 'DB_HOST' => $host, 'DB_PORT' => $port !== '' ? $port : '3306', 'DB_DATABASE' => defined('DB_NAME') ? (string) DB_NAME : '', 'DB_USERNAME' => defined('DB_USER') ? (string) DB_USER : '', 'DB_PASSWORD' => defined('DB_PASSWORD') ? (string) DB_PASSWORD : '', 'DB_SOCKET' => $socket, 'SESSION_DRIVER' => 'file', 'QUEUE_CONNECTION' => 'sync', 'CACHE_STORE' => 'file', 'FILESYSTEM_DISK' => 'local', ]; } /** Hand-set keys of an old .env file: set, not managed by the plugin, and different from .env.example. */ function aismart_env_custom_values($content) { $content = (string) $content; if ($content === '' || !preg_match_all('/^\s*([A-Z][A-Z0-9_]*)\s*=\s*([^\r\n]*)$/m', $content, $matches, PREG_SET_ORDER)) { return []; } $example_file = aismart_runtime_plugin_dir() . '/aismart-core/.env.example'; $example = is_readable($example_file) ? (string) @file_get_contents($example_file) : ''; $managed = aismart_embedded_env_managed_keys(); $custom = []; foreach ($matches as $match) { $key = $match[1]; $value = trim((string) $match[2], " \t\n\r\0\x0B\"'"); if (in_array($key, $managed, true) || $value === '' || $value === 'null') { continue; } if ($value === aismart_resolve_env_value_from_content($example, $key)) { continue; } $custom[$key] = $value; } return $custom; } function aismart_runtime_random_hex($bytes) { $bytes = max(1, (int) $bytes); try { return bin2hex(random_bytes($bytes)); } catch (Throwable $e) { return substr(hash('sha256', wp_generate_password(64, true, true) . '|' . microtime(true) . '|' . wp_rand()), 0, $bytes * 2); } } /** Creates the settings folder with its deny-all .htaccess and empty index.html. */ function aismart_runtime_settings_prepare_dir($dir) { $dir = rtrim((string) $dir, '/\\'); if ($dir === '' || (!is_dir($dir) && !wp_mkdir_p($dir)) || !is_dir($dir) || !is_writable($dir)) { return ['ok' => false, 'message' => 'The uploads folder is not writable.']; } $htaccess = $dir . '/.htaccess'; if (!is_file($htaccess)) { @file_put_contents($htaccess, "# AISmart Content Studio runtime settings. Never served.\n" . "\n\tRequire all denied\n\n" . "\n\tOrder deny,allow\n\tDeny from all\n\n"); } if (!is_file($dir . '/index.html')) { @file_put_contents($dir . '/index.html', ''); } return ['ok' => true, 'dir' => $dir]; } /** Writes the settings file in one step (temporary file, then rename), readable by its owner only. */ function aismart_runtime_settings_write($dir, array $data) { $dir = rtrim((string) $dir, '/\\'); $json = function_exists('wp_json_encode') ? wp_json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if (!is_string($json) || $json === '') { return ['ok' => false, 'message' => 'Failed to encode runtime settings.']; } $file = aismart_runtime_settings_file($dir); if ($file === '') { $file = $dir . '/runtime-' . aismart_runtime_random_hex(16) . '.php'; } $content = aismart_runtime_settings_guard() . $json . "\n"; $tmp = $dir . '/.runtime-' . aismart_runtime_random_hex(8) . '.tmp'; $handle = @fopen($tmp, 'xb'); if ($handle === false) { return ['ok' => false, 'message' => 'Failed to write runtime settings.']; } @chmod($tmp, 0600); $written = fwrite($handle, $content); fclose($handle); if ($written !== strlen($content) || !@rename($tmp, $file)) { @unlink($tmp); return ['ok' => false, 'message' => 'Failed to write runtime settings.']; } @chmod($file, 0600); foreach ((array) @scandir($dir) as $name) { if (preg_match('/^runtime-[0-9a-f]{32}\.php$/', (string) $name) && $dir . '/' . $name !== $file) { @unlink($dir . '/' . $name); } } return ['ok' => true, 'file' => $file]; } /** * Builds the core's settings from WordPress and saves them in the protected uploads folder. * Moves an old aismart-core/.env there (app key and hand-set keys), then deletes it. When the uploads folder * cannot be used, it writes the old .env instead, as earlier versions did. */ function aismart_sync_embedded_env_file() { $plugin_dir = aismart_runtime_plugin_dir(); if (!is_dir($plugin_dir . '/aismart-core')) { return ['ok' => false, 'message' => 'aismart-core directory is missing.']; } $legacy_file = aismart_get_embedded_env_file_path(); $legacy_content = ''; if (file_exists($legacy_file) && is_readable($legacy_file)) { $loaded = @file_get_contents($legacy_file); if ($loaded === false) { return ['ok' => false, 'message' => 'Failed to read .env file.']; } $legacy_content = (string) $loaded; } // The old full copy of these settings in the options table is no longer kept. delete_option('aismart_embedded_env_snapshot'); $runtime_dir = function_exists('aismart_runtime_settings_dir') ? aismart_runtime_settings_dir($plugin_dir) : ''; $ready = $runtime_dir !== '' ? aismart_runtime_settings_prepare_dir($runtime_dir) : ['ok' => false]; $lock = !empty($ready['ok']) ? @fopen($runtime_dir . '/.lock', 'c') : false; if ($lock) { @flock($lock, LOCK_EX); } $existing = !empty($ready['ok']) ? aismart_runtime_settings_read(aismart_runtime_settings_file($runtime_dir)) : null; $existing_env = is_array($existing) ? $existing['env'] : []; // Keep the app key the core already uses, so nobody is signed out of the Content Manager. $app_key = ''; foreach ([(string) ($existing_env['APP_KEY'] ?? ''), aismart_resolve_env_value_from_content($legacy_content, 'APP_KEY')] as $candidate) { if (aismart_is_valid_laravel_app_key($candidate)) { $app_key = $candidate; break; } } if ($app_key === '') { $app_key = aismart_generate_laravel_app_key(); } $values = aismart_embedded_env_values($app_key); if (!empty($ready['ok'])) { $env = $existing_env; foreach (aismart_env_custom_values($legacy_content) as $key => $value) { if (!array_key_exists($key, $env)) { $env[$key] = $value; } } // One file serves every site of a network: each site adds its own address, and a site other than the // main site keeps the default address that is already there. $hosts = is_array($existing) ? $existing['hosts'] : []; $previous_url = (string) ($existing_env['APP_URL'] ?? aismart_resolve_env_value_from_content($legacy_content, 'APP_URL')); $previous_host = aismart_runtime_normalize_host((string) wp_parse_url($previous_url, PHP_URL_HOST)); if ($previous_host !== '' && !isset($hosts[$previous_host]) && preg_match('#^https?://\S+$#i', $previous_url)) { $hosts[$previous_host] = rtrim($previous_url, '/'); } $site_host = aismart_runtime_normalize_host((string) wp_parse_url($values['APP_URL'], PHP_URL_HOST)); if ($site_host !== '') { $hosts[$site_host] = $values['APP_URL']; } ksort($hosts); $env = array_merge($env, $values); $keep_default = function_exists('is_multisite') && is_multisite() && function_exists('is_main_site') && !is_main_site(); if ($keep_default && $previous_host !== '' && isset($hosts[$previous_host])) { $env['APP_URL'] = $hosts[$previous_host]; } $saved = aismart_runtime_settings_write($runtime_dir, ['version' => 1, 'env' => $env, 'hosts' => $hosts]); if ($lock) { @flock($lock, LOCK_UN); @fclose($lock); $lock = false; } if (!empty($saved['ok'])) { if (file_exists($legacy_file)) { @unlink($legacy_file); } $legacy_left = file_exists($legacy_file); return [ 'ok' => true, 'storage' => 'runtime', 'legacy_env_left' => $legacy_left, 'message' => $legacy_left ? 'Runtime settings synchronized from WordPress config. Delete aismart-core/.env from the plugin folder.' : 'Runtime settings synchronized from WordPress config.', ]; } } if ($lock) { @flock($lock, LOCK_UN); @fclose($lock); } // Fallback for hosts where the uploads folder cannot be used: the .env file of earlier versions. $content = $legacy_content; foreach ($values as $key => $value) { $content = aismart_upsert_env_content_value($content, $key, $value); } $persist = aismart_persist_embedded_env_file($content); if (empty($persist['ok'])) { return [ 'ok' => false, 'message' => isset($persist['message']) ? (string) $persist['message'] : 'Failed to persist .env file.', ]; } return [ 'ok' => true, 'storage' => 'legacy-env', 'message' => isset($persist['message']) ? (string) $persist['message'] : '.env synchronized.', ]; } /** * Keeps the runtime settings in place: writes them when they are missing (after a fresh install or an update), * when this site's address is not in them yet, or when an old aismart-core/.env still has to be moved. * After a failed or partial attempt it waits an hour before trying again. */ add_action('admin_init', 'aismart_runtime_settings_self_heal'); function aismart_runtime_settings_self_heal() { if ((defined('DOING_AJAX') && DOING_AJAX) || !function_exists('aismart_runtime_settings_dir')) { return; } if (get_transient('aismart_runtime_settings_retry')) { return; } $settings = aismart_runtime_settings_read(aismart_runtime_settings_file(aismart_runtime_settings_dir(aismart_runtime_plugin_dir()))); $site_host = aismart_runtime_normalize_host((string) wp_parse_url(aismart_resolve_wp_app_url(), PHP_URL_HOST)); $legacy_exists = file_exists(aismart_get_embedded_env_file_path()); if ($settings !== null && !$legacy_exists && ($site_host === '' || isset($settings['hosts'][$site_host]))) { return; } $sync = aismart_sync_embedded_env_file(); if (empty($sync['ok']) || ($sync['storage'] ?? '') !== 'runtime' || !empty($sync['legacy_env_left'])) { set_transient('aismart_runtime_settings_retry', 1, HOUR_IN_SECONDS); } } add_action('admin_post_aismart_reconfigure_embedded_env', 'aismart_reconfigure_embedded_env_action'); function aismart_reconfigure_embedded_env_action() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Unauthorized', 'aismart-content-studio'), 403); } check_admin_referer('aismart_reconfigure_embedded_env'); $default_return = admin_url('admin.php?page=aismart-token'); $requested_return = isset($_REQUEST['return']) ? wp_unslash((string) $_REQUEST['return']) : $default_return; $return_url = wp_validate_redirect($requested_return, $default_return); $sync = aismart_sync_embedded_env_file(); if (!empty($sync['ok'])) { wp_safe_redirect(add_query_arg('env_reconfigured', '1', $return_url)); exit; } $onboarding_url = add_query_arg( [ 'page' => 'aismart-token-onboarding', 'setup' => 'env-reconfigure-failed', 'env_message' => isset($sync['message']) ? (string) $sync['message'] : 'env sync failed', ], admin_url('admin.php') ); wp_safe_redirect($onboarding_url); exit; } function aismart_resolve_env_connection_from_file($env_file) { if (!is_readable($env_file)) { return ''; } $content = (string) file_get_contents($env_file); if ($content === '') { return ''; } if (preg_match('/^\s*DB_CONNECTION\s*=\s*([^\r\n#]+)/mi', $content, $m)) { return trim((string) $m[1], " \t\n\r\0\x0B\"'"); } return ''; } function aismart_prepare_embedded_laravel_env() { $app_key = aismart_generate_laravel_app_key(); aismart_set_runtime_env_value('APP_ENV', aismart_get_wp_app_env()); aismart_set_runtime_env_value('APP_DEBUG', (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false'); aismart_set_runtime_env_value('APP_URL', aismart_resolve_wp_app_url()); aismart_set_runtime_env_value('APP_KEY', $app_key); if (defined('DB_HOST')) { [$host, $port, $socket] = aismart_parse_wp_db_host((string) DB_HOST); aismart_set_runtime_env_value('DB_HOST', $host); aismart_set_runtime_env_value('DB_PORT', $port !== '' ? $port : '3306'); if ($socket !== '') { aismart_set_runtime_env_value('DB_SOCKET', $socket); } } if (defined('DB_NAME')) { aismart_set_runtime_env_value('DB_DATABASE', (string) DB_NAME); } if (defined('DB_USER')) { aismart_set_runtime_env_value('DB_USERNAME', (string) DB_USER); } aismart_set_runtime_env_value('DB_CONNECTION', 'mysql'); if (defined('DB_PASSWORD')) { aismart_set_runtime_env_value('DB_PASSWORD', (string) DB_PASSWORD); } // Keep runtime stores file/sync to avoid database-backed cache/session tables. aismart_set_runtime_env_value('CACHE_STORE', 'file'); aismart_set_runtime_env_value('CACHE_DRIVER', 'file'); aismart_set_runtime_env_value('SESSION_DRIVER', 'file'); aismart_set_runtime_env_value('QUEUE_CONNECTION', 'sync'); aismart_set_runtime_env_value('QUEUE_DRIVER', 'sync'); }