<?php

/**
 * @file classes/site/VersionCheck.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class VersionCheck
 *
 * @ingroup site
 *
 * @see Version
 *
 * @brief Provides methods to check for the latest version of the application.
 */

namespace PKP\site;

use APP\core\Application;
use DateInterval;
use Exception;
use Illuminate\Support\Facades\Cache;
use PKP\config\Config;
use PKP\core\PKPString;
use PKP\db\DAORegistry;
use PKP\file\FileManager;
use SimpleXMLElement;

class VersionCheck
{
    /** Max lifetime for the version cache */
    protected const MAX_CACHE_LIFETIME = '1 year';

    public const VERSION_CODE_PATH = 'dbscripts/xml/version.xml';

    /**
     * Return information about the latest available version.
     *
     * @return array
     */
    public static function getLatestVersion()
    {
        $application = Application::get();
        $includeId = Application::isInstalled() &&
            !Application::isUpgrading() &&
            Config::getVar('general', 'enable_beacon', true);

        if ($includeId) {
            $uniqueSiteId = Application::get()->getUUID();
        } else {
            $uniqueSiteId = null;
        }

        $request = $application->getRequest();
        return self::parseVersionXML(
            $application->getVersionDescriptorUrl() .
            ($includeId ? '?id=' . urlencode($uniqueSiteId) .
                '&oai=' . urlencode($request->url('index', 'oai'))
            : '')
        );
    }

    /**
     * Return the currently installed database version.
     *
     * @return Version
     */
    public static function getCurrentDBVersion()
    {
        $versionDao = DAORegistry::getDAO('VersionDAO'); /** @var VersionDAO $versionDao */
        return $versionDao->getCurrentVersion();
    }

    /**
     * Return the current code version.
     */
    public static function getCurrentCodeVersion(): ?Version
    {
        return self::parseVersionXML(self::VERSION_CODE_PATH)['version'] ?? null;
    }

    /**
     * Parse information from a version XML file.
     */
    public static function parseVersionXML(string $path): ?array
    {
        $isVirtual = FileManager::isVirtualPath($path);
        $key = __METHOD__ . static::MAX_CACHE_LIFETIME . $path . ($isVirtual ? '' : filemtime($path));
        $expiration = $isVirtual ? 0 : DateInterval::createFromDateString(static::MAX_CACHE_LIFETIME);
        $version = Cache::remember($key, $expiration, function () use ($path) {
            $xml = new SimpleXMLElement(FileManager::getStream($path));
            $version = [];
            foreach (['application', 'class', 'type', 'release', 'tag', 'date', 'info', 'package', 'lazy-load', 'sitewide'] as $name) {
                if (isset($xml->$name)) {
                    $version[$name] = (string) $xml->$name;
                }
            }
            $version['sitewide'] = (int) ($version['sitewide'] ?? 0);
            $version['lazy-load'] = (int) ($version['lazy-load'] ?? 0);
            if (isset($xml->patch)) {
                $version['patch'] = [];
                foreach ($xml->patch as $patch) {
                    $version['patch'][$patch['from']] = (string) $patch;
                }
            }
            return $version;
        });

        // Built outside of the cache section to avoid serializing the Version (which would need a __set_state implementation)
        if (isset($version['release']) && isset($version['application'])) {
            $version['version'] = Version::fromString(
                $version['release'] ?? '',
                $version['type'] ?? '',
                $version['application'] ?? '',
                $version['class'] ?? '',
                $version['lazy-load'],
                $version['sitewide']
            );
        }
        return $version;
    }

    /**
     * Find the applicable patch for the current code version (if available).
     *
     * @param array $versionInfo as returned by parseVersionXML()
     * @param Version $codeVersion as returned by getCurrentCodeVersion()
     *
     * @return string
     */
    public static function getPatch($versionInfo, $codeVersion = null)
    {
        if (!isset($codeVersion)) {
            $codeVersion = self::getCurrentCodeVersion();
        }
        if (isset($versionInfo['patch'][$codeVersion->getVersionString()])) {
            return $versionInfo['patch'][$codeVersion->getVersionString()];
        }
        return null;
    }

    /**
     * Checks whether the given version file exists and whether it
     * contains valid data. Returns a Version object if everything
     * is ok, otherwise throws an Exception.
     *
     * @param string $versionFile
     *
     * @return Version
     */
    public static function getValidPluginVersionInfo($versionFile)
    {
        $fileManager = new FileManager();
        if ($fileManager->fileExists($versionFile)) {
            $versionInfo = self::parseVersionXML($versionFile);
        } else {
            throw new Exception(__('manager.plugins.versionFileNotFound'));
        }

        // Validate plugin name and type to avoid abuse
        $productType = explode('.', $versionInfo['type']);
        if (count($productType) != 2 || $productType[0] != 'plugins') {
            throw new Exception(__('manager.plugins.versionFileInvalid'));
        }

        $pluginVersion = $versionInfo['version'];
        $namesToValidate = [$pluginVersion->getProduct(), $productType[1]];
        foreach ($namesToValidate as $nameToValidate) {
            if (!PKPString::regexp_match('/[a-z][a-zA-Z0-9]+/', $nameToValidate)) {
                throw new Exception(__('manager.plugins.versionFileInvalid'));
            }
        }

        return $pluginVersion;
    }

    /**
     * Checks the application's version against the latest version
     * on the PKP servers.
     *
     * @return string|false Version description or false if no newer version
     */
    public static function checkIfNewVersionExists()
    {
        try {
            $versionInfo = self::getLatestVersion();
        } catch (\GuzzleHttp\Exception\TransferException $e) {
            error_log('Failed to retrieve the latest version info: ' . $e->getMessage());
            return false;
        }
        $latestVersion = $versionInfo['release'];

        $currentVersion = self::getCurrentDBVersion();
        if ($currentVersion->compare($latestVersion) < 0) {
            return $latestVersion;
        }
        return false;
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\site\VersionCheck', '\VersionCheck');
    define('VERSION_CODE_PATH', VersionCheck::VERSION_CODE_PATH);
}
