ajaxController = $config['homedir'].'/include/ajax/manage_extensions.ajax'; $this->url = ui_get_full_url( 'index.php?sec=gservers&sec2=godmode/servers/discovery&wiz=magextensions' ); } /** * Checks if target method is available to be called using AJAX. * * @param string $method Target method. * * @return boolean True allowed, false not. */ public function ajaxMethod($method) { // Check access. check_login(); return in_array($method, $this->AJAXMethods); } /** * Implements load method. * * @return mixed Skeleton for button. */ public function load() { return [ 'icon' => $this->icon, 'label' => $this->label, 'url' => $this->url, ]; } /** * Generates a JSON error. * * @param string $msg Error message. * * @return void */ public function errorAjax(string $msg) { echo json_encode( ['error' => $msg] ); } /** * Implements run method. * * @return void */ public function run() { global $config; // Load styles. parent::run(); $uploadDisco = get_parameter('upload_disco', ''); $action = get_parameter('action', ''); $shortName = get_parameter('short_name', ''); if (empty($uploadDisco) === false) { if ($_FILES['file']['error'] == 0) { $result = $this->uploadExtension($_FILES['file']); if ($result === true) { ui_print_success_message( __('Uploaded extension') ); } else { if (is_string($result)) { echo $this->error($result); } else { echo $this->error(__('Failed to upload extension')); } } } else { echo $this->error(__('Failed to upload extension')); } } if (empty($action) === false && empty($shortName) === false) { switch ($action) { case 'delete': $result = $this->uninstallExtension($shortName); if ($result === true) { ui_print_success_message( __('Deleted extension') ); } else { echo $this->error(__('Fail delete extension')); } case 'sync_server': $syncAction = get_parameter('sync_action', ''); if ($syncAction === 'refresh') { $installationFolder = $config['homedir'].'/'.$this->path.'/'.$shortName; $result = $this->copyExtensionToServer($installationFolder, $shortName); if ($result === true) { ui_print_success_message( __('Extension folder created successfully') ); } else { echo $this->error(__('Fail created extension folder')); } } break; default: continue; } } $this->prepareBreadcrum( [ [ 'link' => ui_get_full_url( 'index.php?sec=gservers&sec2=godmode/servers/discovery' ), 'label' => __('Discovery'), ], [ 'link' => '', 'label' => _('Manage disco packages'), 'selected' => 1, ], ] ); // Header. ui_print_page_header( __('Manage disco packages'), '', false, '', true, '', false, '', GENERIC_SIZE_TEXT, '', $this->printHeader(true) ); $table = new stdClass(); $table->width = '100%'; $table->class = 'databox filters'; $table->size = []; $table->size[0] = '80%'; $table->align[3] = 'right'; $table->data = []; $table->data[0][0] = html_print_label_input_block( __('Load DISCO'), html_print_div( [ 'id' => 'upload_file', 'content' => html_print_input_file( 'file', true, ['style' => 'width:100%'] ), 'class' => 'mrgn_top_15px', ], true ) ); $table->data[0][3] = html_print_submit_button( __('Upload DISCO'), 'upload_button', false, [ 'class' => 'sub ok float-right', 'icon' => 'next', ], true ); echo '
'; echo ''; ui_require_javascript_file('manage_extensions'); try { $columns = [ 'name', 'short_name', 'section', 'description', 'version', [ 'text' => 'actions', 'class' => 'flex flex-items-center', ], ]; $columnNames = [ __('Name'), __('Short name'), __('Section'), __('Description'), __('Version'), __('Actions'), ]; // Load datatables user interface. ui_print_datatable( [ 'id' => 'list_extensions', 'class' => 'info_table', 'style' => 'width: 99%', 'dom_elements' => 'plfti', 'filter_main_class' => 'box-flat white_table_graph fixed_filter_bar', 'columns' => $columns, 'column_names' => $columnNames, 'ajax_url' => $this->ajaxController, 'ajax_data' => ['method' => 'getExtensionsInstalled'], 'no_sortable_columns' => [-1], 'order' => [ 'field' => 'name', 'direction' => 'asc', ], 'search_button_class' => 'sub filter float-right', ] ); } catch (Exception $e) { echo $e->getMessage(); } } /** * Upload extension to server. * * @param array $disco File disco tu upload. * * @return boolean $result Of operation, true if is ok. */ private function uploadExtension($disco) { global $config; if (substr($disco['name'], -6) !== '.disco') { return false; } $nameFile = str_replace('.disco', '.zip', $disco['name']); $nameTempDir = $config['attachment_store'].'/downloads/'; if (file_exists($nameTempDir) === false) { mkdir($nameTempDir); } $tmpPath = Files::tempdirnam( $nameTempDir, 'extensions_uploaded_' ); $result = move_uploaded_file($disco['tmp_name'], $tmpPath.'/'.$nameFile); if ($result === true) { $unzip = $this->unZip($tmpPath.'/'.$nameFile, $tmpPath); if ($unzip === true) { unlink($tmpPath.'/'.$nameFile); db_process_sql_begin(); $this->iniFile = parse_ini_file($tmpPath.'/discovery_definition.ini', true, INI_SCANNER_TYPED); if ($this->iniFile === false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return __('Failed to upload extension: Error while parsing dicovery_definition.ini'); } $error = ExtensionsDiscovery::validateIni($this->iniFile); if ($error !== false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return $error; } $id = $this->installExtension(); if ($id === false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return false; } $result = $this->autoLoadConfigExec($id); if ($result === false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return false; } $result = $this->autoUpdateDefaultMacros($id); if ($result === false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return false; } $nameFolder = $this->iniFile['discovery_extension_definition']['short_name']; $installationFolder = $config['homedir'].'/'.$this->path.'/'.$nameFolder; if (file_exists($installationFolder) === false) { mkdir($installationFolder, 0777, true); } else { Files::rmrf($installationFolder, true); } $result = Files::move($tmpPath, $installationFolder, true); if ($result === false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return false; } $this->setPermissionfiles($installationFolder, $this->iniFile['discovery_extension_definition']['execution_file']); $this->setPermissionfiles( $installationFolder, [ $this->iniFile['discovery_extension_definition']['passencrypt_script'], $this->iniFile['discovery_extension_definition']['passdecrypt_script'], ] ); $result = $this->copyExtensionToServer($installationFolder, $nameFolder); if ($result === false) { db_process_sql_rollback(); Files::rmrf($tmpPath); return false; } Files::rmrf($tmpPath); db_process_sql_commit(); return true; } } else { Files::rmrf($tmpPath); return false; } } /** * Copy the extension folder into remote path server. * * @param string $path Path extension folder. * @param string $nameFolder Name of extension folder. * * @return boolean Result of operation. */ public function copyExtensionToServer($path, $nameFolder) { global $config; $filesToExclude = [ 'discovery_definition.ini', 'logo.png', ]; $serverPath = $config['remote_config'].'/discovery/'.$nameFolder; if (file_exists($serverPath) === false) { mkdir($serverPath, 0777, true); } else { Files::rmrf($serverPath, true); } $result = $this->copyFolder($path, $serverPath, $filesToExclude); $this->setPermissionfiles($serverPath, $this->iniFile['discovery_extension_definition']['execution_file']); return $result; } /** * Copy from $source path to $destination * * @param string $source Initial folder path. * @param string $destination Destination folder path. * @param array $exclude Files to exlcude in copy. * * @return boolean Result of operation. */ public function copyFolder($source, $destination, $exclude=[]) { if (file_exists($destination) === false) { mkdir($destination, 0777, true); } $files = scandir($source); foreach ($files as $file) { if ($file !== '.' && $file !== '..') { if (is_dir($source.'/'.$file)) { $result = $this->copyFolder($source.'/'.$file, $destination.'/'.$file); if ($result === false) { return false; } } else { if (in_array($file, $exclude) === false) { $result = copy($source.'/'.$file, $destination.'/'.$file); if ($result === false) { return false; } } } } } return true; } /** * Delete extension from database and delete folder * * @param integer $shortName Short name app for delete. * * @return boolean Result of operation. */ private function uninstallExtension($shortName) { global $config; $result = db_process_sql_delete( 'tdiscovery_apps', ['short_name' => $shortName] ); if ($result !== false) { Files::rmrf($config['homedir'].'/'.$this->path.'/'.$shortName); Files::rmrf($config['remote_config'].'/discovery/'.$shortName); return true; } else { return false; } } /** * Load the basic information of the app into database. * * @return boolean Result of query. */ private function installExtension() { $exist = db_get_row_filter( 'tdiscovery_apps', [ 'short_name' => $this->iniFile['discovery_extension_definition']['short_name'], ] ); $version = $this->iniFile['discovery_extension_definition']['version']; if ($version === null) { $version = ''; } $description = $this->iniFile['discovery_extension_definition']['description']; if ($description === null) { $description = ''; } if ($exist === false) { return db_process_sql_insert( 'tdiscovery_apps', [ 'short_name' => $this->iniFile['discovery_extension_definition']['short_name'], 'name' => io_safe_input($this->iniFile['discovery_extension_definition']['name']), 'description' => io_safe_input($description), 'section' => $this->iniFile['discovery_extension_definition']['section'], 'version' => $version, ] ); } else { $result = db_process_sql_update( 'tdiscovery_apps', [ 'name' => io_safe_input($this->iniFile['discovery_extension_definition']['name']), 'description' => io_safe_input($description), 'section' => $this->iniFile['discovery_extension_definition']['section'], 'version' => $version, ], [ 'short_name' => $this->iniFile['discovery_extension_definition']['short_name'], ] ); if ($result !== false) { return $exist['id_app']; } } } /** * Return all extension installed by ajax. * * @return void */ public function getExtensionsInstalled() { global $config; $data = []; $start = get_parameter('start', 0); $length = get_parameter('length', $config['block_size']); $orderDatatable = get_datatable_order(true); $pagination = ''; $order = ''; try { ob_start(); if (isset($orderDatatable)) { $order = sprintf( ' ORDER BY %s %s', $orderDatatable['field'], $orderDatatable['direction'] ); } if (isset($length) && $length > 0 && isset($start) && $start >= 0 ) { $pagination = sprintf( ' LIMIT %d OFFSET %d ', $length, $start ); } $sql = sprintf( 'SELECT short_name, name, section, description, version FROM tdiscovery_apps %s %s', $order, $pagination ); $data = db_get_all_rows_sql($sql); $sqlCount = sprintf( 'SELECT short_name, name, section, description, version FROM tdiscovery_apps %s', $order, ); $count = db_get_num_rows($sqlCount); foreach ($data as $key => $row) { $logo = $this->path.'/'.$row['short_name'].'/logo.png'; if (file_exists($logo) === false) { $logo = $this->defaultLogo; } $logo = html_print_image($logo, true, ['style' => 'max-width: 30px; margin-right: 15px;']); $data[$key]['name'] = $logo.io_safe_output($row['name']); $data[$key]['short_name'] = $row['short_name']; $data[$key]['description'] = io_safe_output($row['description']); $data[$key]['version'] = $row['version']; $data[$key]['actions'] = ''; if ($this->checkFolderConsole($row['short_name']) === true) { $data[$key]['actions'] .= ''; } else { $data[$key]['actions'] .= html_print_image( 'images/error_red.png', true, [ 'title' => __('The extension directory or .ini does not exist in console.'), 'alt' => __('The extension directory or .ini does not exist in console.'), 'class' => 'main_menu_icon invert_filter', ], ); } $migrationHash = $this->canMigrate($row['short_name']); if ($migrationHash !== false && empty($migrationHash) !== true) { // Migrate button $data[$key]['actions'] .= html_print_input_image( 'button_migrate', 'images/reset.png', '', '', true, [ 'onclick' => 'show_migration_form(\''.$row['short_name'].'\',\''.$migrationHash.'\')', ] ); } } if (empty($data) === true) { $total = 0; $data = []; } else { $total = $count; } echo json_encode( [ 'data' => $data, 'recordsTotal' => $total, 'recordsFiltered' => $total, ] ); // Capture output. $response = ob_get_clean(); } catch (Exception $e) { echo json_encode(['error' => $e->getMessage()]); exit; } json_decode($response); if (json_last_error() === JSON_ERROR_NONE) { echo $response; } else { echo json_encode( [ 'success' => false, 'error' => $response, ] ); } exit; } /** * Insert new the default values for extension. * * @param integer $id Id of extension. * * @return boolean Result of query. */ private function autoUpdateDefaultMacros($id) { $defaultValues = $this->iniFile['discovery_extension_definition']['default_value']; foreach ($defaultValues as $macro => $value) { $sql = 'INSERT IGNORE INTO `tdiscovery_apps_tasks_macros` (`id_task`, `macro`, `type`, `value`, `temp_conf`) SELECT `id_rt`, "'.$macro.'", "custom", "'.(string) io_safe_input($value).'", "0" FROM `trecon_task` WHERE `id_app` = "'.$id.'";'; $result = db_process_sql($sql); if ($result === false) { return false; } } $tempFiles = $this->iniFile['tempfile_confs']['file']; foreach ($tempFiles as $macro => $value) { $sql = 'UPDATE `tdiscovery_apps_tasks_macros` SET `value` = "'.(string) io_safe_input($value).'" WHERE `id_task` IN (SELECT `id_rt` FROM `trecon_task` WHERE `id_app` = "'.$id.'") AND `macro` = "'.$macro.'"'; $result = db_process_sql($sql); if ($result === false) { return false; } $sql = 'INSERT IGNORE INTO `tdiscovery_apps_tasks_macros` (`id_task`, `macro`, `type`, `value`, `temp_conf`) SELECT `id_rt`, "'.$macro.'", "custom", "'.(string) io_safe_input($value).'", "1" FROM `trecon_task` WHERE `id_app` = "'.$id.'";'; $result = db_process_sql($sql); if ($result === false) { return false; } } return true; } /** * Load the exec files in database * * @param integer $id Id of extension. * * @return boolean Result of query. */ private function autoLoadConfigExec($id) { $executionFiles = $this->iniFile['discovery_extension_definition']['execution_file']; foreach ($executionFiles as $key => $value) { $exist = db_get_row_filter( 'tdiscovery_apps_scripts', [ 'id_app' => $id, 'macro' => $key, ] ); if ($exist === false) { $result = db_process_sql_insert( 'tdiscovery_apps_scripts', [ 'id_app' => $id, 'macro' => $key, 'value' => io_safe_input($value), ] ); if ($result === false) { return false; } } else { $result = db_process_sql_update( 'tdiscovery_apps_scripts', ['value' => io_safe_input($value)], [ 'id_app' => $id, 'macro' => $key, ] ); if ($result === false) { return false; } } } $execCommands = $this->iniFile['discovery_extension_definition']['exec']; $result = db_process_sql_delete( 'tdiscovery_apps_executions', ['id_app' => $id] ); if ($result === false) { return false; } foreach ($execCommands as $key => $value) { $result = db_process_sql_insert( 'tdiscovery_apps_executions', [ 'id_app' => $id, 'execution' => io_safe_input($value), ] ); if ($result === false) { return false; } } return true; } /** * Check if exist folder extension in console. * * @param string $shortName Name of folder. * * @return boolean Return true if exist folder */ private function checkFolderConsole($shortName) { global $config; $folderPath = $config['homedir'].'/'.$this->path.'/'.$shortName; $iniPath = $config['homedir'].'/'.$this->path.'/'.$shortName.'/discovery_definition.ini'; if (file_exists($folderPath) === false || file_exists($iniPath) === false) { return false; } else { return true; } } /** * Validate the ini name by ajax. * * @return void */ public function validateIniName() { global $config; $uploadDisco = get_parameter('upload_disco', ''); if (empty($uploadDisco) === false) { if ($_FILES['file']['error'] == 0) { $disco = $_FILES['file']; } else { echo json_encode(['success' => false, 'message' => 'Failed to upload extension']); return; } } if (substr($disco['name'], -6) !== '.disco') { echo json_encode(['success' => false, 'message' => 'Failed to upload extension']); return; } $nameFile = str_replace('.disco', '.zip', $disco['name']); $nameTempDir = $config['attachment_store'].'/downloads/'; if (file_exists($nameTempDir) === false) { mkdir($nameTempDir); } $tmpPath = Files::tempdirnam( $nameTempDir, 'extensions_uploaded_' ); $result = move_uploaded_file($disco['tmp_name'], $tmpPath.'/'.$nameFile); if ($result === true) { $unzip = $this->unZip($tmpPath.'/'.$nameFile, $tmpPath, 'discovery_definition.ini'); if ($unzip === true) { unlink($tmpPath.'/'.$nameFile); $this->iniFile = parse_ini_file($tmpPath.'/discovery_definition.ini', true, INI_SCANNER_TYPED); if ($this->iniFile === false) { Files::rmrf($tmpPath); echo json_encode(['success' => false, 'message' => __('Failed to upload extension: Error while parsing dicovery_definition.ini')]); return; } $message = false; $shortName = $this->iniFile['discovery_extension_definition']['short_name']; if (strpos($shortName, 'pandorafms.') === 0) { $message = __('The \'short_name\' starting with \'pandorafms.\' is reserved for Pandora FMS applications. If this is not an official Pandora FMS application, consider changing the \'short_name\'. Do you want to continue?'); } $exist = db_get_row_filter( 'tdiscovery_apps', ['short_name' => $shortName] ); if ($exist !== false) { $message = __('There is another application with the same \'short_name\': \'%s\'. Do you want to overwrite the application and all of its contents?', $shortName); } if ($message !== false) { echo json_encode( [ 'success' => true, 'warning' => true, 'message' => $message, ] ); } else { echo json_encode(['success' => true]); } Files::rmrf($tmpPath); return; } } else { Files::rmrf($tmpPath); echo json_encode(['success' => false, 'message' => __('Failed to upload extension')]); return; } } /** * Return all extensions from section. * * @param string $section Section to filter. * * @return array List of sections. */ static public function getExtensionBySection($section) { return db_get_all_rows_filter( 'tdiscovery_apps', ['section' => $section] ); } /** * Set execution permission in folder items and subfolders. * * @param string $path Array of files to apply permissions. * @param array $filter Array of files for apply permission only. * * @return void */ private function setPermissionfiles($path, $filter=false) { global $config; if ($filter !== false && is_array($filter) === true) { foreach ($filter as $key => $file) { if (substr($file, 0, 1) !== '/') { $file = $path.'/'.$file; } chmod($file, 0777); } } else { chmod($path, 0777); if (is_dir($path)) { $items = scandir($path); foreach ($items as $item) { if ($item != '.' && $item != '..') { $itemPath = $path.'/'.$item; $this->setPermissionfiles($itemPath); } } } } } /** * Unzip folder or only file. * * @param string $zipFile File to unzip. * @param string $target_path Target path into unzip. * @param string $file If only need unzip one file. * * @return boolean $result True if the file has been successfully decompressed. */ public function unZip($zipFile, $target_path, $file=null) { $zip = new \ZipArchive; if ($zip->open($zipFile) === true) { $zip->extractTo($target_path, $file); $zip->close(); return true; } else { return false; } } /** * Checks if the discovery app can be migrated to .disco system. * If app is migrated or is not in .ini file, it cannot be migrated. * * @param string $shortName Short name of the discovery app. * * @return string App hash, false in case hash doesnt exist on ini file, or is already migraeted.. */ private function canMigrate(string $shortName='') { global $config; if (empty($shortName) === true) { return false; } // 1. Check if app is already migrated: // Get migrated Discovery Apps from config. $migratedAppsJson = db_get_value('value', 'tconfig', 'token', 'migrated_discovery_apps'); if ($migratedAppsJson === false) { return false; } // Decode JSON migrated apps. $migrateApps = json_decode($migratedAppsJson, true); if (json_last_error() !== JSON_ERROR_NONE) { return false; } // Check app migrated. if (array_key_exists($shortName, $migrateApps)) { if (defined($migrateApps[$shortName]) === true && (bool) $migrateApps[$shortName] === true) { // Already migrated. return false; } } // 2. If app not migrated yet, check DiscoveryApplicationsMigrateCodes.ini // Path to the INI file $filePath = $this->path.'/DiscoveryApplicationsMigrateCodes.ini'; // Parse the INI file $migrationCodes = parse_ini_file($filePath, true); if ($migrationCodes === false) { return false; } // Check shortname in ini file. if (array_key_exists($shortName, $migrationCodes) === false) { return false; } else { return $migrationCodes[$shortName]; } // All checks ok, discovery app can be migrated return false; } /** * Prints html for migrate modal * * @param string $shortName Short name of app * @param string $hash App hash * * @return void */ public function loadMigrateModal() { $shortname = get_parameter('shortname', null); $hash = get_parameter('hash', null); $form = [ 'action' => '#', 'id' => 'modal_migrate_form', 'onsubmit' => 'return false;', 'class' => 'modal', ]; $inputs = []; $migrateMessage = __( 'All ‘legacy‘ tasks for this application will be migrated to the new ‘.disco’ package system. All configurations and executions will be managed with the new system. This process will not be reversible.' ); $migrateMessage .= _('Please check the migration code for the application before proceeding.'); $inputs[] = [ 'wrapper' => 'div', 'block_id' => 'div_migrate_message', 'class' => 'hole flex-row flex-items-center w98p', 'direct' => 1, 'block_content' => [ [ 'label' => $migrateMessage, 'arguments' => [ 'class' => 'first_lbl w98p', 'name' => 'lbl_migrate_message', 'id' => 'lbl_migrate_message', ], ], ], ]; $inputs[] = [ 'label' => __('Applicattion hash'), 'id' => 'div-hash', 'arguments' => [ 'name' => 'hash', 'type' => 'text', 'value' => $hash, 'return' => true, ], ]; $this->printForm( [ 'form' => $form, 'inputs' => $inputs, ], false ); } }