old_cacti_version = get_cacti_db_version_raw(); $this->setRuntime($install_params['Runtime'] ?? 'unknown'); $step = read_config_option('install_step', true); log_install_high('step', 'Initial: ' . clean_up_lines(var_export($step, true))); if (empty($step)) { $step = $this->getStepDefault(); } $this->stepError = false; if ($step == Installer::STEP_INSTALL) { $install_version = read_config_option('install_version', true); log_install_high('step', 'Previously complete: ' . clean_up_lines(var_export($install_version, true))); if (empty($install_version)) { $install_version = $this->old_cacti_version; } $install_params = []; $install_error = read_config_option('install_error', true); if (!empty($install_error)) { $step = Installer::STEP_ERROR; } elseif (!is_install_needed($install_version)) { log_install_debug('step', 'Does match: ' . clean_up_lines(var_export($this->old_cacti_version, true))); } } elseif ($step >= Installer::STEP_COMPLETE) { $install_version = read_config_option('install_version', true); log_install_high('step', 'Previously complete: ' . clean_up_lines(var_export($install_version, true))); if (is_install_needed($install_version)) { log_install_debug('step', 'Does not match: ' . clean_up_lines(var_export(CACTI_VERSION_FULL, true))); $this->stepError = Installer::STEP_WELCOME; db_execute('DELETE FROM settings WHERE name LIKE \'install_%\''); } else { $install_params = []; } } log_install_high('step', 'After: ' . clean_up_lines(var_export($step, true))); $this->setStep($step); $this->iconClass = [ DB_STATUS_ERROR => 'ti ti-thumb-down', DB_STATUS_WARNING => 'ti ti-alert-triangle-filled', DB_STATUS_RESTART => 'ti ti-alert-triangle-filled', DB_STATUS_SUCCESS => 'ti ti-thumb-up', DB_STATUS_SKIPPED => 'fa-solid fa-circle-check' ]; $this->defaultAutomation = [ [ 'name' => 'Net-SNMP Device', 'hash' => '07d3fe6a52915f99e642d22e27d967a4', 'sysDescrMatch' => 'Linux', 'sysNameMatch' => '', 'sysOidMatch' => '', 'availMethod' => 2, 'sequence' => 1 ], [ 'name' => 'Windows Device', 'hash' => '5b8300be607dce4f030b026a381b91cd', 'sysDescrMatch' => 'Windows', 'sysNameMatch' => '', 'sysOidMatch' => '', 'availMethod' => 2, 'sequence' => 2 ], [ 'name' => 'Cisco Router', 'hash' => 'cae6a879f86edacb2471055783bec6d0', 'sysDescrMatch' => '(Cisco Internetwork Operating System Software|IOS)', 'sysNameMatch' => '', 'sysOidMatch' => '', 'availMethod' => 2, 'sequence' => 3 ] ]; $this->errors = []; $this->templates = []; $this->eula = intval(read_config_option('install_eula', true)); $this->cronInterval = intval(read_config_option('cron_interval', true)); $this->locales = get_installed_locales(); $this->stepData = null; $this->setLanguage($this->getLanguage()); $this->setTheme($this->getTheme()); if (empty($this->theme)) { $this->setTheme('modern'); } if ($step < Installer::STEP_INSTALL) { $this->setDefaults($install_params); } log_install_debug('step', 'Error: ' . clean_up_lines(var_export($this->stepError, true))); log_install_debug('step', 'Done: ' . clean_up_lines(var_export($this->stepCurrent, true))); } /** * jsonSerialize() - provides JSON object of return data with optional * values output dependent on Runtime mode. * * @return array - An array of options data */ public function jsonSerialize(): array { if (empty($this->output)) { $this->output = $this->processCurrentStep(); } $basics = [ 'Mode' => $this->mode, 'Step' => $this->stepCurrent, 'Errors' => $this->errors ]; $webdata = []; // Are we running in either Web or Json mode? if ($this->runtime == 'Web' || $this->runtime == 'Json') { $webdata += [ 'Eula' => $this->eula, 'Prev' => $this->buttonPrevious, 'Next' => $this->buttonNext, 'Test' => $this->buttonTest, 'Theme' => $this->theme, 'StepData' => $this->stepData, ]; } // Are we running in only Web mode? if ($this->runtime == 'Web') { $webdata += [ 'Html' => $this->output, ]; } return array_merge($basics, $webdata); } /** * getData() - alias for jsonSerialize() * * @return array - A serialized json string */ public function getData() : array { return $this->jsonSerialize(); } /** * getErrors() - retrieve an array of all recorded errors * * @return array - An array of errors */ public function getErrors() : array { return $this->errors; } /** * processParameters - process array of parameters to override defaults * matches XXX from setXXX/getXXX functions * * @param array $params_install - array of parameters to process where key * * @return void */ protected function processParameters($params_install = []) { // Always set the current step first if (isset($params_install['Step'])) { $this->setStep($params_install['Step']); unset($params_install['Step']); } foreach ($params_install as $key => $value) { switch ($key) { case 'Mode': $this->setMode($value); break; case 'Eula': $this->setEula($value); break; case 'Prev': $this->buttonPrevious = new InstallerButton($value); break; case 'Next': $this->buttonNext = new InstallerButton($value); break; case 'Test': $this->buttonTest = new InstallerButton($value); break; case 'Language': $this->setLanguage($value); break; case 'Profile': $this->setProfile($value); break; case 'CronInterval': $this->setCronInterval($value); break; case 'DefaultTemplate': $this->setDefaultTemplate($value); break; case 'AutomationMode': $this->setAutomationMode(intval($value)); break; case 'AutomationOverride': $this->setAutomationOverride($value); break; case 'AutomationRange': $this->setAutomationRange($value); break; case 'Templates': $this->setTemplates($value); break; case 'Tables': $this->setTables($value); break; case 'Paths': $this->setPaths($value); break; case 'RRDVersion': $this->setRRDVersion($value); break; case 'Theme': $this->setTheme($value); break; case 'SnmpOptions': $this->setSnmpOptions($value); break; case 'AdminEmailAddress': $this->setAdminEmailAddress($value); break; case 'Runtime': break; default: log_install_always('badkey', "$key => $value"); } } } /** * setDefaults - apply default values from array object * * @param array $install_params - optional key/value array where key matches * * @return void */ private function setDefaults($install_params = []) : void { $this->tables = $this->getTables(); // You have to set the paths before you start executing commands $this->paths = install_file_paths(); $this->setPaths($this->getPaths()); $this->permissions = $this->getPermissions(); $this->modules = $this->getModules(); $this->setDefaultTemplate($this->getDefaultTemplate()); $this->setTemplates($this->getTemplates()); $this->setProfile($this->getProfile()); $this->setAutomationMode($this->getAutomationMode()); $this->setAutomationOverride($this->getAutomationOverride()); $this->setAutomationRange($this->getAutomationRange()); $this->setRRDVersion($this->getRRDVersion(), 'default '); $this->snmpOptions = $this->getSnmpOptions(); $this->setAdminEmailAddress($this->getAdminEmailAddress()); $this->setMode($this->getMode()); log_install_high('', 'Installer::processParameters(' . clean_up_lines(json_encode($install_params)) . ')'); if (!empty($install_params)) { $this->processParameters($install_params); } if ($this->stepError !== false && $this->stepCurrent > $this->stepError) { $this->setStep($this->stepError); } } /** * setTrueFalse() - determine whether param can be mapped to either * True or False and if so, assign result to $field * * @param mixed $param - value to be set if it can be mapped to True or False * @param mixed $field - variable reference to be set * @param string $option - name of the option * @param bool $save - Indication to save state * * @return bool - The result of the function */ private function setTrueFalse(mixed $param, mixed &$field, string $option = '', bool $save = true) : bool { $value = null; if ($param === true || $param === 'true' || $param === 'on' || $param === 1 || $param === '1') { $value = true; } elseif ($param === false || $param === 'false' || $param === '' || $param === 0 || $param === '0') { $value = false; } if ($value !== null) { $field = $value; if ($save) { set_install_config_option('install_' . $option, $param); } } $result = $value !== null; log_install_high('', "setTrueFalse($option, " . var_export($param, true) . " sets $value, returns $result"); return $result; } /** * addError() - adds a new error to the array or updates an existing one * @param int $step Which step of the installer reports the error and * should be Installer::STEP_ constant * @param string $section Title of section causing a problem * @param string $item Individual item that caused the problem * @param mixed $text Descriptive text of the error * * @return void */ public function addError(int $step, string $section, string $item, mixed $text = false) : void { if (!isset($this->errors[$section])) { $this->errors[$section] = []; } if ($text === false) { $this->errors[$section][] = $item; log_install_medium('errors', "addError($section, $item)"); } else { $this->errors[$section][$item] = $text; log_install_medium('errors', "addError($section, $item, $text)"); } log_install_debug('errors', 'stepError = ' . $step . ' -> ' . clean_up_lines(var_export($this->stepError, true))); if ($this->stepError === false || $this->stepError > $step) { $this->stepError = $step; } log_install_debug('errors-json', clean_up_lines(var_export($this->errors, true))); } /** * setProgress() - set the progress point of Installer::STEP_INSTALL * * @param int $param_progress One of Installer::PROGRESS_ constants * * @return void */ private function setProgress(int $param_progress) : void { log_install_medium('', "Progress: $param_progress"); set_install_config_option('install_progress', $param_progress); set_install_config_option('install_updated', microtime(true)); } /** * sanitizeRRDVersion() - ensure version number is valid * * @param string $param_rrdver The version to be sanitized * @param string $default_version The default version if any * * @return string The rrdtool version */ private function sanitizeRRDVersion(string $param_rrdver = '', string $default_version = '') : string { $rrdver = $default_version; if ($param_rrdver != '') { log_install_debug('rrdversion', 'sanitizeRRDVersion() - Checking for version string'); if (preg_match("/(?:version|v)*\s*((?:[0-9]+\.?)+)/i", $param_rrdver, $matches)) { log_install_debug('rrdversion', 'sanitizeRRDVersion() - Checking for version string - ' . (cacti_sizeof($matches) - 1) . ' matches found'); if (cacti_sizeof($matches) > 1) { log_install_debug('rrdversion', 'sanitizeRRDVersion() - Comparing ' . $param_rrdver . ' <= 1.3'); if (cacti_version_compare('1.3', $matches[1], '<=')) { $rrdver = $matches[1]; log_install_debug('rrdversion', 'sanitizeRRDVersion() - Valid version'); } } } } elseif (!empty($default_version)) { $rrdver = $this->sanitizeRRDVersion($default_version, '1.3'); } log_install_medium('rrdversion', 'sanitizeRRDVersion() - returning ' . $rrdver); return $rrdver; } /* * * * Setters/Getters * * --------------- * * * * This section contains various setXXX and getXXX functions * * to prime the installer. Most of these functions are private * * to prevent external usage which must come via the installData * * parameter when creating a new Installer class object * * * */ /** * getPermissions() - gets the permissions for folders that we require * to be available for writing during install or * always (after install) * * @return array An array of permissions */ private function getPermissions() : array { global $config; $permissions = ['install' => [], 'always' => []]; $install_paths = [ 'snmp_queries' => CACTI_PATH_RESOURCE . '/snmp_queries', 'script_server' => CACTI_PATH_RESOURCE . '/script_server', 'script_queries' => CACTI_PATH_RESOURCE . '/script_queries', 'scripts' => CACTI_PATH_SCRIPTS . '', ]; $always_paths = [ 'sys_temp' => sys_get_temp_dir(), 'log' => CACTI_PATH_LOG . '', 'boost' => CACTI_PATH_CACHE . '/boost', 'mibcache' => CACTI_PATH_CACHE . '/mibcache', 'realtime' => CACTI_PATH_CACHE . '/realtime', 'spikekill' => CACTI_PATH_CACHE . '/spikekill' ]; /* The csrf secret is deliberately absent from $install_paths. It only feeds the legacy token fallback for sites upgrading from csrf-magic, which a new install has no use for, and its historical default lives under a directory that no longer ships. Demanding it here failed the permission step, and setStep() clamps to the first errored step, so a fresh install could never advance past a path the user cannot create. */ $install_key = 'always'; if ($this->mode != Installer::MODE_POLLER) { $install_key = 'install'; } foreach ($install_paths as $name => $path) { if (is_dir($path)) { $valid = (is_resource_writable($path . '/')); $permissions[$install_key][$path . '/'] = $valid; } else { $valid = (is_resource_writable($path)); $permissions[$install_key][$path] = $valid; } log_install_debug('permission',"($name) $path = $valid ($install_key)"); if (!$valid) { $this->addError(Installer::STEP_PERMISSION_CHECK, 'Permission', $name . ':' . $path, __('Path was not writable')); } } foreach ($always_paths as $name => $path) { $path = rtrim($path, '/') . '/'; $valid = (is_resource_writable($path)); $permissions['always'][$path] = $valid; log_install_debug('permission',"($name) $path = $valid ($install_key)"); if (!$valid) { $this->addError(Installer::STEP_PERMISSION_CHECK, 'Permission', $name . ':' . $path, __('Path was not writable')); } } return $permissions; } /** * setRuntime() - sets the runtime mode of the Installer * * @param string $param_runtime Default is 'unknown', acceptable modes are 'Cli' * and 'Json'. * * @return void */ public function setRuntime(string $param_runtime = 'unknown') : void { if ($param_runtime == 'Web' || $param_runtime == 'Cli' || $param_runtime == 'Json') { $this->runtime = $param_runtime; } else { $this->addError(Installer::STEP_WELCOME, '', 'Failed to apply specified runtime'); } } private function getLanguage() : string { $language = read_config_option('install_language'); $section = 'install'; if (empty($language)) { $language = read_config_option('i18n_default_language'); $section = 'i18n'; if (empty($language) && isset($_SESSION[SESS_USER_ID])) { $language = read_user_setting('user_language', get_new_user_default_language(), true); $section = 'user'; } } log_install_medium('language', 'getLanguage(): ' . $language . ' [' . $section . ']'); return $language; } /** * setLanguage() - sets the language of the Installer * * @param string $param_language Must be a valid language which is returned from * apply_locale() function located in Core * * @return void * * Errors: will add an error at STEP_WELCOME if invalid language. */ private function setLanguage(string $param_language = '') : void { if ($param_language != '') { $language = apply_locale($param_language); if (empty($language)) { $this->addError(Installer::STEP_WELCOME, 'Language', 'Failed to apply specified language'); } else { log_install_debug('language', 'setLanguage(): ' . $param_language); $this->language = $param_language; set_install_config_option('i18n_default_language', $param_language); set_install_config_option('install_language', $param_language); if (isset($_SESSION[SESS_USER_ID])) { $_SESSION[SESS_USER_LANGUAGE] = $param_language; set_user_setting('user_language', $param_language); } $test_i18n = read_config_option('i18n_default_language'); $test_install = read_config_option('install_language'); log_install_debug('language','setLanguage(): ' . $test_i18n . ' / ' . $test_install); } } } /** * setEula() - sets whether the Eula was accepted or not * * @param $string param_eula Valid values are 'Accepted', 'True' * * @return void * * Errors: will add an error at STEP_WELCOME if not accepted. */ private function setEula(string $param_eula = '') : void { if ($param_eula == 'Accepted' || $param_eula === 'true') { $param_eula = 1; } elseif (!is_numeric($param_eula)) { $param_eula = 0; } $this->eula = $param_eula; if (!$this->eula) { $this->addError(Installer::STEP_WELCOME, 'Eula', 'Eula not accepted'); } set_install_config_option('install_eula', $this->eula); } /** * getRRDVersion() - gets the RRDVersion from the system or if overridden * during the installer, from the installer option * * @return string */ private function getRRDVersion() : string { $rrdver = read_config_option('install_rrdtool_version', true); if (empty($rrdver)) { log_install_high('rrdversion', 'getRRDVersion(): Getting installed tool version'); $detected_version = get_installed_rrdtool_version(); if ($detected_version === false) { log_install_high('rrdversion', 'getRRDVersion(): Unable to detect RRDtool; retaining the configured capability level'); $rrdver = get_rrdtool_version(); } else { $rrdver = $detected_version; } } log_install_medium('rrdversion', 'getRRDVersion(): ' . $rrdver); return $rrdver; } /** * setCSRFSecret() - Initializes the csrf secret file for * csrf protection. * * @return void */ private function setCSRFSecret() : void { $this->setProgress(Installer::PROGRESS_CSRF_BEGIN); if (CACTI_CSRF_SECRET != '') { $file = csrf_secret_file_path(CACTI_CSRF_SECRET); log_install_debug('csrf', 'setCSRFSecret(): secret ' . $file); $secret = @file_exists($file) ? file_get_contents($file) : ''; log_install_debug('csrf', 'setCSRFSecret(): secret ' . (empty($secret) ? 'not ' : '') . 'empty'); if (empty($secret)) { if (!is_dir(dirname($file))) { /* The historical default sits under the retired csrf-magic directory, so on a new install there is nowhere to write. Nothing needs one either: the secret exists only to verify tokens minted before the upgrade. */ log_install_debug('csrf', 'setCSRFSecret(): No directory for "' . $file . '", skipping'); } elseif (is_resource_writable($file)) { log_install_medium('csrf', 'setCSRFSecret(): Updated CSRF secret - "' . $file . '"'); install_create_csrf_secret($file); } else { log_install_high('csrf', 'setCSRFSecret(): Unable to create file - "' . $file . '"'); } } else { log_install_debug('csrf', 'setCSRFSecret(): Secret already exists - "' . $file . '"'); } } $this->setProgress(Installer::PROGRESS_CSRF_END); } /** * setRRDVersion() - sets the RRDVersion installer option, overrides * the system default. * * @param string $param_rrdver A valid version number. * @param string $prefix A display prefix, not used in values * * @return void * * Errors: will add an error at STEP_BINARY_LOCATIONS if invalid version * was detected. */ private function setRRDVersion(string $param_rrdver = '', string $prefix = '') { if ($param_rrdver != '') { $rrdver = $this->sanitizeRRDVersion($param_rrdver, ''); if (empty($rrdver)) { $this->addError(Installer::STEP_BINARY_LOCATIONS, 'RRDVersion', 'setRRDVersion()', __('Failed to set specified %sRRDTool version: %s', $prefix, $param_rrdver)); } else { $this->paths['rrdtool_version']['default'] = $param_rrdver; set_install_config_option('install_rrdtool_version', $param_rrdver); set_install_config_option('rrdtool_version', $param_rrdver); } } } /** * getTheme() - gets the current theme * * @return string The current theme */ private function getTheme() : string { $theme = read_config_option('install_theme', true); if (empty($theme)) { if (isset($_SESSION[SESS_USER_ID])) { $theme = read_user_setting('selected_theme'); } if (empty($theme)) { $theme = read_config_option('selected_theme', true); } } $theme = empty($theme) ? 'modern' : $theme; log_install_medium('theme', 'getTheme(): ' . $theme); return $theme; } /** * setTheme() - sets the Theme installer option, override the system default. * * @param string $param_theme A valid theme which must exist in /include/themes/ * * @return void * * Errors: will add an error at STEP_BINARY_WELCOME if invalid theme * was detected. */ private function setTheme(string $param_theme = '') : void { if ($param_theme != '') { log_install_medium('theme', 'Checking theme: ' . $param_theme); $themePath = CACTI_PATH_INCLUDE . '/themes/' . $param_theme . '/main.css'; if (file_exists($themePath)) { log_install_debug('theme', 'setTheme(): ' . $param_theme); $this->theme = $param_theme; set_install_config_option('install_theme', $this->theme); set_install_config_option('selected_theme', $this->theme); if (isset($_SESSION[SESS_USER_ID])) { set_user_setting('selected_theme', $this->theme); $_SESSION['selected_theme'] = $this->theme; } } else { $this->addError(Installer::STEP_WELCOME, 'Theme', __('Invalid Theme Specified')); } } } /** * getPaths() - gets the various programs/paths to that are defined in * $this->paths (setup in constructor) where they are * either prefixed with path_ or specifically the sendmail * path. If a default exists, that is usd, otherwise the * system configuration option is read and set back into * $this->paths array. * * @return array An array of paths */ public function getPaths() : array { $paths = []; foreach ($this->paths as $name => $array) { if (str_starts_with($name, 'path_') || $name == 'settings_sendmail_path') { if (array_key_exists('default', $array)) { $paths[$name] = $array['default']; } else { $paths[$name] = read_config_option($name, true); } } } return $paths; } /** * setPaths() - sets paths set in the array, checking for a number of * issues. The array should be key->path which is compared * against $this->paths * * @param array $param_paths A list of paths * * @return void * * Errors: will add an error to STEP_BINARY_LOCATIONS if a problem is * found with the value. */ private function setPaths(array $param_paths = []) : void { log_install_debug('paths', 'setPaths(' . $this->stepCurrent . ', ' . cacti_count($param_paths) . ')'); // get all items on the form and write values for them foreach ($param_paths as $name => $path) { $key_exists = array_key_exists($name, $this->paths); $check = $this->paths[$name]['install_check'] ?? 'file_exists'; $optional = $this->paths[$name]['install_optional'] ?? false; $blank = $this->paths[$name]['install_blank'] ?? false; log_install_high('paths', sprintf('setPaths(): name: %-25s, key_exists: %-5s, optional: %-5s, check: %s, path: %s', $name, $key_exists, $optional, $check, $path)); if ($key_exists) { $should_set = true; if ($blank && empty($path)) { // allow this } elseif ($check == 'writable') { $should_set = is_resource_writable(dirname($path) . '/') || $optional; if ($should_set) { $should_set = is_resource_writable($path) || $optional; } if ($should_set === false && $optional === false) { $this->addError(Installer::STEP_BINARY_LOCATIONS, 'Paths', $name, __('Resource is not writable')); } } elseif ($check == 'file_exists') { $should_set = (!empty($path) && file_exists($path)) || $optional; if (!$should_set) { $this->addError(Installer::STEP_BINARY_LOCATIONS, 'Paths', $name, __('File not found')); } } if ($should_set && $name == 'path_php_binary') { $input = mt_rand(2, 64); $output = shell_exec( cacti_escapeshellcmd($path) . ' -q ' . cacti_escapeshellarg(CACTI_PATH_INSTALL . '/cli_test.php') . ' ' . $input ); if ($output != $input * $input) { $this->addError(Installer::STEP_BINARY_LOCATIONS, 'Paths', $name, __('PHP did not return expected result')); $should_set = false; } } if ($should_set && $name == 'path_composer' && !empty($path) && file_exists($path)) { // cacti_escapeshellcmd() replaces backslashes on Windows; normalize first $output = shell_exec(cacti_escapeshellcmd(str_replace('\\', '/', $path)) . ' --version 2>&1'); if ($output === null || !str_contains((string) $output, 'Composer')) { $this->addError(Installer::STEP_BINARY_LOCATIONS, 'Paths', $name, __('Composer did not return expected result')); $should_set = false; } } if ($should_set) { unset($this->errors['Paths'][$name]); set_install_config_option($name, empty($path) ? '' : $path); } $this->paths[$name]['default'] = $path; log_install_debug('paths', sprintf('setPaths(): name: %-25s, key_exists: %-5s, optional: %-5s, should_set: %3s, check: %s', $name, $key_exists, $optional, $should_set ? 'Yes' : 'No', $check)); log_install_debug('paths', sprintf('setPaths(): name: %-25s, data: %s', $name, clean_up_lines(var_export($this->paths[$name], true)))); } else { $this->addError(Installer::STEP_BINARY_LOCATIONS, 'Paths', $name, __('Unexpected path parameter')); } } } /** * getProfile() - gets the data source profile to be used as the system * default once installation has been completed. It is * also used by the package installation to attribute * the installed packages to this collector * * @return int */ private function getProfile() : int { $db_profile = read_config_option('install_profile', true); if (empty($db_profile) && db_table_exists('data_source_profiles')) { $db_profile = db_fetch_cell('SELECT id FROM data_source_profiles WHERE `default` = \'on\' LIMIT 1'); if ($db_profile === false) { $db_profile = db_fetch_cell('SELECT id FROM data_source_profiles ORDER BY id LIMIT 1'); } } log_install_medium('automation', 'getProfile() returns with ' . $db_profile); return intval($db_profile); } /** * setProfile() - sets the data source profile as the default one to be * be used by the system. * * @param int $param_profile Must be an existing data_source_profile id * * @return void * * Error: will add an error to STEP_PROFILE_AND_AUTOMATION when an * invalid id is passed. */ private function setProfile(int $param_profile = 0) { if (db_table_exists('data_source_profiles')) { if (!empty($param_profile)) { $valid = db_fetch_cell_prepared('SELECT id FROM data_source_profiles WHERE id = ?', [$param_profile]); if ($valid === false || $valid != $param_profile) { $this->addError(Installer::STEP_PROFILE_AND_AUTOMATION, 'Profile', __('Failed to apply specified profile %s != %s', $valid, $param_profile)); } else { $this->profile = $valid; set_install_config_option('install_profile', $valid); } } log_install_medium('automation', "setProfile($param_profile) returns with $this->profile"); } else { $this->profile = 1; } } /** * getAutomationMode() - Gets the automation mode option, if not found * uses the system default * * @return int */ public function getAutomationMode() : int { $enabled = read_config_option('install_automation_mode', true); log_install_debug('automation', 'automation_mode: ' . clean_up_lines($enabled)); if ($enabled == null && db_table_exists('automation_networks')) { $row = db_fetch_row('SELECT id, enabled FROM automation_networks LIMIT 1'); log_install_debug('automation', 'Network data: ' . clean_up_lines(var_export($row, true))); $enabled = 0; if (!empty($row)) { if ($row['enabled'] == 'on') { $enabled = 1; } } } log_install_medium('automation', "getAutomationMode() returns '$enabled'"); return $enabled; } /** * setAutomationMode() - sets whether the automation system should * be enabled or disabled by default. * * @param int $param_mode Must be a valid true or false value * * @return void * * Errors: will add an error to STEP_PROFILE_AND_AUTOMATION if an * invalid value is passed. */ private function setAutomationMode(int $param_mode = -1) : void { if ($param_mode != -1) { if (!$this->setTrueFalse($param_mode, $this->automationMode, 'automation_mode')) { $this->addError(Installer::STEP_PROFILE_AND_AUTOMATION, 'Automation', 'Mode', __('Failed to apply specified mode: %s', $param_mode)); } } log_install_medium('automation', "setAutomationMode($param_mode) returns with $this->automationMode"); } /** * getAutomationOverride() - gets whether the automation snmp options * are being overridden. * * @return string */ public function getAutomationOverride() : string { return (string) read_config_option('install_automation_override', true); } /** * setAutomationOverride() - sets whether the extra snmp options are to * be overwritten by the SnmpOptions provided. * * @param string $param_override Must be a valid true or false value * * @return void * * Errors: will add an error to STEP_PROFILE_AND_AUTOMATION if an * invalid value is passed. */ private function setAutomationOverride(string $param_override = '') : void { if ($param_override != '') { if (!$this->setTrueFalse($param_override, $this->automationOverride, 'automation_override')) { $this->addError(Installer::STEP_PROFILE_AND_AUTOMATION, 'Automation', 'Override', __('Failed to apply specified automation override: %s', $param_override)); } } log_install_medium('automation', "setAutomationOverride($param_override) returns with $this->automationOverride"); } /** * setCronInterval() - sets the expected system cron interval but does * not actually affect the system cron. * * @param string $param_interval A value that must exist in the system global * variables $cron_intervals. * * @return void * * Errors: will set an error in STEP_PROFILE_AND_AUTOMATION when an * invalid value is passed. */ private function setCronInterval(mixed $param_interval = null) : void { global $cron_intervals; if ($param_interval != null) { if (array_key_exists($param_interval, $cron_intervals)) { $this->cronInterval = intval($param_interval); set_install_config_option('cron_interval', $param_interval); } else { $this->addError(Installer::STEP_PROFILE_AND_AUTOMATION, 'Poller', 'Cron', __('Failed to apply specified cron interval')); } } log_install_medium('automation', "setCronInterval($param_interval) returns with $this->cronInterval"); } /** * getAutomationRange() - get the default network range to be used by * Automation for scanning the network. If no previous value is found, * defaults to 192.168.1.0/24 * * @return string The specified network range */ public function getAutomationRange() : string { $range = read_config_option('install_automation_range', true); if (empty($range) && db_table_exists('automation_networks')) { $row = db_fetch_row('SELECT id, subnet_range FROM automation_networks LIMIT 1'); $enabled = 0; $network = ''; log_install_debug('automation', "getAutomationRange(): found '" . clean_up_lines(var_export($row, true))); if (!empty($row)) { $range = $row['subnet_range']; } } $result = empty($range) ? '192.168.0.1/24' : $range; log_install_medium('automation', "getAutomationRange() returns '$result'"); return $result; } /** * setAutomationRange() - sets the network range to be used by * Automation when scanning the network. * * @param string $param_range A valid network range which is converted and returned * by cacti_pton(). If the return value is false, it is * considered invalid * * @return void * * Errors: will add an error to STEP_PROFILE_AND_AUTOMATION if an invalid value is passed */ private function setAutomationRange(string $param_range = '') : void { if ($param_range != '') { $param_array = explode(',', $param_range); if (cacti_sizeof($param_array)) { foreach ($param_array as $param_network) { $ip_details = automation_get_network_info($param_network); if ($ip_details === false) { $this->addError(Installer::STEP_PROFILE_AND_AUTOMATION, 'Automation', 'Range', __('Failed to apply \'%s\' as Automation Range', $param_network)); } } $this->automationRange = $param_range; set_install_config_option('install_automation_range', $param_range); } } else { $param_range = ''; $this->automationRange = $param_range; } log_install_medium('automation', "setAutomationRange($param_range) returns with $this->automationRange"); } /** * getSnmpOptions() - gets an array of all the extra SNMP options to be * set which Automation will use when scanning the * network. * * @return array The snmp option strings */ private function getSnmpOptions() : array { global $fields_snmp_item_with_retry; $db_snmp_options = db_fetch_assoc('SELECT name, value FROM settings where name like \'install_snmp_option_%\''); $options = []; foreach ($db_snmp_options as $row) { $key = str_replace('install_snmp_option_', '', $row['name']); $options[$key] = $row['value']; } log_install_debug('snmp_options', 'Option array: ' . clean_up_lines(var_export($options, true))); return $options; } /** * setSnmpOptions() - sets the extra SNMP options to be used by * Automation when scanning the network. * * @param array $param_snmp_options An array of snmp options * * Errors: will add an error to STEP_PROFILE_AND_AUTOMATION if an * invalid value is passed * * @return void */ private function setSnmpOptions($param_snmp_options = []) : void { global $fields_snmp_item_with_retry; $known_snmp_options = $fields_snmp_item_with_retry; db_execute('DELETE FROM settings WHERE name like \'install_snmp_option_%\''); log_install_medium('snmp_options', 'Updating snmp_options'); log_install_debug('snmp_options', 'Parameter data:' . clean_up_lines(var_export($param_snmp_options, true))); $known_map = [ 'SnmpVersion' => 'snmp_version', 'SnmpCommunity' => 'snmp_community', 'SnmpSecurityLevel' => 'snmp_security_level', 'SnmpUsername' => 'snmp_username', 'SnmpAuthProtocol' => 'snmp_auth_protocol', 'SnmpPassword' => 'snmp_password', 'SnmpPrivProtocol' => 'snmp_priv_protocol', 'SnmpPrivPassphrase' => 'snmp_priv_passphrase', 'SnmpContext' => 'snmp_context', 'SnmpEngineId' => 'snmp_engine_id', 'SnmpPort' => 'snmp_port', 'SnmpTimeout' => 'snmp_timeout', 'SnmpMaxOids' => 'max_oids', 'SnmpRetries' => 'snmp_retries', ]; foreach ($param_snmp_options as $option_name => $option_value) { log_install_high('snmp_options', 'snmp_option: ' . clean_up_lines(var_export($option_name, true))); $bad_option = true; if (array_key_exists($option_name, $known_map)) { $key = $known_map[$option_name]; log_install_debug('snmp_options', 'snmp_option found:' . clean_up_lines(var_export($key, true))); if (array_key_exists($key, $known_snmp_options)) { $bad_option = false; log_install_high('snmp_options', 'snmp_option set:' . clean_up_lines(var_export($option_value, true))); log_install_debug('snmp_options', "Set snmp_option: install_snmp_option_$key = $option_value"); set_install_config_option("install_snmp_option_$key", $option_value); } } if ($bad_option) { $this->addError(Installer::STEP_TEMPLATE_INSTALL, 'SnmpOptions', $option_name, __('No matching snmp option exists')); } } } /** * getModules() - returns a list of required modules and their * installation status. * * @return array An array of modules */ private function getModules() : array { if (isset($this->extensions) || empty($this->extensions)) { $extensions = utility_php_extensions(); foreach ($extensions as $name => $e) { if (!$e['installed']) { $this->addError(Installer::STEP_CHECK_DEPENDENCIES, 'Modules', $name . ' is missing'); } } $this->extensions = $extensions; } return $this->extensions; } private function getDefaultTemplate(bool $force = false) : int { $default_template = intval(read_config_option('default_template', true)); if (read_config_option('install_get_template_message_logged', true) == '') { log_install_always('templates', 'getDefaultTemplate(): Default Device Template is \'' . $default_template . '\''); set_config_option('install_get_template_message_logged', 'on'); } $sysDescrMatch = CACTI_SERVER_OS == 'win32' ? 'Windows' : 'Linux'; $template_ids = $this->getDefaultAutomationTemplateIds(); foreach ($this->defaultAutomation as $item) { if ($item['sysDescrMatch'] == $sysDescrMatch || !empty($default_template)) { $host_template_id = $template_ids[$item['hash']] ?? 0; if (!empty($host_template_id)) { if ($host_template_id != $default_template) { log_install_always('templates', 'getDefaultTemplate(): Changing Device Template to \'' . $item['name'] . '\''); set_install_config_option('default_template', $host_template_id); } break; } else { log_install_debug('templates', 'getDefaultTemplate(): Unable to find Device Template for \'' . $item['name'] . '\''); } } } if (empty($default_template) && !$force) { set_default_action('default_template'); $default_template = $this->getDefaultTemplate(true); } return $default_template; } private function setDefaultTemplate(int $param_default_template = 0) : void { $default_template = null; if (!empty($param_default_template)) { $template_ids = $this->getDefaultAutomationTemplateIds(); foreach ($this->defaultAutomation as $item) { $id = $template_ids[$item['hash']] ?? 0; if ($id == $param_default_template) { $default_template = $param_default_template; break; } } } if (empty($default_template)) { $default_template = $this->getDefaultTemplate(); } set_install_config_option('default_template', $default_template); $default_template = $this->getDefaultTemplate(); if (read_config_option('install_set_template_message_logged', true) == '') { log_install_always('templates', 'setDefaultTemplate(): Default Device Template is \'' . $default_template . '\''); set_config_option('install_set_template_message_logged', 'on'); } } /** * Resolve the installer automation template hashes in one query. * * This intentionally does not cache results because package import can add * host templates between installer phases. * * @return array Host template IDs keyed by template hash. */ private function getDefaultAutomationTemplateIds() : array { $hashes = array_values(array_unique(array_column($this->defaultAutomation, 'hash'))); if (!cacti_sizeof($hashes)) { return []; } $placeholders = implode(', ', array_fill(0, cacti_sizeof($hashes), '?')); $templates = db_fetch_assoc_prepared("SELECT id, hash FROM host_template WHERE hash IN ($placeholders)", $hashes); return array_map('intval', array_rekey($templates, 'hash', 'id')); } /** * getTemplates() - returns a list of expected templates and whether * they have been selected for installation. * * @return array */ private function getTemplates() : array { $known_templates = install_setup_get_templates(); $db_templates = array_rekey( db_fetch_assoc('SELECT name, value FROM settings WHERE name LIKE \'install_tp_%\''), 'name', 'value' ); $hasTemplates = read_config_option('install_has_templates', true); $selected = []; $select_count = 0; log_install_debug('templates', 'getTemplates(): First: ' . (empty($hasTemplates) ? 'Yes' : 'No') . ', Templates - ' . clean_up_lines(var_export($known_templates, true))); log_install_debug('templates', 'getTemplates(): DB: ' . clean_up_lines(var_export($db_templates, true))); foreach ($known_templates as $known) { $filename = $known['filename']; $key_base = str_replace('.', '_', $filename); $key_install = 'install_tp_' . $key_base; $key_check = 'chk_template_' . $key_base; log_install_high('templates', 'getTemplates(): Checking template ' . $known['name'] . ' using base: ' . $key_base); log_install_debug('templates', 'getTemplates(): Checking template ' . $known['name'] . ' using key.: ' . $key_install); log_install_debug('templates', 'getTemplates(): Checking template ' . $known['name'] . ' filename..: ' . $filename); $value = ''; if (array_key_exists($key_install, $db_templates)) { $value = $db_templates[$key_install]; } log_install_debug('templates', 'getTemplates(): Checking template ' . $known['name'] . ' against...: ' . $value); $isSelected = !empty($hasTemplates) && ($value == $filename); $selected[$key_check] = $isSelected; if ($isSelected) { $select_count++; } } $selected['all'] = ($select_count == cacti_count($selected) || empty($hasTemplates)); log_install_high('templates', 'getTemplates(): Returning with ' . clean_up_lines(var_export($selected, true))); return $selected; } /** * Confirm that a browser submitted the complete server-rendered selection. * * Web selection forms submit both checked and unchecked rows. Requiring an * exact key set prevents a missing selector, stale page, or truncated request * from being interpreted as an intentional request to clear every selection. * CLI callers remain sparse by design and are validated by the individual * template/table allowlists. * * @param array $submitted * @param array $expected */ private static function isCompleteSelectionPayload(array $submitted, array $expected) : bool { unset($submitted['all']); $submittedKeys = array_keys($submitted); sort($submittedKeys); sort($expected); if ($submittedKeys !== $expected) { return false; } foreach ($submitted as $value) { if (!in_array($value, [true, false, 'true', 'false', 'on', '', 1, 0, '1', '0'], true)) { return false; } } return true; } /** * Determines whether the web installer renders a conversion checkbox for a table. * * @param array $table The table metadata * * @return bool Whether the table can be selected in the web installer */ private static function isTableSelectable(array $table) : bool { return isset($table['Rows']) && is_numeric($table['Rows']) && (int) $table['Rows'] < 1000000; } /** * setTemplates() - sets a list of templates that should be installed * during the installServer() phase. * * @param array $param_templates An array of templates to install in the form of * 'template'=>(true|false) * * Errors: will add an error to STEP_TEMPLATE_INSTALL if a template is * passed that is not expected */ private function setTemplates(array $param_templates = []) : void { $known_templates = install_setup_get_templates(); $expected_keys = []; if (!is_array($known_templates)) { $this->addError(Installer::STEP_TEMPLATE_INSTALL, 'Templates', 'SelectionPayload', __('Unable to load the available templates')); return; } foreach ($known_templates as $known) { $expected_keys[] = 'chk_template_' . str_replace('.', '_', $known['filename']); } if ($this->runtime === 'Web' && !self::isCompleteSelectionPayload($param_templates, $expected_keys)) { $this->addError( Installer::STEP_TEMPLATE_INSTALL, 'Templates', 'SelectionPayload', __('The template selection was incomplete. Reload this installer step and select the templates again.') ); return; } db_execute("DELETE FROM settings WHERE name LIKE 'install_tp_%'"); log_install_medium('templates', 'setTemplates(): Updating templates'); log_install_debug('templates', 'setTemplates(): Parameter data:' . clean_up_lines(var_export($param_templates, true))); log_install_debug('templates', 'setTemplates(): Template data:' . clean_up_lines(var_export($known_templates, true))); $param_all = false; if (array_key_exists('all', $param_templates)) { $this->setTrueFalse($param_templates['all'], $param_all, 'param_all', false); unset($param_templates['all']); log_install_debug('templates', 'setTemplates(): All flag:' . clean_up_lines(var_export($param_all, true))); } $count = 0; foreach ($param_templates as $name => $enabled) { $template = false; log_install_high('templates', 'setTemplates(): ' . $name . ' => ' . ($enabled ? 'true' : 'false')); foreach ($known_templates as $known) { $filename = $known['filename']; $key = 'chk_template_' . str_replace('.', '_', $filename); if ($name == $key || $name == $filename) { $template = $known; break; } } if ($template === false) { $this->addError(Installer::STEP_TEMPLATE_INSTALL, 'Templates', $name, __('No matching template exists')); } else { $set = false; $key = str_replace('.', '_', $template['filename']); $this->setTrueFalse($enabled, $set, $key, false); $use = ($set) || ($param_all); $value = ($use) ? $template['filename'] : ''; log_install_high('templates', "setTemplates(): Use: $use, Set: $set, All: $param_all, key: install_tp_$key = " . $value); // Don't default install templates if upgrade if ($this->getMode() == Installer::MODE_DOWNGRADE) { $value = ''; $use = false; } set_install_config_option("install_tp_$key", $value); $this->templates[$name] = $use; } } $all = true; if (array_key_exists('all', $this->templates)) { unset($this->templates['all']); } foreach ($this->templates as $use) { if (!$use) { $all = false; } } $this->templates['all'] = $all; set_install_config_option('install_has_templates', true); } /** * getTables() - gets a list of tables that require conversion to * mb4_unicode_utf8. * * @return array */ private function getTables() : array { $known_tables = install_setup_get_tables(); $db_tables = array_rekey( db_fetch_assoc('SELECT name, value FROM settings where name like \'install_table_%\''), 'name', 'value' ); $hasTables = read_config_option('install_has_tables', true); $selected = []; $select_count = 0; foreach (($known_tables ?: []) as $known) { $table = $known['Name']; $key = $known['Name']; $option = ''; if (array_key_exists('install_table_' . $key, $db_tables)) { $option = $db_tables['install_table_' . $key]; } $isSelected = !empty($hasTables) && (!empty($option)); $selected['chk_table_' . $key] = $isSelected; if ($isSelected) { $select_count++; } } $selected['all'] = ($select_count == cacti_count($selected) || empty($hasTables)); return $selected; } /** * setTables - sets a list of tables to be converted to the latest * default coalition * * @param array $param_tables Array of table names * * Errors: does not add errors as a table may not be present in the * * @return void */ private function setTables(array $param_tables = []) : void { $known_tables = install_setup_get_tables(); $expected_keys = []; if (!is_array($known_tables)) { $this->addError(Installer::STEP_CHECK_TABLES, 'Tables', 'SelectionPayload', __('Unable to load the tables requiring conversion')); return; } foreach ($known_tables as $known) { if (self::isTableSelectable($known)) { $expected_keys[] = 'chk_table_' . $known['Name']; } } if ($this->runtime === 'Web' && !self::isCompleteSelectionPayload($param_tables, $expected_keys)) { $this->addError( Installer::STEP_CHECK_TABLES, 'Tables', 'SelectionPayload', __('The table selection was incomplete. Reload this installer step and select the tables again.') ); return; } db_execute('DELETE FROM settings WHERE name like \'install_table_%\''); log_install_medium('tables', 'setTables(): Updating Tables'); log_install_debug('tables', 'setTables(): Parameter data:' . clean_up_lines(var_export($param_tables, true))); $param_all = false; if (array_key_exists('all', $param_tables)) { $this->setTrueFalse($param_tables['all'], $param_all, 'allTables', false); unset($param_tables['all']); } foreach ($known_tables as $known) { $name = $known['Name']; $key = 'chk_table_' . $name; $set = false; log_install_high('tables', "setTables(): Checking table '$name' against key $key ..."); log_install_debug('tables', 'setTables(): Table: ' . clean_up_lines(var_export($known, true))); if (!array_key_exists($key, $param_tables)) { $param_tables[$key] = null; } $this->setTrueFalse($param_tables[$key], $set, 'table_' . $name, false); $use = ($set || $param_all); $value = $use ? $name : ''; log_install_high('tables', "setTables(): Use: $use, Set: $set, All: $param_all, key: install_table_$name = " . $value); set_install_config_option("install_table_$name", $value); $this->tables[$key] = $use; } $all = true; if (array_key_exists('all', $this->tables)) { unset($this->tables['all']); } foreach ($this->tables as $use) { if (!$use) { $all = false; } } $this->tables['all'] = $all; set_install_config_option('install_has_tables', true); } // getMode - gets the current mode public function getMode() : int { if ($this->mode != -1) { $mode = $this->mode; } else { $mode = Installer::MODE_INSTALL; if ($this->old_cacti_version != 'new_install') { if (cacti_version_compare($this->old_cacti_version, CACTI_VERSION, '=')) { $equal = '='; $mode = Installer::MODE_NONE; } elseif (cacti_version_compare($this->old_cacti_version, CACTI_VERSION, '<')) { $equal = '<'; $mode = Installer::MODE_UPGRADE; } else { $equal = '>='; $mode = Installer::MODE_DOWNGRADE; } log_install_high('mode', 'getMode(): Version Mode - ' . clean_up_lines(var_export($this->old_cacti_version, true)) . $equal . clean_up_lines(var_export(CACTI_VERSION, true))); } elseif ($this->hasRemoteDatabaseInfo()) { $mode = Installer::MODE_POLLER; } if ($mode == Installer::MODE_POLLER || $mode == Installer::MODE_INSTALL) { $db_mode = read_config_option('install_mode', true); log_install_high('mode', 'getMode(): DB Install mode ' . clean_up_lines(var_export($db_mode, true))); if ($db_mode !== false && $db_mode !== null) { $mode = $db_mode; } } $this->mode = $mode; } log_install_high('mode', 'getMode(): ' . clean_up_lines(var_export($mode, true))); return $mode; } /** * setMode() - sets the current mode of operation * * @param int $param_mode The mode to set, must be one of the MODE_ constants * * Errors: will add an error when an invalid value is passed * * @return void */ private function setMode(int $param_mode = 0) : void { if (intval($param_mode) > Installer::MODE_NONE && intval($param_mode) <= Installer::MODE_DOWNGRADE) { log_install_high('mode', 'setMode(' . $param_mode . ')'); set_install_config_option('install_mode', $param_mode); $this->mode = $param_mode; $this->updateButtons(); } elseif ($param_mode != 0) { $this->addError(Installer::STEP_INSTALL_TYPE, 'mode', 'Failed to apply mode: ' . $param_mode); } } // getSetDefault() - returns the default step private function getStepDefault() : int { $mode = $this->getMode(); $step = $mode == Installer::MODE_NONE ? Installer::STEP_COMPLETE : Installer::STEP_WELCOME; log_install_medium('step', "getStepDefault(): Resetting to $step as not found ($mode)"); return $step; } // getStep() - returns the current step public function getStep() : int { return $this->stepCurrent; } /** * setStep() - sets the current step * * @param int $param_step Must be a valid value as defined by STEP_ constants * * @return void */ private function setStep(int $param_step = -1) : void { $step = Installer::STEP_WELCOME; if (empty($param_step)) { $param_step = Installer::STEP_WELCOME; $step = $param_step; } elseif ($param_step > Installer::STEP_NONE && $param_step <= Installer::STEP_ERROR) { if ($param_step == Installer::STEP_WELCOME) { $step = $param_step; } else { $step = $param_step; } } if ($step === Installer::STEP_NONE) { $step = Installer::STEP_WELCOME; } log_install_debug('step', 'setStep(): ' . var_export($step, true)); // Make current step the first if it is unknown log_install_high('step', 'setStep(): stepError ' . clean_up_lines(var_export($this->stepError, true)) . ' < ' . clean_up_lines(var_export($step, true))); if ($this->stepError !== false && $this->stepError < $step) { $step = $this->stepError; } $this->stepCurrent = ($step == Installer::STEP_NONE ? Installer::STEP_WELCOME : $step); $this->stepPrevious = Installer::STEP_NONE; $this->stepNext = Installer::STEP_NONE; if ($step <= Installer::STEP_COMPLETE) { $this->stepNext = ($step >= Installer::STEP_COMPLETE ? Installer::STEP_NONE : $step + 1); if ($step >= Installer::STEP_WELCOME) { $this->stepPrevious = ($step <= Installer::STEP_WELCOME ? Installer::STEP_NONE : $step - 1); } } set_install_config_option('install_step', $this->stepCurrent); $this->updateButtons(); set_install_config_option('install_prev', $this->stepPrevious); set_install_config_option('install_next', $this->stepNext); } private function setAdminEmailAddress(string $admin_email_address = '') : void { if ($admin_email_address != '') { if (filter_var($admin_email_address, FILTER_VALIDATE_EMAIL)) { db_execute_prepared('UPDATE user_auth SET email_address = ? WHERE username = \'admin\'', [$admin_email_address] ); log_install_always('email', 'Admin email address set to ' . $admin_email_address); } else { $this->addError(Installer::STEP_PROFILE_AND_AUTOMATION, 'Email', __('Incorrect email address')); } } else { log_install_always('email', 'Admin email address not set.'); } $this->adminEmailAddress = $admin_email_address; } private function getAdminEmailAddress() : mixed { $admin_email_address = db_fetch_cell("SELECT email_address FROM user_auth WHERE username = 'admin' LIMIT 1"); return $admin_email_address; } // Some utility functions public function shouldRedirectToHome() : bool { return (cacti_version_compare($this->old_cacti_version, CACTI_VERSION, '=')); } public function shouldExitWithReason() : int|false { if ($this->isDatabaseEmpty()) { return Installer::EXIT_DB_EMPTY; } if ($this->isDatabaseTooOld()) { return Installer::EXIT_DB_OLD; } return false; } public function isValidCollation(array $collation_vars, array $charset_vars, string $type) : bool { $collation_value = ''; $charset_value = ''; if ( cacti_sizeof($collation_vars) && array_key_exists('collation_' . $type, $collation_vars) ) { $collation_value = $collation_vars['collation_' . $type]; } if ( cacti_sizeof($charset_vars) && array_key_exists('charset_' . $type, $charset_vars) ) { $charset_value = $charset_vars['charset_' . $type]; } return ($collation_value == 'utf8mb4_unicode_ci' || $charset_value == 'utf8mb4'); } public function isDatabaseEmpty() : bool { return empty($this->old_cacti_version); } public function isDatabaseTooOld() : bool { return preg_match('/^0\.6/', $this->old_cacti_version); } public function isNewInstall() : bool { return ($this->old_cacti_version == 'new_install'); } public function isPre_v0_8_UpgradeNeeded() : bool { return version_compare($this->old_cacti_version, '0.8.5a', '<='); } public function hasRemoteDatabaseInfo() : bool { global $rdatabase_default; return !empty($rdatabase_default); } public function isConfigurationWritable() : bool { return is_resource_writable(CACTI_PATH_INCLUDE . '/config.php'); } public function isRemoteDatabaseGood() : bool { global $rdatabase_default, $rdatabase_username, $rdatabase_hostname, $rdatabase_port; return (isset($rdatabase_default) && isset($rdatabase_username) && isset($rdatabase_hostname) && isset($rdatabase_port)) ? true : false; } public function exitWithReason(mixed $reason) : string { switch ($reason) { case Installer::EXIT_DB_EMPTY: return $this->exitSqlNeeded(); case Installer::EXIT_DB_OLD: return $this->exitDbTooOld(); default: return $this->exitWithUnknownReason($reason); } } private function exitWithUnknownReason(string $reason) : string { $output = Installer::sectionTitleError(); $output .= Installer::sectionNormal(__('The Installer could not proceed due to an unexpected error.')); $output .= Installer::sectionNormal(__('Please report this to the Cacti Group.')); $output .= Installer::sectionCode(__('Unknown Reason: %s', $reason)); return $output; } private function exitDbTooOld() : string { global $database_username, $database_default; $output = Installer::sectionTitleError(); $output .= Installer::sectionNormal(__('You are attempting to install Cacti %s onto a 0.6.x database. Unfortunately, this can not be performed.', CACTI_VERSION)); $output .= Installer::sectionNormal(__('To be able continue, you MUST create a new database, import "cacti.sql" into it:', CACTI_VERSION)); $output .= Installer::sectionCode(sprintf('mysql -u %s -p [new_database] %s < cacti.sql', $database_username, $database_default)); $output .= Installer::sectionNormal(__('You MUST then update "include/config.php" to point to the new database.')); $output .= Installer::sectionNormal(__('NOTE: Your existing data will not be modified, nor will it or any history be available to the new install')); return $output; } private function exitSqlNeeded() : string { global $database_username, $database_default, $database_password; $output = Installer::sectionTitleError(); $output .= Installer::sectionNormal(__("You have created a new database, but have not yet imported the 'cacti.sql' file. At the command line, execute the following to continue:")); $output .= Installer::sectionCode(sprintf('mysql -u %s -p %s < cacti.sql', $database_username, $database_default)); $output .= Installer::sectionNormal(__('This error may also be generated if the cacti database user does not have correct permissions on the Cacti database. Please ensure that the Cacti database user has the ability to SELECT, INSERT, DELETE, UPDATE, CREATE, ALTER, DROP, INDEX on the Cacti database.')); $output .= Installer::sectionNormal(__('You MUST also import MySQL TimeZone information into MySQL and grant the Cacti user SELECT access to the mysql.time_zone_name table')); if (CACTI_SERVER_OS == 'unix') { $output .= Installer::sectionNormal(__("On Linux/UNIX, run the following as 'root' in a shell:")); $output .= Installer::sectionCode(sprintf('mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql')); } else { $output .= Installer::sectionNormal(__("On Windows, you must follow the instructions here Time zone description table. Once that is complete, you can issue the following command to grant the Cacti user access to the tables:")); } $output .= Installer::sectionNormal(__('Then run the following within MySQL as an administrator:')); $output .= Installer::sectionCode(sprintf("mysql > GRANT SELECT ON mysql.time_zone_name to '%s'@'localhost' IDENTIFIED BY '%s'", $database_username, $database_password)); return $output; } // updateButtons() - update the buttons used by the installer private function updateButtons() : void { if (empty($this->buttonNext)) { $this->buttonNext = new InstallerButton(); } if (empty($this->buttonPrevious)) { $this->buttonPrevious = new InstallerButton(); } if (empty($this->buttonTest)) { $this->buttonTest = new InstallerButton(); $this->buttonTest->Enabled = false; $this->buttonTest->Text = __('Test Connection'); } $this->buttonNext->Text = __('Next'); $this->buttonPrevious->Text = __('Previous'); $this->buttonTest->Visible = false; $this->buttonTest->setStep(Installer::STEP_TEST_REMOTE); switch ($this->stepCurrent) { case Installer::STEP_WELCOME: $this->buttonNext->Text = __('Begin'); break; case Installer::STEP_INSTALL_CONFIRM: // checkdependencies - send to install/upgrade if ($this->mode == Installer::MODE_UPGRADE) { $this->buttonNext->Text = __('Upgrade'); } elseif ($this->mode == Installer::MODE_DOWNGRADE) { $this->buttonNext->Text = __('Downgrade'); } break; case Installer::STEP_PERMISSION_CHECK: switch ($this->mode) { case Installer::MODE_UPGRADE: case Installer::MODE_DOWNGRADE: $this->stepNext = Installer::STEP_TEMPLATE_INSTALL; break; } break; case Installer::STEP_BINARY_LOCATIONS: switch ($this->mode) { case Installer::MODE_POLLER: $this->stepNext = Installer::STEP_CHECK_TABLES; break; } break; case Installer::STEP_CHECK_TABLES: switch ($this->mode) { case Installer::MODE_POLLER: $this->stepPrevious = Installer::STEP_BINARY_LOCATIONS; break; case Installer::MODE_UPGRADE: case Installer::MODE_DOWNGRADE: $this->stepPrevious = Installer::STEP_PERMISSION_CHECK; break; } break; case Installer::STEP_INSTALL_CONFIRM: // upgrade - if user upgrades send to settings check if ($this->isPre_v0_8_UpgradeNeeded()) { // upgrade - if user runs old version send to upgrade-oldversion $this->stepNext = Installer::STEP_INSTALL_OLDVERSION; } else { $this->stepNext = Installer::STEP_INSTALL; } break; case Installer::STEP_COMPLETE: $this->stepPrevious = Installer::STEP_NONE; break; } $this->buttonNext->setStep($this->stepNext); $this->buttonPrevious->setStep($this->stepPrevious); } /* * The following sections of code are all related * to the installation process of the current step */ public function processCurrentStep() : string { $exitReason = $this->shouldExitWithReason(); if ($exitReason !== false) { $this->buttonNext->Enabled = false; $this->buttonPrevious->Enabled = false; $this->buttonTest->Enabled = false; return $this->exitWithReason($exitReason); } switch ($this->stepCurrent) { case Installer::STEP_WELCOME: return $this->processStepWelcome(); case Installer::STEP_CHECK_DEPENDENCIES: return $this->processStepCheckDependencies(); case Installer::STEP_INSTALL_TYPE: return $this->processStepMode(); case Installer::STEP_BINARY_LOCATIONS: return $this->processStepBinaryLocations(); case Installer::STEP_PERMISSION_CHECK: return $this->processStepPermissionCheck(); case Installer::STEP_INPUT_VALIDATION: return $this->processStepNoticesRecommendations(); case Installer::STEP_PROFILE_AND_AUTOMATION: return $this->processStepProfileAndAutomation(); case Installer::STEP_TEMPLATE_INSTALL: return $this->processStepTemplateInstall(); case Installer::STEP_CHECK_TABLES: return $this->processStepCheckTables(); case Installer::STEP_INSTALL_CONFIRM: return $this->processStepInstallConfirm(); case Installer::STEP_INSTALL: return $this->processStepInstall(); case Installer::STEP_ERROR: return $this->processStepComplete(); case Installer::STEP_COMPLETE: return $this->processStepComplete(); default: return $this->exitWithReason(($this->stepCurrent)); } } public function processStepWelcome() : string { global $cacti_version_codes; $output = Installer::sectionTitle(__('Cacti Version') . ' ' . CACTI_VERSION_BRIEF . ' - ' . __('License Agreement')); if (!array_key_exists(CACTI_VERSION, $cacti_version_codes)) { $output .= Installer::sectionError(__('This version of Cacti (%s) does not appear to have a valid version code, please contact the Cacti Development Team to ensure this is corrected. If you are seeing this error in a release, please raise a report immediately on GitHub', CACTI_VERSION)); } $output .= Installer::sectionNormal(__('Thanks for taking the time to download and install Cacti, the complete graphing solution for your network. Before you can start making cool graphs, there are a few pieces of data that Cacti needs to know.')); $output .= Installer::sectionNormal(__('Make sure you have read and followed the required steps needed to install Cacti before continuing. Install information can be found for Unix and Win32-based operating systems.', '../docs/html/install_unix.html', '../docs/html/install_windows.html')); if ($this->mode == Installer::MODE_UPGRADE) { $output .= Installer::sectionNote(__('This process will guide you through the steps for upgrading from version \'%s\'. ', $this->old_cacti_version)); $output .= Installer::sectionNormal(__('Also, if this is an upgrade, be sure to read the Upgrade information file.', '../docs/html/upgrade.html')); } if ($this->mode == Installer::MODE_DOWNGRADE) { $output .= Installer::sectionNote(__('It is NOT recommended to downgrade as the database structure may be inconsistent')); } $output .= Installer::sectionNormal(__('Cacti is licensed under the GNU General Public License, you must agree to its provisions before continuing:')); $output .= Installer::sectionCode( __('This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.') . '

' . __('This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.') ); $langOutput = ''; $themePath = CACTI_PATH_INCLUDE . '/themes/'; $themes = glob($themePath . '*', GLOB_ONLYDIR); $themeOutput = ''; $eula = ($this->eula) ? ' checked' : ''; $output .= Installer::sectionNormal('' . __('Select default theme: ') . $themeOutput . '' . $langOutput . ''); $this->stepData = ['Eula' => $this->eula, 'Theme' => $this->theme, 'Language' => $this->language]; $this->buttonNext->Enabled = ($this->eula == 1); return $output; } public function processStepCheckDependencies() : string { global $local_db_cnn_id, $remote_db_cnn_id; global $database_default, $database_username, $database_port; global $rdatabase_default, $rdatabase_username, $rdatabase_port; cacti_system_zone_set(); $enabled = [ 'location' => DB_STATUS_SUCCESS, 'php_modules' => DB_STATUS_SUCCESS, 'php_optional' => DB_STATUS_SUCCESS, 'mysql_timezone' => DB_STATUS_SUCCESS, 'mysql_performance' => DB_STATUS_SUCCESS ]; $output = Installer::sectionTitle(__('Pre-installation Checks')); $output .= Installer::sectionSubTitle(__('Location checks'), 'location'); // Get request URI and break into parts if ($this->runtime == 'Cli' || $this->runtime == 'Json') { $test_request_uri = 'index.php'; } else { $test_request_uri = $_SERVER['REQUEST_URI']; } $test_request_parts = parse_url($test_request_uri); $test_request_path = $test_request_parts['path']; $test_request_len = strlen($test_request_parts['path']); // Get current script name (filename only) $test_script_name = get_current_script_name(); $test_script_len = strlen($test_script_name); // Get end of the path of URI and see if it's our script $test_script_part = substr($test_request_path, $test_request_len - $test_script_len); $test_script_result = strcmp($test_script_part, $test_script_name); // Assume desired path is the URI $test_final_path = $test_request_parts['path']; // Script was found in path, so remove it if ($test_script_result === 0) { $test_final_path = substr($test_final_path, 0, strlen($test_final_path) - $test_script_len); } // Add the install subfolder to defined path location // and check if it matches, if not there will likely be problems $test_config_path = CACTI_PATH_URL . 'install/'; $test_compare_result = strcmp($test_config_path, $test_final_path); // The path was not what we expected so print an error if ($test_compare_result !== 0) { $output .= Installer::sectionNormal('' . __('ERROR:') . ' ' . __('Please update config.php with the correct relative URI location of Cacti (url_path).') . ''); $enabled['location'] = DB_STATUS_ERROR; } else { $output .= Installer::sectionNormal(__('Your Cacti configuration has the relative correct path (url_path) in config.php.')); } $output .= Installer::sectionSubTitleEnd(); $recs = utility_php_recommends(); foreach ($recs as $rec_title => $recommends) { if (!empty($recommends)) { $output .= Installer::sectionSubTitle(__('PHP - Recommendations (%s)', $rec_title), 'php_recommends_' . $rec_title); ob_start(); html_start_box(__('PHP Recommendations' . ' (' . $recommends[0]['value'] . ')'), '100%', false, 3, '', false); html_header([__('Name'), __('Current'), __('Recommended'), __('Status'), __('Description')]); $status = DB_STATUS_SUCCESS; if (!cacti_sizeof($recommends)) { $recommends = [ [ 'status' => DB_STATUS_ERROR, 'name' => __('PHP Binary'), 'current' => read_config_option('path_php_binary', true), 'value' => '', 'description' => __('The PHP binary location is not valid and must be updated.'), ], [ 'status' => DB_STATUS_WARNING, 'name' => '', 'current' => '', 'value' => '', 'description' => __('Update the path_php_binary value in the settings table.'), ] ]; } foreach ($recommends as $recommend) { if ($recommend['name'] == 'location') { continue; } if ($recommend['status'] == DB_STATUS_SUCCESS) { $status_font = 'green'; $status_text = __('Passed'); } elseif ($recommend['status'] == DB_STATUS_WARNING) { $status_font = 'orange'; $status_text = __('Warning'); if ($status > DB_STATUS_WARNING) { $status = DB_STATUS_WARNING; } } elseif ($recommend['status'] == DB_STATUS_RESTART) { $status_font = 'orange'; $status_text = '' . __('Restart Required') . ''; if ($status > DB_STATUS_RESTART) { $status = DB_STATUS_RESTART; } } else { $status_font = 'red'; $status_text = __('Error'); if ($status > DB_STATUS_ERROR) { $status = DB_STATUS_ERROR; } } form_alternate_row('php_' . $recommend['name'], true); form_selectable_ecell($recommend['name'], ''); form_selectable_ecell($recommend['current'], ''); form_selectable_ecell('>= ' . $recommend['value'], ''); form_selectable_cell("$status_text", ''); form_selectable_ecell($recommend['description'], ''); form_end_row(); } html_end_box(false); $output .= Installer::sectionNormal((string) ob_get_contents()); ob_clean(); $output .= Installer::sectionSubTitleEnd(); $enabled['php_recommends_' . $rec_title] = $status; } } $output .= Installer::sectionSubTitle(__('PHP - Module Support (Required)'), 'php_modules'); $output .= Installer::sectionNormal(__('Cacti requires several PHP Modules to be installed to work properly. If any of these are not installed, you will be unable to continue the installation until corrected. In addition, for optimal system performance Cacti should be run with certain MySQL system variables set. Please follow the MySQL recommendations at your discretion. Always seek the MySQL documentation if you have any questions.')); $output .= Installer::sectionNormal(__('The following PHP extensions are mandatory, and MUST be installed before continuing your Cacti install.')); ob_clean(); html_start_box(__('Required PHP Modules'), '100%', false, 3, 'center', false); html_header([__('Name'), __('Required'), __('Installed')]); $enabled['php_modules'] = DB_STATUS_SUCCESS; foreach ($this->modules as $id => $e) { form_alternate_row('line' . $id); form_selectable_cell($id, ''); form_selectable_cell('' . __('Yes') . '', ''); form_selectable_cell(Installer::formatModuleStatus($this->modules[$id], $id), ''); form_end_row(); if (!$e['installed']) { $enabled['php_modules'] = DB_STATUS_ERROR; } } html_end_box(false); $output .= Installer::sectionNormal((string) ob_get_contents()); ob_clean(); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('PHP - Module Support (Optional)'), 'php_optional'); $output .= Installer::sectionNormal(__('The following PHP extensions are recommended, and should be installed before continuing your Cacti install. NOTE: If you are planning on supporting SNMPv3 with IPv6 or SNMPv3 with stronger ciphers, you should not install the php-snmp module at this time.')); $ext = utility_php_optionals(); html_start_box(__('Optional Modules'), '100%', false, 3, 'center', false); html_header([__('Name'), __('Optional'), __('Installed')]); foreach ($ext as $id => $e) { form_alternate_row('line' . $id, true); form_selectable_cell($id, ''); form_selectable_cell('' . __('Yes') . '', ''); form_selectable_cell(Installer::formatModuleStatus($ext[$id], $id, 'orange'), ''); form_end_row(); if (!$e['installed']) { $enabled['php_optional'] = DB_STATUS_WARNING; } } html_end_box(); $output .= Installer::sectionNormal((string) ob_get_contents()); ob_clean(); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('MySQL - TimeZone Support'), 'mysql_timezone'); if (POLLER_ID == 1) { $mysql_timezone_access = db_fetch_assoc('SHOW COLUMNS FROM mysql.time_zone_name', false); } else { $mysql_timezone_access = db_fetch_assoc('SHOW COLUMNS FROM mysql.time_zone_name', false, $local_db_cnn_id); } if (cacti_sizeof($mysql_timezone_access)) { $timezone_populated = db_fetch_cell('SELECT COUNT(*) FROM mysql.time_zone_name'); if (!$timezone_populated) { $output .= Installer::sectionNormal('' . __('ERROR:') . ' ' . __('Your MySQL TimeZone database is not populated. Please populate this database before proceeding.') . ''); $enabled['mysql_timezone'] = DB_STATUS_ERROR; } } else { $output .= Installer::sectionNormal('' . __('ERROR:') . ' ' . __('Your Cacti database login account does not have access to the MySQL TimeZone database. Please provide the Cacti database account "select" access to the "time_zone_name" table in the "mysql" database, and populate MySQL\'s TimeZone information before proceeding.') . ''); $enabled['mysql_timezone'] = DB_STATUS_ERROR; } if ($enabled['mysql_timezone'] == DB_STATUS_SUCCESS) { $output .= Installer::sectionNormal(__('Your Cacti database account has access to the MySQL TimeZone database and that database is populated with global TimeZone information.')); } $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('MySQL - Settings'), 'mysql_performance'); $output .= Installer::sectionNormal(__('These MySQL performance tuning settings will help your Cacti system perform better without issues for a longer time.')); html_start_box(__('Recommended MySQL System Variable Settings'), '100%', false, 3, 'center', false); $output_temp = ob_get_contents(); ob_clean(); $enabled['mysql_performance'] = utilities_get_mysql_recommendations(); $output_util = ob_get_contents(); ob_clean(); html_end_box(false); $output .= Installer::sectionNormal($output_temp . $output_util . ob_get_contents()); ob_end_clean(); $output .= Installer::sectionSubTitleEnd(); $this->stepData = ['Sections' => $enabled]; $this->buttonNext->Enabled = ($enabled['php_modules'] != DB_STATUS_ERROR) && ($enabled['php_recommends_cli'] != DB_STATUS_ERROR) && ($enabled['php_recommends_web'] != DB_STATUS_ERROR) && ($enabled['mysql_timezone'] != DB_STATUS_ERROR); return $output; } public function processStepMode() : string { global $database_default, $database_username, $database_hostname, $database_port; global $rdatabase_default, $rdatabase_username, $rdatabase_hostname, $rdatabase_port; // install/upgrade $output = Installer::sectionTitle(__('Installation Type')); switch ($this->mode) { case Installer::MODE_UPGRADE: // upgrade detected $output .= Installer::sectionSubTitle(__('Upgrade')); $output .= Installer::sectionNormal(__('Upgrade from %s to %s', $this->old_cacti_version, CACTI_VERSION_FULL)); $output .= Installer::sectionWarning(__('In the event of issues, It is highly recommended that you clear your browser cache, closing then reopening your browser (not just the tab Cacti is on) and retrying, before raising an issue with The Cacti Group')); $output .= Installer::sectionNormal(__('On rare occasions, we have had reports from users who experience some minor issues due to changes in the code. These issues are caused by the browser retaining pre-upgrade code and whilst we have taken steps to minimise the chances of this, it may still occur. If you need instructions on how to clear your browser cache, https://www.refreshyourcache.com/ is a good starting point.')); $output .= Installer::sectionNormal(__('If after clearing your cache and restarting your browser, you still experience issues, please raise the issue with us and we will try to identify the cause of it.')); $output .= Installer::sectionSubTitleEnd(); break; case Installer::MODE_DOWNGRADE: $output .= Installer::sectionSubTitle(__('Upgrade')); $output .= Installer::sectionNormal(__('Downgrade from %s to %s', $this->old_cacti_version, CACTI_VERSION_FULL)); $output .= Installer::sectionWarning(__('You appear to be downgrading to a previous version. Database changes made for the newer version will not be reversed and could cause issues.')); $output .= Installer::sectionSubTitleEnd(); break; default: // new install $output .= Installer::sectionSubTitle(__('Please select the type of installation')); $output .= Installer::sectionNormal(__('Installation options:')); $output .= Installer::sectionNormal( '
    ' . '
  • ' . __('New Primary Server') . ' - ' . __('Choose this for the Primary site.') . '
  • ' . '
  • ' . __('New Remote Poller') . ' - ' . __('Remote Pollers are used to access networks that are not readily accessible to the Primary site.') . '
  • ' . '
' ); $selectedInstall = ''; $selectedPoller = ''; $sections = [ 'connection_local' => 0, 'connection_remote' => 0, 'error_file' => 0, 'error_poller' => 0, 'poller_vars' => 0, 'poller_steps' => 0, ]; $this->buttonNext->Enabled = true; switch ($this->mode) { case Installer::MODE_POLLER: $selectedPoller = ' selected'; $sections['connection_local'] = 1; $sections['connection_remote'] = 1; $sections['poller_steps'] = 1; $sections['error_file'] = !$this->isConfigurationWritable(); $sections['error_poller'] = !$this->isRemoteDatabaseGood(); if ($sections['error_poller']) { $sections['poller_vars'] = 1; $sections['connection_remote'] = 0; } if (!($sections['error_file'] || $sections['error_poller'])) { $this->buttonNext->Enabled = ($this->mode != Installer::MODE_POLLER); $this->buttonTest->Enabled = true; $this->buttonTest->Visible = true; } else { $this->buttonNext->Enabled = false; $this->buttonNext->Visible = false; } break; default: $selectedInstall = ' selected'; $sections['connection_local'] = 1; break; } $output .= Installer::sectionNormal( '' ); $output .= Installer::sectionNormal(__('The following information has been determined from Cacti\'s configuration file. If it is not correct, please edit "include/config.php" before continuing.')); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Local Database Connection Information'), 'connection_local'); $output .= Installer::sectionCode( __('Database: %s', $database_default) . '
' . __('Database User: %s', $database_username) . '
' . __('Database Hostname: %s', $database_hostname) . '
' . __('Port: %s', $database_port) . '
' . __('Server Operating System Type: %s', CACTI_SERVER_OS) . '
' ); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Central Database Connection Information'), 'connection_remote'); $output .= Installer::sectionCode( __('Database: %s', $rdatabase_default) . '
' . __('Database User: %s', $rdatabase_username) . '
' . __('Database Hostname: %s', $rdatabase_hostname) . '
' . __('Port: %s', $rdatabase_port) . '
' . __('Server Operating System Type: %s', CACTI_SERVER_OS) . '
' ); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Configuration Readonly!'), 'error_file'); $output .= Installer::sectionNormal('' . __('ERROR:') . ' ' . __('Your config.php file must be writable by the web server during install in order to configure the Remote poller. Once installation is complete, you must set this file to Read Only to prevent possible security issues.') . ''); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Configuration of Poller'), 'error_poller'); $output .= Installer::sectionNormal('' . __('ERROR:') . ' ' . __('Your Remote Cacti Poller information has not been included in your config.php file. Please review the config.php.dist, and set the variables: $rdatabase_default, $rdatabase_username, etc. These variables must be set and point back to your Primary Cacti database server. Correct this and try again.') . '', 'config_remote'); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Remote Poller Variables'), 'poller_vars'); $output .= Installer::sectionNormal(__('The variables that must be set in the config.php file include the following:')); $output .= Installer::sectionCode( '$rdatabase_type = \'mysql\';
' . '$rdatabase_default = \'cacti\';
' . '$rdatabase_hostname = \'cacti.example.com\'; // Central Cacti server.
' . '$rdatabase_username = \'cactiuser\';
' . '$rdatabase_password = \'cactiuser\';
' . '$rdatabase_port = \'3306\';
' . '$rdatabase_ssl = false;
' ); $output .= Installer::sectionNormal(__('The Installer automatically assigns a $poller_id and adds it to the config.php file.'), 'config_remote_poller'); $output .= Installer::sectionNormal(__('Once the variables are all set in the config.php file, you must also grant the $rdatabase_username access to the main Cacti database server. Follow the same procedure you would with any other Cacti install. You may then press the \'Test Connection\' button. If the test is successful you will be able to proceed and complete the install.'), 'config_remote_var'); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Additional Steps After Installation'), 'poller_steps'); $output .= Installer::sectionNormal(__('It is essential that the Central Cacti server can communicate via MySQL to each remote Cacti database server. Once the install is complete, you must edit the Remote Data Collector and ensure the settings are correct. You can verify using the \'Test Connection\' when editing the Remote Data Collector.'), 'config_remote_db'); $output .= Installer::sectionSubTitleEnd(); $this->stepData = ['Sections' => $sections]; $this->buttonNext->Enabled = ($this->mode != Installer::MODE_POLLER); break; } return $output; } public function processStepBinaryLocations() : string { $output = Installer::sectionTitle(__('Critical Binary Locations and Versions')); $output .= Installer::sectionNormal(__('Make sure all of these values are correct before continuing.')); if (!empty($this->errors)) { $output .= Installer::sectionWarning(__('One or more paths appear to be incorrect, unable to proceed')); } $i = 0; ob_start(); print '
'; /** * find the appropriate value for each 'config name' above by config.php, database, * or a default for fall back */ $class = 'odd'; $errors = []; if (array_key_exists('Paths', $this->errors)) { $errors = $this->errors['Paths']; } foreach ($this->paths as $name => $array) { $class = ($class == 'even' ? 'odd' : 'even'); $current_value = ''; if (isset($array['default'])) { $current_value = $array['default']; } log_install_debug('paths', 'processStepBinaryLocations(): Displaying ' . $array['friendly_name'] . ' (' . $name . ' - ' . $class . '): ' . $current_value); /* run a check on the path specified only if specified above, then fill a string with the results ('FOUND' or 'NOT FOUND') so they can be displayed on the form */ $form_check_string = ''; // draw the actual header and textbox on the form print "
" . $array['friendly_name'] . "
" . $array['description'] . '
'; print "
"; $data = ['text' => 'Valid']; if (array_key_exists($name, $errors)) { $data['text'] = $errors[$name]; $data['error'] = true; } switch ($array['method']) { case 'textbox': form_text_box($name, $current_value, '', '', 40, 'text'); break; case 'filepath': form_filepath_box($name, $current_value, '', '', 40, 'text', 0, $data); break; case 'drop_array': form_dropdown($name, $array['array'], '', '', $current_value, '', ''); break; } /* Disable output of error for now, pending QA *** if (isset($this->errors['Paths'][$name])) { print Installer::sectionError(__($this->errors['Paths'][$name])); } */ print '
'; print '
'; $i++; } print '
'; $html = ob_get_contents(); $output .= Installer::sectionNormal((string) $html); ob_end_clean(); return $output; } public function processStepPermissionCheck() : string { // Print message and error logs $output = Installer::sectionTitle(__('Directory Permission Checks')); $output .= Installer::sectionNormal(__('Please ensure the directory permissions below are correct before proceeding. During the install, these directories need to be owned by the Web Server user. These permission changes are required to allow the Installer to install Device Template packages which include XML and script files that will be placed in these directories. If you choose not to install the packages, there is an \'install_package.php\' cli script that can be used from the command line after the install is complete.')); if ($this->mode == Installer::MODE_INSTALL) { $output .= Installer::sectionNormal(__('After the install is complete, you can make some of these directories read only to increase security.')); } else { $output .= Installer::sectionNormal(__('These directories will be required to stay read writable after the install so that the Cacti remote synchronization process can update them as the Main Cacti Web Site changes')); } if ($this->mode != Installer::MODE_POLLER) { $output .= Installer::sectionNote(__('If you are installing packages, once the packages are installed, you should change the scripts directory back to read only as this presents some exposure to the web site.')); } else { $output .= Installer::sectionNote(__('For remote pollers, it is critical that the paths that you will be updating frequently, including the plugins, scripts, and resources paths have read/write access as the data collector will have to update these paths from the main web server content.')); } $sections = []; $permissions = ''; if ($this->mode != Installer::MODE_POLLER) { $permissions .= Installer::sectionSubTitle(__('Required Writable at Install Time Only'), 'writable_install'); $sections['writable_install'] = DB_STATUS_SUCCESS; $class = 'even'; foreach ($this->permissions['install'] as $path => $valid) { $class = ($class == 'even' ? 'odd' : 'even'); // draw the actual header and textbox on the form $permissions .= "
" . $path . '
'; $permissions .= "
"; if ($valid) { $permissions .= ' ' . '' . __('Writable') . ''; } else { $permissions .= ' ' . '' . __('Not Writable') . ''; $writable = false; $sections['writable_install'] = DB_STATUS_ERROR; } $permissions .= '
'; } } $permissions .= Installer::sectionSubTitleEnd(); $permissions .= Installer::sectionSubTitle(__('Required Writable after Install Complete'), 'writable_always'); $sections['writable_always'] = DB_STATUS_SUCCESS; $class = 'even'; foreach ($this->permissions['always'] as $path => $valid) { $class = ($class == 'even' ? 'odd' : 'even'); // draw the actual header and textbox on the form $permissions .= "
" . $path . '
'; $permissions .= "
"; if ($valid) { $permissions .= ' ' . '' . __('Writable') . ''; } else { $permissions .= ' ' . '' . __('Not Writable') . ''; $sections['writable_always'] = DB_STATUS_ERROR; $writable = false; } $permissions .= '
'; } $permissions .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitleEnd(); $output .= Installer::sectionSubTitle(__('Potential permission issues'), 'host_access'); $sections['host_access'] = DB_STATUS_SUCCESS; // Print help message for unix and windows if directory is not writable if (isset($writable)) { $running_user = get_running_user(); $sections['host_access'] = DB_STATUS_WARNING; $text = __('Please make sure that your webserver has read/write access to the cacti folders that show errors below.'); $paths = array_merge($this->permissions['install'], $this->permissions['always']); if (CACTI_SERVER_OS == 'unix') { $text .= ' ' . __('If SELinux is enabled on your server, you can either permanently disable this, or temporarily disable it and then add the appropriate permissions using the SELinux command-line tools.'); $code = ''; foreach ($paths as $path => $valid) { if (!$valid) { $code .= sprintf('chown -R %s:%s %s
', $running_user, $running_user, $path); } } } else { // NOTE: $code part needs updating with the correct command $text .= ' ' . __('The user \'%s\' should have MODIFY permission to enable read/write.', $running_user); $code = ''; foreach ($paths as $valid) { if (!$valid) { $code = sprintf('icacls %s %s/resource/', $running_user, CACTI_PATH_BASE); } } } $output .= Installer::sectionNormal($text); $output .= Installer::sectionNormal(__('An example of how to set folder permissions is shown here, though you may need to adjust this depending on your operating system, user accounts and desired permissions.')); $output .= Installer::sectionNote('' . $code . '', '', '', __('EXAMPLE:')); $output .= Installer::sectionNote(__('Once installation has completed the CSRF path, should be set to read-only.')); } else { $output .= Installer::sectionNormal('' . __('All folders are writable') . ''); } $output .= Installer::sectionSubTitleEnd(); $output .= $permissions; $this->buttonNext->Enabled = !isset($writable); $this->stepData = ['Sections' => $sections]; return $output; } public function processStepNoticesRecommendations() : string { $output = Installer::sectionTitle(__('Linux kernel security modules')); $output .= Installer::sectionNormal(__('Cacti needs to establish network connections, execute binaries and write files to work. Linux security features such as SELinux, AppArmor or ModSecurity may cause Cacti to become inoperable in some configurations. If problems occur, check the settings of these security tools.

')); $output .= Installer::sectionTitle(__('Input Validation Whitelist Protection')); $output .= Installer::sectionNormal(__('Cacti Data Input methods that call a script can be exploited in ways that a non-administrator can perform damage to either files owned by the poller account, and in cases where someone runs the Cacti poller as root, can compromise the operating system allowing attackers to exploit your infrastructure.')); $output .= Installer::sectionNormal(__('Therefore, several versions ago, Cacti was enhanced to provide Whitelist capabilities on the these types of Data Input Methods. Though this does secure Cacti more thoroughly, it does increase the amount of work required by the Cacti administrator to import and manage Templates and Packages.')); $output .= Installer::sectionNormal(__('The way that the Whitelisting works is that when you first import a Data Input Method, or you re-import a Data Input Method, and the script and or arguments change in any way, the Data Input Method, and all the corresponding Data Sources will be immediately disabled until the administrator validates that the Data Input Method is valid.')); $output .= Installer::sectionNormal(__('To make identifying Data Input Methods in this state, we have provided a validation script in Cacti\'s CLI directory that can be run with the following options:')); $output .= Installer::sectionNormal( '
    ' . '
  • php -q input_whitelist.php --audit - ' . __('This script option will search for any Data Input Methods that are currently banned and provide details as to why.') . '
  • ' . '
  • php -q input_whitelist.php --update - ' . __('This script option un-ban the Data Input Methods that are currently banned.') . '
  • ' . '
  • php -q input_whitelist.php --push - ' . __('This script option will re-enable any disabled Data Sources.') . '
  • ' . '
' ); $output .= Installer::sectionNormal(__('It is strongly suggested that you update your config.php to enable this feature by uncommenting the $input_whitelist variable and then running the three CLI script options above after the web based install has completed.

')); $output .= Installer::sectionNormal(__('Check the Checkbox below to acknowledge that you have read and understand this security concern')); $output .= Installer::sectionNormal(''); $this->buttonNext->Enabled = false; $this->buttonNext->Step = Installer::STEP_PROFILE_AND_AUTOMATION; return $output; } public function processStepProfileAndAutomation() : string { global $cron_intervals; $profiles = db_fetch_assoc('SELECT dsp.id, dsp.name, dsp.default FROM data_source_profiles AS dsp ORDER BY dsp.step, dsp.name'); if (cacti_sizeof($profiles)) { $output = Installer::sectionTitle(__('Admin email address')); $output .= Installer::sectionNormal(__('In case of problems cacti can send emails to the administrator. The address filled in here will be assigned to the admin account and "Notify Primary Admin of Issue" will be enabled. You can change this setting and the email address later. Do not forget to set up the email settings after installation.')); $fields_admin_email = [ 'admin_email_address' => [ 'method' => 'textbox', 'friendly_name' => __('Admin email address'), 'value' => '|arg1:admin_email_address|', 'max_length' => '100', ] ]; ob_start(); $values = [ 'admin_email_address' => $this->adminEmailAddress]; draw_edit_form( [ 'config' => ['no_form_tag' => true], 'fields' => inject_form_variables($fields_admin_email, $values), ] ); $html = ob_get_contents(); ob_end_clean(); $output .= Installer::sectionNormal((string) $html); $output .= Installer::sectionTitle(__('Default Profile')); $output .= Installer::sectionNormal(__('Please select the default Data Source Profile to be used for polling sources. This is the maximum amount of time between scanning devices for information so the lower the polling interval, the more work is placed on the Cacti Server host. Long retention profiles store more data, so they take up more disk capacity. Their advantage is that older data is not as consolidated and is therefore more accurate than using shorter retention.')); $output .= Installer::sectionNormal(__('Also, select the intended, or configured Cron interval that you wish to use for Data Collection.')); $fields_schedule = [ 'default_profile' => [ 'method' => 'drop_sql', 'friendly_name' => __('Default Profile'), 'sql' => 'SELECT dsp.id, dsp.name, dsp.default FROM data_source_profiles AS dsp ORDER BY dsp.step, dsp.name', 'value' => '|arg1:default_profile|', ], 'cron_interval' => [ 'method' => 'drop_array', 'friendly_name' => __('Cron Interval'), 'array' => $cron_intervals, 'value' => '|arg1:cron_interval|', ] ]; ob_start(); $values = ['default_profile' => $this->profile, 'cron_interval' => $this->cronInterval]; draw_edit_form( [ 'config' => ['no_form_tag' => true], 'fields' => inject_form_variables($fields_schedule, $values), ] ); $html = ob_get_contents(); ob_end_clean(); $output .= Installer::sectionNormal((string) $html); $output .= Installer::sectionTitle(__('Default Automation Network')); $output .= Installer::sectionNormal(__('Cacti can automatically scan the network once installation has completed. This will utilise the network range below to work out the range of IPs that can be scanned. A predefined set of options are defined for scanning which include using both \'public\' and \'private\' communities.')); $output .= Installer::sectionNormal(__('If your devices require a different set of options to be used first, you may define them below and they will be utilized before the defaults')); $output .= Installer::sectionNormal(__('All options may be adjusted post installation')); global $fields_snmp_item_with_retry; $output .= Installer::sectionSubTitle(__('Default Options')); $fields_automation = [ 'automation_mode' => [ 'method' => 'checkbox', 'friendly_name' => __('Scan Mode'), 'value' => '|arg1:automation_mode|', ], 'automation_range' => [ 'method' => 'textbox', 'friendly_name' => __('Network Range'), 'value' => '|arg1:automation_range|', 'max_length' => '100', ], 'automation_override' => [ 'method' => 'checkbox', 'friendly_name' => __('Additional Defaults'), 'value' => '|arg1:automation_override|', ] ]; ob_start(); $values = [ 'automation_mode' => $this->automationMode ? 'on' : '', 'automation_range' => $this->automationRange, 'automation_override' => $this->automationOverride ? 'on' : '', ]; draw_edit_form( [ 'config' => ['no_form_tag' => true], 'fields' => inject_form_variables($fields_automation, $values), ] ); $html = ob_get_contents(); $output .= Installer::sectionNormal((string) $html); $output .= Installer::sectionSubTitle(__('Additional SNMP Options'), 'automation_snmp_options'); ob_clean(); draw_edit_form( [ 'config' => ['no_form_tag' => true], 'fields' => inject_form_variables($fields_snmp_item_with_retry, $this->snmpOptions), ] ); $html = ob_get_contents(); $output .= Installer::sectionNormal((string) $html); ob_end_clean(); } else { $output = Installer::sectionTitleError(__('Error Locating Profiles')); $output .= Installer::sectionNormal(__('The installation cannot continue because no profiles could be found.')); $output .= Installer::sectionNote(__('This may occur if you have a blank database and have not yet imported the cacti.sql file')); $output .= Installer::sectionCode('mysql> source cacti.sql'); } return $output; } public function processStepTemplateInstall() : string { $output = Installer::sectionTitle(__('Template Setup')); if ($this->getMode() == Installer::MODE_UPGRADE) { $output .= Installer::sectionNormal(__('Please select the Device Templates that you wish to update during the Upgrade.')); $output .= Installer::sectionWarning(__('Updating Templates that you have already made modifications to is not advisable. The Upgrade of the Templates will NOT remove modifications to Graph and Data Templates, and can lead to unexpected behavior. However, if you have not made changes to any Graph, Data Query, or Data Template, reimporting the Package should not have any affect. In that case, you would have to \'Sync Graphs\' to from the Templates after update.')); } else { $output .= Installer::sectionNormal(__('Please select the Device Templates that you wish to use after the Install. If your Operating System is Windows, you need to ensure that you select the \'Windows Device\' Template. If your Operating System is Linux/UNIX, make sure you select the \'Local Linux Machine\' Device Template.')); } $templates = install_setup_get_templates(); ob_start(); html_start_box(__('Templates'), '100%', false, 3, 'center', '', ''); html_header_checkbox([__('Name'), __('Description'), __('Author'), __('Homepage')]); foreach ($templates as $id => $p) { $name = (isset($p['name']) && !empty($p['name'])) ? $p['name'] : ''; $description = (isset($p['description']) && !empty($p['description'])) ? $p['description'] : ''; $author = (isset($p['author']) && !empty($p['author'])) ? $p['author'] : ''; if (isset($p['homepage']) && !empty($p['homepage'])) { if (!str_starts_with($p['homepage'], 'http://') && strncmp($p['homepage'], 'http://', 8) !== 0) { $p['homepage'] = 'https://' . $p['homepage']; } $homepage = '' . $p['homepage'] . ''; } else { $homepage = ''; } form_alternate_row('line' . $id, true); form_selectable_ecell($name, $id); form_selectable_ecell($description, $id); form_selectable_ecell($author, $id); form_selectable_cell($homepage, $id); form_checkbox_cell($p['name'], 'template_' . str_replace('.', '_', $p['filename'])); form_end_row(); } html_end_box(false); $output .= Installer::sectionNormal((string) ob_get_contents()); ob_end_clean(); $output .= Installer::sectionNormal(__('Device Templates allow you to monitor and graph a vast assortment of data within Cacti. After you select the desired Device Templates, press \'Next\' and the installation will complete. Please be patient on this step, as the importation of the Device Templates can take a few minutes.')); $this->stepData = ['Templates' => $this->templates]; return $output; } public function processStepCheckTables() : string { $output = Installer::sectionTitle(__('Server Collation')); $collation_vars = array_rekey(db_fetch_assoc('SHOW VARIABLES LIKE "collation_%";'), 'Variable_name', 'Value'); $charset_vars = array_rekey(db_fetch_assoc('SHOW VARIABLES LIKE "character_set_%";'), 'Variable_name', 'Value'); $collation_valid = $this->isValidCollation($collation_vars, $charset_vars, 'server'); if ($collation_valid) { $output .= Installer::sectionNormal(__('Your server collation appears to be UTF8 compliant')); } else { $output .= Installer::sectionWarning( __('Your server collation does NOT appear to be fully UTF8 compliant. ') . __('Under the [mysqld] section, locate the entries named \'character-set-server\' and \'collation-server\' and set them as follows:') . Installer::sectionCode('[mysqld]
' . 'character-set-server=utf8mb4
' . 'collation-server=utf8mb4_unicode_ci') ); } $output .= Installer::sectionTitle(__('Database Collation')); $database = db_fetch_cell('SELECT DATABASE()'); $collation_valid = $this->isValidCollation($collation_vars, $charset_vars, 'database'); if ($collation_valid) { $output .= Installer::sectionNormal(__('Your database default collation appears to be UTF8 compliant')); } else { $output .= Installer::sectionWarning( __('Your database default collation does NOT appear to be full UTF8 compliant. ') . __('Any tables created by plugins may have issues linked against Cacti Core tables if the collation is not matched. Please ensure your database is changed to \'utf8mb4_unicode_ci\' by running the following: ') . Installer::sectionCode( 'mysql> ALTER DATABASE `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;' ) ); } $output .= Installer::sectionTitle(__('Table Setup')); $tables = install_setup_get_tables(); if (cacti_sizeof($tables)) { $max_vars = ini_get('max_input_vars'); if (empty($max_vars)) { $max_vars = 1000; } if ($max_vars < cacti_count($tables) + 10) { $output .= Installer::sectionError(__('You have more tables than your PHP configuration will allow us to display/convert. Please modify the max_input_vars setting in php.ini to a value above %s', cacti_count($tables) + 100)); $this->buttonNext->Enabled = false; } else { $output .= Installer::sectionWarning(__('Conversion of tables may take some time especially on larger tables. The conversion of these tables will occur in the background but will not prevent the installer from completing. This may slow down some servers if there are not enough resources for MySQL to handle the conversion.')); $output .= Installer::sectionNote('max_input_vars: ' . $max_vars . ', tables: ' . cacti_count($tables)); $show_warning = false; ob_start(); html_start_box(__('Tables'), '100%', false, 3, 'center', '', false); html_header_checkbox([__('Name'), __('Collation'), __('Row Format'), __('Engine'), __('Rows')]); foreach (($tables ?: []) as $id => $p) { $enabled = self::isTableSelectable($p); $style = ($enabled ? '' : 'text-decoration: line-through;'); if ($enabled) { $cstyle = ($p['Collation'] != 'utf8mb4_unicode_ci' ? 'deviceDown' : $style); $estyle = ($p['Engine'] != 'InnoDB' ? 'deviceDown' : $style); $rstyle = ($p['Row_format'] != 'Dynamic' ? 'deviceDown' : $style); } else { $cstyle = $estyle = $rstyle = $style; } form_alternate_row('line' . $id, true, $enabled); form_selectable_cell($p['Name'], $id, '', $style); form_selectable_cell($p['Collation'], $id, '', $cstyle); form_selectable_cell($p['Row_format'], $id, '', $rstyle); form_selectable_cell($p['Engine'], $id, '', $estyle); form_selectable_cell($p['Rows'], $id, '', $style); if ($enabled) { form_checkbox_cell($p['Name'], 'table_' . $p['Name']); } else { $show_warning = true; form_selectable_cell('', $id, '', $style); } form_end_row(); } html_end_box(false); if ($show_warning) { $output .= Installer::sectionWarning(__('One or more tables are too large to convert during the installation. You should use the cli/convert_tables.php script to perform the conversion, then refresh this page. For example: ')); $output .= Installer::sectionCode(read_config_option('path_php_binary') . ' -q ' . CACTI_PATH_CLI . '/convert_tables.php -u -i'); } $output .= Installer::sectionNormal(__('The following tables should be converted to UTF8 and InnoDB with a Dynamic row format. Please select the tables that you wish to convert during the installation process.')); $output .= Installer::sectionNormal((string) ob_get_contents()); ob_end_clean(); } } else { $output .= Installer::sectionNormal(__('All your tables appear to be UTF8 and Dynamic row format compliant')); } $this->stepData = ['Tables' => $this->tables]; return $output; } public function processStepInstallConfirm() : string { switch ($this->mode) { case Installer::MODE_UPGRADE: $title = __('Confirm Upgrade'); $button = __('Upgrade'); break; case Installer::MODE_DOWNGRADE: $title = __('Confirm Downgrade'); $button = __('Downgrade'); break; default: $title = __('Confirm Installation'); $button = __('Install'); break; } if ($this->mode == Installer::MODE_DOWNGRADE) { $output = Installer::sectionTitleError(__('DOWNGRADE DETECTED')); $output .= Installer::sectionCode(__('YOU MUST MANUALLY CHANGE THE CACTI DATABASE TO REVERT ANY UPGRADE CHANGES THAT HAVE BEEN MADE.
THE INSTALLER HAS NO METHOD TO DO THIS AUTOMATICALLY FOR YOU')); $output .= Installer::sectionNormal(__('Downgrading should only be performed when absolutely necessary and doing so may break your installation')); } else { $output = Installer::sectionTitle($title); $output .= '
'; $output .= Installer::sectionNormal(__('Your Cacti Server is almost ready. Please check that you are happy to proceed.')); $output .= Installer::showInstallOptions(); $output .= Installer::sectionNote( __('Press \'%s\' then click \'%s\' to complete the installation process after selecting your Device Templates.', $title, $button) ); } $output .= Installer::sectionNormal('