%PDF- %PDF-
Direktori : /var/www/projetos/suporte.iigd.com.br/src/ |
Current File : /var/www/projetos/suporte.iigd.com.br/src/Plugin.php |
<?php /** * --------------------------------------------------------------------- * * GLPI - Gestionnaire Libre de Parc Informatique * * http://glpi-project.org * * @copyright 2015-2024 Teclib' and contributors. * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- * * LICENSE * * This file is part of GLPI. * * 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 3 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * --------------------------------------------------------------------- */ /** * Based on cacti plugin system */ use Glpi\Application\View\TemplateRenderer; use Glpi\Cache\CacheManager; use Glpi\Dashboard\Grid; use Glpi\Marketplace\Controller as MarketplaceController; use Glpi\Marketplace\View as MarketplaceView; use Glpi\Plugin\Hooks; use Glpi\Toolbox\VersionParser; use Glpi\Event; class Plugin extends CommonDBTM { // Class constant : Plugin state /** * @var int Unknown plugin state */ const UNKNOWN = -1; /** * @var int Plugin was discovered but not installed * * @note Plugins are never actually set to this status? */ const ANEW = 0; /** * @var int Plugin is installed and enabled */ const ACTIVATED = 1; /** * @var int Plugin is not installed */ const NOTINSTALLED = 2; /** * @var int Plugin is installed but needs configured before it can be enabled */ const TOBECONFIGURED = 3; /** * @var int Plugin is installed but not enabled */ const NOTACTIVATED = 4; /** * @var int Plugin was previously discovered, but the plugin directory is missing now. The DB needs cleaned. */ const TOBECLEANED = 5; /** * @var int The plugin's files are for a newer version than installed. An update is needed. */ const NOTUPDATED = 6; /** * The plugin has been replaced by another one. */ const REPLACED = 7; /** * Option used to indicates that auto installation of plugin should be disabled (bool value expected). * * @var string */ const OPTION_AUTOINSTALL_DISABLED = 'autoinstall_disabled'; /** * Plugin key validation pattern. */ private const PLUGIN_KEY_PATTERN = '/^[a-z0-9]+$/i'; public static $rightname = 'config'; /** * Indicates whether plugin states have been checked. * * @var boolean */ private static $plugins_state_checked = false; /** * Activated plugin list * * @var string[] */ private static $activated_plugins = []; /** * Loaded plugin list * * @var string[] */ private static $loaded_plugins = []; /** * Store additional infos for each plugins * * @var array */ private array $plugins_information = []; /** * Store keys of plugins found on filesystem. * * @var array|null */ private ?array $filesystem_plugin_keys = null; public static function getTypeName($nb = 0) { return _n('Plugin', 'Plugins', $nb); } public static function getMenuName() { return static::getTypeName(Session::getPluralNumber()); } public static function getMenuContent() { $menu = parent::getMenuContent() ?: []; if (static::canView()) { $redirect_mp = MarketplaceController::getPluginPageConfig(); $menu['title'] = self::getMenuName(); $menu['page'] = $redirect_mp == MarketplaceController::MP_REPLACE_YES ? '/front/marketplace.php' : '/front/plugin.php'; $menu['icon'] = self::getIcon(); } if (count($menu)) { return $menu; } return false; } public static function getAdditionalMenuLinks() { if (!static::canView()) { return false; } $mp_icon = MarketplaceView::getIcon(); $mp_title = MarketplaceView::getTypeName(); $marketplace = "<i class='$mp_icon pointer' title='$mp_title'></i><span class='d-none d-xxl-block'>$mp_title</span>"; $cl_icon = Plugin::getIcon(); $cl_title = Plugin::getTypeName(Session::getPluralNumber()); $classic = "<i class='$cl_icon pointer' title='$cl_title'></i><span class='d-none d-xxl-block'>$cl_title</span>"; return [ $marketplace => MarketplaceView::getSearchURL(false), $classic => Plugin::getSearchURL(false), ]; } public static function getAdditionalMenuOptions() { if (static::canView()) { return [ 'marketplace' => [ 'icon' => MarketplaceView::geticon(), 'title' => MarketplaceView::getTypeName(), 'page' => MarketplaceView::getSearchURL(false), ] ]; } return false; } public function prepareInputForAdd($input) { $input = $this->prepareInput($input); return $input; } public function prepareInputForUpdate($input) { $input = $this->prepareInput($input); return $input; } private function prepareInput(array $input) { if ($this->isNewItem() || array_key_exists('directory', $input)) { if (preg_match(self::PLUGIN_KEY_PATTERN, $input['directory'] ?? '') !== 1) { Session::addMessageAfterRedirect( __s('Invalid plugin directory'), false, ERROR ); return false; } } return $input; } /** * Retrieve an item from the database using its directory * * @param string $dir directory of the plugin * * @return boolean **/ public function getFromDBbyDir($dir) { return $this->getFromDBByCrit([$this->getTable() . '.directory' => $dir]); } /** * Init plugins list. * * @param boolean $load_plugins Whether to load active/configurable plugins or not. * @param array $excluded_plugins List of plugins to exclude * * @return void **/ public function init(bool $load_plugins = false, array $excluded_plugins = []) { /** @var \DBmysql $DB */ global $DB; self::$plugins_state_checked = false; self::$activated_plugins = []; self::$loaded_plugins = []; if (!($DB instanceof DBmysql) || !$DB->connected) { // Cannot init plugins list if DB is not connected self::$plugins_state_checked = true; return; } $this->checkStates(false, $excluded_plugins); $plugins = $this->find(['state' => [self::ACTIVATED, self::TOBECONFIGURED]]); // Store plugins that are loadable and still marked as active in DB after call to `self::checkStates()`, // but before actually calling plugins init functions, // in order to not have to do a DB query on `self::isActivated()` calls which are commonly use in plugins init functions. $directories_to_load = []; foreach ($plugins as $plugin) { if ( in_array($plugin['directory'], $excluded_plugins) || !$this->isLoadable($plugin['directory']) ) { continue; } $directories_to_load[] = $plugin['directory']; if ((int)$plugin['state'] === self::ACTIVATED) { self::$activated_plugins[] = $plugin['directory']; } } if ($load_plugins) { foreach ($directories_to_load as $directory) { \Glpi\Debug\Profiler::getInstance()->start("{$directory}:init", \Glpi\Debug\Profiler::CATEGORY_PLUGINS); Plugin::load($directory); \Glpi\Debug\Profiler::getInstance()->stop("{$directory}:init"); } // For plugins which require action after all plugin init Plugin::doHook(Hooks::POST_INIT); } } /** * Init a plugin including setup.php file * launching plugin_init_NAME function after checking compatibility * * @param string $plugin_key System name (Plugin directory) * @param boolean $withhook Load hook functions (false by default) * * @return void **/ public static function load($plugin_key, $withhook = false) { $loaded = false; foreach (PLUGINS_DIRECTORIES as $base_dir) { if (!is_dir($base_dir)) { continue; } $plugin_directory = "$base_dir/$plugin_key"; if (!file_exists($plugin_directory)) { continue; } if ((new self())->loadPluginSetupFile($plugin_key)) { $loaded = true; if (!in_array($plugin_key, self::$loaded_plugins)) { // Register PSR-4 autoloader $psr4_dir = $plugin_directory . '/src'; if (is_dir($psr4_dir)) { $psr4_autoloader = new \Composer\Autoload\ClassLoader(); $psr4_autoloader->addPsr4(NS_PLUG . ucfirst($plugin_key) . '\\', $psr4_dir); $psr4_autoloader->register(); } // Init plugin self::$loaded_plugins[] = $plugin_key; $init_function = "plugin_init_$plugin_key"; if (function_exists($init_function)) { try { $init_function(); } catch (\Throwable $e) { trigger_error( sprintf( 'Error while loading plugin %s: %s', $plugin_key, $e->getMessage() ), E_USER_WARNING ); // Plugin has errored, so it should be disabled if it isn't already $plugin = new self(); if ($plugin->isActivated($plugin_key)) { // We don't want to override another status like TOBECONFIGURED or NOTUPDATED $plugin->getFromDBbyDir($plugin_key); $plugin->unactivate($plugin->getID()); } continue; } self::loadLang($plugin_key); } } } if ($withhook) { self::includeHook($plugin_key); } if ($loaded) { break; } } } /** * Unload a plugin. * * @param string $plugin_key System name (Plugin directory) * * @return void */ private function unload($plugin_key) { if (($key = array_search($plugin_key, self::$activated_plugins)) !== false) { unset(self::$activated_plugins[$key]); } if (($key = array_search($plugin_key, self::$loaded_plugins)) !== false) { unset(self::$loaded_plugins[$key]); } } /** * Load lang file for a plugin * * @param string $plugin_key System name (Plugin directory) * @param string $forcelang Force a specific lang (default '') * @param string $coretrytoload Lang trying to be loaded from core (default '') * * @return void **/ public static function loadLang($plugin_key, $forcelang = '', $coretrytoload = '') { /** * @var array $CFG_GLPI * @var \Laminas\I18n\Translator\TranslatorInterface $TRANSLATE */ global $CFG_GLPI, $TRANSLATE; // For compatibility for plugins using $LANG $trytoload = 'en_GB'; if (isset($_SESSION['glpilanguage'])) { $trytoload = $_SESSION["glpilanguage"]; } // Force to load a specific lang if (!empty($forcelang)) { $trytoload = $forcelang; } // If not set try default lang file if (empty($trytoload)) { $trytoload = $CFG_GLPI["language"]; } if (empty($coretrytoload)) { $coretrytoload = $trytoload; } // New localisation system $mofile = false; foreach (PLUGINS_DIRECTORIES as $base_dir) { if (!is_dir($base_dir)) { continue; } $locales_dir = "$base_dir/$plugin_key/locales/"; if ( array_key_exists($trytoload, $CFG_GLPI["languages"]) && file_exists($locales_dir . $CFG_GLPI["languages"][$trytoload][1]) ) { $mofile = $locales_dir . $CFG_GLPI["languages"][$trytoload][1]; } else if ( !empty($CFG_GLPI["language"]) && array_key_exists($CFG_GLPI["language"], $CFG_GLPI["languages"]) && file_exists($locales_dir . $CFG_GLPI["languages"][$CFG_GLPI["language"]][1]) ) { $mofile = $locales_dir . $CFG_GLPI["languages"][$CFG_GLPI["language"]][1]; } else if (file_exists($locales_dir . "en_GB.mo")) { $mofile = $locales_dir . "en_GB.mo"; } if ($mofile !== false) { break; } } if ($mofile !== false) { $TRANSLATE->addTranslationFile( 'gettext', $mofile, $plugin_key, $coretrytoload ); } $plugin_folders = is_dir(GLPI_LOCAL_I18N_DIR) ? scandir(GLPI_LOCAL_I18N_DIR) : []; $plugin_folders = array_filter($plugin_folders, function ($dir) use ($plugin_key) { if (!is_dir(GLPI_LOCAL_I18N_DIR . "/$dir")) { return false; } if ($dir == $plugin_key) { return true; } return str_starts_with($dir, $plugin_key . '_'); }); foreach ($plugin_folders as $plugin_folder) { $mofile = GLPI_LOCAL_I18N_DIR . "/$plugin_folder/$coretrytoload.mo"; $phpfile = str_replace('.mo', '.php', $mofile); // Load local PHP file if it exists if (file_exists($phpfile)) { $TRANSLATE->addTranslationFile('phparray', $phpfile, $plugin_key, $coretrytoload); } // Load local MO file if it exists -- keep last so it gets precedence if (file_exists($mofile)) { $TRANSLATE->addTranslationFile('gettext', $mofile, $plugin_key, $coretrytoload); } } } /** * Check plugins states and detect new plugins. * * @param boolean $scan_inactive_and_new_plugins * @param array $excluded_plugins List of plugins to exclude * * @return void */ public function checkStates($scan_inactive_and_new_plugins = false, array $excluded_plugins = []) { $directories = []; // Add known plugins to the check list $condition = $scan_inactive_and_new_plugins ? [] : ['state' => self::ACTIVATED]; $known_plugins = $this->find($condition); foreach ($known_plugins as $plugin) { $directories[] = $plugin['directory']; } if ($scan_inactive_and_new_plugins) { array_push($directories, ...$this->getFilesystemPluginKeys()); } // Prevent duplicated checks $directories = array_unique($directories); // Check all directories from the checklist foreach ($directories as $directory) { if (in_array($directory, $excluded_plugins)) { continue; } $this->checkPluginState($directory, $scan_inactive_and_new_plugins); } self::$plugins_state_checked = true; } /** * Get information for a given plugin. */ private function getPluginInformation(string $plugin_key): ?array { if (!array_key_exists($plugin_key, $this->plugins_information)) { $information = $this->getInformationsFromDirectory($plugin_key); $this->plugins_information[$plugin_key] = !empty($information) ? $information : null; } return $this->plugins_information[$plugin_key]; } /** * Return plugin keys corresponding to directories found in filesystem. */ private function getFilesystemPluginKeys(): array { if ($this->filesystem_plugin_keys === null) { $this->filesystem_plugin_keys = []; $plugins_directories = new AppendIterator(); foreach (PLUGINS_DIRECTORIES as $base_dir) { if (!is_dir($base_dir)) { continue; } $plugins_directories->append(new DirectoryIterator($base_dir)); } foreach ($plugins_directories as $plugin_directory) { if ( str_starts_with($plugin_directory->getFilename(), '.') // ignore hidden files || !is_dir($plugin_directory->getRealPath()) ) { continue; } $this->filesystem_plugin_keys[] = $plugin_directory->getFilename(); } } return $this->filesystem_plugin_keys; } /** * Check plugin state. * * @param string $plugin_key System name (Plugin directory) * * return void */ public function checkPluginState($plugin_key, bool $check_for_replacement = false) { $plugin = new self(); $information = $this->getPluginInformation($plugin_key) ?? []; $is_already_known = $plugin->getFromDBByCrit(['directory' => $plugin_key]); $is_loadable = !empty($information); $new_specs = $check_for_replacement ? $this->getNewInfoAndDirBasedOnOldName($plugin_key) : null; $is_replaced = $new_specs !== null; if (!$is_already_known && !$is_loadable) { // Plugin is not known and we are unable to load information, we ignore it. return; } if ($is_already_known && $is_replaced) { // Filesystem contains both the checked plugin and the plugin that is supposed to replace it. // Mark it as REPLACED as it should not be loaded anymore. if ((int)$plugin->fields['state'] !== self::REPLACED) { trigger_error( sprintf( 'Plugin "%s" has been replaced by "%s" and therefore has been deactivated.', $plugin_key, $new_specs['directory'] ), E_USER_WARNING ); $this->update( [ 'id' => $plugin->fields['id'], 'state' => self::REPLACED, ] + $information ); $this->unload($plugin_key); // reset menu if (isset($_SESSION['glpimenu'])) { unset($_SESSION['glpimenu']); } Event::log( '', Plugin::class, 3, "setup", sprintf( __('Plugin %1$s has been replaced by %2$s and therefore has been deactivated.'), $plugin_key, $new_specs['directory'] ) ); } // Plugin has been replaced, we ignore it return; } if (!$is_loadable) { trigger_error( sprintf( 'Unable to load plugin "%s" information.', $plugin_key ), E_USER_WARNING ); // Plugin is known but we are unable to load information, we ignore it return; } if (!$is_already_known) { // Plugin not known, add it in DB $this->add( array_merge( $information, [ 'state' => $is_replaced ? self::REPLACED : self::NOTINSTALLED, 'directory' => $plugin_key, ] ) ); return; } if ( $information['version'] != $plugin->fields['version'] || $plugin_key != $plugin->fields['directory'] ) { // Plugin known version differs from information or plugin has been renamed, // update information in database $input = $information; $input['id'] = $plugin->fields['id']; $input['directory'] = $plugin_key; if (!in_array($plugin->fields['state'], [self::ANEW, self::NOTINSTALLED, self::NOTUPDATED])) { // mark it as 'updatable' unless it was not installed trigger_error( sprintf( 'Plugin "%s" version changed. It has been deactivated as its update process has to be launched.', $plugin_key ), E_USER_WARNING ); $input['state'] = self::NOTUPDATED; Event::log( '', Plugin::class, 3, "setup", sprintf( __('Plugin %1$s version changed. It has been deactivated as its update process has to be launched.'), $plugin_key ) ); } $this->update($input); $this->unload($plugin_key); // reset menu if (isset($_SESSION['glpimenu'])) { unset($_SESSION['glpimenu']); } return; } // Check if replacement state changed if ((int)$plugin->fields['state'] === self::REPLACED && !$is_replaced) { // Reset plugin state as replacement plugin is not present anymore on filesystem $this->update( [ 'id' => $plugin->fields['id'], 'state' => self::NOTINSTALLED ] ); return; } // Check if configuration state changed if (in_array((int)$plugin->fields['state'], [self::ACTIVATED, self::TOBECONFIGURED, self::NOTACTIVATED], true)) { $function = 'plugin_' . $plugin_key . '_check_config'; $is_config_ok = !function_exists($function) || $function(); if ((int)$plugin->fields['state'] === self::TOBECONFIGURED && $is_config_ok) { // Remove TOBECONFIGURED state if configuration is OK now $this->update( [ 'id' => $plugin->fields['id'], 'state' => self::NOTACTIVATED ] ); return; } else if ((int)$plugin->fields['state'] !== self::TOBECONFIGURED && !$is_config_ok) { // Add TOBECONFIGURED state if configuration is required trigger_error( sprintf( 'Plugin "%s" must be configured.', $plugin_key ), E_USER_WARNING ); $this->update( [ 'id' => $plugin->fields['id'], 'state' => self::TOBECONFIGURED ] ); return; } } if (self::ACTIVATED !== (int)$plugin->fields['state']) { // Plugin is not activated, nothing to do return; } // Check that active state of plugin can be kept $usage_ok = true; // Check compatibility ob_start(); if (!$this->checkVersions($plugin_key)) { $usage_ok = false; } ob_end_clean(); // Check prerequisites if ($usage_ok) { $function = 'plugin_' . $plugin_key . '_check_prerequisites'; if (function_exists($function)) { ob_start(); if (!$function()) { $usage_ok = false; } ob_end_clean(); } } if (!$usage_ok) { // Deactivate if not usable trigger_error( sprintf( 'Plugin "%s" prerequisites are not matched. It has been deactivated.', $plugin_key ), E_USER_WARNING ); $this->unactivate($plugin->fields['id']); } } /** * Get plugin information based on its old name. * * @param string $oldname * * @return null|array If a new directory is found, returns an array containing 'directory' and 'information' keys. */ private function getNewInfoAndDirBasedOnOldName($oldname) { foreach ($this->getFilesystemPluginKeys() as $plugin_key) { $information = $this->getPluginInformation($plugin_key); if (($information['oldname'] ?? null) === $oldname) { // Return information if oldname specified in parsed directory matches passed value return [ 'directory' => $plugin_key, 'information' => $information, ]; } } return null; } /** * Get list of all plugins * * @param array $fields Fields to retrieve * @param array $order Query ORDER clause * * @return array */ public function getList(array $fields = [], array $order = ['name', 'directory']) { /** @var \DBmysql $DB */ global $DB; $query = [ 'FROM' => $this->getTable() ]; if (count($fields) > 0) { $query['FIELDS'] = $fields; } if (count($order) > 0) { $query['ORDER'] = $order; } $iterator = $DB->request($query); return iterator_to_array($iterator, false); } /** * Uninstall a plugin * * @param integer $ID ID of the plugin (The `id` field, not directory) **/ public function uninstall($ID) { $message = ''; $type = ERROR; if ($this->getFromDB($ID)) { CronTask::Unregister($this->fields['directory']); self::load($this->fields['directory'], true); // Force load in case plugin is not active FieldUnicity::deleteForItemtype($this->fields['directory']); Link_Itemtype::deleteForItemtype($this->fields['directory']); // Run the Plugin's Uninstall Function first $function = 'plugin_' . $this->fields['directory'] . '_uninstall'; if (function_exists($function)) { $function(); } else { Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s has no uninstall function!'), $this->fields['name']), true, WARNING ); } $this->update([ 'id' => $ID, 'state' => self::NOTINSTALLED, ]); $this->unload($this->fields['directory']); $this->resetHookableCacheEntries($this->fields['directory']); self::doHook(Hooks::POST_PLUGIN_UNINSTALL, $this->fields['directory']); $type = INFO; $message = sprintf(__('Plugin %1$s has been uninstalled!'), $this->fields['name']); Event::log( '', Plugin::class, 3, "setup", sprintf( __('Plugin %1$s has been uninstalled by %2$s.'), $this->fields['name'], User::getNameForLog(Session::getLoginUserID(true)) ) ); } else { $message = sprintf(__('Plugin %1$s not found!'), $ID); } Session::addMessageAfterRedirect( $message, true, $type ); } /** * Install a plugin * * @param integer $ID ID of the plugin (The `id` field, not directory) * @param array $params Additional params to pass to install hook. * * @return void * * @since 9.5.0 Added $param parameter **/ public function install($ID, array $params = []) { /** @var \DBmysql $DB */ global $DB; $message = ''; $type = ERROR; if ($this->getFromDB($ID)) { // Clear locale cache to prevent errors while reloading plugin locales (new CacheManager())->getTranslationsCacheInstance()->clear(); self::load($this->fields['directory'], true); // Load plugin hooks $install_function = 'plugin_' . $this->fields['directory'] . '_install'; if (function_exists($install_function)) { $DB->disableTableCaching(); //prevents issues on table/fieldExists upgrading from old versions if ($install_function($params)) { $type = INFO; $check_function = 'plugin_' . $this->fields['directory'] . '_check_config'; $is_config_ok = !function_exists($check_function) || $check_function(); if ($is_config_ok) { $this->update(['id' => $ID, 'state' => self::NOTACTIVATED ]); $message = sprintf(__('Plugin %1$s has been installed!'), $this->fields['name']); $message .= '<br/><br/>' . str_replace( '%activate_link', Html::getSimpleForm( static::getFormURL(), ['action' => 'activate'], mb_strtolower(_x('button', 'Enable')), ['id' => $ID], '', 'class="pointer"' ), __('Do you want to %activate_link it?') ); } else { $this->update(['id' => $ID, 'state' => self::TOBECONFIGURED ]); $message = sprintf(__('Plugin %1$s has been installed and must be configured!'), $this->fields['name']); } $this->resetHookableCacheEntries($this->fields['directory']); self::doHook(Hooks::POST_PLUGIN_UNINSTALL, $this->fields['directory']); Event::log( '', Plugin::class, 3, "setup", sprintf( __('Plugin %1$s has been installed by %2$s.'), $this->fields['name'], User::getNameForLog(Session::getLoginUserID(true)) ) ); } } else { $type = WARNING; $message = sprintf(__('Plugin %1$s has no install function!'), $this->fields['name']); } } else { $message = sprintf(__('Plugin %1$s not found!'), $ID); } Session::addMessageAfterRedirect( $message, true, $type ); } /** * activate a plugin * * @param integer $ID ID of the plugin (The `id` field, not directory) * * @return boolean about success **/ public function activate($ID) { /** @var array $PLUGIN_HOOKS */ global $PLUGIN_HOOKS; if ($this->getFromDB($ID)) { // Enable autoloader and load plugin hooks self::load($this->fields['directory'], true); // No activation if not CSRF compliant if ( !isset($PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT][$this->fields['directory']]) || !$PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT][$this->fields['directory']] ) { Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s is not CSRF compliant!'), $this->fields['name']), true, ERROR ); return false; } $function = 'plugin_' . $this->fields['directory'] . '_check_prerequisites'; if (function_exists($function)) { ob_start(); $do_activate = $function(); $msg = ''; if (!$do_activate) { $msg = '<span class="error">' . ob_get_contents() . '</span>'; } ob_end_clean(); if (!$do_activate) { $this->unload($this->fields['directory']); Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s prerequisites are not matching, it cannot be activated.'), $this->fields['name']) . ' ' . $msg, true, ERROR ); return false; } } $function = 'plugin_' . $this->fields['directory'] . '_check_config'; if (!function_exists($function) || $function()) { $activate_function = 'plugin_' . $this->fields['directory'] . '_activate'; if (function_exists($activate_function)) { $activate_function(); } $this->update(['id' => $ID, 'state' => self::ACTIVATED ]); $this->resetHookableCacheEntries($this->fields['directory']); // Initialize session for the plugin if ( isset($PLUGIN_HOOKS[Hooks::INIT_SESSION][$this->fields['directory']]) && is_callable($PLUGIN_HOOKS[Hooks::INIT_SESSION][$this->fields['directory']]) ) { call_user_func($PLUGIN_HOOKS[Hooks::INIT_SESSION][$this->fields['directory']]); } // Initialize profile for the plugin if ( isset($PLUGIN_HOOKS[Hooks::CHANGE_PROFILE][$this->fields['directory']]) && is_callable($PLUGIN_HOOKS[Hooks::CHANGE_PROFILE][$this->fields['directory']]) && isset($_SESSION['glpiactiveprofile']) ) { call_user_func($PLUGIN_HOOKS[Hooks::CHANGE_PROFILE][$this->fields['directory']]); } // reset menu if (isset($_SESSION['glpimenu'])) { unset($_SESSION['glpimenu']); } self::doHook(Hooks::POST_PLUGIN_ENABLE, $this->fields['directory']); Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s has been activated!'), $this->fields['name']), true, INFO ); Event::log( '', Plugin::class, 3, "setup", sprintf( __('Plugin %1$s has been activated by %2$s.'), $this->fields['name'], User::getNameForLog(Session::getLoginUserID(true)) ) ); return true; } else { $this->unload($this->fields['directory']); Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s configuration must be done, it cannot be activated.'), $this->fields['name']), true, ERROR ); return false; } } Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s not found!'), $ID), true, ERROR ); return false; } /** * Unactivate a plugin * * @param integer $ID ID of the plugin (The `id` field, not directory) * * @return boolean **/ public function unactivate($ID) { if ($this->getFromDB($ID)) { // Load plugin hooks self::load($this->fields['directory'], true); $deactivate_function = 'plugin_' . $this->fields['directory'] . '_deactivate'; if (function_exists($deactivate_function)) { $deactivate_function(); } $this->update([ 'id' => $ID, 'state' => self::NOTACTIVATED ]); $this->unload($this->fields['directory']); $this->resetHookableCacheEntries($this->fields['directory']); self::doHook(Hooks::POST_PLUGIN_DISABLE, $this->fields['directory']); // reset menu if (isset($_SESSION['glpimenu'])) { unset($_SESSION['glpimenu']); } Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s has been deactivated!'), $this->fields['name']), true, INFO ); Event::log( '', Plugin::class, 3, "setup", sprintf( __('Plugin %1$s has been deactivated by %2$s.'), $this->fields['name'], User::getNameForLog(Session::getLoginUserID(true)) ) ); return true; } Session::addMessageAfterRedirect( sprintf(__('Plugin %1$s not found!'), $ID), true, ERROR ); return false; } /** * Unactivate all activated plugins for update process. * This will prevent any plugin class to be available through autoloader. **/ public function unactivateAll() { /** @var \DBmysql $DB */ global $DB; $DB->update( $this->getTable(), [ 'state' => self::NOTACTIVATED ], [ 'state' => self::ACTIVATED ] ); $dirs = array_keys(self::$activated_plugins); foreach ($dirs as $dir) { self::doHook(Hooks::POST_PLUGIN_DISABLE, $dir); } self::$activated_plugins = []; self::$loaded_plugins = []; // reset menu if (isset($_SESSION['glpimenu'])) { unset($_SESSION['glpimenu']); } Event::log( '', Plugin::class, 3, "setup", __('All plugins have been disabled.') ); } /** * clean a plugin * * @param $ID ID of the plugin **/ public function clean($ID) { if ($this->getFromDB($ID)) { $this->unload($this->fields['directory']); $log_message = sprintf(__('Plugin %1$s cleaned!'), $this->fields['directory']); self::doHook(Hooks::POST_PLUGIN_CLEAN, $this->fields['directory']); $this->delete(['id' => $ID]); Event::log( '', Plugin::class, 3, "setup", $log_message ); } } /** * Is a plugin activated ? * * @param string $directory Plugin directory * * @return boolean */ public function isActivated($directory) { if (!self::$plugins_state_checked) { // Plugins are not actually loaded/activated before plugins state checks, // and so $activated_plugins will be empty. // In this case, plugins states have to be fetched from DB. $self = new self(); return $self->getFromDBbyDir($directory) && $self->fields['state'] == self::ACTIVATED && $self->isLoadable($directory); } // Make a lowercase comparison, as sometime this function is called based on // extraction of plugin name from a classname, which does not use same naming rules than directories. $activated_plugins = array_map('strtolower', self::$activated_plugins); $directory = strtolower($directory); return in_array($directory, $activated_plugins); } /** * Is a plugin updatable ? * * @param string $directory Plugin directory * * @return boolean */ public function isUpdatable($directory) { // Make a lowercase comparison, as sometime this function is called based on // extraction of plugin name from a classname, which does not use same naming rules than directories. $activated_plugins = array_map('strtolower', self::$activated_plugins); if (in_array(strtolower($directory), $activated_plugins)) { // If plugin is marked as activated, no need to query DB on this case. return false; } // If plugin is not marked as activated, check on DB as it may have not been loaded yet. if ($this->getFromDBbyDir($directory)) { return ($this->fields['state'] == self::NOTUPDATED) && $this->isLoadable($directory); } return false; } /** * Is a plugin loadable ? * * @param string $directory Plugin directory * * @return boolean */ public function isLoadable($directory) { return !empty($this->getInformationsFromDirectory($directory, false)); } /** * Is a plugin installed ? * * @param string $directory Plugin directory * * @return boolean */ public function isInstalled($directory) { if ($this->isActivated($directory)) { // If plugin is activated, it is de facto installed. // No need to query DB on this case. return true; } if ($this->getFromDBbyDir($directory)) { return $this->isLoadable($directory) && in_array($this->fields['state'], [self::ACTIVATED, self::TOBECONFIGURED, self::NOTACTIVATED]); } return false; } /** * Migrate itemtype from integer (0.72) to string (0.80) * * @param array $types Array of (num=>name) of type manage by the plugin * @param array $glpitables Array of GLPI table name used by the plugin * @param array $plugtables Array of Plugin table name which have an itemtype * * @return void **/ public static function migrateItemType($types = [], $glpitables = [], $plugtables = []) { /** @var \DBmysql $DB */ global $DB; $typetoname = [0 => "",// For tickets 1 => "Computer", 2 => "NetworkEquipment", 3 => "Printer", 4 => "Monitor", 5 => "Peripheral", 6 => "Software", 7 => "Contact", 8 => "Supplier", 9 => "Infocom", 10 => "Contract", 11 => "CartridgeItem", 12 => "DocumentType", 13 => "Document", 14 => "KnowbaseItem", 15 => "User", 16 => "Ticket", 17 => "ConsumableItem", 18 => "Consumable", 19 => "Cartridge", 20 => "SoftwareLicense", 21 => "Link", 22 => "State", 23 => "Phone", 24 => "Device", 25 => "Reminder", 26 => "Stat", 27 => "Group", 28 => "Entity", 29 => "ReservationItem", 30 => "AuthMail", 31 => "AuthLDAP", 32 => "OcsServer", 33 => "RegistryKey", 34 => "Profile", 35 => "MailCollector", 36 => "Rule", 37 => "Transfer", 38 => "SavedSearch", 39 => "SoftwareVersion", 40 => "Plugin", 41 => "Item_Disk", 42 => "NetworkPort", 43 => "TicketFollowup", 44 => "Budget" ]; // Filter tables that does not exists or does not contains an itemtype field. // This kind of case exist when current method is called from plugins that based their // logic on an old GLPI datamodel that may have changed upon time. // see https://github.com/pluginsGLPI/order/issues/111 $glpitables = array_filter( $glpitables, function ($table) use ($DB) { return $DB->tableExists($table) && $DB->fieldExists($table, 'itemtype'); } ); //Add plugins types $typetoname = self::doHookFunction(Hooks::MIGRATE_TYPES, $typetoname); foreach ($types as $num => $name) { $typetoname[$num] = $name; foreach ($glpitables as $table) { $DB->updateOrDie( $table, [ 'itemtype' => $name, ], [ 'itemtype' => $num ], "update itemtype of table $table for $name" ); } } if (in_array('glpi_infocoms', $glpitables) && count($types)) { $entities = getAllDataFromTable('glpi_entities'); $entities[0] = "Root"; foreach ($types as $num => $name) { $itemtable = getTableForItemType($name); if (!$DB->tableExists($itemtable)) { // Just for security, shouldn't append continue; } $do_recursive = false; if ($DB->fieldExists($itemtable, 'is_recursive')) { $do_recursive = true; } foreach ($entities as $entID => $val) { if ($do_recursive) { // Non recursive ones $sub_query = new \QuerySubQuery([ 'SELECT' => 'id', 'FROM' => $itemtable, 'WHERE' => [ 'entities_id' => $entID, 'is_recursive' => 0 ] ]); $DB->updateOrDie( 'glpi_infocoms', [ 'entities_id' => $entID, 'is_recursive' => 0 ], [ 'itemtype' => $name, 'items_id' => $sub_query ], "update entities_id and is_recursive=0 in glpi_infocoms for $name" ); // Recursive ones $sub_query = new \QuerySubQuery([ 'SELECT' => 'id', 'FROM' => $itemtable, 'WHERE' => [ 'entities_id' => $entID, 'is_recursive' => 1 ] ]); $DB->updateOrDie( 'glpi_infocoms', [ 'entities_id' => $entID, 'is_recursive' => 1 ], [ 'itemtype' => $name, 'items_id' => $sub_query ], "update entities_id and is_recursive=1 in glpi_infocoms for $name" ); } else { $sub_query = new \QuerySubQuery([ 'SELECT' => 'id', 'FROM' => $itemtable, 'WHERE' => [ 'entities_id' => $entID, ] ]); $DB->updateOrDie( 'glpi_infocoms', [ 'entities_id' => $entID ], [ 'itemtype' => $name, 'items_id' => $sub_query ], "update entities_id in glpi_infocoms for $name" ); } } } } foreach ($typetoname as $num => $name) { foreach ($plugtables as $table) { $DB->updateOrDie( $table, [ 'itemtype' => $name ], [ 'itemtype' => $num ], "update itemtype of table $table for $name" ); } } } /** * @param integer $width **/ public function showSystemInformations($width) { // No need to translate, this part always display in english (for copy/paste to forum) echo "\n<tr class='tab_bg_2'><th class='section-header'>Plugins list</th></tr>"; echo "<tr class='tab_bg_1'><td><pre class='section-content'>\n \n"; $plug = new Plugin(); $pluglist = $plug->find([], "name, directory"); foreach ($pluglist as $plugin) { $name = Toolbox::stripTags($plugin['name']); $version = Toolbox::stripTags($plugin['version']); $state = $plug->isLoadable($plugin['directory']) ? $plugin['state'] : self::TOBECLEANED; $state = self::getState($state); $is_marketplace = file_exists(GLPI_MARKETPLACE_DIR . "/" . $plugin['directory']); $install_method = $is_marketplace ? "Marketplace" : "Manual"; $msg = substr(str_pad($plugin['directory'], 30), 0, 20) . " Name: " . Toolbox::substr(str_pad($name, 40), 0, 30) . " Version: " . str_pad($version, 10) . " State: " . str_pad($state, 40) . " Install Method: " . $install_method; echo wordwrap("\t" . $msg . "\n", $width, "\n\t\t"); } echo "\n</pre></td></tr>"; } /** * Define a new class managed by a plugin * * @param string $itemtype Class name * @param array $attrib Array of attributes, a hashtable with index in * (classname, typename, reservation_types) * * @return bool **/ public static function registerClass($itemtype, $attrib = []) { /** @var array $CFG_GLPI */ global $CFG_GLPI; $plug = isPluginItemType($itemtype); if (!$plug) { return false; } $all_types = preg_grep('/.+_types/', array_keys($CFG_GLPI)); $all_types[] = 'networkport_instantiations'; $blacklist = ['device_types']; foreach ($all_types as $att) { if (in_array($att, $blacklist) || !isset($attrib[$att])) { continue; } if ($attrib[$att]) { $CFG_GLPI[$att][] = $itemtype; } unset($attrib[$att]); } if ( isset($attrib['device_types']) && $attrib['device_types'] && method_exists($itemtype, 'getItem_DeviceType') ) { if (class_exists($itemtype::getItem_DeviceType())) { $CFG_GLPI['device_types'][] = $itemtype; } unset($attrib['device_types']); } if (isset($attrib['addtabon'])) { if (!is_array($attrib['addtabon'])) { $attrib['addtabon'] = [$attrib['addtabon']]; } foreach ($attrib['addtabon'] as $form) { CommonGLPI::registerStandardTab($form, $itemtype); } unset($attrib['addtabon']); } //Manage entity forward from a source itemtype to this itemtype if (isset($attrib['forwardentityfrom'])) { CommonDBTM::addForwardEntity($attrib['forwardentityfrom'], $itemtype); unset($attrib['forwardentityfrom']); } // Handle plugins specific configurations foreach ($attrib as $key => $value) { if (preg_match('/^plugin[a-z]+_types$/', $key)) { if ($value) { if (!array_key_exists($key, $CFG_GLPI)) { $CFG_GLPI[$key] = []; } $CFG_GLPI[$key][] = $itemtype; } unset($attrib[$key]); } } // Warn for unmanaged keys if (!empty($attrib)) { trigger_error( sprintf( 'Unknown attributes "%s" used in "%s" class registration', implode('", "', array_keys($attrib)), $itemtype ), E_USER_WARNING ); } return true; } /** * This function executes a hook. * * @param string $name Name of hook to fire * @param mixed $param Parameters if needed : if object limit to the itemtype (default NULL) * * @return mixed $data **/ public static function doHook($name, $param = null) { /** @var array $PLUGIN_HOOKS */ global $PLUGIN_HOOKS; if ($param == null) { $data = func_get_args(); } else { $data = $param; } // Apply hook only for the item if (($param != null) && is_object($param)) { $itemtype = get_class($param); if (isset($PLUGIN_HOOKS[$name]) && is_array($PLUGIN_HOOKS[$name])) { foreach ($PLUGIN_HOOKS[$name] as $plugin_key => $tab) { if (!Plugin::isPluginActive($plugin_key)) { continue; } if (isset($tab[$itemtype])) { \Glpi\Debug\Profiler::getInstance()->start("{$plugin_key}:{$name}", \Glpi\Debug\Profiler::CATEGORY_PLUGINS); self::includeHook($plugin_key); if (is_callable($tab[$itemtype])) { call_user_func($tab[$itemtype], $data); } \Glpi\Debug\Profiler::getInstance()->stop("{$plugin_key}:{$name}"); } } } } else { // Standard hook call if (isset($PLUGIN_HOOKS[$name]) && is_array($PLUGIN_HOOKS[$name])) { foreach ($PLUGIN_HOOKS[$name] as $plugin_key => $function) { if (!Plugin::isPluginActive($plugin_key)) { continue; } \Glpi\Debug\Profiler::getInstance()->start("{$plugin_key}:{$name}", \Glpi\Debug\Profiler::CATEGORY_PLUGINS); self::includeHook($plugin_key); if (is_callable($function)) { call_user_func($function, $data); } \Glpi\Debug\Profiler::getInstance()->stop("{$plugin_key}:{$name}"); } } } /* Variable-length argument lists have a slight problem when */ /* passing values by reference. Pity. This is a workaround. */ return $data; } /** * This function executes a hook. * * @param string $name Name of hook to fire * @param mixed $parm Parameters (default NULL) * * @return mixed $data **/ public static function doHookFunction($name, $parm = null) { /** @var array $PLUGIN_HOOKS */ global $PLUGIN_HOOKS; $ret = $parm; if (isset($PLUGIN_HOOKS[$name]) && is_array($PLUGIN_HOOKS[$name])) { foreach ($PLUGIN_HOOKS[$name] as $plugin_key => $function) { if (!Plugin::isPluginActive($plugin_key)) { continue; } self::includeHook($plugin_key); if (is_callable($function)) { $ret = call_user_func($function, $ret); } } } /* Variable-length argument lists have a slight problem when */ /* passing values by reference. Pity. This is a workaround. */ return $ret; } /** * This function executes a hook for 1 plugin. * * @param string $plugin_key System name of the plugin * @param string|callable $hook suffix used to build function to be called ("plugin_myplugin_{$hook}") * or callable function * @param mixed ...$args [optional] One or more arguments passed to hook function * * @return mixed $data **/ public static function doOneHook($plugin_key, $hook, ...$args) { $plugin_key = strtolower($plugin_key); if (!Plugin::isPluginActive($plugin_key)) { return; } self::includeHook($plugin_key); if (is_string($hook) && !is_callable($hook)) { $hook = "plugin_" . $plugin_key . "_" . $hook; } if (is_callable($hook)) { return call_user_func_array($hook, $args); } } /** * Get dropdowns for plugins * * @return array Array containing plugin dropdowns **/ public static function getDropdowns() { $dps = []; foreach (self::getPlugins() as $plug) { $tab = self::doOneHook($plug, 'getDropdown'); if (is_array($tab)) { $dps = array_merge($dps, [self::getInfo($plug, 'name') => $tab]); } } return $dps; } /** * Get information from a plugin * * @param string $plugin System name (Plugin directory) * @param string $info Wanted info (name, version, ...), NULL for all * * @since 0.84 * * @return string|array The specific information value requested or an array of all information if $info is null. **/ public static function getInfo($plugin, $info = null) { $fct = 'plugin_version_' . strtolower($plugin); if (function_exists($fct)) { $res = $fct(); if (!isset($res['requirements']) && isset($res['minGlpiVersion'])) { $res['requirements'] = ['glpi' => ['min' => $res['minGlpiVersion']]]; } } else { trigger_error("$fct method must be defined!", E_USER_WARNING); $res = []; } if (isset($info)) { return (isset($res[$info]) ? $res[$info] : ''); } return $res; } /** * Get plugin files version. * * @param string $key * * @return string|null */ public static function getPluginFilesVersion(string $key): ?string { return (new self())->getInformationsFromDirectory($key, false)['version'] ?? null; } /** * Returns plugin information from directory. * * @param string $directory * @param bool $with_lang * * @return array */ public function getInformationsFromDirectory($directory, bool $with_lang = true) { if (!$this->loadPluginSetupFile($directory)) { return []; } if ($with_lang) { self::loadLang($directory); } return Toolbox::addslashes_deep(self::getInfo($directory)); } /** * Returns plugin options. * * @param string $plugin_key * * @return array */ public function getPluginOptions(string $plugin_key): array { if (!$this->loadPluginSetupFile($plugin_key)) { return []; } $options_callable = sprintf('plugin_%s_options', $plugin_key); if (!function_exists($options_callable)) { return []; } $options = $options_callable(); if (!is_array($options)) { trigger_error( sprintf('Invalid "options" key provided by plugin `plugin_%s_options()` method.', $plugin_key), E_USER_WARNING ); return []; } return $options; } /** * Returns plugin option. * * @param string $plugin_key * @param string $option_key * @param mixed $default_value * * @return array */ public function getPluginOption(string $plugin_key, string $option_key, $default_value = null)//: mixed { $options = $this->getPluginOptions($plugin_key); return array_key_exists($option_key, $options) ? $options[$option_key] : $default_value; } /** * Load plugin setup file. * * @param string $plugin_key * * @return bool */ private function loadPluginSetupFile(string $plugin_key): bool { if (preg_match(self::PLUGIN_KEY_PATTERN, $plugin_key) !== 1) { // Prevent issues with illegal chars return false; } foreach (PLUGINS_DIRECTORIES as $base_dir) { if (!is_dir($base_dir)) { continue; } $file_path = sprintf('%s/%s/setup.php', $base_dir, $plugin_key); if (file_exists($file_path)) { // Includes are made inside a function to prevent included files to override // variables used in this function. // For example, if the included files contains a $key variable, it will // replace the $key variable used here. $include_fct = function () use ($file_path) { include_once($file_path); }; $include_fct(); return true; } } return false; } /** * Get database relations for plugins * * @return array Array containing plugin database relations **/ public static function getDatabaseRelations() { $dps = []; foreach (self::getPlugins() as $plugin_key) { self::includeHook($plugin_key); $function2 = "plugin_" . $plugin_key . "_getDatabaseRelations"; if (function_exists($function2)) { $dps = array_merge_recursive($dps, $function2()); } } return $dps; } /** * Get additional search options managed by plugins * * @param $itemtype * * @return array Array containing plugin search options for given type **/ public static function getAddSearchOptions($itemtype) { $sopt = []; foreach (self::getPlugins() as $plugin_key) { self::includeHook($plugin_key); $function = "plugin_" . $plugin_key . "_getAddSearchOptions"; if (function_exists($function)) { $tmp = $function($itemtype); if (is_array($tmp) && count($tmp)) { $sopt += $tmp; } } } return $sopt; } /** * Include the hook file for a plugin * * @param string $plugin_key */ public static function includeHook(string $plugin_key = "") { foreach (PLUGINS_DIRECTORIES as $base_dir) { if (file_exists("$base_dir/$plugin_key/hook.php")) { include_once("$base_dir/$plugin_key/hook.php"); break; } } } /** * Get additional search options managed by plugins * * @since 9.2 * * @param string $itemtype Item type * * @return array An *indexed* array of search options * * @see https://glpi-developer-documentation.rtfd.io/en/master/devapi/search.html **/ public static function getAddSearchOptionsNew($itemtype) { $options = []; foreach (self::getPlugins() as $plugin_key) { self::includeHook($plugin_key); $function = "plugin_" . $plugin_key . "_getAddSearchOptionsNew"; if (function_exists($function)) { $tmp = $function($itemtype); foreach ($tmp as $opt) { if (!isset($opt['id'])) { throw new \Exception($itemtype . ': invalid search option! ' . print_r($opt, true)); } $optid = $opt['id']; unset($opt['id']); if (isset($options[$optid])) { $message = "Duplicate key $optid ({$options[$optid]['name']}/{$opt['name']}) in " . $itemtype . " searchOptions!"; trigger_error($message, E_USER_WARNING); } foreach ($opt as $k => $v) { $options[$optid][$k] = $v; } } } } return $options; } /** * Check if there is a plugin enabled that supports importing items * * @return boolean * * @since 0.84 **/ public static function haveImport() { /** @var array $PLUGIN_HOOKS */ global $PLUGIN_HOOKS; return (isset($PLUGIN_HOOKS['import_item']) && count($PLUGIN_HOOKS['import_item'])); } /** * Get an internationalized message for incompatible plugins (either core or php version) * * @param string $type Either 'php' or 'core', defaults to 'core' * @param string $min Minimal required version * @param string $max Maximal required version * * @since 9.2 * * @return string */ public static function messageIncompatible($type = 'core', $min = null, $max = null) { $type = ($type === 'core' ? __('GLPI') : __('PHP')); if ($min === null && $max !== null) { return sprintf( __('This plugin requires %1$s < %2$s.'), $type, $max ); } else if ($min !== null && $max === null) { return sprintf( __('This plugin requires %1$s >= %2$s.'), $type, $min ); } else { return sprintf( __('This plugin requires %1$s >= %2$s and < %3$s.'), $type, $min, $max ); } } /** * Get an internationalized message for missing requirement (extension, other plugin, ...) * * @param string $type Type of what is missing, one of: * - ext (PHP module) * - plugin (other plugin) * - compil (compilation option) * - param (GLPI configuration parameter) * @param string $name Missing name * * @since 9.2 * * @return string */ public static function messageMissingRequirement($type, $name) { switch ($type) { case 'ext': return sprintf( __('This plugin requires PHP extension %1$s'), $name ); break; case 'plugin': return sprintf( __('This plugin requires %1$s plugin'), $name ); break; case 'compil': return sprintf( __('This plugin requires PHP compiled along with "%1$s"'), $name ); break; case 'param': return sprintf( __('This plugin requires PHP parameter %1$s'), $name ); break; case 'glpiparam': return sprintf( __('This plugin requires GLPI parameter %1$s'), $name ); break; default: throw new \RuntimeException("messageMissing type $type is unknown!"); } } /** * Check declared versions (GLPI, PHP, ...) * * @since 9.2 * * @param string $name System name (Plugin directory) * * @return boolean */ public function checkVersions($name) { $infos = self::getInfo($name); $ret = true; if (isset($infos['requirements'])) { if (isset($infos['requirements']['glpi'])) { $glpi = $infos['requirements']['glpi']; if (isset($glpi['min']) || isset($glpi['max'])) { $ret = $ret && $this->checkGlpiVersion($infos['requirements']['glpi']); } if (isset($glpi['params'])) { $ret = $ret && $this->checkGlpiParameters($glpi['params']); } if (isset($glpi['plugins'])) { $ret = $ret && $this->checkGlpiPlugins($glpi['plugins']); } } if (isset($infos['requirements']['php'])) { $php = $infos['requirements']['php']; if (isset($php['min']) || isset($php['max'])) { $ret = $ret && $this->checkPhpVersion($php); } if (isset($php['exts'])) { $ret = $ret && $this->checkPhpExtensions($php['exts']); } if (isset($php['params'])) { $ret = $ret && $this->checkPhpParameters($php['params']); } } } return $ret; } /** * Check for GLPI version * * @since 9.2 * @since 9.3 Removed the 'dev' key of $info parameter. * * @param array $infos Requirements infos: * - min: minimal supported version, * - max: maximal supported version * One of min or max is required. * * @return boolean */ public function checkGlpiVersion($infos) { if (!isset($infos['min']) && !isset($infos['max'])) { throw new \LogicException('Either "min" or "max" is required for GLPI requirements!'); } $glpiVersion = $this->getGlpiVersion(); $compat = true; if (isset($infos['min']) && !version_compare($glpiVersion, $infos['min'], '>=')) { $compat = false; } if (isset($infos['max']) && !version_compare($glpiVersion, $infos['max'], '<')) { $compat = false; } if (!$compat) { echo Plugin::messageIncompatible( 'core', (isset($infos['min']) ? $infos['min'] : null), (isset($infos['max']) ? $infos['max'] : null) ); } return $compat; } /** * Check for PHP version * * @since 9.2 * * @param array $infos Requirements infos: * - min: minimal supported version, * - max: maximal supported version. * One of min or max is required. * * @return boolean */ public function checkPhpVersion($infos) { $compat = true; if (isset($infos['min']) && isset($infos['max'])) { $compat = !(version_compare($this->getPhpVersion(), $infos['min'], 'lt') || version_compare($this->getPhpVersion(), $infos['max'], 'ge')); } else if (isset($infos['min'])) { $compat = !(version_compare($this->getPhpVersion(), $infos['min'], 'lt')); } else if (isset($infos['max'])) { $compat = !(version_compare($this->getPhpVersion(), $infos['max'], 'ge')); } else { throw new \LogicException('Either "min" or "max" is required for PHP requirements!'); } if (!$compat) { echo Plugin::messageIncompatible( 'php', (isset($infos['min']) ? $infos['min'] : null), (isset($infos['max']) ? $infos['max'] : null) ); } return $compat; } /** * Check fo required PHP extensions * * @since 9.2 * * @param array $exts Extensions lists/config @see Config::checkExtensions() * * @return boolean */ public function checkPhpExtensions($exts) { $report = Config::checkExtensions($exts); if (count($report['missing'])) { foreach (array_keys($report['missing']) as $ext) { echo self::messageMissingRequirement('ext', $ext) . '<br/>'; } return false; } return true; } /** * Check expected GLPI parameters * * @since 9.2 * * @param array $params Expected parameters to be setup * * @return boolean */ public function checkGlpiParameters($params) { /** @var array $CFG_GLPI */ global $CFG_GLPI; $compat = true; foreach ($params as $param) { if (!isset($CFG_GLPI[$param]) || trim($CFG_GLPI[$param]) == '' || !$CFG_GLPI[$param]) { echo self::messageMissingRequirement('glpiparam', $param) . '<br/>'; $compat = false; } } return $compat; } /** * Check expected PHP parameters * * @since 9.2 * * @param array $params Expected parameters to be setup * * @return boolean */ public function checkPhpParameters($params) { $compat = true; foreach ($params as $param) { if (!ini_get($param) || trim(ini_get($param)) == '') { echo self::messageMissingRequirement('param', $param) . '<br/>'; $compat = false; } } return $compat; } /** * Check expected GLPI plugins * * @since 9.2 * * @param array $plugins Expected plugins * * @return boolean */ public function checkGlpiPlugins($plugins) { $compat = true; foreach ($plugins as $plugin) { if (!$this->isActivated($plugin)) { echo self::messageMissingRequirement('plugin', $plugin) . '<br/>'; $compat = false; } } return $compat; } /** * Get GLPI version * Used from unit tests to mock. * * @since 9.2 * * @return string */ public function getGlpiVersion() { return VersionParser::getNormalizedVersion(GLPI_VERSION, false); } /** * Get PHP version * Used from unit tests to mock. * * @since 9.2 * * @return string */ public function getPhpVersion() { return PHP_VERSION; } /** * Return label for an integer plugin state * * @since 9.3 * * @param integer $state see this class constants (ex self::ANEW, self::ACTIVATED) * @return string the label */ public static function getState($state = 0) { switch ($state) { case self::ANEW: return _x('status', 'New'); case self::ACTIVATED: return _x('plugin', 'Enabled'); case self::NOTINSTALLED: return _x('plugin', 'Not installed'); case self::NOTUPDATED: return __('To update'); case self::TOBECONFIGURED: return _x('plugin', 'Installed / not configured'); case self::NOTACTIVATED: return _x('plugin', 'Installed / not activated'); case self::REPLACED: return _x('plugin', 'Replaced'); } return __('Error / to clean'); } /** * Return key for an integer plugin state * purpose is to have a corresponding css class name * * @since 9.5 * * @param integer $state see this class constants (ex self::ANEW, self::ACTIVATED) * @return string the key */ public static function getStateKey(int $state = 0): string { switch ($state) { case self::ANEW: return "new"; case self::ACTIVATED: return "activated"; case self::NOTINSTALLED: return "notinstalled"; case self::NOTUPDATED: return "notupdated"; case self::TOBECONFIGURED: return "tobeconfigured"; case self::NOTACTIVATED: return "notactived"; } return ""; } /** * Get plugins list * * @since 9.3.2 * * @return array */ public static function getPlugins() { return self::$activated_plugins; } /** * Check if a plugin is loaded * * @since 9.3.2 * * @param string $plugin_key System name (Plugin directory) * * @return boolean */ public static function isPluginLoaded($plugin_key) { // Make a lowercase comparison, as sometime this function is called based on // extraction of plugin name from a classname, which does not use same naming rules than directories. $loadedPlugins = array_map('strtolower', self::$loaded_plugins); return in_array(strtolower($plugin_key), $loadedPlugins); } /** * Check if a plugin is active * * @since 9.5.0 * * @param string $plugin_key System name (Plugin directory) * * @return boolean */ public static function isPluginActive($plugin_key) { $plugin = new self(); return $plugin->isActivated($plugin_key); } public function rawSearchOptions() { $tab = []; $tab[] = [ 'id' => 'common', 'name' => __('Characteristics') ]; $tab[] = [ 'id' => '1', 'table' => $this->getTable(), 'field' => 'name', 'name' => __('Name'), 'datatype' => 'specific', 'massiveaction' => false, // implicit key==1 'additionalfields' => ['state', 'directory'], ]; $tab[] = [ 'id' => '2', 'table' => $this->getTable(), 'field' => 'directory', 'name' => __('Directory'), 'massiveaction' => false, 'nosearch' => true, 'noremove' => true ]; $tab[] = [ 'id' => '3', 'table' => $this->getTable(), 'field' => 'version', 'name' => _n('Version', 'Versions', 1), 'datatype' => 'specific', 'massiveaction' => false ]; $tab[] = [ 'id' => '4', 'table' => $this->getTable(), 'field' => 'license', 'name' => SoftwareLicense::getTypeName(1), 'datatype' => 'specific', 'massiveaction' => false ]; $tab[] = [ 'id' => '5', 'table' => $this->getTable(), 'field' => 'state', 'name' => __('Status'), 'searchtype' => 'equals', 'noremove' => true, 'additionalfields' => ['directory'], ]; $tab[] = [ 'id' => '6', 'table' => $this->getTable(), 'field' => 'author', 'name' => __('Authors'), 'datatype' => 'specific', ]; $tab[] = [ 'id' => '7', 'table' => $this->getTable(), 'field' => 'homepage', 'name' => __('Website'), 'datatype' => 'specific' ]; $tab[] = [ 'id' => '8', 'table' => $this->getTable(), 'field' => 'id', 'name' => __('Actions'), 'massiveaction' => false, 'nosearch' => true, 'datatype' => 'specific', 'noremove' => true, 'additionalfields' => ['directory'] ]; return $tab; } public static function getSpecificValueToDisplay($field, $values, array $options = []) { /** * @var array $CFG_GLPI * @var array $PLUGIN_HOOKS */ global $CFG_GLPI, $PLUGIN_HOOKS; if (!is_array($values)) { $values = [$field => $values]; } switch ($field) { case 'id': //action... $ID = $values[$field]; $plugin = new self(); $plugin->getFromDB($ID); $directory = $plugin->fields['directory']; $state = (int)$plugin->fields['state']; if ($plugin->isLoadable($directory)) { self::load($directory, true); } else { $state = self::TOBECLEANED; } $output = ''; if ( in_array($state, [self::ACTIVATED, self::TOBECONFIGURED], true) && isset($PLUGIN_HOOKS['config_page'][$directory]) ) { // Configuration button for activated or configurable plugins $plugin_dir = self::getWebDir($directory, true); $config_url = "$plugin_dir/" . $PLUGIN_HOOKS['config_page'][$directory]; $output .= '<a href="' . $config_url . '" title="' . __s('Configure') . '">' . '<i class="fas fa-wrench fa-2x"></i>' . '<span class="sr-only">' . __s('Configure') . '</span>' . '</a>' . ' '; } if ($state === self::ACTIVATED) { // Deactivate button for active plugins $output .= Html::getSimpleForm( static::getFormURL(), ['action' => 'unactivate'], _x('button', 'Disable'), ['id' => $ID], 'fa-fw fa-toggle-on fa-2x enabled' ) . ' '; } else if ($state === self::NOTACTIVATED) { // Activate button for configured and up to date plugins ob_start(); $do_activate = $plugin->checkVersions($directory); if (!$do_activate) { $output .= "<span class='error'>" . ob_get_contents() . "</span>"; } ob_end_clean(); $function = 'plugin_' . $directory . '_check_prerequisites'; if ( !isset($PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT][$directory]) || !$PLUGIN_HOOKS[Hooks::CSRF_COMPLIANT][$directory] ) { $output .= "<span class='error'>" . __('Not CSRF compliant') . "</span>"; $do_activate = false; } else if (function_exists($function) && $do_activate) { ob_start(); $do_activate = $function(); if (!$do_activate) { $output .= '<span class="error">' . ob_get_contents() . '</span>'; } ob_end_clean(); } if ($do_activate) { $output .= Html::getSimpleForm( static::getFormURL(), ['action' => 'activate'], _x('button', 'Enable'), ['id' => $ID], 'fa-fw fa-toggle-off fa-2x disabled' ) . ' '; } } if (in_array($state, [self::ANEW, self::NOTINSTALLED, self::NOTUPDATED], true)) { // Install button for new, not installed or not up to date plugins if (function_exists("plugin_" . $directory . "_install")) { $function = 'plugin_' . $directory . '_check_prerequisites'; ob_start(); $do_install = $plugin->checkVersions($directory); if (!$do_install) { $output .= "<span class='error'>" . ob_get_contents() . "</span>"; } ob_end_clean(); if ($do_install && function_exists($function)) { ob_start(); $do_install = $function(); $msg = ''; if (!$do_install) { $msg = '<span class="error">' . ob_get_contents() . '</span>'; } ob_end_clean(); $output .= $msg; } if ($state == self::NOTUPDATED) { $msg = _x('button', 'Upgrade'); } else { $msg = _x('button', 'Install'); } if ($do_install) { $output .= Html::getSimpleForm( static::getFormURL(), ['action' => 'install'], $msg, ['id' => $ID], 'fa-fw fa-folder-plus fa-2x me-1' ); } } else { $missing = ''; if (!function_exists("plugin_" . $directory . "_install")) { $missing .= "plugin_" . $directory . "_install"; } //TRANS: %s is the list of missing functions $output .= sprintf( __('%1$s: %2$s'), __('Non-existent function'), $missing ); } } if (in_array($state, [self::ACTIVATED, self::NOTUPDATED, self::TOBECONFIGURED, self::NOTACTIVATED], true)) { // Uninstall button for installed plugins if (function_exists("plugin_" . $directory . "_uninstall")) { $output .= TemplateRenderer::getInstance()->render('components/plugin_uninstall_modal.html.twig', [ 'plugin_name' => $plugin->getField('name'), 'modal_id' => 'uninstallModal' . $plugin->getField('directory'), 'open_btn' => '<a class="pointer"><span class="fas fa-fw fa-folder-minus fa-2x me-1" data-bs-toggle="modal" data-bs-target="#uninstallModal' . $plugin->getField('directory') . '" title="' . __s("Uninstall") . '"> <span class="sr-only">' . __s("Uninstall") . '</span> </span></a>', 'uninstall_btn' => Html::getSimpleForm( static::getFormURL(), ['action' => 'uninstall'], _x('button', 'Uninstall'), ['id' => $ID], '', 'class="btn btn-danger w-100"' ), ]); } else { //TRANS: %s is the list of missing functions $output .= sprintf( __('%1$s: %2$s'), __('Non-existent function'), "plugin_" . $directory . "_uninstall" ); } } else if ($state === self::TOBECLEANED) { $output .= Html::getSimpleForm( static::getFormURL(), ['action' => 'clean'], _x('button', 'Clean'), ['id' => $ID], 'fa-fw fas fa-broom fa-2x' ); } return "<div style='text-align:right'>$output</div>"; break; case 'state': $plugin = new self(); $state = $plugin->isLoadable($values['directory']) ? $values[$field] : self::TOBECLEANED; return self::getState($state); break; case 'homepage': $value = Html::entities_deep(Toolbox::formatOutputWebLink($values[$field])); if (!empty($value)) { return "<a href=\"" . $value . "\" target='_blank'> <i class='fas fa-external-link-alt fa-2x'></i><span class='sr-only'>$value</span> </a>"; } return " "; break; case 'name': $value = Toolbox::stripTags($values[$field]); $state = $values['state']; $directory = $values['directory']; self::load($directory); // Load plugin to give it ability to define its config_page hook if ( in_array($state, [self::ACTIVATED, self::TOBECONFIGURED]) && isset($PLUGIN_HOOKS['config_page'][$directory]) ) { $plugin_dir = self::getWebDir($directory, true); $config_url = "$plugin_dir/" . $PLUGIN_HOOKS['config_page'][$directory]; return "<a href='$config_url'><span class='b'>$value</span></a>"; } else { return $value; } break; case 'author': case 'license': case 'version': return $value = Toolbox::stripTags($values[$field]); break; } return parent::getSpecificValueToDisplay($field, $values, $options); } public static function getSpecificValueToSelect($field, $name = '', $values = '', array $options = []) { if (!is_array($values)) { $values = [$field => $values]; } $options['display'] = false; switch ($field) { case 'state': $tab = [ self::ANEW => _x('status', 'New'), self::ACTIVATED => _x('plugin', 'Enabled'), self::NOTINSTALLED => _x('plugin', 'Not installed'), self::NOTUPDATED => __('To update'), self::TOBECONFIGURED => _x('plugin', 'Installed / not configured'), self::NOTACTIVATED => _x('plugin', 'Installed / not activated'), self::TOBECLEANED => __('Error / to clean') ]; $options['value'] = $values[$field]; return Dropdown::showFromArray($name, $tab, $options); break; } return parent::getSpecificValueToSelect($field, $name, $values, $options); } public function getForbiddenStandardMassiveAction() { $forbidden = parent::getForbiddenStandardMassiveAction(); $forbidden[] = 'update'; $forbidden[] = 'clone'; $forbidden[] = 'purge'; return $forbidden; } /** * Return the system path for a given plugin key * * @since 9.5 * * @param string $plugin_key plugin system key * @param bool $full true for absolute path * * @return false|string the path */ public static function getPhpDir(string $plugin_key = "", $full = true) { $directory = false; foreach (PLUGINS_DIRECTORIES as $plugins_directory) { if (is_dir("$plugins_directory/$plugin_key")) { $directory = "$plugins_directory/$plugin_key"; break; } } if ($directory === false) { return false; } if (!$full) { $directory = str_replace(GLPI_ROOT, "", $directory); } return str_replace('\\', '/', $directory); } /** * Return the web path for a given plugin key * * @since 9.5 * * @param string $plugin_key plugin system key * @param bool $full if true, append root_doc from config * @param bool $use_url_base if true, url_base instead root_doc * * @return false|string the web path */ public static function getWebDir(string $plugin_key = "", $full = true, $use_url_base = false) { /** @var array $CFG_GLPI */ global $CFG_GLPI; $directory = self::getPhpDir($plugin_key, false); if ($directory === false) { return false; } $directory = ltrim($directory, '/\\'); if ($full) { $root = $use_url_base ? $CFG_GLPI['url_base'] : $CFG_GLPI["root_doc"]; $directory = "$root/$directory"; } return str_replace('\\', '/', $directory); } public static function getIcon() { return "ti ti-puzzle"; } public function getSpecificMassiveActions($checkitem = null) { $actions = []; if (Session::getCurrentInterface() === 'central' && Config::canUpdate()) { $actions[__CLASS__ . MassiveAction::CLASS_ACTION_SEPARATOR . 'install'] = "<i class='fas fa-code-branch'></i>" . __('Install'); $actions[__CLASS__ . MassiveAction::CLASS_ACTION_SEPARATOR . 'uninstall'] = "<i class='fas fa-code-branch'></i>" . __('Uninstall'); $actions[__CLASS__ . MassiveAction::CLASS_ACTION_SEPARATOR . 'enable'] = "<i class='fas fa-code-branch'></i>" . __('Enable'); $actions[__CLASS__ . MassiveAction::CLASS_ACTION_SEPARATOR . 'disable'] = "<i class='fas fa-code-branch'></i>" . __('Disable'); $actions[__CLASS__ . MassiveAction::CLASS_ACTION_SEPARATOR . 'clean'] = "<i class='fas fa-broom'></i>" . __('Clean'); } $actions += parent::getSpecificMassiveActions($checkitem); return $actions; } public static function showMassiveActionsSubForm(MassiveAction $ma) { switch ($ma->getAction()) { case 'install': echo "<table class='mx-auto'><tr>"; echo "<td colspan='4'>"; echo Html::submit(_x('button', 'Install'), [ 'name' => 'install', ]); echo "</td></tr></table>"; return true; case 'uninstall': echo "<table class='mx-auto'><tr>"; echo "<td>" . __('This will only affect plugins already installed') . "</td><td colspan='3'>"; echo Html::submit(_x('button', 'Uninstall'), [ 'name' => 'uninstall', ]); echo "</td></tr></table>"; return true; case 'enable': echo "<table class='mx-auto'><tr>"; echo "<td>" . __('This will only affect plugins already installed') . "</td><td colspan='3'>"; echo Html::submit(_x('button', 'Enable'), [ 'name' => 'enable', ]); echo "</td></tr></table>"; return true; case 'disable': echo "<table class='mx-auto'><tr>"; echo "<td>" . __('This will only affect plugins already enabled') . "</td><td colspan='3'>"; echo Html::submit(_x('button', 'Disable'), [ 'name' => 'disable', ]); echo "</td></tr></table>"; return true; case 'clean': echo "<table class='mx-auto'><tr>"; echo "<td>" . __('This will only affect plugins ready to be cleaned') . "</td><td colspan='3'>"; echo Html::submit(_x('button', 'Clean'), [ 'name' => 'clean', ]); echo "</td></tr></table>"; return true; } return parent::showMassiveActionsSubForm($ma); } public static function processMassiveActionsForOneItemtype( MassiveAction $ma, CommonDBTM $item, array $ids ) { $plugin = new self(); switch ($ma->getAction()) { case 'install': foreach ($ids as $id) { $plugin->getFromDB($id); if (!$plugin->isInstalled($plugin->fields['directory'])) { $plugin->install($id); if ($plugin->isInstalled($plugin->fields['directory'])) { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); } } else { $ma->itemDone($item->getType(), $id, MassiveAction::NO_ACTION); } } return; case 'uninstall': foreach ($ids as $id) { $plugin->getFromDB($id); if ($plugin->isInstalled($plugin->fields['directory'])) { $plugin->uninstall($id); if (!$plugin->isInstalled($plugin->fields['directory'])) { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); } } else { $ma->itemDone($item->getType(), $id, MassiveAction::NO_ACTION); } } return; case 'enable': foreach ($ids as $id) { $plugin->getFromDB($id); if ($plugin->isInstalled($plugin->fields['directory']) && !$plugin->isActivated($plugin->fields['directory'])) { $plugin->activate($id); if ($plugin->isActivated($plugin->fields['directory'])) { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); } } else { $ma->itemDone($item->getType(), $id, MassiveAction::NO_ACTION); } } return; case 'disable': foreach ($ids as $id) { $plugin->getFromDB($id); if ($plugin->isActivated($plugin->fields['directory'])) { $plugin->unactivate($id); if (!$plugin->isActivated($plugin->fields['directory'])) { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); } else { $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); } } else { $ma->itemDone($item->getType(), $id, MassiveAction::NO_ACTION); } } return; case 'clean': foreach ($ids as $id) { $plugin->getFromDB($id); if (!$plugin->isLoadable($plugin->fields['directory'])) { $plugin->clean($id); $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); } else { $ma->itemDone($item->getType(), $id, MassiveAction::NO_ACTION); } } return; } parent::processMassiveActionsForOneItemtype($ma, $item, $ids); } /** * Reset cache entries that may be indirectly altered by plugins. * * @param string $plugin_key * * @return bool */ private function resetHookableCacheEntries(string $plugin_key): bool { /** * @var array $CFG_GLPI * @var \Psr\SimpleCache\CacheInterface $GLPI_CACHE */ global $CFG_GLPI, $GLPI_CACHE; $to_clear = [ // Plugin lowercase/case-sensitive class names mapping. // see `DbUtils::fixItemtypeCase()` sprintf('itemtype-case-mapping-%s', $plugin_key), // Hookable using `$CFG_GLPI['*_types']` and `$CFG_GLPI['itemdevices_itemaffinity']`. 'item_device_affinities', // Will be stale as long as a plugin adds/remove a custom right. 'all_possible_rights', ]; foreach (array_keys($CFG_GLPI['languages']) as $language) { // Hookable using `$CFG_GLPI['itemdevices']`, `$CFG_GLPI['device_types']`, `$CFG_GLPI['asset_types']`, // and `Hooks::DASHBOARD_FILTERS`. $to_clear[] = Grid::getAllDashboardCardsCacheKey($language); } return $GLPI_CACHE->deleteMultiple($to_clear); } }