mirror of
https://github.com/VladPolskiy/dokuwiki.git
synced 2025-08-01 15:51:59 +00:00

Previously each extension was fetched separately from the API, this fetches all installed ones in one go, speeding up the first open on cold cache significantly.
311 lines
9.0 KiB
PHP
311 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace dokuwiki\plugin\extension;
|
|
|
|
use dokuwiki\Cache\Cache;
|
|
use dokuwiki\HTTP\DokuHTTPClient;
|
|
use JsonException;
|
|
|
|
class Repository
|
|
{
|
|
public const EXTENSION_REPOSITORY_API = 'https://www.dokuwiki.org/lib/plugins/pluginrepo/api.php';
|
|
|
|
protected const CACHE_PREFIX = '##extension_manager##';
|
|
protected const CACHE_SUFFIX = '.repo';
|
|
protected const CACHE_TIME = 3600 * 24;
|
|
|
|
protected static $instance;
|
|
protected $hasAccess;
|
|
|
|
/**
|
|
* Protected Constructor
|
|
*
|
|
* Use Repository::getInstance() to get an instance
|
|
*/
|
|
protected function __construct()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @return Repository
|
|
*/
|
|
public static function getInstance()
|
|
{
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Check if access to the repository is possible
|
|
*
|
|
* On the first call this will throw an exception if access is not possible. On subsequent calls
|
|
* it will return the cached result. Thus it is recommended to call this method once when instantiating
|
|
* the repository for the first time and handle the exception there. Subsequent calls can then be used
|
|
* to access cached data.
|
|
*
|
|
* @return bool
|
|
* @throws Exception
|
|
*/
|
|
public function checkAccess()
|
|
{
|
|
if ($this->hasAccess !== null) {
|
|
return $this->hasAccess; // we already checked
|
|
}
|
|
|
|
// check for SSL support
|
|
if (!in_array('ssl', stream_get_transports())) {
|
|
throw new Exception('nossl');
|
|
}
|
|
|
|
// ping the API
|
|
$httpclient = new DokuHTTPClient();
|
|
$httpclient->timeout = 5;
|
|
$data = $httpclient->get(self::EXTENSION_REPOSITORY_API . '?cmd=ping');
|
|
if ($data === false) {
|
|
$this->hasAccess = false;
|
|
throw new Exception('repo_error');
|
|
} elseif ($data !== '1') {
|
|
$this->hasAccess = false;
|
|
throw new Exception('repo_badresponse');
|
|
} else {
|
|
$this->hasAccess = true;
|
|
}
|
|
return $this->hasAccess;
|
|
}
|
|
|
|
/**
|
|
* Fetch the data for multiple extensions from the repository
|
|
*
|
|
* @param string[] $ids A list of extension ids
|
|
* @throws Exception
|
|
*/
|
|
protected function fetchExtensions($ids)
|
|
{
|
|
if (!$this->checkAccess()) return;
|
|
|
|
$httpclient = new DokuHTTPClient();
|
|
$data = [
|
|
'fmt' => 'json',
|
|
'ext' => $ids
|
|
];
|
|
|
|
$response = $httpclient->post(self::EXTENSION_REPOSITORY_API, $data);
|
|
if ($response === false) {
|
|
$this->hasAccess = false;
|
|
throw new Exception('repo_error');
|
|
}
|
|
|
|
try {
|
|
$found = [];
|
|
$extensions = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
|
|
foreach ($extensions as $extension) {
|
|
$this->storeCache($extension['plugin'], $extension);
|
|
$found[] = $extension['plugin'];
|
|
}
|
|
// extensions that have not been returned are not in the repository, but we should cache that too
|
|
foreach (array_diff($ids, $found) as $id) {
|
|
$this->storeCache($id, []);
|
|
}
|
|
} catch (JsonException $e) {
|
|
$this->hasAccess = false;
|
|
throw new Exception('repo_badresponse', 0, $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This creates a list of Extension objects from the given list of ids
|
|
*
|
|
* The extensions are initialized by fetching their data from the cache or the repository.
|
|
* This is the recommended way to initialize a whole bunch of extensions at once as it will only do
|
|
* a single API request for all extensions that are not in the cache.
|
|
*
|
|
* Extensions that are not found in the cache or the repository will be initialized as null.
|
|
*
|
|
* @param string[] $ids
|
|
* @return (Extension|null)[] [id => Extension|null, ...]
|
|
* @throws Exception
|
|
*/
|
|
public function initExtensions($ids)
|
|
{
|
|
$result = [];
|
|
$toload = [];
|
|
|
|
// first get all that are cached
|
|
foreach ($ids as $id) {
|
|
$data = $this->retrieveCache($id);
|
|
if ($data === null || $data === []) {
|
|
$toload[] = $id;
|
|
} else {
|
|
$result[$id] = Extension::createFromRemoteData($data);
|
|
}
|
|
}
|
|
|
|
// then fetch the rest at once
|
|
if ($toload) {
|
|
$this->fetchExtensions($toload);
|
|
foreach ($toload as $id) {
|
|
$data = $this->retrieveCache($id);
|
|
if ($data === null || $data === []) {
|
|
$result[$id] = null;
|
|
} else {
|
|
$result[$id] = Extension::createFromRemoteData($data);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Initialize a new Extension object from remote data for the given id
|
|
*
|
|
* @param string $id
|
|
* @return Extension|null
|
|
* @throws Exception
|
|
*/
|
|
public function initExtension($id)
|
|
{
|
|
$result = $this->initExtensions([$id]);
|
|
return $result[$id];
|
|
}
|
|
|
|
/**
|
|
* Get the pure API data for a single extension
|
|
*
|
|
* Used when lazy loading remote data in Extension
|
|
*
|
|
* @param string $id
|
|
* @return array|null
|
|
* @throws Exception
|
|
*/
|
|
public function getExtensionData($id)
|
|
{
|
|
$data = $this->retrieveCache($id);
|
|
if ($data === null) {
|
|
$this->fetchExtensions([$id]);
|
|
$data = $this->retrieveCache($id);
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Search for extensions using the given query string
|
|
*
|
|
* @param string $q the query string
|
|
* @return Extension[] a list of matching extensions
|
|
* @throws Exception
|
|
*/
|
|
public function searchExtensions($q)
|
|
{
|
|
if (!$this->checkAccess()) return [];
|
|
|
|
$query = $this->parseQuery($q);
|
|
$query['fmt'] = 'json';
|
|
|
|
$httpclient = new DokuHTTPClient();
|
|
$response = $httpclient->post(self::EXTENSION_REPOSITORY_API, $query);
|
|
if ($response === false) {
|
|
$this->hasAccess = false;
|
|
throw new Exception('repo_error');
|
|
}
|
|
|
|
try {
|
|
$items = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
|
|
} catch (JsonException $e) {
|
|
$this->hasAccess = false;
|
|
throw new Exception('repo_badresponse', 0, $e);
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($items as $item) {
|
|
$this->storeCache($item['plugin'], $item);
|
|
$results[] = Extension::createFromRemoteData($item);
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Parses special queries from the query string
|
|
*
|
|
* @param string $q
|
|
* @return array
|
|
*/
|
|
protected function parseQuery($q)
|
|
{
|
|
$parameters = [
|
|
'tag' => [],
|
|
'mail' => [],
|
|
'type' => 0,
|
|
'ext' => []
|
|
];
|
|
|
|
// extract tags
|
|
if (preg_match_all('/(^|\s)(tag:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $m) {
|
|
$q = str_replace($m[2], '', $q);
|
|
$parameters['tag'][] = $m[3];
|
|
}
|
|
}
|
|
// extract author ids
|
|
if (preg_match_all('/(^|\s)(authorid:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $m) {
|
|
$q = str_replace($m[2], '', $q);
|
|
$parameters['mail'][] = $m[3];
|
|
}
|
|
}
|
|
// extract extensions
|
|
if (preg_match_all('/(^|\s)(ext:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $m) {
|
|
$q = str_replace($m[2], '', $q);
|
|
$parameters['ext'][] = $m[3];
|
|
}
|
|
}
|
|
// extract types
|
|
if (preg_match_all('/(^|\s)(type:([\S]+))/', $q, $matches, PREG_SET_ORDER)) {
|
|
$typevalues = array_flip(Extension::COMPONENT_TYPES);
|
|
$typevalues = array_change_key_case($typevalues, CASE_LOWER);
|
|
|
|
foreach ($matches as $m) {
|
|
$q = str_replace($m[2], '', $q);
|
|
$t = strtolower($m[3]);
|
|
if (isset($typevalues[$t])) {
|
|
$parameters['type'] += $typevalues[$t];
|
|
}
|
|
}
|
|
}
|
|
|
|
$parameters['q'] = trim($q);
|
|
return $parameters;
|
|
}
|
|
|
|
|
|
/**
|
|
* Store the data for a single extension in the cache
|
|
*
|
|
* @param string $id
|
|
* @param array $data
|
|
*/
|
|
protected function storeCache($id, $data)
|
|
{
|
|
$cache = new Cache(self::CACHE_PREFIX . $id, self::CACHE_SUFFIX);
|
|
$cache->storeCache(serialize($data));
|
|
}
|
|
|
|
/**
|
|
* Retrieve the data for a single extension from the cache
|
|
*
|
|
* @param string $id
|
|
* @return array|null the data or null if not in cache
|
|
*/
|
|
protected function retrieveCache($id)
|
|
{
|
|
$cache = new Cache(self::CACHE_PREFIX . $id, self::CACHE_SUFFIX);
|
|
if ($cache->useCache(['age' => self::CACHE_TIME])) {
|
|
return unserialize($cache->retrieveCache(false));
|
|
}
|
|
return null;
|
|
}
|
|
}
|