<?php
/**
* @brief Repository modules XML feed reader
*
* Provides an object to parse XML feed of modules from repository.
*
* @package Dotclear
* @subpackage Core
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*
* @since 2.6
*/
if (!defined('DC_RC_PATH')) {
return;
}
class dcStore
{
/** @var object dcCore instance */
public $core;
/** @var object dcModules instance */
public $modules;
/** @var array Modules fields to search on and their weighting */
public static $weighting = [
'id' => 10,
'name' => 8,
'tags' => 6,
'desc' => 4,
'author' => 2,
];
/** @var string User agent used to query repository */
protected $user_agent = 'DotClear.org RepoBrowser/0.1';
/** @var string XML feed URL */
protected $xml_url;
/** @var array Array of new/update modules from repository */
protected $data;
/**
* Constructor.
*
* @param dcModules $modules dcModules instance
* @param string $xml_url XML feed URL
* @param boolean $force Force query repository
*/
public function __construct(dcModules $modules, $xml_url, $force = false)
{
$this->core = $modules->core;
$this->modules = $modules;
$this->xml_url = $xml_url;
$this->user_agent = sprintf('Dotclear/%s)', DC_VERSION);
$this->check($force);
}
/**
* Check repository.
*
* @param boolean $force Force query repository
* @return boolean True if get feed or cache
*/
public function check($force = false)
{
if (!$this->xml_url) {
return false;
}
try {
/* @phpstan-ignore-next-line */
$parser = DC_STORE_NOT_UPDATE ? false : dcStoreReader::quickParse($this->xml_url, DC_TPL_CACHE, $force);
} catch (Exception $e) {
return false;
}
$raw_datas = !$parser ? [] : $parser->getModules(); // @phpstan-ignore-line
uasort($raw_datas, ['self', 'sort']);
$skipped = array_keys($this->modules->getDisabledModules());
foreach ($skipped as $p_id) {
if (isset($raw_datas[$p_id])) {
unset($raw_datas[$p_id]);
}
}
$updates = [];
$current = $this->modules->getModules();
foreach ($current as $p_id => $p_infos) {
# non privileged user has no info
if (!is_array($p_infos)) {
continue;
}
# main repository
if (isset($raw_datas[$p_id])) {
if (dcUtils::versionsCompare($raw_datas[$p_id]['version'], $p_infos['version'], '>')) {
$updates[$p_id] = $raw_datas[$p_id];
$updates[$p_id]['root'] = $p_infos['root'];
$updates[$p_id]['root_writable'] = $p_infos['root_writable'];
$updates[$p_id]['current_version'] = $p_infos['version'];
}
unset($raw_datas[$p_id]);
}
# per module third-party repository
if (!empty($p_infos['repository']) && DC_ALLOW_REPOSITORIES) { // @phpstan-ignore-line
try {
$dcs_url = substr($p_infos['repository'], -12, 12) == '/dcstore.xml' ? $p_infos['repository'] : http::concatURL($p_infos['repository'], 'dcstore.xml');
$dcs_parser = dcStoreReader::quickParse($dcs_url, DC_TPL_CACHE, $force);
if ($dcs_parser !== false) {
$dcs_raw_datas = $dcs_parser->getModules();
if (isset($dcs_raw_datas[$p_id]) && dcUtils::versionsCompare($dcs_raw_datas[$p_id]['version'], $p_infos['version'], '>')) {
if (!isset($updates[$p_id]) || dcUtils::versionsCompare($dcs_raw_datas[$p_id]['version'], $updates[$p_id]['version'], '>')) {
$dcs_raw_datas[$p_id]['repository'] = true;
$updates[$p_id] = $dcs_raw_datas[$p_id];
$updates[$p_id]['root'] = $p_infos['root'];
$updates[$p_id]['root_writable'] = $p_infos['root_writable'];
$updates[$p_id]['current_version'] = $p_infos['version'];
}
}
}
} catch (Exception $e) {
}
}
}
$this->data = [
'new' => $raw_datas,
'update' => $updates,
];
return true;
}
/**
* Get a list of modules.
*
* @param boolean $update True to get update modules, false for new ones
*
* @return array List of update/new modules
*/
public function get($update = false)
{
/* @phpstan-ignore-next-line */
return is_array($this->data) ? $this->data[$update ? 'update' : 'new'] : [];
}
/**
* Search a module.
*
* Search string is cleaned, split and compare to split:
* - module id and clean id,
* - module name, clean name,
* - module desccription.
*
* Every time a part of query is find on module,
* result accuracy grow. Result is sorted by accuracy.
*
* @param string $pattern String to search
* @return array Match modules
*/
public function search($pattern)
{
$result = [];
$sorter = [];
# Split query into small clean words
if (!($patterns = self::patternize($pattern))) {
return $result;
}
# For each modules
foreach ($this->data['new'] as $id => $module) {
$module['id'] = $id;
# Loop through required module fields
foreach (self::$weighting as $field => $weight) {
# Skip fields which not exsist on module
if (empty($module[$field])) {
continue;
}
# Split field value into small clean word
if (!($subjects = self::patternize($module[$field]))) {
continue;
}
# Check contents
if (!($nb = preg_match_all('/(' . implode('|', $patterns) . ')/', implode(' ', $subjects), $_))) {
continue;
}
# Add module to result
if (!isset($sorter[$id])) {
$sorter[$id] = 0;
$result[$id] = $module;
}
# Increment score by matches count * field weight
$sorter[$id] += $nb * $weight;
$result[$id]['score'] = $sorter[$id];
}
}
# Sort response by matches count
if (!empty($result)) {
array_multisort($sorter, SORT_DESC, $result);
}
return $result;
}
/**
* Quick download and install module.
*
* @param string $url Module package URL
* @param string $dest Path to install module
* @return integer 1 = installed, 2 = update
*/
public function process($url, $dest)
{
$this->download($url, $dest);
return $this->install($dest);
}
/**
* Download a module.
*
* @param string $url Module package URL
* @param string $dest Path to put module package
*/
public function download($url, $dest)
{
// Check and add default protocol if necessary
if (!preg_match('%^http[s]?:\/\/%', $url)) {
$url = 'http://' . $url;
}
// Download package
if ($client = netHttp::initClient($url, $path)) {
try {
$client->setUserAgent($this->user_agent);
$client->useGzip(false);
$client->setPersistReferers(false);
$client->setOutput($dest);
$client->get($path);
unset($client);
} catch (Exception $e) {
unset($client);
throw new Exception(__('An error occurred while downloading the file.'));
}
} else {
throw new Exception(__('An error occurred while downloading the file.'));
}
}
/**
* Install a previously downloaded module.
*
* @param string $path Module package URL
* @param string $path Path to module package
* @return integer 1 = installed, 2 = update
*/
public function install($path)
{
return dcModules::installPackage($path, $this->modules);
}
/**
* User Agent String.
*
* @param string $str User agent string
*/
public function agent($str)
{
$this->user_agent = $str;
}
/**
* Split and clean pattern.
*
* @param string $str String to sanitize
* @return array Array of cleaned pieces of string or false if none
*/
public static function patternize($str)
{
$arr = [];
foreach (explode(' ', $str) as $_) {
$_ = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $_));
if (strlen($_) >= 2) {
$arr[] = $_;
}
}
return empty($arr) ? false : $arr;
}
/**
* Compare version.
*
* @param string $v1 Version
* @param string $v2 Version
* @param string $op Comparison operator
* @return boolean True is comparison is true, dude!
*/
private static function compare($v1, $v2, $op)
{
return version_compare(
preg_replace('!-r(\d+)$!', '-p$1', $v1),
preg_replace('!-r(\d+)$!', '-p$1', $v2),
$op
);
}
/**
* Sort modules list.
*
* @param array $a A module
* @param array $b A module
* @return integer
*/
private static function sort($a, $b)
{
$c = strtolower($a['id']);
$d = strtolower($b['id']);
if ($c == $d) {
return 0;
}
return ($c < $d) ? -1 : 1;
}
}