Files
cacti/lib/installer.php
Thomas Vincent 06c32ab13f fix(installer): preserve upgrade state and propagate failures (#8266)
* fix(installer): preserve upgrade state and propagate failures

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* docs(changelog): record installer recovery fixes for #8266

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(security): refresh installer sink inventories

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(installer): retain merge precedence for browser parameters

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(installer): defer debug settings until the process lock is held

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(installer): enforce table conversion failures and postconditions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(installer): refresh SQL audit baseline after clause refactor

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 17:08:12 -04:00

4907 lines
171 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| 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. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDTool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
include_once(__DIR__ . '/../lib/poller.php');
class Installer implements JsonSerializable {
const EXIT_DB_EMPTY = 1;
const EXIT_DB_OLD = 2;
/*
* The STEP_ constants are defined in the following files: *
* *
* lib/installer.php *
* install/install.js *
* *
* All files must be updated to match for the installation *
* process to work properly *
*/
const STEP_NONE = 0;
const STEP_WELCOME = 1;
const STEP_CHECK_DEPENDENCIES = 2;
const STEP_INSTALL_TYPE = 3;
const STEP_PERMISSION_CHECK = 4;
const STEP_BINARY_LOCATIONS = 5;
const STEP_INPUT_VALIDATION = 6;
const STEP_PROFILE_AND_AUTOMATION = 7;
const STEP_TEMPLATE_INSTALL = 8;
const STEP_CHECK_TABLES = 9;
const STEP_INSTALL_CONFIRM = 10;
const STEP_INSTALL_OLDVERSION = 11;
const STEP_INSTALL = 97;
const STEP_COMPLETE = 98;
const STEP_ERROR = 99;
const STEP_GO_SITE = -1;
const STEP_GO_FORUMS = -2;
const STEP_GO_GITHUB = -3;
const STEP_TEST_REMOTE = -4;
// Installer mode
const MODE_NONE = 0;
const MODE_INSTALL = 1;
const MODE_POLLER = 2;
const MODE_UPGRADE = 3;
const MODE_DOWNGRADE = 4;
// Progress through the STEP_INSTALL section
const PROGRESS_NONE = 0;
const PROGRESS_START = 1;
const PROGRESS_CSRF_BEGIN = 2;
const PROGRESS_CSRF_END = 3;
const PROGRESS_UPGRADES_BEGIN = 5;
const PROGRESS_UPGRADES_END = 30;
const PROGRESS_TABLES_BEGIN = 35;
const PROGRESS_TEMPLATES_BEGIN = 41;
const PROGRESS_TEMPLATES_END = 60;
const PROGRESS_PROFILE_START = 61;
const PROGRESS_PROFILE_POLLER = 63;
const PROGRESS_PROFILE_DEFAULT = 64;
const PROGRESS_PROFILE_END = 65;
const PROGRESS_AUTOMATION_START = 66;
const PROGRESS_AUTOMATION_END = 68;
const PROGRESS_DEVICE_START = 71;
const PROGRESS_DEVICE_TEMPLATE = 72;
const PROGRESS_DEVICE_GRAPH = 73;
const PROGRESS_DEVICE_TREE = 74;
const PROGRESS_DEVICE_END = 75;
const PROGRESS_COLLECTOR_SYNC_START = 77;
const PROGRESS_COLLECTOR_SYNC_END = 78;
const PROGRESS_VERSION_BEGIN = 80;
const PROGRESS_VERSION_END = 85;
const PROGRESS_COMPLETE = 100;
private string $old_cacti_version;
// Common variables
private int $mode = -1;
private int $stepCurrent;
private int $stepPrevious;
private int $stepNext;
private mixed $stepData = null;
private mixed $stepError = 0;
private string $output;
private array $templates;
private array $paths;
private string $theme;
private array $locales;
private string $language;
private array $tables;
private string $runtime;
private array $errors;
private int $profile;
private mixed $automationMode = null;
private mixed $automationOverride = null;
private mixed $buttonNext = null;
private mixed $buttonPrevious = null;
private mixed $buttonTest = null;
private array $iconClass;
private int $eula;
private int $cronInterval;
private array $defaultAutomation;
private array $permissions;
private array $extensions;
private array $modules;
private string $automationRange;
private array $snmpOptions;
private string $adminEmailAddress;
/**
* class Installer initialization
*
* usage:
*
* $installer = new Installer($installData)
*
* @param array $install_params - array of fields to update
*
*/
public function __construct(array $install_params = []) {
log_install_high('step', 'Install Parameters: ' . clean_up_lines(var_export($install_params, true)));
$this->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<string, int> 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<string, bool>
*/
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<string, mixed> $submitted
* @param array<int, string> $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<string, mixed> $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 <b>MUST</b> 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 <b>MUST</b> 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 <b>MUST</b> 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 <a target='_blank' href='https://dev.mysql.com/downloads/timezones.html'>Time zone description table</a>. 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 &gt; 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 <a href="%1$s">Unix</a> and <a href="%2$s">Win32</a>-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 <a href="%s">Upgrade</a> 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.') . '<br/><br/>' .
__('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 = '<select id=\'language\' name=\'language\'>';
foreach ($this->locales as $key => $value) {
$selected = '';
$langOutput .= PHP_EOL . $this->language . " == $key [$value]" . PHP_EOL;
if ($this->language == $key) {
$selected = ' selected';
}
$flags = explode('-', $key);
if (cacti_count($flags) > 1) {
$flagName = cacti_strtolower($flags[1]);
} else {
$flagName = cacti_strtolower($flags[0]);
}
$langOutput .= '<option value=\'' . $key . '\'' . $selected . ' data-class=\'fi-' . $flagName . '\'><span class="fi fis fi-' . $flagName . '"></span>' . $value . '</option>';
}
$langOutput .= '</select>';
$themePath = CACTI_PATH_INCLUDE . '/themes/';
$themes = glob($themePath . '*', GLOB_ONLYDIR);
$themeOutput = '<select id=\'theme\' name=\'theme\'>';
foreach (($themes ?: []) as $themeFolder) {
$theme = substr($themeFolder, strlen($themePath));
if (file_exists($themePath . $theme . '/main.css')) {
$selected = '';
if ($theme == $this->theme) {
$selected = ' selected';
}
$themeOutput .= '<option value=\'' . $theme . '\'' . $selected . '>' . ucfirst($theme) . '</option>';
}
}
$themeOutput .= '</select>';
$eula = ($this->eula) ? ' checked' : '';
$output .= Installer::sectionNormal('<span>' . __('Select default theme: ') . $themeOutput . '</span><span style=\'float: right\'><input type=\'checkbox\' id=\'accept\' name=\'accept\'' . $eula . '><label for=\'accept\'>' . __('Accept GPL License Agreement') . '</label></span><span>' . $langOutput . '</span>');
$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('<span class="textError"><strong>' . __('ERROR:') . '</strong> ' . __('Please update config.php with the correct relative URI location of Cacti (url_path).') . '</span>');
$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 = '<span title="' . __('The specified value appears to be different in the running config versus the INI file.') . '">' . __('Restart Required') . '</span>';
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("<font color='$status_font'>$status_text</font>", '');
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('<font color=green>' . __('Yes') . '</font>', '');
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('<font color=green>' . __('Yes') . '</font>', '');
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('<span class="textError"><strong>' . __('ERROR:') . '</strong> ' . __('Your MySQL TimeZone database is not populated. Please populate this database before proceeding.') . '</span>');
$enabled['mysql_timezone'] = DB_STATUS_ERROR;
}
} else {
$output .= Installer::sectionNormal('<span class="textError"><strong>' . __('ERROR:') . '</strong> ' . __('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.') . '</span>');
$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 <strong>%s</strong> to <strong>%s</strong>', $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, <a href=\'https://www.refreshyourcache.com\' target=\'_blank\'>https://www.refreshyourcache.com/</a> 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 <strong>%s</strong> to <strong>%s</strong>', $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 <i>could</i> 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(
'<ul>' .
'<li><b><i>' . __('New Primary Server') . '</i></b> - ' . __('Choose this for the Primary site.') . '</li>' .
'<li><b><i>' . __('New Remote Poller') . '</i></b> - ' . __('Remote Pollers are used to access networks that are not readily accessible to the Primary site.') . '</li>' .
'</ul>'
);
$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(
'<select id="install_type" name="install_type">' .
'<option value="1"' . $selectedInstall . '>' . __('New Primary Server') . '</option>' .
'<option value="2"' . $selectedPoller . '>' . __('New Remote Poller') . '</option>' .
'</select>'
);
$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: <b>%s</b>', $database_default) . '<br>' .
__('Database User: <b>%s</b>', $database_username) . '<br>' .
__('Database Hostname: <b>%s</b>', $database_hostname) . '<br>' .
__('Port: <b>%s</b>', $database_port) . '<br>' .
__('Server Operating System Type: <b>%s</b>', CACTI_SERVER_OS) . '<br>'
);
$output .= Installer::sectionSubTitleEnd();
$output .= Installer::sectionSubTitle(__('Central Database Connection Information'), 'connection_remote');
$output .= Installer::sectionCode(
__('Database: <b>%s</b>', $rdatabase_default) . '<br>' .
__('Database User: <b>%s</b>', $rdatabase_username) . '<br>' .
__('Database Hostname: <b>%s</b>', $rdatabase_hostname) . '<br>' .
__('Port: <b>%s</b>', $rdatabase_port) . '<br>' .
__('Server Operating System Type: <b>%s</b>', CACTI_SERVER_OS) . '<br>'
);
$output .= Installer::sectionSubTitleEnd();
$output .= Installer::sectionSubTitle(__('Configuration Readonly!'), 'error_file');
$output .= Installer::sectionNormal('<span class="textError"><strong>' . __('ERROR:') . '</strong> ' . __('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.') . '</span>');
$output .= Installer::sectionSubTitleEnd();
$output .= Installer::sectionSubTitle(__('Configuration of Poller'), 'error_poller');
$output .= Installer::sectionNormal('<span class="textError"><strong>' . __('ERROR:') . '</strong> ' . __('Your Remote Cacti Poller information has not been included in your config.php file. Please review the config.php.dist, and set the variables: <i>$rdatabase_default, $rdatabase_username</i>, etc. These variables must be set and point back to your Primary Cacti database server. Correct this and try again.') . '</span>', '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\';<br>' .
'$rdatabase_default = \'cacti\';<br>' .
'$rdatabase_hostname = \'cacti.example.com\'; // Central Cacti server.<br>' .
'$rdatabase_username = \'cactiuser\';<br>' .
'$rdatabase_password = \'cactiuser\';<br>' .
'$rdatabase_port = \'3306\';<br>' .
'$rdatabase_ssl = false;<br>'
);
$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 '<div class="cactiTable" style="width:100%;text-align:center">';
/**
* 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 "<div class='formRow $class'><div class='formColumnLeft'><div class='formFieldName'>" . $array['friendly_name'] . "<div class='formTooltip'><div class='cactiTooltipHint ti ti-help'><span style='display:none;'>" . $array['description'] . '</span></div></div></div></div>';
print "<div class='formColumnRight'><div class='formData'>";
$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 '</div></div>';
print '</div>';
$i++;
}
print '</div>';
$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 .= "<div class='formRow $class'><div class='formColumnLeft'><div class='formFieldName'>" . $path . '</div></div>';
$permissions .= "<div class='formColumnRight'><div class='formData' width='100%'>";
if ($valid) {
$permissions .=
'<i class="' . $this->iconClass[DB_STATUS_SUCCESS] . '"></i> ' .
'<font color="#008000">' . __('Writable') . '</font>';
} else {
$permissions .=
'<i class="' . $this->iconClass[DB_STATUS_ERROR] . '"></i> ' .
'<font color="#FF0000">' . __('Not Writable') . '</font>';
$writable = false;
$sections['writable_install'] = DB_STATUS_ERROR;
}
$permissions .= '</div></div></div>';
}
}
$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 .= "<div class='formRow $class'><div class='formColumnLeft'><div class='formFieldName'>" . $path . '</div></div>';
$permissions .= "<div class='formColumnRight'><div class='formData' width='100%'>";
if ($valid) {
$permissions .=
'<i class="' . $this->iconClass[DB_STATUS_SUCCESS] . '"></i> ' .
'<font color="#008000">' . __('Writable') . '</font>';
} else {
$permissions .=
'<i class="' . $this->iconClass[DB_STATUS_ERROR] . '"></i> ' .
'<font color="#FF0000">' . __('Not Writable') . '</font>';
$sections['writable_always'] = DB_STATUS_ERROR;
$writable = false;
}
$permissions .= '</div></div></div>';
}
$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<br />', $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('<span class="cactiInstallSectionCode" style="width: 95%; display: inline-flex;">' . $code . '</span>', '', '', __('EXAMPLE:'));
$output .= Installer::sectionNote(__('Once installation has completed the CSRF path, should be set to read-only.'));
} else {
$output .= Installer::sectionNormal('<font color="#008000">' . __('All folders are writable') . '</font>');
}
$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 <b>SELinux</b>, <b>AppArmor</b> or <b>ModSecurity</b> may cause Cacti to become inoperable in some configurations. If problems occur, check the settings of these security tools.<br/><br/>'));
$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(
'<ul>' .
'<li><b><i>php -q input_whitelist.php --audit</i></b> - ' . __('This script option will search for any Data Input Methods that are currently banned and provide details as to why.') . '</li>' .
'<li><b><i>php -q input_whitelist.php --update</i></b> - ' . __('This script option un-ban the Data Input Methods that are currently banned.') . '</li>' .
'<li><b><i>php -q input_whitelist.php --push</i></b> - ' . __('This script option will re-enable any disabled Data Sources.') . '</li>' .
'</ul>'
);
$output .= Installer::sectionNormal(__('It is strongly suggested that you update your config.php to enable this feature by uncommenting the <b>$input_whitelist</b> variable and then running the three CLI script options above after the web based install has completed.<br/><br/>'));
$output .= Installer::sectionNormal(__('Check the Checkbox below to acknowledge that you have read and understand this security concern'));
$output .= Installer::sectionNormal('<input type="checkbox" id="confirm" name="confirm"><label for="confirm">' . __('I have read this statement') . '</label>');
$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 = '<a href="' . $p['homepage'] . '" target="_new">' . $p['homepage'] . '</a>';
} 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]<br>' .
'character-set-server=utf8mb4<br>' .
'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.<br/>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 .= '<hr>';
$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('<input type="checkbox" id="confirm" name="confirm"><label for="confirm">' . $title);
$this->buttonNext->Text = $button;
$this->buttonNext->Enabled = false;
$this->buttonNext->Step = Installer::STEP_INSTALL;
return $output;
}
private function showInstallOptions() : string {
$output = '';
$output .= Installer::sectionTitle(__('General Settings'));
$output .= '<hr>';
$opt = array_rekey(
db_fetch_assoc('SELECT *
FROM settings
WHERE name LIKE "install_%"
AND name NOT LIKE "install_table_%"
AND name NOT LIKE "install_tp_%"'),
'name', 'value'
);
$output .= Installer::sectionNormal(__('The following General Install Options will be applied.'));
$output .= Installer::sectionNormal('<b>' . __('EULA') . '</b>: ' . __('GPL License Accepted'));
switch ($opt['install_mode']) {
case Installer::MODE_INSTALL:
$output .= Installer::sectionNormal('<b>' . __('Install Type') . '</b>: ' . __('New Install'));
break;
case Installer::MODE_UPGRADE:
$output .= Installer::sectionNormal('<b>' . __('Install Type') . '</b>: ' . __('Upgrade'));
break;
case Installer::MODE_DOWNGRADE:
$output .= Installer::sectionNormal('<b>' . __('Install Type') . '</b>: ' . __('Downgrade'));
break;
case Installer::MODE_POLLER:
$output .= Installer::sectionNormal('<b>' . __('Install Type') . '</b>: ' . __('Poller'));
break;
case Installer::MODE_NONE:
$output .= Installer::sectionNormal('<b>' . __('Install Type') . '</b>: ' . __('None'));
break;
default:
$output .= Installer::sectionNormal('<b>' . __('Install Type') . '</b>: ' . __('Unknown'));
break;
}
$topts = db_fetch_assoc('SELECT *
FROM settings
WHERE name LIKE "install_tp_%"
AND value != ""');
$taopts = db_fetch_assoc('SELECT *
FROM settings
WHERE name LIKE "install_table_%"');
$output .= Installer::sectionNormal('<b>' . __('RRDtool Version') . '</b>: ' . htmle($opt['install_rrdtool_version']));
$output .= Installer::sectionNormal('<b>' . __('Default Theme') . '</b>: ' . ucfirst(htmle($opt['install_theme'])));
$output .= Installer::sectionNormal('<b>' . __('Install Language') . '</b>: ' . htmle($opt['install_language']));
$profile = db_fetch_cell_prepared('SELECT name
FROM data_source_profiles
WHERE id = ?',
[$opt['install_profile']]);
$output .= Installer::sectionNormal('<b>' . __('Default Poller Interval') . '</b>: ' . htmle($profile));
if ($opt['install_mode'] == Installer::MODE_INSTALL) {
$output .= Installer::sectionTitle(__('Device/Graph Automation'));
$output .= '<hr>';
$output .= Installer::sectionNormal(__('The following Device and Graph Automation rules will be applied. These rules allow you to quickly setup monitoring of your network or networks.'));
if (isset($opt['install_automation_mode']) && $opt['install_automation_mode'] == 1) {
$output .= Installer::sectionNormal('<b>' . __('Automation Mode') . '</b>: ' . __('Automation Is Enabled'));
} else {
$output .= Installer::sectionNormal('<b>' . __('Automation Mode') . '</b>: ' . __('Automation Is Disabled'));
}
if (isset($opt['install_automation_range'])) {
$output .= Installer::sectionNormal('<b>' . __('IP Address Range') . '</b>: ' . htmle($opt['install_automation_range']));
}
if (isset($opt['install_automation_override'])) {
$output .= Installer::sectionNormal('<b>' . __('SNMP Options') . '</b>: ' . __('Using a Custom SNMP Options'));
} else {
$output .= Installer::sectionNormal('<b>' . __('SNMP Options') . '</b>: ' . __('Using the Default SNMP Options'));
}
}
$output .= Installer::sectionTitle(__('Device Packages'));
$output .= '<hr>';
if (cacti_sizeof($topts)) {
$output .= Installer::sectionNormal('<b>' . __('Device Packages') . '</b>: ' . __('%d Device Packages to be Installed', cacti_sizeof($topts)));
$output .= Installer::sectionNormal(__('The following Device Packages will be Installed or Upgraded'));
foreach ($topts as $o) {
$output .= Installer::sectionNormal('<b>' . __('Package:') . '</b>: ' . str_replace(['.xml.gz', '_'], '', $o['value']) . '</b>');
}
} else {
$output .= Installer::sectionNormal('<b>' . __('Device Packages') . '</b>: ' . __('No Device Packages to be Installed') . '</b>');
}
$output .= Installer::sectionTitle(__('Table Upgrades'));
$output .= '<hr>';
if (cacti_sizeof($taopts)) {
$output .= Installer::sectionNormal('<b>' . __('Table Upgrades') . '</b>: ' . __('%d Tables to be Upgraded', cacti_sizeof($taopts)));
$output .= Installer::sectionNormal(__('The following Tables will be Upgraded to InnoDB and Converted to utf8mb4 for performance and internationalization.'));
foreach ($taopts as $o) {
$output .= Installer::sectionNormal('<b>' . __('Table:') . '</b>: ' . htmle($o['value']) . '</b>');
}
} else {
$output .= Installer::sectionNormal('<b>' . __('Table Upgrades') . '</b>: ' . __('No Tables to be Upgraded') . '</b>');
}
$output .= '<hr>';
return $output;
}
public function processStepInstall() : string {
$time = intval(read_config_option('install_updated', true));
$output = Installer::sectionTitle(__('Installing Cacti Server v%s', CACTI_VERSION_FULL));
$output .= Installer::sectionNormal(__('Your Cacti Server is now installing'));
$output .= Installer::sectionNormal(
'<table width="100%"><tr>' .
'<td class="cactiInstallProgressLeft">Refresh in</td>' .
'<td class="cactiInstallProgressCenter">&nbsp;</td>' .
'<td class="cactiInstallProgressRight">Progress<span style=\'float:right\'>Last updated: ' . date('H:i:s', intval($time)) . '</span></td>' .
'</tr><tr>' .
'<td class="cactiInstallProgressLeft">' .
'<div id="cactiInstallProgressCountdown"><div></div></div>' .
'</td>' .
'<td class="cactiInstallProgressCenter">&nbsp;</td>' .
'<td class="cactiInstallProgressRight">' .
'<div id="cactiInstallProgressBar"><div></div></div>' .
'</td>' .
'</tr></table>'
);
$backgroundTime = read_config_option('install_started', true);
if (empty($backgroundTime)) {
$backgroundTime = false;
}
$backgroundLast = read_config_option('install_updated', true);
if (empty($backgroundLast)) {
$backgroundLast = false;
}
$backgroundNeeded = $backgroundTime === false;
if ($backgroundTime === false) {
$backgroundTime = microtime(true);
set_install_config_option('install_started', $backgroundTime);
}
log_install_debug('background', 'backgroundTime = ' . $backgroundTime, false);
log_install_debug('background', 'backgroundNeeded = ' . $backgroundNeeded);
// Check if background started too long ago
if (!$backgroundNeeded) {
log_install_debug('background', PHP_EOL . '----------------' . PHP_EOL . 'Check Expire' . PHP_EOL . '----------------');
$backgroundDateStarted = Installer::dateFromMicrotime($backgroundTime);
$backgroundLast = read_config_option('install_updated', true);
log_install_debug('background', 'backgroundDateStarted = ' . $backgroundDateStarted->format('Y-m-d H:i:s') . PHP_EOL);
log_install_debug('background', 'backgroundLast = ' . $backgroundTime);
if (empty($backgroundLast) || $backgroundLast < $backgroundTime) {
log_install_high('background', 'backgroundLast = ' . $backgroundTime . ' (Replaced)');
$backgroundLast = $backgroundTime;
}
$backgroundExpire = time() - 1500;
log_install_debug('background', 'backgroundExpire = ' . $backgroundExpire);
if ($backgroundLast < $backgroundExpire) {
$newTime = microtime(true);
set_install_config_option('install_started', $newTime);
set_install_config_option('install_updated', $newTime);
$backgroundTime = read_config_option('install_started', true);
if (empty($backgroundTime)) {
$backgroundTime = false;
}
$backgroundLast = read_config_option('install_updated', true);
if (empty($backgroundLast)) {
$backgroundLast = false;
}
$backgroundNeeded = ("$newTime" == "$backgroundTime");
log_install_debug('background', PHP_EOL . '=======' . PHP_EOL . 'Expired' . PHP_EOL . '=======' . PHP_EOL);
log_install_debug('background', ' newTime = ' . $newTime);
log_install_debug('background', ' backgroundTime = ' . $backgroundTime);
log_install_debug('background', ' backgroundLast = ' . $backgroundLast);
log_install_debug('background', 'backgroundNeeded = ' . $backgroundNeeded);
}
}
if ($backgroundNeeded) {
$php = cacti_escapeshellcmd((string) read_config_option('path_php_binary', true));
$php_file = cacti_escapeshellarg(CACTI_PATH_INSTALL . '/background.php') . ' ' . $backgroundTime;
log_install_always('', __('Spawning background process: %s %s', $php, $php_file));
exec_background($php, $php_file);
}
$output .= Installer::getInstallLog();
$this->buttonNext->Visible = false;
$this->buttonPrevious->Visible = false;
$progressCurrent = read_config_option('install_progress', true);
if (empty($progressCurrent)) {
$progressCurrent = Installer::PROGRESS_NONE;
}
$stepData = ['Current' => $progressCurrent, 'Total' => Installer::PROGRESS_COMPLETE];
$this->stepData = $stepData;
return $output;
}
public function processStepComplete() : string {
global $cacti_version_codes, $database_statuses;
$cacheFile = read_config_option('install_cache_db', true);
$output = '';
if ($this->stepCurrent == Installer::STEP_COMPLETE) {
$output = Installer::sectionTitle(__('Complete'));
$output .= Installer::sectionNormal(__('Your Cacti Server v%s has been installed/updated. You may now start using the software.', CACTI_VERSION_FULL));
db_execute('DELETE FROM settings WHERE name LIKE "install_%"');
// Remove integrated plugin references only after a successful install.
api_plugin_uninstall_integrated();
} elseif ($this->stepCurrent == Installer::STEP_ERROR) {
$output = Installer::sectionTitleError();
$output .= Installer::sectionNormal(__('Your Cacti Server v%s has been installed/updated with errors', CACTI_VERSION_BRIEF_FULL));
}
$output .= Installer::sectionSubTitleEnd();
$sections = [];
clearstatcache();
if ((!empty($cacheFile)) && (is_file($cacheFile))) {
$cacti_versions = array_keys($cacti_version_codes);
$sql_text = $database_statuses;
$sqlclass = [
DB_STATUS_ERROR => 'cactiInstallSqlFailure',
DB_STATUS_WARNING => 'cactiInstallSqlWarning',
DB_STATUS_SUCCESS => 'cactiInstallSqlSuccess',
DB_STATUS_SKIPPED => 'cactiInstallSqlSkipped',
];
$file = fopen($cacheFile, 'r');
$sectionId = null;
$sectionStatus = null;
if ($file !== false) {
$version_last = '';
$line = 0;
while (!feof($file)) {
$line++;
$change = fgets($file);
if (empty($change)) {
break;
}
$action = preg_split('~[ ]*<\[(version|status|sql|error)\]>[ ]*~i', $change);
if (empty($action) || cacti_sizeof($action) != 5) {
log_install_medium('upgrade', $cacheFile . '[' . $line . ']: Read unexpected change - ' . cacti_sizeof($action) . ' - \'' . clean_up_lines(var_export($change, true)) . '\'');
} else {
$version = $action[1];
if (!empty($version)) {
if ($version != $version_last) {
$version_last = $version;
if (!empty($sectionId)) {
$sections[$sectionId] = $sectionStatus;
$output .= '</table>';
$output .= $this->sectionSubTitleEnd();
}
$sectionId = str_replace('.', '_', $version);
$output .= $this->sectionSubTitle('Database Upgrade - Version ' . $version, $sectionId);
$output .= $this->sectionNormal('The following table lists the status of each upgrade performed on the database');
$output .= '<table class=\'cactiInstallSqlResults\'>';
$sectionStatus = DB_STATUS_SKIPPED;
}
// show results from version upgrade
$sql_temp = $action[3];
if (isset($sqlclass[$action[2]])) {
$cssClass = $sqlclass[$action[2]];
} else {
$cssClass = $action[2];
}
$dbIcon = '';
if (isset($this->iconClass[$action[2]])) {
$dbIcon = $this->iconClass[$action[2]];
}
if (isset($database_statuses[$action[2]])) {
$dbStatus = $database_statuses[$action[2]];
} else {
$dbStatus = 'Unknown ' . $action[2];
}
$output .= '<tr class=\'cactiInstallSqlRow\'>';
$output .= '<td class=\'cactiInstallSqlIcon ' . $cssClass . '\' width=\'50\'><i class=\'' . $dbIcon . '\'></i></td>';
$output .= '<td class=\'cactiInstallSqlLeft\'>' . html_escape($sql_temp) . '</td>';
$output .= '<td class=\'cactiInstallSqlRight ' . $cssClass . '\'>' . $dbStatus . '</td>';
$output .= '</tr>';
// set sql failure if status set to zero on any action
if ($action[2] < $sectionStatus) {
$sectionStatus = $action[2];
}
}
}
}
if (!empty($sectionId)) {
$output .= '</table>';
$sections[$sectionId] = $sectionStatus;
}
$output .= Installer::sectionSubTitleEnd();
fclose($file);
}
}
$output .= $this->sectionSubTitle('Process Log');
$output .= Installer::getInstallLog();
$this->buttonPrevious->Visible = false;
$this->buttonNext->Enabled = true;
$this->stepData = ['Sections' => $sections];
if ($this->stepCurrent == Installer::STEP_COMPLETE && is_string($cacheFile) && is_file($cacheFile)) {
unlink($cacheFile);
}
if ($this->stepCurrent == Installer::STEP_ERROR) {
$this->buttonPrevious->Text = __('Get Help');
$this->buttonPrevious->Step = Installer::STEP_GO_FORUMS;
$this->buttonPrevious->Visible = true;
$this->buttonPrevious->Enabled = true;
$this->buttonNext->Text = __('Report Issue');
$this->buttonNext->Step = Installer::STEP_GO_GITHUB;
} else {
$this->buttonNext->Text = __('Get Started');
$this->buttonNext->Step = Installer::STEP_GO_SITE;
}
return $output;
}
// The following functions perform the leg work for installation
/**
* refreshVendorDependencies() - runs composer install when the configured
* composer binary reports the committed lock file is ahead of the
* include/vendor tree. A missing or failing composer is never fatal:
* the vendor tree was complete enough to boot the installer, so the
* outcome is logged and the install continues.
*
* @return void
*/
private function refreshVendorDependencies() : void {
$composer = read_config_option('path_composer', true);
if (empty($composer) || !file_exists($composer)) {
return;
}
// composer refuses to run without a home for its cache
if (getenv('COMPOSER_HOME') == '' && getenv('HOME') == '') {
putenv('COMPOSER_HOME=' . sys_get_temp_dir() . '/cacti-composer');
}
$command = [$composer, 'install', '--no-interaction', '--working-dir=' . CACTI_PATH_BASE];
$result = $this->runCommand([...$command, '--dry-run']);
$check = implode(PHP_EOL, $result['output']);
if ($result['exitCode'] !== 0) {
log_install_always('', __('WARNING: Composer could not be executed. Run \'composer install\' from the Cacti directory manually and check its output.'));
return;
}
if (str_contains((string) $check, 'Nothing to install, update or remove')) {
log_install_always('', __('Composer dependencies are current, no vendor refresh needed.'));
return;
}
/* only a dry-run that lists package operations proves the vendor tree
is stale; any other output means composer itself has a problem */
if (!preg_match('/Package operations: (\d+) installs?, (\d+) updates?, (\d+) removals?/', $check, $ops)) {
log_install_always('', __('WARNING: Composer could not determine the vendor state. Run \'composer install\' from the Cacti directory manually and check its output.'));
return;
}
/* a checkout without vendor produces installs only. Updates or
removals mean the committed tree differs from the lock file; that
is a repository question, not something to change during install */
if ($ops[1] == 0 || $ops[2] > 0 || $ops[3] > 0) {
log_install_always('', __('NOTE: Composer reports the vendor state as \'%s\'. No refresh was attempted.', trim($ops[0])));
return;
}
// composer writes into include/vendor, or creates it under include/
$vendor_dir = CACTI_PATH_INCLUDE . '/vendor';
$write_path = is_dir($vendor_dir) ? $vendor_dir . '/' : CACTI_PATH_INCLUDE . '/';
if (!is_resource_writable($write_path)) {
log_install_always('', __('WARNING: Composer reports include/vendor is missing packages, but it is not writable by the web server. Run \'composer install\' from the Cacti directory manually.'));
return;
}
log_install_always('', __('Refreshing Composer dependencies in include/vendor.'));
$result = $this->runCommand([...$command, '--no-progress']);
if ($result['output'] !== []) {
log_install_debug('composer', implode(PHP_EOL, $result['output']));
}
if ($result['exitCode'] !== 0 || !is_file(CACTI_PATH_INCLUDE . '/vendor/autoload.php')) {
log_install_always('', __('WARNING: Composer refresh did not complete. Run \'composer install\' from the Cacti directory manually.'));
} else {
log_install_always('', __('Composer dependencies refreshed.'));
}
}
private function install() : void {
$failure = $this->mode == Installer::MODE_UPGRADE ? '' : $this->validateRequiredSchema();
switch ($this->mode) {
case Installer::MODE_UPGRADE:
$which = 'UPGRADE';
break;
case Installer::MODE_DOWNGRADE:
$which = 'DOWNGRADE';
break;
default:
$which = 'INSTALL';
break;
}
log_install_always('', __('Starting %s Process for v%s', $which, CACTI_VERSION_FULL));
$this->setProgress(Installer::PROGRESS_START);
if ($failure === '') {
$this->setCSRFSecret();
$this->refreshVendorDependencies();
$failure = $this->convertDatabase();
if ($failure === '') {
if ($this->mode == Installer::MODE_POLLER) {
$failure = $this->installPoller();
} else {
if ($this->mode == Installer::MODE_INSTALL) {
$failure = $this->installTemplate();
if ($failure === '') {
$failure = $this->installServer();
}
} elseif ($this->mode == Installer::MODE_UPGRADE) {
$failure = $this->upgradeDatabase();
if ($failure === '') {
$failure = $this->installTemplate();
}
}
Installer::disableInvalidPlugins();
}
}
}
if ($failure === '') {
$this->setDefaultTemplate();
$failure = $this->validateRequiredSchema();
}
log_install_always('', __('Finished %s Process for v%s', $which, CACTI_VERSION));
set_install_config_option('install_error', $failure);
if ($failure === '') {
// No failures so lets update the version
$this->setProgress(Installer::PROGRESS_VERSION_BEGIN);
if (!$this->writeInstalledVersion()) {
$failure = __('Unable to persist and verify the installed Cacti version');
set_install_config_option('install_error', $failure);
} else {
set_install_config_option('install_version', CACTI_VERSION);
$this->setProgress(Installer::PROGRESS_VERSION_END);
}
}
if ($failure === '') {
// Sync the remote data collectors
if ($this->mode != Installer::MODE_POLLER) {
$this->setProgress(Installer::PROGRESS_COLLECTOR_SYNC_START);
Installer::fullSyncDataCollectors();
$this->setProgress(Installer::PROGRESS_COLLECTOR_SYNC_END);
}
$this->setProgress(Installer::PROGRESS_COMPLETE);
$this->setStep(Installer::STEP_COMPLETE);
} else {
log_install_always('', $failure);
$this->setProgress(Installer::PROGRESS_COMPLETE);
$this->setStep(Installer::STEP_ERROR);
}
}
/**
* Verify that every table declared by cacti.sql exists in the active schema.
*
* @return string An empty string on success, otherwise a user-facing error.
*/
private function validateRequiredSchema() : string {
$requiredTables = get_cacti_base_tables();
$tableRows = db_fetch_assoc('SELECT TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()');
$actualTables = array_column($tableRows, 'TABLE_NAME');
$missingTables = array_values(array_diff($requiredTables, $actualTables));
if ($missingTables === []) {
return '';
}
$failure = __('The Cacti database schema is incomplete. Missing required tables: %s', implode(', ', $missingTables));
log_install_always('', $failure);
$this->addError(Installer::STEP_ERROR, 'Schema', 'RequiredTables', $failure);
return $failure;
}
/**
* Persist the final version as one row and verify the database handoff.
*/
private function writeInstalledVersion() : bool {
if (!db_execute('START TRANSACTION')) {
return false;
}
if (!db_execute('DELETE FROM version') ||
!db_execute_prepared('INSERT INTO version (cacti) VALUES (?)', [CACTI_VERSION]) ||
!db_execute('COMMIT')
) {
db_execute('ROLLBACK');
return false;
}
$version = db_fetch_row('SELECT COUNT(*) AS row_count, MAX(cacti) AS cacti FROM version');
return $version['cacti'] === CACTI_VERSION && (int) $version['row_count'] === 1;
}
private function installTemplate() : string {
$failure = '';
$templates = db_fetch_assoc("SELECT value
FROM settings
WHERE name LIKE 'install_tp_%'
AND value <> ''");
$this->setProgress(Installer::PROGRESS_TEMPLATES_BEGIN);
if (cacti_sizeof($templates)) {
log_install_always('', __('Found %s templates to install', cacti_sizeof($templates)));
$path = CACTI_PATH_INSTALL . '/templates/';
$i = 0;
foreach ($templates as $template) {
$i++;
$result = false;
$package = $template['value'];
log_install_always('', __('About to import Package #%s \'%s\'.', $i, $package));
if (!empty($package)) {
set_install_config_option('install_updated', microtime(true));
$result = import_package($path . $package, $this->profile, false, false, false, false, true, [], []);
if ($this->packageImportSucceeded($result)) {
log_install_always('', __('Import of Package #%s \'%s\' under Profile \'%s\' succeeded', $i, $package, $this->profile));
$this->setProgress(Installer::PROGRESS_TEMPLATES_BEGIN + $i);
}
}
if (!$this->packageImportSucceeded($result)) {
log_install_always('', __('Import of Package #%s \'%s\' under Profile \'%s\' failed', $i, $package, $this->profile));
$this->addError(Installer::STEP_ERROR, 'Package:' . $package, 'FAIL: XML version code error');
$failure = __('One or more template packages failed to import');
}
}
// Repair automation rules if broken
repair_automation();
$template_ids = $this->getDefaultAutomationTemplateIds();
$mapped_ids = [];
if (cacti_sizeof($template_ids)) {
$host_template_ids = array_values($template_ids);
$placeholders = implode(', ', array_fill(0, cacti_sizeof($host_template_ids), '?'));
$mapped_ids = array_rekey(
db_fetch_assoc_prepared("SELECT host_template
FROM automation_templates
WHERE host_template IN ($placeholders)",
$host_template_ids
),
'host_template',
'host_template'
);
}
foreach ($this->defaultAutomation as $item) {
$host_template_id = $template_ids[$item['hash']] ?? 0;
if (!empty($host_template_id)) {
log_install_always('', __('Mapping Automation Template for Device Template \'%s\'', $item['name']));
if (!isset($mapped_ids[$host_template_id])) {
db_execute_prepared('INSERT INTO automation_templates
(host_template, availability_method, sysDescr, sysName, sysOid, sequence)
VALUES (?, ?, ?, ?, ?, ?)',
[
$host_template_id, $item['availMethod'], $item['sysDescrMatch'],
$item['sysNameMatch'], $item['sysOidMatch'], $item['sequence']
]
);
}
}
}
} else {
log_install_always('', __('No templates were selected for import'));
}
$this->setProgress(Installer::PROGRESS_TEMPLATES_END);
return $failure;
}
/**
* Validate both the XML import and every package file handoff.
*/
private function packageImportSucceeded(mixed $result) : bool {
if (!is_array($result) || !array_key_exists(1, $result) || !is_array($result[1])) {
return false;
}
foreach ($result[1] as $status) {
if ($status !== __('written')) {
return false;
}
}
return true;
}
private function installPoller() : string {
log_install_always('', __('Updating remote configuration file'));
global $local_db_cnn_id;
$failure = remote_update_config_file();
if ($failure == '') {
// change cacti version
db_execute('DELETE FROM version', true, $local_db_cnn_id);
db_execute("INSERT INTO version (cacti) VALUES ('" . CACTI_VERSION_FULL . "')", true, $local_db_cnn_id);
// make the poller and poller_output_boost InnoDB
db_execute('ALTER TABLE poller_output ENGINE=InnoDB');
db_execute('ALTER TABLE poller_output_boost ENGINE=InnoDB');
}
return $failure;
}
private function installServer() : string {
$this->setProgress(Installer::PROGRESS_PROFILE_START);
$profile_id = intval($this->profile);
$profile = db_fetch_row_prepared('SELECT id, name, step, heartbeat
FROM data_source_profiles
WHERE id = ?',
[$profile_id]
);
log_install_high('automation', "Profile ID: $profile_id (" . $this->profile . ') returned ' . clean_up_lines(var_export($profile, true)));
if (!is_array($profile) || !isset($profile['id'], $profile['name'], $profile['step'], $profile['heartbeat'])) {
return __('Failed to find selected profile (%s)', $profile_id);
}
if ((int) $profile['id'] == $profile_id) {
log_install_always('automation', __('Setting default data source profile to %s (%s)', $profile['name'], $profile['id']));
$this->setProgress(Installer::PROGRESS_PROFILE_DEFAULT);
$profileUpdated = db_execute('UPDATE data_source_profiles
SET `default` = ""') &&
db_execute_prepared('UPDATE data_source_profiles
SET `default` = \'on\'
WHERE `id` = ?',
[$profile['id']]
) &&
db_execute_prepared('UPDATE data_template_data
SET rrd_step = ?, data_source_profile_id = ?',
[$profile['step'], $profile['id']]
) &&
db_execute_prepared('UPDATE data_template_rrd
SET rrd_heartbeat = ?',
[$profile['heartbeat']]
);
if (!$profileUpdated) {
return __('Unable to apply the selected data source profile');
}
$this->setProgress(Installer::PROGRESS_PROFILE_POLLER);
set_install_config_option('poller_interval', $profile['step']);
}
$this->setProgress(Installer::PROGRESS_PROFILE_END);
$this->setProgress(Installer::PROGRESS_AUTOMATION_START);
$automation_row = db_fetch_row('SELECT id, enabled, subnet_range FROM automation_networks ORDER BY id LIMIT 1');
log_install_debug('automation', 'Automation Row:' . clean_up_lines(var_export($automation_row, true)));
if (!empty($automation_row)) {
log_install_always('', __(
'Updating automation network (%s), mode "%s" => "%s", subnet "%s" => %s"',
$automation_row['id'],
$automation_row['enabled'],
$this->automationMode ? 'on' : '',
$automation_row['subnet_range'],
$this->automationRange
));
db_execute_prepared('UPDATE automation_networks SET
subnet_range = ?,
enabled = ?
WHERE id = ?',
[$this->automationRange, ($this->automationMode ? 'on' : ''), $automation_row['id']]
);
} else {
log_install_always('', __('Failed to find automation network, no changes were made'));
}
if ($this->automationOverride) {
log_install_always('', __('Adding extra snmp settings for automation'));
$snmp_options = db_fetch_assoc('select name, value from settings where name like \'install_snmp_option_%\'');
$snmp_id = db_fetch_cell('select id from automation_snmp_items limit 1');
if ($snmp_id) {
log_install_always('', __('Selecting Automation Option Set %s', $snmp_id));
$save = ['id' => '', 'snmp_id' => $snmp_id];
foreach ($snmp_options as $snmp_option) {
$snmp_name = str_replace('install_snmp_option_', '', $snmp_option['name']);
$snmp_value = $snmp_option['value'];
if ($snmp_name != 'snmp_security_level') {
$save[$snmp_name] = $snmp_value;
set_install_config_option($snmp_name, $snmp_value);
}
}
log_install_always('', __('Updating Automation Option Set %s', $snmp_id));
$item_id = sql_save($save, 'automation_snmp_items');
if ($item_id) {
log_install_always('', __('Successfully updated Automation Option Set %s', $snmp_id));
log_install_always('', __('Resequencing Automation Option Set %s', $snmp_id));
db_execute_prepared('UPDATE automation_snmp_items
SET sequence = sequence + 1
WHERE snmp_id = ?',
[$snmp_id]
);
} else {
log_install_always('', __('Failed to updated Automation Option Set %s', $snmp_id));
}
} else {
log_install_always('', __('Failed to find any automation option set'));
}
}
$this->setProgress(Installer::PROGRESS_AUTOMATION_END);
$this->setProgress(Installer::PROGRESS_DEVICE_START);
// Add the correct device type
if (CACTI_SERVER_OS == 'win32') {
$hash = '5b8300be607dce4f030b026a381b91cd';
$version = 2;
$community = 'public';
$avail = 'snmp';
$ip = 'localhost';
$description = 'Local Windows Machine';
} else {
$hash = '2d3e47f416738c2d22c87c40218cc55e';
$version = 0;
$community = 'public';
$avail = 'none';
$ip = 'localhost';
$description = 'Local Linux Machine';
}
$host_template_id = db_fetch_cell_prepared('SELECT id FROM host_template WHERE hash = ?', [$hash]);
// Add the host
if (!empty($host_template_id)) {
$this->setProgress(Installer::PROGRESS_DEVICE_TEMPLATE);
log_install_always('', __('Device Template for First Cacti Device is %s', $host_template_id));
$result = $this->runCliCommand(CACTI_PATH_CLI . '/add_device.php', [
'--description=' . $description,
'--ip=' . $ip,
'--template=' . $host_template_id,
'--notes=Initial Cacti Device',
'--poller=1',
'--site=0',
'--avail=' . $avail,
'--version=' . $version,
'--community=' . $community,
]);
if ($result['exitCode'] != 0) {
log_install_always('', __('WARNING: Default Device Failed to be Added error to follow.'));
} else {
log_install_always('', __('Default Device Added to Cacti.'));
}
if ($result['output'] !== []) {
foreach ($result['output'] as $l) {
log_install_always('', __('Output: %s.', $l));
}
}
$host_id = db_fetch_cell_prepared('SELECT id
FROM host
WHERE host_template_id = ?
LIMIT 1',
[$host_template_id]
);
if ($result['exitCode'] != 0 && empty($host_id)) {
return __('The default device could not be created');
}
if (!empty($host_id)) {
$this->setProgress(Installer::PROGRESS_DEVICE_GRAPH);
$templates = db_fetch_assoc_prepared('SELECT *
FROM host_graph
WHERE host_id = ?',
[$host_id]
);
if (cacti_sizeof($templates)) {
log_install_always('', __('Creating Graphs for Default Device'));
foreach ($templates as $template) {
set_install_config_option('install_updated', microtime(true));
automation_execute_graph_template($host_id, $template['graph_template_id']);
}
$this->setProgress(Installer::PROGRESS_DEVICE_TREE);
log_install_always('', __('Adding Device to Default Tree'));
$treeResult = $this->runCliCommand(CACTI_PATH_CLI . '/add_tree.php', [
'--type=node',
'--node-type=host',
'--tree-id=1',
'--host-id=' . $host_id,
]);
if ($treeResult['exitCode'] != 0) {
log_install_always('', __('WARNING: The default device could not be added to the default tree.'));
}
} else {
log_install_always('', __('No templated graphs for Default Device were found'));
}
}
} else {
log_install_always('', __('WARNING: Device Template for your Operating System Not Found. You will need to import Device Templates or Cacti Packages to monitor your Cacti server.'));
}
// just in case we have hard drive graphs to deal with
$host_id = db_fetch_cell("SELECT id FROM host WHERE hostname='127.0.0.1'");
if (!empty($host_id)) {
log_install_always('', __('Running first-time data query for local host'));
run_data_query($host_id, 6);
}
/* it's always a good idea to re-populate
* the poller cache to make sure everything
* is refreshed and up-to-date */
set_install_config_option('install_updated', microtime(true));
log_install_always('', __('Repopulating poller cache'));
repopulate_poller_cache();
// fill up the snmpcache
set_install_config_option('install_updated', microtime(true));
log_install_always('', __('Repopulating SNMP Agent cache'));
snmpagent_cache_rebuilt();
// generate RSA key pair
set_install_config_option('install_updated', microtime(true));
log_install_always('', __('Generating RSA Key Pair'));
rsa_check_keypair();
$this->setProgress(Installer::PROGRESS_DEVICE_END);
return '';
}
/**
* Run a Cacti CLI script without invoking a command shell.
*
* @param array<int, string> $arguments
*
* @return array{exitCode: int, output: array<int, string>}
*/
private function runCliCommand(string $script, array $arguments = [], ?string $phpBinary = null) : array {
$phpBinary ??= (string) read_config_option('path_php_binary');
if ($phpBinary === '' || !is_file($phpBinary) || !is_executable($phpBinary) || !is_file($script)) {
return ['exitCode' => 127, 'output' => [__('Unable to execute the configured PHP binary or CLI script')]];
}
return $this->runCommand([$phpBinary, '-q', $script, ...$arguments]);
}
/**
* @param list<string> $command
* @return array{exitCode: int, output: array<int, string>}
*/
private function runCommand(array $command) : array {
$pipes = [];
$errorFile = tmpfile();
if ($errorFile === false) {
return ['exitCode' => 127, 'output' => [__('Unable to create a temporary file for CLI errors')]];
}
$process = proc_open(
$command,
[
1 => ['pipe', 'w'],
2 => $errorFile,
],
$pipes,
null,
null,
['bypass_shell' => true]
);
if (!is_resource($process)) {
fclose($errorFile);
return ['exitCode' => 127, 'output' => [__('Unable to start the Cacti CLI process')]];
}
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$exitCode = proc_close($process);
rewind($errorFile);
$errors = stream_get_contents($errorFile);
fclose($errorFile);
$combinedOutput = ($output === false ? '' : $output) . ($errors === false ? '' : $errors);
$lines = preg_split('/\R/', trim($combinedOutput));
return [
'exitCode' => $exitCode,
'output' => is_array($lines) ? array_values(array_filter($lines, static fn (string $line) : bool => $line !== '')) : [],
];
}
private function convertDatabase() : string {
$tables = db_fetch_assoc("SELECT name, value FROM settings WHERE name like 'install_table_%'");
$failures = [];
if (cacti_sizeof($tables)) {
log_install_always('', __('Found %s tables to convert', cacti_sizeof($tables)));
$this->setProgress(Installer::PROGRESS_TABLES_BEGIN);
$i = 0;
foreach ($tables as $table) {
$i++;
$name = $table['value'];
if (!empty($name)) {
log_install_always('', __('Converting Table #%s \'%s\'', $i, $name), true);
$result = $this->runCliCommand(CACTI_PATH_CLI . '/convert_tables.php', [
'--table=' . $name,
'--utf8',
'--innodb',
'--dynamic',
]);
$results = implode(PHP_EOL, $result['output']);
set_install_config_option('install_updated', microtime(true));
log_install_debug('convert', sprintf('Convert table #%s \'%s\' results: %s', $i, $name, $results), true);
if ($result['exitCode'] === 0) {
set_install_config_option($table['name'], '');
} else {
$failures[] = $name;
log_install_always('', __('Conversion failed for table \'%s\' with exit code %s', $name, $result['exitCode']));
}
}
}
} else {
log_install_always('', __('No tables were found or selected for conversion'));
}
return $failures === []
? ''
: __('One or more database tables failed conversion: %s', implode(', ', $failures));
}
private function upgradeDatabase() : string {
global $cacti_version_codes, $cacti_upgrade_version, $database_statuses, $database_upgrade_status;
$failure = DB_STATUS_SKIPPED;
$cachePrev = read_config_option('install_cache_db', true);
$cacheFile = tempnam(sys_get_temp_dir(), 'cdu');
if ($cacheFile === false) {
return __('Unable to create the database-upgrade status file');
}
log_install_always('', __('Switched from %s to %s', $cachePrev, $cacheFile));
set_install_config_option('install_cache_db', $cacheFile);
$database_upgrade_status = ['file' => $cacheFile];
log_install_always('', __('NOTE: Using temporary file for db cache: %s', $cacheFile));
$prev_cacti_version = format_cacti_version($this->old_cacti_version, CACTI_VERSION_FORMAT_SHORT);
$orig_cacti_version = format_cacti_version(get_cacti_db_version(), CACTI_VERSION_FORMAT_SHORT);
// loop through versions from old version to the current, performing updates for each version in the chain
foreach ($cacti_version_codes as $cacti_upgrade_version => $hash_code) {
// skip versions old than the database version
if (cacti_version_compare($this->old_cacti_version, $cacti_upgrade_version, '>=')) {
// log_install_always('', 'Skipping v' . $cacti_upgrade_version . ' upgrade');
continue;
}
// log_install_always('', 'Checking v' . $cacti_upgrade_version . ' upgrade routines');
// construct version upgrade include path
$upgrade_file = CACTI_PATH_INSTALL . '/upgrades/' . str_replace('.', '_', $cacti_upgrade_version) . '.php';
$upgrade_function = 'upgrade_to_' . str_replace('.', '_', $cacti_upgrade_version);
// check for upgrade version file, then include, check for function and execute
$ver_status = DB_STATUS_SKIPPED;
if (file_exists($upgrade_file)) {
log_install_always('', __('Upgrading from v%s (DB %s) to v%s', $prev_cacti_version, $orig_cacti_version, $cacti_upgrade_version));
include_once($upgrade_file);
if (function_exists($upgrade_function)) {
call_user_func($upgrade_function);
print PHP_EOL;
$ver_status = $this->checkDatabaseUpgrade($cacti_upgrade_version);
} else {
log_install_always('', __('WARNING: Failed to find upgrade function for v%s', $cacti_upgrade_version));
$ver_status = DB_STATUS_WARNING;
}
// Only update database version if database successfully upgraded
if ($ver_status == DB_STATUS_SUCCESS || $ver_status == DB_STATUS_SKIPPED) {
if (cacti_version_compare($orig_cacti_version, $cacti_upgrade_version, '<')) {
if (!db_execute_prepared('UPDATE version SET cacti = ?', [$cacti_upgrade_version])) {
$ver_status = DB_STATUS_ERROR;
} else {
$orig_cacti_version = $cacti_upgrade_version;
}
}
if ($ver_status != DB_STATUS_ERROR) {
$prev_cacti_version = $cacti_upgrade_version;
}
}
}
if ($failure > $ver_status) {
$failure = $ver_status;
}
if ($failure <= DB_STATUS_WARNING) {
break;
}
}
set_install_config_option('install_cache_result', $failure);
if ($failure <= DB_STATUS_WARNING) {
return __('One or more database upgrades failed or completed with warnings; the database version was not advanced');
}
if (cacti_version_compare($orig_cacti_version, $cacti_upgrade_version, '<=') &&
!db_execute_prepared('UPDATE version SET cacti = ?', [CACTI_VERSION_FULL])
) {
return __('Unable to persist the upgraded database version');
}
return '';
}
private function checkDatabaseUpgrade(string $cacti_upgrade_version) : int {
global $database_upgrade_status;
$failure = DB_STATUS_SKIPPED;
if (cacti_sizeof($database_upgrade_status)) {
if (isset($database_upgrade_status[$cacti_upgrade_version])) {
foreach ($database_upgrade_status[$cacti_upgrade_version] as $cache_item) {
log_install_debug('dbc', $cacti_upgrade_version . ': ' . clean_up_lines(var_export($cache_item, true)));
if ($cache_item['status'] < $failure) {
$failure = $cache_item['status'];
}
if ($cache_item['status'] == DB_STATUS_ERROR) {
$this->addError(Installer::STEP_ERROR, 'DB:' . $cacti_upgrade_version, 'FAIL: ' . $cache_item['sql']);
}
}
}
}
return $failure;
}
/*
* Installer timestamps are stored as (string) microtime(true), which has no
* fractional part whenever the float lands on a whole second, and an install
* carried over from an older release may hold a bare integer. 'U.u' rejects
* both, so fall back to whole seconds rather than let a log message abort the
* install with a format() call on false.
*/
public static function dateFromMicrotime(mixed $value) : DateTime {
$date = DateTime::createFromFormat('U.u', (string) $value);
if ($date === false && is_numeric($value)) {
$date = DateTime::createFromFormat('U', (string) (int) $value);
}
return $date === false ? new DateTime() : $date;
}
public static function beginInstall(string $backgroundArg, mixed $installer = null) : bool {
$eula = read_config_option('install_eula', true);
if (empty($eula)) {
log_install_always('', __('Install aborted due to no EULA acceptance'));
return false;
}
$backgroundTime = read_config_option('install_started', true);
if (empty($backgroundTime)) {
$backgroundTime = false;
}
log_install_high('', "beginInstall(): '$backgroundTime' (time) != '$backgroundArg' (arg) && '-b' != '$backgroundArg' (arg)");
if ("$backgroundTime" != "$backgroundArg" && "$backgroundArg" != '-b') {
$dateTime = Installer::dateFromMicrotime($backgroundTime);
$dateArg = Installer::dateFromMicrotime($backgroundArg);
$background_error = __(
'Background was already started at %s, this attempt at %s was skipped',
$dateTime->format('Y-m-d H:i:s.u'),
$dateArg->format('Y-m-d H:i:s.u')
);
log_install_always('', $background_error);
if ($installer != null) {
$installer->addError(Installer::STEP_INSTALL, '', $background_error);
}
return false;
}
Installer::setPhpOption('max_execution_time', 0);
Installer::setPhpOption('memory_limit', -1);
$completed = false;
try {
$backgroundTime = (string) microtime(true);
if ($installer == null) {
$installer = new Installer();
}
$installer->setDefaults();
$installer->install();
$completed = $installer->getStep() == Installer::STEP_COMPLETE;
} catch (Throwable $e) {
$error = __('Exception occurred during installation: #%s - %s', $e->getCode(), $e->getMessage());
log_install_always('', $error);
set_install_config_option('install_error', $error);
if ($installer != null) {
$installer->addError(Installer::STEP_ERROR, 'Install', 'Exception', $error);
}
}
$backgroundDone = (string) microtime(true);
$dateBack = Installer::dateFromMicrotime($backgroundTime);
$dateTime = Installer::dateFromMicrotime($backgroundDone);
if ($completed) {
set_install_config_option('install_complete', $backgroundDone);
set_install_config_option('install_step', Installer::STEP_COMPLETE);
log_install_always('', __('Installation was started at %s, completed at %s', (string) $dateBack->format('Y-m-d H:i:s'), (string) $dateTime->format('Y-m-d H:i:s')));
return true;
}
set_install_config_option('install_step', Installer::STEP_ERROR);
log_install_always('', __('Installation failed after starting at %s', (string) $dateBack->format('Y-m-d H:i:s')));
return false;
}
public static function getInstallLog() : string {
$page_nr = 1;
$total_rows = 500;
$logcontents = tail_file(CACTI_PATH_LOG . '/cacti.log', 100, -1, ' INSTALL:', $page_nr, $total_rows);
$output_log = '';
foreach ($logcontents as $logline) {
$output_log = html_escape($logline) . '<br/>' . $output_log;
}
if (empty($output_log)) {
$output_log = '--- NO LOG FOUND ---';
}
$output = Installer::sectionCode($output_log);
return $output;
}
public static function formatModuleStatus(array &$module, string $name, string $badColor = 'red') : string {
$pcntl_special = false;
if ($name == 'pcntl') {
if (!$module['web']) {
$pcntl_special = true;
}
}
if ($module['installed']) {
return '<font color=green>' . __('Yes') . '</font>';
}
if (!($module['web'] || $module['cli'])) {
return '<font color=' . $badColor . '>' . __('No - %s', __('Both')) . '</font>';
}
if (!$module['web']) {
if ($pcntl_special) {
$module['web'] = true;
$module['installed'] = true;
return '<font color=orange>' . __('%s - N/A', __('Web')) . '</font>';
} else {
return '<font color=orange>' . __('%s - No', __('Web')) . '</font>';
}
} elseif (!$module['cli']) {
return '<font color=orange>' . __('%s - No', __('Cli')) . '</font>';
}
return '';
}
public static function setPhpOption(string $option_name, mixed $option_value) : void {
log_install_always('', __('Setting PHP Option %s = %s', $option_name, $option_value));
$value = ini_get($option_name);
if ($value != $option_value) {
ini_set($option_name, $option_value);
$value = ini_get($option_name);
if ($value != $option_value) {
log_install_always('', __('Failed to set PHP option %s, is %s (should be %s)', $option_name, $value, $option_value));
}
}
}
private static function fullSyncDataCollectorLog(array $poller_ids, string $format) : void {
if (cacti_sizeof($poller_ids) > 0) {
$unique_ids = array_values(array_unique($poller_ids, SORT_REGULAR));
$placeholders = implode(', ', array_fill(0, cacti_sizeof($unique_ids), '?'));
$pollers = array_rekey(
db_fetch_assoc_prepared("SELECT id, name
FROM poller
WHERE id IN ($placeholders)",
$unique_ids
),
'id',
'name'
);
foreach ($poller_ids as $id) {
$poller = $pollers[$id] ?? false;
log_install_always('sync', __($format, $poller, $id));
}
}
}
private static function fullSyncDataCollectors() : void {
// Perform full sync to complete upgrade
$status = install_full_sync();
if ($status['total'] == 0) {
log_install_always('sync', __('No Remote Data Collectors found for full synchronization'));
} else {
Installer::fullSyncDataCollectorLog($status['timeout'], "The Remote Data Collector named '%s' (ID: %d) timed out. Check Network/Server status then, please perform a manual sync to finalize the upgrade.");
Installer::fullSyncDataCollectorLog($status['skipped'], "The Remote Data Collector named '%s' (ID: %d) is currently unavailable Check Network/Server status. Once it is online, please perform a manual sync to finalize the upgrade.");
Installer::fullSyncDataCollectorLog($status['failed'], "The Remote Data Collector named '%s' (ID: %d) encountered an error during the full synchronization.please perform a manual sync to finalize the upgrade.");
Installer::fullSyncDataCollectorLog($status['success'], "The Remote Data Collector named '%s' (ID: %d) successfully completed the full synchronization.");
}
}
private static function disableInvalidPlugins() : void {
global $plugins_integrated;
foreach ($plugins_integrated as $plugin) {
if (api_plugin_is_enabled($plugin)) {
set_install_config_option('install_updated', microtime(true));
api_plugin_remove_hooks($plugin);
api_plugin_remove_realms($plugin);
}
}
$plugins = array_rekey(
db_fetch_assoc('SELECT directory AS plugin, version
FROM plugin_config
WHERE status IN(1,2)'),
'plugin',
'version'
);
if (cacti_sizeof($plugins)) {
foreach ($plugins as $plugin => $version) {
$disable = true;
$integrated = in_array($plugin, $plugins_integrated, true);
set_install_config_option('install_updated', microtime(true));
if (
is_dir(CACTI_PATH_PLUGINS . '/' . $plugin)
&& file_exists(CACTI_PATH_PLUGINS . "/$plugin/setup.php")
&& file_exists(CACTI_PATH_PLUGINS . "/$plugin/INFO")
&& !$integrated
) {
$info = parse_ini_file(CACTI_PATH_PLUGINS . "/$plugin/INFO", true);
if (isset($info['info']['compat']) && version_compare(CACTI_VERSION, $info['info']['compat']) > -1) {
$disable = false;
}
}
if ($disable) {
if ($integrated) {
cacti_log("Removing $plugin version $version as it is now integrated with Cacti " . CACTI_VERSION);
} else {
cacti_log("Disabling $plugin version $version as it is missing or not compatible with Cacti " . CACTI_VERSION);
}
api_plugin_disable_all($plugin);
}
}
}
foreach ($plugins_integrated as $plugin) {
if (!api_plugin_is_enabled($plugin)) {
db_execute_prepared('DELETE FROM plugin_config
WHERE directory = ?',
[$plugin]
);
}
}
}
/*
* *
* The following functions are for rendering output which is *
* returned when $this->Runtime is in Web mode only *
* *
*/
public static function sectionTitleError(string $title = '') : string {
if (empty($title)) {
$title = __('Error');
}
return Installer::sectionTitle($title, null, 'cactiInstallSectionTitleError');
}
public static function sectionTitle(string $title = '', mixed $id = '', string $class = '') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
return Installer::section($title, $id, $class, 'cactiInstallSectionTitle', 'h2');
}
public static function sectionSubTitle(string $title = '', string $id = '', string $class = '') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
$subtitle = Installer::section($title, $id, $class, 'cactiInstallSectionTitle', 'h3');
$subtitle .= '<div class="installSubSection">';
return $subtitle;
}
public static function sectionSubTitleEnd() : string {
return '</div>';
}
public static function sectionNormal(string $text = '', string $id = '', string $class = '') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
$class .= ' cactiInstallSectionNormal';
return Installer::section($text, $id, trim($class), 'cactiInstallSection', 'p');
}
public static function sectionNote(string $text = '', string $id = '', string $class = '', string $title = '') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
if (empty($title)) {
$title = 'NOTE:';
}
$class .= ' cactiInstallSectionNote';
return Installer::section('<span class="cactiInstallSectionNoteTitle">' . $title . '</span><span class=\'cactiInstallSectionNoteBody\'>' . $text . '</span>', $id, trim($class), '', 'p');
}
public static function sectionWarning(string $text = '', string $id = '', string $class = '', string $title = '') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
if (empty($title)) {
$title = __('WARNING:');
}
$class .= ' cactiInstallSectionWarning';
return Installer::section('<span class="cactiInstallSectionWarningTitle">' . $title . '</span><span class=\'cactiInstallSectionWarningBody\'>' . $text . '</span>', $id, trim($class), '', 'p');
}
public static function sectionError(string $text = '', string $id = '', string $class = '') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
$class .= ' cactiInstallSectionError';
return Installer::section('<span class="cactiInstallSectionErrorTitle">' . __('ERROR:') . '</span><span class=\'cactiInstallSectionErrorBody\'>' . $text . '</span>', $id, trim($class), '', 'p');
}
public static function sectionCode(string $text = '', string $id = '', string $class = '', string $elementType = 'p') : string {
if (empty($class)) {
$class = '';
}
if (empty($id)) {
$id = '';
}
$class .= ' cactiInstallSectionCode';
return Installer::section($text, $id, trim($class), '', $elementType);
}
public static function section(string $text = '', string $id = '', string $class = '', string $baseClass = 'cactiInstallSection', string $elementType = 'div') : string {
if (empty($elementType)) {
$elementType = 'div';
}
$output = '<' . $elementType;
if (empty($baseClass)) {
$baseClass = 'cactiInstallSection';
}
if (!empty($class)) {
$baseClass = trim($baseClass . ' ' . $class);
}
if (!empty($baseClass)) {
$output .= ' class=\'' . $baseClass . '\'';
}
if (!empty($id)) {
$output .= ' id=\'' . $id . '\'';
}
$output .= '>' . $text . '</' . $elementType . '>';
return $output;
}
}
/**
* InstallerButton class is an internal class that handles the button status
* that appears in the GUI installer
*/
class InstallerButton implements JsonSerializable {
public string $Text = '';
public int $Step = 0;
public bool $Visible = true;
public bool $Enabled = true;
public function __construct(array $params = []) {
foreach ($params as $key => $value) {
switch ($key) {
case 'Text':
$this->Text = $value;
break;
case 'Step':
$this->setStep(intval($value));
break;
case 'Visible':
$this->Visible = intval($value) > 0 ? true : false;
break;
case 'Enabled':
$this->Enabled = intval($value) > 0 ? true : false;
break;
}
}
}
public function setStep(int $step) : void {
$this->Step = $step;
$this->Enabled = !empty($this->Step);
}
public function jsonSerialize(): array {
return [
'Text' => $this->Text,
'Step' => $this->Step,
'Enabled' => $this->Enabled,
'Visible' => $this->Visible
];
}
}