. * --------------------------------------------------------------------- */ namespace Glpi\Marketplace; if (!defined('GLPI_ROOT')) { die("Sorry. You can't access directly to this file"); } use Glpi\Marketplace\Api\Plugins as PluginsApi; use Glpi\Marketplace\Controller as Controller; use \Html; use \Plugin; use \Config; use \CommonGLPI; use \GLPINetwork; use \Toolbox; class View extends CommonGLPI { static $rightname = 'config'; static $api = null; public $get_item_to_display_tab = true; public const COL_PAGE = 12; /** * singleton return the current api instance * * @return PluginsApi */ static function getAPI(): PluginsApi { return self::$api ?? (self::$api = new PluginsApi()); } static function getTypeName($nb = 0) { return __('Marketplace'); } static function canCreate() { return self::canUpdate(); } static function getIcon() { return "fas fa-store"; } static function getSearchURL($full = true) { global $CFG_GLPI; $dir = ($full ? $CFG_GLPI['root_doc'] : ''); return "$dir/front/marketplace.php"; } function defineTabs($options = []) { $tabs = [ 'no_all_tab' => true ]; $this->addStandardTab(__CLASS__, $tabs, $options); return $tabs; } function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { if ($item->getType() == __CLASS__) { return [ self::createTabEntry(__("Installed")), self::createTabEntry(__("Discover")), ]; } return ''; } static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { if ($item->getType() == __CLASS__) { switch ($tabnum) { case 0: self::installed(); break; case 1: default: self::discover(); break; } } return true; } /** * Check current reigstration status and display warning messages * * @return bool */ static function checkRegister() { global $CFG_GLPI; $messages = []; $registered = false; if (!GLPINetwork::isServicesAvailable()) { array_push( $messages, sprintf(__("%1$s services website seems not available from your network or offline"), 'GLPI Network'), "". __("Maybe you could setup a proxy"). " ". __("or please check later") ); } else { $registered = GLPINetwork::isRegistered(); if (!$registered) { $config_url = $CFG_GLPI['root_doc']."/front/config.form.php?forcetab=". urlencode('GLPINetwork$1'); array_push( $messages, sprintf(__('Your %1$s registration is not valid.'), 'GLPI Network'), __('A registration, at least a free one, is required to use marketplace!'), "".sprintf(__('Register on %1$s'), 'GLPI Network')." ". __('and'). " ". "".__("fill your registration key in setup.")."" ); } } if (count($messages)) { echo "
"; echo ""; echo ""; echo "
"; echo "
"; } return $registered; } /** * Display installed tab (only currently installed plugins) * * @param bool $force_refresh do not rely on cache to get plugins list * @param bool $only_lis display only the li tags in return html (used by ajax queries) * @param string $tag_filter filter the plugin list by given tag * @param string $string_filter filter the plugin by given string * * @return void display things */ static function installed( bool $force_refresh = false, bool $only_lis = false, string $string_filter = "" ) { $plugin_inst = new Plugin; $plugin_inst->init(true); // reload plugins $installed = $plugin_inst->getList(); $apiplugins = []; if (self::checkRegister()) { $api = self::getAPI(); $apiplugins = $api->getAllPlugins($force_refresh); } $plugins = []; foreach ($installed as $plugin) { $key = $plugin['directory']; $apidata = $apiplugins[$key] ?? []; if (strlen($string_filter) && strpos(strtolower(json_encode($plugin)), strtolower($string_filter)) === false) { continue; } $clean_plugin = [ 'key' => $key, 'name' => $plugin['name'], 'logo_url' => $apidata['logo_url'] ?? "", 'description' => $apidata['descriptions'][0]['short_description'] ?? "", 'authors' => $apidata['authors'] ?? [['id' => 'all', 'name' => $plugin['author'] ?? ""]], 'license' => $apidata['license'] ?? $plugin['license'] ?? "", 'note' => $apidata['note'] ?? -1, 'homepage_url' => $apidata['homepage_url'] ?? "", 'issues_url' => $apidata['issues_url'] ?? "", 'readme_url' => $apidata['readme_url'] ?? "", 'version' => $plugin['version'] ?? "", ]; $plugins[] = $clean_plugin; } self::displayList($plugins, "installed", $only_lis); } /** * Display discover tab (all availble plugins) * * @param bool $force_refresh do not rely on cache to get plugins list * @param bool $only_lis display only the li tags in return html (used by ajax queries) * @param string $tag_filter filter the plugin list by given tag * @param string $string_filter filter the plugin by given string * @param int $page What's sub page of plugin we want to display * @param string $sort sort-alpha-asc|sort-alpha-desc|sort-dl|sort-update|sort-added|sort-note * * @return void display things */ static function discover( bool $force = false, bool $only_lis = false, string $tag_filter = "", string $string_filter = "", int $page = 1, string $sort = 'sort-alpha-asc' ) { if (!self::checkRegister()) { return; } $api = self::getAPI(); $plugins = $api->getPaginatedPlugins( $force, $tag_filter, $string_filter, $page, self::COL_PAGE, $sort ); if (strlen($string_filter) > 0) { $nb_plugins = count($plugins); } else { $nb_plugins = $api->getNbPlugins($tag_filter); } header("X-GLPI-Marketplace-Total: $nb_plugins"); self::displayList($plugins, "discover", $only_lis, $nb_plugins, $sort); } /** * Return HTML part for tags list * * @return string tags list */ static function getTagsHtml() { $api = self::getAPI(); $tags = $api->getTopTags(); $tags_li = "
  • ".__("All")."
  • "; foreach ($tags as $tag) { $tags_li.= "
  • ".ucfirst($tag['tag'])."
  • "; } return ""; } /** * Display a list of plugins * * @param array $plugins list of plugins returned by * - \Plugin::getList * - \Glpi\Marketplace\Api\Plugins::getPaginatedPlugins * @param string $tab current display tab (discover or installed) * @param bool $only_lis display only the li tags in return html (used by ajax queries) * @param int $nb_plugins total of plugins ($plugins contains only the current page) * @param string $sort sort-alpha-asc|sort-alpha-desc|sort-dl|sort-update|sort-added|sort-note * * @return false|void displays things */ static function displayList( array $plugins = [], string $tab = "", bool $only_lis = false, int $nb_plugins = 0, string $sort = 'sort-alpha-asc' ) { if (!self::canView()) { return false; } $plugins_li = ""; foreach ($plugins as $plugin) { $plugin['description'] = self::getLocalizedDescription($plugin); $plugins_li.= self::getPluginCard($plugin, $tab); } if (!$only_lis) { // check writable state if (!Controller::hasWriteAccess()) { echo "
    ". sprintf(__("We can't write on the markeplace directory (%s)."), GLPI_MARKETPLACE_DIR)."
    ". __("If you want to ease the plugins download, please check permissions and ownership of this directory.")."
    ". __("Otherwise, you will need to download and unzip the plugins archives manually.")."
    ". "
    "; } $tags_list = $tab != "installed" ? "
    ".self::getTagsHtml()."
    " : ""; $pagination = $tab != "installed" ? self::getPaginationHtml(1, $nb_plugins) : ""; $sort_controls = ""; if ($tab === "discover") { $sort_controls = " "; } $yourplugin = __("Your plugin here ? Contact us."); $networkmail = GLPI_NETWORK_MAIL; $refresh_lbl = __("Refresh plugin list"); $search_label = __("Filter plugin list"); $marketplace = << {$tags_list}
    $sort_controls
    $pagination $yourplugin 
    HTML; echo $marketplace; } else { echo $plugins_li; } $js = << "+option.text+""); }; $('.sort-control').select2({ templateResult: displaySortIcon, templateSelection: displaySortIcon, width: 135, }); }); JS; echo Html::scriptBlock($js); } /** * Return HTML part for plugin card * * @param array $plugin informations (title, description, etc) of the plugins * @param string $tab current displayed tab (installed or discover) * * @return string the plugin card */ static function getPluginCard(array $plugin = [], string $tab = "discover"):string { $plugin_key = $plugin['key']; $plugin_inst = new Plugin; $plugin_inst->getFromDBbyDir($plugin_key); $plugin_state = Plugin::getStateKey($plugin_inst->fields['state'] ?? -1); $buttons = self::getButtons($plugin_key); $authors = implode(', ', array_column($plugin['authors'] ?? [], 'name', 'id')); $authors_title = Html::clean($authors); $authors = strlen($authors) ? "$authors" : ""; $licence = is_string($plugin['license']) && isset($plugin['license']) && strlen($plugin['license']) ? "{$plugin['license']}" : ""; $version = strlen($plugin['version'] ?? "") ? "".$plugin['version'] : ""; $stars = ($plugin['note'] ?? -1) > 0 ? self::getStarsHtml($plugin['note']) : ""; $home_url = strlen($plugin['homepage_url'] ?? "") ? " " : ""; $issues_url = strlen($plugin['issues_url'] ?? "") ? " " : ""; $readme_url = strlen($plugin['readme_url'] ?? "") ? " " : ""; $icon = self::getPluginIcon($plugin); $network = self::getNetworkInformations($plugin); if ($tab === "discover") { $card = <<
    {$icon}

    {$plugin['name']}

    $network

    {$plugin['description']}

    {$buttons}
    HTML; } else { $card = <<
    {$icon}

    {$plugin['name']}

    {$licence}
    {$authors}
    {$version}
    {$buttons}
    HTML; } return $card; } /** * Return HTML part for plugin stars * * @param float|int $value current stars note on 5 * * @return string plugins stars html */ static function getStarsHtml(float $value = 0):string { $value = min(floor($value * 2) / 2, 5); $stars = ""; for ($i = 1; $i < 6; $i++) { if ($value >= $i) { $stars.= ""; } else if ($value + 0.5 == $i) { $stars.= ""; } else { $stars.= ""; } } return $stars; } /** * Return HTML part for plugin buttons * * @param string $plugin_key system name for the plugin * * @return string the buttons html */ static function getButtons(string $plugin_key = ""): string { global $CFG_GLPI, $PLUGIN_HOOKS; $rand = mt_rand(); $plugin_inst = new Plugin; $exists = $plugin_inst->getFromDBbyDir($plugin_key); $is_installed = $plugin_inst->isInstalled($plugin_key); $is_actived = $plugin_inst->isActivated($plugin_key); $mk_controller = new Controller($plugin_key); $web_update_version = $mk_controller->checkUpdate($plugin_inst); $has_web_update = $web_update_version !== false; $has_loc_update = $plugin_inst->isUpdatable($plugin_key); $can_be_overwritten = $mk_controller->canBeOverwritten(); $can_be_downloaded = $mk_controller->canBeDownloaded(); $required_offers = $mk_controller->getRequiredOffers(); $can_be_updated = $has_web_update && $can_be_overwritten; $can_be_cleaned = $exists && !$plugin_inst->isLoadable($plugin_key); $config_page = $PLUGIN_HOOKS['config_page'][$plugin_key] ?? ""; $error = ""; if ($exists) { ob_start(); $do_activate = $plugin_inst->checkVersions($plugin_key); if (!$do_activate) { $error.= "" . ob_get_contents() . ""; } ob_end_clean(); } $buttons = ""; if (strlen($error)) { $buttons .=""; Html::showToolTip($error, [ 'applyto' => "plugin-error-$rand", ]); } if ($can_be_cleaned) { $buttons .=""; } else if ((!$exists && !$mk_controller->hasWriteAccess()) || ($has_web_update && !$can_be_overwritten)) { $plugin_data = $mk_controller->getAPI()->getPlugin($plugin_key); if (array_key_exists('installation_url', $plugin_data) && $can_be_downloaded) { $warning = ""; if ($has_web_update) { $warning = __s("The plugin has an available update but its directory is not writable.")."
    "; } $warning.= sprintf( __s("Download archive manually, you must uncompress it in plugins directory (%s)"), GLPI_ROOT . '/plugins' ); // Use "marketplace.download.php" proxy if archive is downloadable from GLPI marketplace plugins API // as this API will refuse to serve the archive if registration key is not set in headers. $download_url = Toolbox::startsWith($plugin_data['installation_url'], GLPI_MARKETPLACE_PLUGINS_API_URI) ? $CFG_GLPI['root_doc'] . '/front/marketplace.download.php?key=' . $plugin_key : $plugin_data['installation_url']; $buttons .=" "; } } else if ($can_be_downloaded) { if (!$exists) { $buttons .=""; } else if ($can_be_updated) { $update_title = sprintf( __s("A new version (%s) is available, update ?", 'marketplace'), $web_update_version ); $buttons .=""; } } if ($mk_controller->requiresHigherOffer()) { $warning = sprintf( __s("You need a superior GLPI-Network offer to access to this plugin (%s)"), implode(', ', $required_offers) ); $buttons .=" "; } if ($exists && !$can_be_cleaned && !$is_installed && !strlen($error)) { $title = __s("Install"); $icon = "fas fa-folder-plus"; if ($has_loc_update) { $title = __s("Update"); $icon = "far fa-caret-square-up"; } $buttons .=""; } if ($is_installed) { if (!strlen($error)) { if ($is_actived) { $buttons .=""; } else { $buttons .=""; } } $buttons .=""; if (!strlen($error) && $is_actived && $config_page) { $plugin_dir = Plugin::getWebDir($plugin_key, true); $config_url = "$plugin_dir/$config_page"; $buttons .=" "; } } return $buttons; } /** * Return HTML part for plugin logo/icon * * @param array $plugin data of the plugin. * If it contains a key logo_url, the current will be inserted in a img tag * else, it will use initials from plugin friendly name to construct * a short and colored logo * * @return string the jtml for plugin logo */ static function getPluginIcon(array $plugin = []) { $icon = ""; if (strlen($plugin['logo_url'])) { $icon = ""; } else { $words = explode(" ", $plugin['name']); $initials = ""; for ($i = 0; $i < 2; $i++) { if (isset($words[$i])) { $initials.= mb_substr($words[$i], 0, 1); } } $bg_color = Toolbox::getColorForString($initials); $fg_color = Toolbox::getFgColor($bg_color); $icon = "$initials"; } return $icon; } /** * Return HTML part for Glpi Network informations for a given plugin * @param array $plugin data of the plugin. * if check agains plugin key if we need some subscription to use it * @return string the subscription information html */ static function getNetworkInformations(array $plugin = []): string { $mk_controller = new Controller($plugin['key']); $require_offers = $mk_controller->getRequiredOffers(); $html = ""; if (count($require_offers)) { $fst_offer = array_splice($require_offers, 0, 1); $offerkey = key($fst_offer); $offerlabel = current($fst_offer); $html = "
    GLPI Network $offerlabel
    "; } return $html; } /** * Retrieve localized description for a given plugin and matching the session lang * * @param array $plugin data of the plugin. * in the `description` key, we must found an array of localized descirption * indexed by lang key, return the good one * @param string $version short_description or long_description * * @return string the localized description */ static function getLocalizedDescription(array $plugin = [], string $version = 'short_description'): string { global $CFG_GLPI; $userlang = $CFG_GLPI['languages'][$_SESSION['glpilanguage']][3] ?? "en"; if (!isset($plugin['descriptions'])) { return ""; } $description = ""; $fallback = ""; foreach ($plugin['descriptions'] as $current) { if ($current['lang'] == $userlang) { $description = $current[$version]; break; } if ($current['lang'] == "en") { $fallback = $current[$version]; } } if (strlen($description) === 0) { $description = $fallback; } return $description; } /** * Return HTML part for plugins pagination * * @param int $current_page * @param int $total * @param bool $only_li display only the li tags in return html (used by ajax queries) * * @return string the pagination html */ static function getPaginationHtml(int $current_page = 1, int $total = 1, bool $only_li = false): string { if ($total <= self::COL_PAGE) { return ""; } $nb_pages = ceil($total / self::COL_PAGE); $prev = max($current_page - 1, 1); $next = min($current_page + 1, $nb_pages); $p_cls = $current_page === 1 ? "class='nav-disabled'" : ""; $n_cls = $current_page == $nb_pages ? "class='nav-disabled'" : ""; $html = ""; if (!$only_li) { $html.= ""; } return $html; } /** * Display a dialog inviting the user to switch from former plugin list to marketplace new view. * * @return void display things */ static function showFeatureSwitchDialog() { global $CFG_GLPI; if (isset($_POST['marketplace_replace'])) { $mp_value = isset($_POST['marketplace_replace_plugins_yes']) ? Controller::MP_REPLACE_YES : (isset($_POST['marketplace_replace_plugins_never']) ? Controller::MP_REPLACE_NEVER : Controller::MP_REPLACE_ASK); Config::setConfigurationValues('core', [ 'marketplace_replace_plugins' => $mp_value ]); // is user agree, redirect him to marketplace if ($mp_value === Controller::MP_REPLACE_YES) { Html::redirect($CFG_GLPI["root_doc"]."/front/marketplace.php"); } // avoid annoying user for the current session $_SESSION['skip_marketplace_invitation'] = true; } // show modal for asking user preference if (Controller::getPluginPageConfig() == Controller::MP_REPLACE_ASK && !isset($_SESSION['skip_marketplace_invitation']) && GLPI_INSTALL_MODE !== 'CLOUD') { echo "
    "; echo Html::image($CFG_GLPI['root_doc']."/pics/screenshots/marketplace.png", [ 'style' => 'width: 600px', ]); echo "

    "; echo __("GLPI provides a new marketplace to download and install plugins."); echo "

    "; echo "".__("Do you want to replace the plugins setup page by the new marketplace ?").""; echo "

    "; echo Html::submit(" ".__('Yes'), [ 'name' => 'marketplace_replace_plugins_yes' ]); echo " "; echo Html::submit(" ".__('No'), [ 'name' => 'marketplace_replace_plugins_never', 'class' => 'secondary' ]); echo " "; echo Html::submit(" ".__('Later'), [ 'name' => 'marketplace_replace_plugins_later', 'class' => 'secondary' ]); echo Html::hidden('marketplace_replace'); Html::closeForm(); echo Html::scriptBlock("$(document).ready(function() { $('#marketplace_dialog').dialog({ 'modal': true, 'width': 'auto', 'title': \"".__s("Switch to marketplace")."\" }); });"); } } }