Files
MYSOPHAL/inc/marketplace/api/plugins.class.php
2025-08-07 13:15:31 +01:00

507 lines
16 KiB
PHP

<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2020 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI 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 GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Marketplace\Api;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access directly to this file");
}
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Response;
use \GuzzleHttp\Client as Guzzle_Client;
use \GLPINetwork;
use \Toolbox;
use \Session;
class Plugins {
protected $httpClient = null;
protected $last_error = "";
public const COL_PAGE = 200;
protected const TIMEOUT = 5;
static $plugins = [];
function __construct(bool $connect = false) {
global $CFG_GLPI;
$options = [
'base_uri' => GLPI_MARKETPLACE_PLUGINS_API_URI,
'connect_timeout' => self::TIMEOUT,
];
// add proxy string if configured in glpi
if (!empty($CFG_GLPI["proxy_name"])) {
$proxy_creds = !empty($CFG_GLPI["proxy_user"])
? $CFG_GLPI["proxy_user"].":".Toolbox::sodiumDecrypt($CFG_GLPI["proxy_passwd"])."@"
: "";
$proxy_string = "http://{$proxy_creds}".$CFG_GLPI['proxy_name'].":".$CFG_GLPI['proxy_port'];
$options['proxy'] = $proxy_string;
}
// init guzzle client with base options
$this->httpClient = new Guzzle_Client($options);
}
/**
* Send a http request to services api
* using the base url set in constructor and the current endpoint
*
* @param string $endpoint which resource whe need to query
* @param array $options array of options for guzzle lib
* @param string $method GET/POST, etc
*
* @return Psr\Http\Message\ResponseInterface|false
*/
private function request(
string $endpoint = '',
array $options = [],
string $method = 'GET'
) {
if (!GLPINetwork::isRegistered()) {
// Simulate empty response if registration key is not valid
return new Response(200, [], '[]');
}
$options['headers'] = array_merge_recursive(
[
'Accept' => 'application/json',
'User-Agent' => GLPINetwork::getGlpiUserAgent(),
'X-Registration-Key' => GLPINetwork::getRegistrationKey(),
'X-Glpi-Network-Uid' => GLPINetwork::getGlpiNetworkUid(),
],
$options['headers'] ?? []
);
try {
$response = $this->httpClient->request($method, $endpoint, $options);
} catch (RequestException $e) {
$this->last_error = [
'title' => "Plugins API error",
'exception' => $e->getMessage(),
'request' => Psr7\str($e->getRequest()),
];
if ($e->hasResponse()) {
$this->last_error['response'] = Psr7\str($e->getResponse());
}
if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) {
Toolbox::logDebug($this->last_error);
}
return false;
}
return $response;
}
/**
* Send an http request on an endpoint accepting paginated queries
*
* @param string $endpoint which resource whe need to query
* @param array $options array of options for guzzle lib
* @param string $method GET/POST, etc
*
* @return array full collection
*/
private function getPaginatedCollection(
string $endpoint = '',
array $options = [],
string $method = 'GET'
): array {
$collection = [];
$i = 0;
do {
$request_options = array_merge_recursive([
'headers' => [
'X-Range' => ($i * self::COL_PAGE)."-".(($i + 1) * self::COL_PAGE - 1),
],
], $options);
$response = $this->request($endpoint, $request_options, $method);
if ($current = ($response !== false ? json_decode($response->getBody(), true) : false)) {
$collection = array_merge($collection, $current);
}
$i++;
} while ($current !== false && count($current));
return $collection;
}
/**
* Return the full list of avaibles plugins on services API
*
* @param bool $force_refresh if false, we will return results stored in local cache
* @param string $tag_filter filter the plugin list by given tag
* @param string $string_filter filter the plugin list by given string
* @param string $sort sort-alpha-asc|sort-alpha-desc|sort-dl|sort-update|sort-added|sort-note
*
* @return array collection of plugins
*/
function getAllPlugins(
bool $force_refresh = false,
string $tag_filter = "",
string $string_filter = "",
string $sort = 'sort-alpha-asc'
) {
global $GLPI_CACHE;
$plugins_colct = [];
if (!$force_refresh && $GLPI_CACHE->has('marketplace_all_plugins')) {
$plugins_colct = $GLPI_CACHE->get('marketplace_all_plugins');
}
if (!count($plugins_colct)) {
$plugins = $this->getPaginatedCollection('plugins');
// replace keys indexes by system names
$plugins_keys = array_column($plugins, 'key');
$plugins_colct = array_combine($plugins_keys, $plugins);
foreach ($plugins_colct as &$plugin) {
if (count($plugin['versions']) === 0) {
continue;
}
if (!GLPI_MARKETPLACE_PRERELEASES) {
$plugin['versions'] = array_filter($plugin['versions'], function($version) {
return !isset($version['stability']) || $version['stability'] === "stable";
});
}
$first_found_version = current($plugin['versions']);
if (is_array($first_found_version)) {
$plugin['installation_url'] = $first_found_version['download_url'];
$plugin['version'] = $first_found_version['num'];
}
}
self::$plugins = $plugins_colct;
$GLPI_CACHE->set('marketplace_all_plugins', self::$plugins, HOUR_TIMESTAMP);
}
if (strlen($tag_filter) > 0) {
$tagged_plugins = array_column($this->getPluginsForTag($tag_filter), 'key');
$plugins_colct = array_intersect_key($plugins_colct, array_flip($tagged_plugins));
}
if (strlen($string_filter) > 0) {
$plugins_colct = array_filter($plugins_colct, function($plugin) use ($string_filter) {
return strpos(strtolower(json_encode($plugin)), strtolower($string_filter)) !== false;
});
}
// manage sorting of collection
uasort($plugins_colct, function($plugin1, $plugin2) use ($sort) {
switch ($sort) {
case "sort-alpha-asc":
return strnatcasecmp($plugin1['name'], $plugin2['name']);
case "sort-alpha-desc":
return strnatcasecmp($plugin2['name'], $plugin1['name']);
case "sort-dl":
return strnatcmp($plugin2['download_count'], $plugin1['download_count']);
case "sort-update":
return strnatcmp($plugin2['date_updated'], $plugin1['date_updated']);
case "sort-added":
return strnatcmp($plugin2['date_added'], $plugin1['date_added']);
case "sort-note":
return strnatcmp($plugin2['note'], $plugin1['note']);
}
});
return $plugins_colct;
}
/**
* Return plugins list for the given page
*
* @param bool $force_refresh if false, we will return results stored in local cache
* @param string $tag_filter filter the plugin list by given tag
* @param string $string_filter filter the plugin list by given string
* @param int $page which page to query
* @param int $nb_per_page how manyu per page we want
* @param string $sort sort-alpha-asc|sort-alpha-desc|sort-dl|sort-update|sort-added|sort-note
*
* @return array full collection
*/
function getPaginatedPlugins(
bool $force_refresh = false,
string $tag_filter = "",
string $string_filter = "",
int $page = 1,
int $nb_per_page = 15,
string $sort = 'sort-alpha-asc'
) {
$plugins = $this->getAllPlugins($force_refresh, $tag_filter, $string_filter, $sort);
$plugins_page = array_splice($plugins, max($page - 1, 0) * $nb_per_page, $nb_per_page);
return $plugins_page;
}
/**
* return the number of available plugins in distant API
*
* @param string $tag_filter filter the plugin list by given tag
*
* @return int number of plugins
*/
function getNbPlugins(string $tag_filter = "") {
$plugins = $this->getAllPlugins(false, $tag_filter);
return count($plugins);
}
/**
* get top 10 plugins sorted by trending (most downloaded in the last month) criterion
*
* @return array collection of plugins
*/
function getTrendingPlugins() {
return $this->getTopPlugins("trending");
}
/**
* get top 10 plugins sorted by popular (most downloaded all time) criterion
*
* @return array collection of plugins
*/
function getPopularPlugins() {
return $this->getTopPlugins("popular");
}
/**
* get top 10 plugins sorted by their submition date (DESC sort) criterion
*
* @return array collection of plugins
*/
function getNewPlugins() {
return $this->getTopPlugins("new");
}
/**
* get top 10 plugins sorted by their update date (DESC sort) criterion
*
* @return array collection of plugins
*/
function getUpdatedPlugins() {
return $this->getTopPlugins("updated");
}
/**
* get top 10 plugins sorted by given criterion (see other getTopXXX methods)
*
* @param string $endpoint criterion to filter plugsin
*
* @return array collection of plugins
*/
function getTopPlugins(string $endpoint = "") {
$response = $this->request("plugin/{$endpoint}");
if ($response === false) {
return [];
}
$top = json_decode($response->getBody(), true);
$key_list = array_column($top, 'key', 'key');
$plugins = $this->getAllPlugins();
$top_plugins = array_filter($plugins, function($plugin) use($key_list) {
return in_array($plugin['key'], $key_list);
});
return $top_plugins;
}
/**
* Get a single plugin array
*
* @param string $key plugin system name
* @param bool $force_refresh if false, we will return results stored in local cache
*
* @return array plugin data
*/
public function getPlugin(string $key = "", bool $force_refresh = false): array {
$plugins_list = [];
if ($force_refresh || !count(self::$plugins)) {
$plugins_list = $this->getAllPlugins($force_refresh);
} else {
$plugins_list = self::$plugins;
}
return $plugins_list[$key] ?? [];
}
/**
* Inform plugins API that a plugin (by its key) has been downloaded
* and the download counter must be incremented
*
* @param string $key plugin system key
*
* @return void we don't wait for a response, this a fire and forget request
*/
public function incrementPluginDownload(string $key = "") {
$this->request(
"plugin/{$key}/download",
[
'allow_redirects' => false, // Prevent follow redirects to download page sent by Plugins API
]
);
}
/**
* Get top list of tags for current session language
*
* @return array top tags
*/
public function getTopTags(): array {
global $CFG_GLPI;
$response = $this->request('tags/top', [
'headers' => [
'X-Lang' => $CFG_GLPI['languages'][$_SESSION['glpilanguage']][2]
]
]);
if ($response === false) {
return [];
}
$toptags = json_decode($response->getBody(), true);
return $toptags;
}
/**
* get a plugins collection for the givent tag
*
* @param string $tag to filter plugins
* @param bool $force_refresh if false, we will return results stored in local cache
*
* @return array filtered plugin collection
*/
public function getPluginsForTag(string $tag = "", bool $force_refresh = false): array {
global $GLPI_CACHE;
$plugins_colct = [];
if (!$force_refresh && $GLPI_CACHE->has("marketplace_tag_$tag")) {
$plugins_colct = $GLPI_CACHE->get("marketplace_tag_$tag");
}
if (!count($plugins_colct)) {
$plugins_colct = $this->getPaginatedCollection("tags/{$tag}/plugin");
$GLPI_CACHE->set("marketplace_tag_$tag", $plugins_colct, HOUR_TIMESTAMP);
}
return $plugins_colct;
}
/**
* Download plugin archive and follow progress with a session var `marketplace_dl_progress`
*
* @param string $url where is the plugin
* @param string $dest where we store it it
* @param string $plugin_key plugin system name
*
* @return bool
*/
public function downloadArchive(string $url, string $dest, string $plugin_key, bool $track_progress = true): bool {
if ($track_progress) {
if (!isset($_SESSION['marketplace_dl_progress'])) {
$_SESSION['marketplace_dl_progress'] = [];
}
$_SESSION['marketplace_dl_progress'][$plugin_key] = 0;
}
// close session to permits polling of progress by frontend
session_write_close();
$options = [
'headers' => [
'Accept' => '*/*',
],
'sink' => $dest,
];
if ($track_progress) {
// track download progress
$options['progress'] = function($downloadTotal, $downloadedBytes) use ($plugin_key) {
// Prevent "net::ERR_RESPONSE_HEADERS_TOO_BIG" error
// Each time Session::start() is called, PHP add a 'Set-Cookie' header,
// so if a plugin takes more than a few seconds to be downloaded, PHP will set too many
// 'Set-Cookie' headers and response will not be accepted by browser.
// We can remove the 'Set-Cookie' here as it will be put back on next instruction (Session::start()).
header_remove('Set-Cookie');
// restart session to store percentage of download for this plugin
Session::start();
// calculate percent based on the size and store it in session
$percent = 0;
if ($downloadTotal > 0) {
$percent = round($downloadedBytes * 100 / $downloadTotal);
}
$_SESSION['marketplace_dl_progress'][$plugin_key] = $percent;
// reclose session to avoid blocking ajax requests
session_write_close();
};
}
$response = $this->request($url, $options);
// restart session to permits write of vars
// (later, we also may have some addMessageAfterRedirect to provider errors to user)
Session::start();
if ($track_progress) {
// force finish of download (to avoid keeping js loop in case of errors)
$_SESSION['marketplace_dl_progress'][$plugin_key] = 100;
}
return $response !== false && $response->getStatusCode() === 200;
}
}