<?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;
    }
}
