<?php
/**
* @file classes/plugins/PluginGalleryDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PluginGalleryDAO
*
* @ingroup plugins
*
* @see DAO
*
* @brief Operations for retrieving content from the PKP plugin gallery.
*/
namespace PKP\plugins;
use APP\core\Application;
use DOMDocument;
use DOMElement;
use PKP\cache\CacheManager;
use PKP\cache\FileCache;
use PKP\controllers\grid\plugins\PluginGalleryGridHandler;
use PKP\core\PKPApplication;
use PKP\core\PKPString;
use PKP\db\DAORegistry;
use PKP\site\VersionDAO;
use Throwable;
class PluginGalleryDAO extends \PKP\db\DAO
{
public const PLUGIN_GALLERY_XML_URL = 'https://pkp.sfu.ca/ojs/xml/plugins.xml';
/**
* The default timeout (in seconds) to wait plugins.xml request
*
* @see https://docs.guzzlephp.org/en/6.5/request-options.html#timeout
*/
public const DEFAULT_TIMEOUT = 10;
/**
* TTL's Cache in seconds
*/
public const TTL_CACHE_SECONDS = 86400;
/**
* Get a set of GalleryPlugin objects describing the available
* compatible plugins in their newest versions.
*
* @param PKPApplication $application
* @param string $category Optional category name to use as filter
* @param string $search Optional text to use as filter
*
* @return array GalleryPlugin objects
*/
public function getNewestCompatible($application, $category = null, $search = null)
{
$doc = $this->_getDocument();
$plugins = [];
foreach ($doc->getElementsByTagName('plugin') as $index => $element) {
$plugin = $this->_compatibleFromElement($element, $application);
// May be null if no compatible version exists; also
// apply search filters if any supplied.
if (
$plugin &&
($category == '' || $category == PluginGalleryGridHandler::PLUGIN_GALLERY_ALL_CATEGORY_SEARCH_VALUE || $plugin->getCategory() == $category) &&
($search == '' || PKPString::strpos(PKPString::strtolower(serialize($plugin)), PKPString::strtolower($search)) !== false)
) {
$plugins[$index] = $plugin;
}
}
return $plugins;
}
/**
* Get the external Plugin XML document
*
* @return ?string
*/
protected function getExternalDocument(): ?string
{
$application = Application::get();
$client = $application->getHttpClient();
/** @var VersionDAO */
$versionDao = DAORegistry::getDAO('VersionDAO');
$currentVersion = $versionDao->getCurrentVersion();
try {
$response = $client->request(
'GET',
static::PLUGIN_GALLERY_XML_URL,
[
'query' => [
'application' => $application->getName(),
'version' => $currentVersion->getVersionString()
],
'timeout' => self::DEFAULT_TIMEOUT,
]
);
return $response->getBody();
} catch (Throwable $e) {
error_log($e->getMessage());
return null;
}
}
/**
* Get the cached Plugin XML document
*
* @return ?string
*/
protected function getCachedDocument(): ?string
{
$cacheManager = CacheManager::getManager();
/** @var FileCache */
$cache = $cacheManager->getCache(
'loadPluginsXML',
Application::CONTEXT_SITE,
function (FileCache $cache) {
$cache->setEntireCache($this->getExternalDocument());
}
);
$cacheTime = $cache->getCacheTime();
// Checking if the cache is older than 1 day, or its null
if ($cacheTime === null || (time() - $cacheTime > self::TTL_CACHE_SECONDS)) {
// This cache is out of date; so, lets request a new version.
$response = $this->getExternalDocument();
// The plugins.xml request wasnt empty, so lets replace it
if ($response !== null) {
$cache->setEntireCache($response);
}
}
return $cache->getContents();
}
/**
* Get the DOM document for the plugin gallery.
*
* @return DOMDocument
*/
private function _getDocument()
{
$doc = new DOMDocument('1.0', 'utf-8');
$doc->loadXML($this->getCachedDocument());
return $doc;
}
/**
* Construct a new data object.
*
* @return GalleryPlugin
*/
public function newDataObject()
{
return new GalleryPlugin();
}
/**
* Build a GalleryPlugin from a DOM element, using the newest compatible
* release with the supplied Application.
*
* @param DOMElement $element
* @param Application $application
*
* @return GalleryPlugin|null, if no compatible plugin was available
*/
protected function _compatibleFromElement($element, $application)
{
$plugin = $this->newDataObject();
$plugin->setCategory($element->getAttribute('category'));
$plugin->setProduct($element->getAttribute('product'));
$doc = $element->ownerDocument;
$foundRelease = false;
for ($n = $element->firstChild; $n; $n = $n->nextSibling) {
if (!($n instanceof DOMElement)) {
continue;
}
switch ($n->tagName) {
case 'name':
$plugin->setName($n->nodeValue, $n->getAttribute('locale'));
break;
case 'homepage':
$plugin->setHomepage($n->nodeValue);
break;
case 'description':
$plugin->setDescription($n->nodeValue, $n->getAttribute('locale'));
break;
case 'installation':
$plugin->setInstallationInstructions($n->nodeValue, $n->getAttribute('locale'));
break;
case 'summary':
$plugin->setSummary($n->nodeValue, $n->getAttribute('locale'));
break;
case 'maintainer':
$this->_handleMaintainer($n, $plugin);
break;
case 'release':
// If a compatible release couldn't be
// found, return null.
if ($this->_handleRelease($n, $plugin, $application)) {
$foundRelease = true;
}
break;
default:
// Not erroring out here so that future
// additions won't break old releases.
}
}
if (!$foundRelease) {
// No compatible release was found.
return null;
}
return $plugin;
}
/**
* Handle a maintainer element
*
* @param GalleryPlugin $plugin
*/
public function _handleMaintainer($element, $plugin)
{
for ($n = $element->firstChild; $n; $n = $n->nextSibling) {
if (!($n instanceof DOMElement)) {
continue;
}
switch ($n->tagName) {
case 'name':
$plugin->setContactName($n->nodeValue);
break;
case 'institution':
$plugin->setContactInstitutionName($n->nodeValue);
break;
case 'email':
$plugin->setContactEmail($n->nodeValue);
break;
default:
// Not erroring out here so that future
// additions won't break old releases.
}
}
}
/**
* Handle a release element
*
* @param GalleryPlugin $plugin
* @param PKPApplication $application
*/
public function _handleRelease($element, $plugin, $application)
{
$release = [
'date' => strtotime($element->getAttribute('date')),
'version' => $element->getAttribute('version'),
'md5' => $element->getAttribute('md5'),
];
$compatible = false;
for ($n = $element->firstChild; $n; $n = $n->nextSibling) {
if (!($n instanceof DOMElement)) {
continue;
}
switch ($n->tagName) {
case 'description':
$release[$n->tagName][$n->getAttribute('locale')] = $n->nodeValue;
break;
case 'package':
$release['package'] = $n->nodeValue;
break;
case 'compatibility':
// If a compatible release couldn't be
// found, return null.
if ($this->_handleCompatibility($n, $plugin, $application)) {
$compatible = true;
}
break;
case 'certification':
$release[$n->tagName][] = $n->getAttribute('type');
break;
default:
// Not erroring out here so that future
// additions won't break old releases.
}
}
if ($compatible && (!$plugin->getData('version') || version_compare($plugin->getData('version'), $release['version'], '<'))) {
// This release is newer than the one found earlier, or
// this is the first compatible release we've found.
$plugin->setDate($release['date']);
$plugin->setVersion($release['version']);
$plugin->setReleaseMD5($release['md5']);
$plugin->setReleaseDescription($release['description']);
$plugin->setReleaseCertifications($release['certification'] ?? []);
$plugin->setReleasePackage($release['package']);
return true;
}
return false;
}
/**
* Handle a compatibility element, fishing out the most recent statement
* of compatibility.
*
* @param GalleryPlugin $plugin
* @param PKPApplication $application
*
* @return bool True iff a compatibility statement matched this app
*/
public function _handleCompatibility($element, $plugin, $application)
{
// Check that the compatibility statement refers to this app
if ($element->getAttribute('application') != $application->getName()) {
return false;
}
for ($n = $element->firstChild; $n; $n = $n->nextSibling) {
if (!($n instanceof DOMElement)) {
continue;
}
switch ($n->tagName) {
case 'version':
$installedVersion = $application->getCurrentVersion();
if ($installedVersion->isCompatible($n->nodeValue)) {
// Compatibility was determined.
return true;
}
break;
}
}
// No applicable compatibility statement found.
return false;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\plugins\PluginGalleryDAO', '\PluginGalleryDAO');
define('PLUGIN_GALLERY_XML_URL', PluginGalleryDAO::PLUGIN_GALLERY_XML_URL);
}
|