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');
}