From 27b0868ab97d6461891411386be07bc3cdbbbc64 Mon Sep 17 00:00:00 2001 From: Mark Hanna Date: Tue, 5 May 2026 16:37:26 -0600 Subject: [PATCH] updates to support composer based installs and allow installing while codebase includes civicrm.settings.php template --- civicrm.php | 50 ++++++++++++++++- includes/civicrm.admin.php | 112 +++++++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 5 deletions(-) diff --git a/civicrm.php b/civicrm.php index a71a969..ffc6a56 100644 --- a/civicrm.php +++ b/civicrm.php @@ -102,6 +102,54 @@ define('CIVICRM_INSTALLED', FALSE); } +/** + * Determine whether CiviCRM's database schema has actually been bootstrapped. + * + * In Bedrock-style deployments, civicrm.settings.php may be shipped as a + * template (driven by env vars) before the database has ever been initialized. + * In that case CIVICRM_INSTALLED is TRUE but the schema is empty. Without this + * check, the activation hook would skip the installer redirect and admin + * bootstrap would later fail trying to read from a missing schema. + * + * Probes the civicrm_domain table on CIVICRM_DSN. Cached per-request. + */ +if (!function_exists('civicrm_schema_is_installed')) { + function civicrm_schema_is_installed() { + static $cached = NULL; + if ($cached !== NULL) { + return $cached; + } + if (!CIVICRM_INSTALLED) { + return $cached = FALSE; + } + if (!defined('CIVICRM_DSN')) { + @include_once CIVICRM_SETTINGS_PATH; + } + if (!defined('CIVICRM_DSN')) { + return $cached = FALSE; + } + $dsn = @parse_url(CIVICRM_DSN); + if (empty($dsn['host']) || empty($dsn['user']) || empty($dsn['path'])) { + return $cached = FALSE; + } + $db_name = ltrim($dsn['path'], '/'); + $port = !empty($dsn['port']) ? (int) $dsn['port'] : 3306; + try { + $mysqli = @new mysqli($dsn['host'], $dsn['user'], $dsn['pass'] ?? '', $db_name, $port); + if ($mysqli->connect_errno) { + return $cached = FALSE; + } + $result = @$mysqli->query("SHOW TABLES LIKE 'civicrm_domain'"); + $found = ($result && $result->num_rows > 0); + $mysqli->close(); + return $cached = (bool) $found; + } + catch (\Throwable $e) { + return $cached = FALSE; + } + } +} + /** * Setting this to 'TRUE' will replace all mailing URLs calls to 'extern/url.php' * and 'extern/open.php' with their REST counterpart 'civicrm/v3/url' and @@ -435,7 +483,7 @@ public function activation() { // When installed via the WordPress UI, try and redirect to the Installer page. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $activate_multi = isset($_GET['activate-multi']) ? sanitize_text_field(wp_unslash($_GET['activate-multi'])) : ''; - if (!is_multisite() && empty($activate_multi) && !CIVICRM_INSTALLED) { + if (!is_multisite() && empty($activate_multi) && (!CIVICRM_INSTALLED || !civicrm_schema_is_installed())) { wp_safe_redirect(admin_url('admin.php?page=civicrm-install')); exit; } diff --git a/includes/civicrm.admin.php b/includes/civicrm.admin.php index 74ebc25..5ca5db3 100644 --- a/includes/civicrm.admin.php +++ b/includes/civicrm.admin.php @@ -273,6 +273,98 @@ public function run_installer() { } + $vendor_setup_paths = []; + $filtered_paths = array_values(array_filter(explode(DIRECTORY_SEPARATOR, CIVICRM_PLUGIN_DIR))); + $potential_vendor_paths = []; + $count = count($filtered_paths); + for ($i = 0; $i <= $count; $i++) { + if ($i < $count) { + $slice = array_slice($filtered_paths, 0, $count - $i); + $potential_vendor_paths[] = '/' . implode('/', $slice); + } + } + foreach ($potential_vendor_paths as $potential_path) { + $civicrm_core_path = implode(DIRECTORY_SEPARATOR, array_merge(explode(DIRECTORY_SEPARATOR, $potential_path), ['vendor', 'civicrm', 'civicrm-core'])); + $civicrm_setup_autoload_path = $civicrm_core_path . DIRECTORY_SEPARATOR . 'setup' . DIRECTORY_SEPARATOR . 'civicrm-setup-autoload.php'; + $civicrm_classloader_path = $civicrm_core_path . DIRECTORY_SEPARATOR . 'CRM' . DIRECTORY_SEPARATOR . 'Core' . DIRECTORY_SEPARATOR . 'ClassLoader.php'; + if (file_exists($civicrm_setup_autoload_path)) { + require_once $civicrm_setup_autoload_path; + require_once $civicrm_classloader_path; + CRM_Core_ClassLoader::singleton()->register(); + \Civi\Setup::assertProtocolCompatibility(1.0); + \Civi\Setup::init([ + 'cms' => 'WordPress', + 'srcPath' => $civicrm_core_path, + 'doNotCreateSettingsFile' => TRUE, + ]); + // Bedrock-style deployments ship civicrm.settings.php as a template, + // so its presence does not imply CiviCRM has been installed. Force + // setSettingInstalled(FALSE) so only the database check gates whether + // SetupController::runStart() short-circuits to the "finished" page. + \Civi\Setup::dispatcher()->addListener( + 'civi.setup.checkInstalled', + function (\Civi\Setup\Event\CheckInstalledEvent $e) { + $e->setSettingInstalled(FALSE); + }, + -1000 + ); + // The default checkRequirements listener errors when the settings file + // path isn't writable. When doNotCreateSettingsFile is set, the file is + // managed externally and writability is irrelevant — overwrite the + // 'settingsWritable' message (keyed by name) with an info entry. + \Civi\Setup::dispatcher()->addListener( + 'civi.setup.checkRequirements', + function (\Civi\Setup\Event\CheckRequirementsEvent $e) { + $m = $e->getModel(); + if (!empty($m->doNotCreateSettingsFile)) { + $e->addInfo('system', 'settingsWritable', sprintf('The settings file "%s" is managed externally; writability is not checked.', $m->settingsPath)); + } + }, + -1000 + ); + // The default WordPress init listener sets $model->db to the WP + // database creds. When CIVICRM_DSN is already defined (Bedrock-style + // templated settings file), prefer that — the CiviCRM database is + // typically separate from the WordPress one. The civi.setup.init event + // is dispatched inside \Civi\Setup::init() before listeners can be + // attached, so mutate the model directly. + if (!defined('CIVICRM_DSN') && file_exists(CIVICRM_SETTINGS_PATH)) { + @include_once CIVICRM_SETTINGS_PATH; + } + if (defined('CIVICRM_DSN')) { + $civi_dsn_parsed = \Civi\Setup\DbUtil::parseDsn(CIVICRM_DSN); + \Civi\Setup::instance()->getModel()->db = $civi_dsn_parsed; + \Civi\Setup::instance()->getModel()->extras['advanced']['db'] = \Civi\Setup\DbUtil::encodeDsn($civi_dsn_parsed); + } + // Keep the editable "advanced.db" field in sync with $model->db when + // the user hasn't typed their own value yet — the default advanced + // listener at PRIORITY_LATE forces the input back to a placeholder, so + // run after it (PRIORITY_END = -2000). + \Civi\Setup::dispatcher()->addListener( + 'civi.setupui.run', + function (\Civi\Setup\UI\Event\UIBootEvent $e) { + $model = $e->getModel(); + $placeholder = 'mysql://USER:PASS@HOST/DB'; + $values = $e->getField('advanced', []); + if (empty($values['db']) || $values['db'] === $placeholder) { + $model->extras['advanced']['db'] = \Civi\Setup\DbUtil::encodeDsn($model->db); + } + }, + \Civi\Setup::PRIORITY_END + ); + $ctrl = \Civi\Setup::instance()->createController()->getCtrl(); + $ctrl->setUrls([ + 'ctrl' => menu_page_url('civicrm-install', FALSE), + 'res' => CIVICRM_PLUGIN_URL . 'civicrm/core/setup/res/', + 'jquery.js' => CIVICRM_PLUGIN_URL . 'civicrm/core/bower_components/jquery/dist/jquery.min.js', + 'font-awesome.css' => CIVICRM_PLUGIN_URL . 'civicrm/core/bower_components/font-awesome/css/all.min.css', + 'finished' => admin_url('admin.php?page=CiviCRM&q=civicrm&reset=1'), + ]); + \Civi\Setup\BasicRunner::run($ctrl); + return; + } + } + wp_die(__('Installer unavailable. Failed to locate CiviCRM libraries.', 'civicrm')); } @@ -378,6 +470,18 @@ public function initialize() { return FALSE; } + /* + * Settings file exists, but the database schema may not have been + * bootstrapped yet (e.g. Bedrock layouts that ship a templated settings + * file driven by env vars). In that case route to the installer rather + * than attempting to bootstrap CiviCRM against an empty schema. + */ + if (function_exists('civicrm_schema_is_installed') && !civicrm_schema_is_installed()) { + $this->error_flag = 'settings-missing'; + $initialized = FALSE; + return FALSE; + } + // Check PHP version in case of upgrade. if (!$this->assert_php_support()) { $initialized = FALSE; @@ -438,13 +542,13 @@ public function initialize() { return FALSE; } - // Initialize the Class Loader. - require_once CIVICRM_PLUGIN_DIR . 'civicrm/CRM/Core/ClassLoader.php'; - CRM_Core_ClassLoader::singleton()->register(); - // Access global defined in "civicrm.settings.php". global $civicrm_root; + // Initialize the Class Loader. + require_once $civicrm_root . 'CRM/Core/ClassLoader.php'; + CRM_Core_ClassLoader::singleton()->register(); + // Bail if the config file isn't found. if (!file_exists($civicrm_root . 'CRM/Core/Config.php')) { $this->error_flag = 'config-missing';