264 lines
11 KiB
PHP
264 lines
11 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file classes/plugins/PluginHelper.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 PluginHelper
|
|
*
|
|
* @ingroup classes_plugins
|
|
*
|
|
* @brief Helper class implementing plugin administration functions.
|
|
*/
|
|
|
|
namespace PKP\plugins;
|
|
|
|
use APP\install\Install;
|
|
use APP\install\Upgrade;
|
|
use DirectoryIterator;
|
|
use Exception;
|
|
use Illuminate\Support\Arr;
|
|
use PharData;
|
|
use PKP\config\Config;
|
|
use PKP\core\Core;
|
|
use PKP\db\DAORegistry;
|
|
use PKP\file\FileManager;
|
|
use PKP\site\SiteDAO;
|
|
use PKP\site\Version;
|
|
use PKP\site\VersionCheck;
|
|
use PKP\site\VersionDAO;
|
|
use Throwable;
|
|
|
|
class PluginHelper
|
|
{
|
|
public const PLUGIN_ACTION_UPLOAD = 'upload';
|
|
public const PLUGIN_ACTION_UPGRADE = 'upgrade';
|
|
|
|
public const PLUGIN_VERSION_FILE = 'version.xml';
|
|
public const PLUGIN_INSTALL_FILE = 'install.xml';
|
|
public const PLUGIN_UPGRADE_FILE = 'upgrade.xml';
|
|
|
|
/**
|
|
* Extract the plugin, executes the callback, then cleanup the files
|
|
*
|
|
* @template T of mixed
|
|
*
|
|
* @param string $filePath Full path to plugin archive
|
|
* @param string $originalFileName Original filename of plugin archive
|
|
* @param callable(string $pluginDirectory):T $onExtracted The function will receive as parameter the directory of the plugin
|
|
*
|
|
* @throws Exception If any unexpected error happens (failure to unpack, absence of version.xml, etc)
|
|
*
|
|
* @return T Returns the result of the $onExtracted call
|
|
*/
|
|
private function extractPlugin(string $filePath, string $originalFileName, callable $onExtracted): mixed
|
|
{
|
|
$fileManager = new FileManager();
|
|
$extension = $this->sanitizeFilename($fileManager->parseFileExtension($originalFileName));
|
|
$baseName = $this->sanitizeFilename(basename($originalFileName, ".{$extension}")) ?: 'plugin';
|
|
|
|
// If the extension doesn't match the original one, copy (we don't know the original file) the file to another location to avoid issues with the PharData class
|
|
$filePathWithExtension = null;
|
|
if ($fileManager->parseFileExtension($filePath) !== $extension) {
|
|
$filePathWithExtension = ($fileManager->getTemporaryFile($baseName, ".{$extension}"))->getPathname();
|
|
$fileManager->copyFile($filePath, $filePathWithExtension) || throw new Exception('Failed to copy plugin file');
|
|
}
|
|
$extractPath = null;
|
|
try {
|
|
// Create a random directory to avoid symlink attacks.
|
|
$extractPath = rtrim(sys_get_temp_dir(), '\\/') . "/{$baseName}" . substr(md5(random_int(0, PHP_INT_MAX)), 0, 10) . '/';
|
|
$fileManager->mkdir($extractPath) || throw new Exception("Could not create directory {$extractPath}");
|
|
|
|
// Extract files
|
|
(new PharData($filePathWithExtension ?? $filePath))->extractTo($extractPath, null, true);
|
|
|
|
// Ensure there's a file named "version.xml" at the main directory or at the direct sub-directories
|
|
foreach (new DirectoryIterator($extractPath) as $current) {
|
|
if ($current->isDir() && $current->getBasename() !== '..' && is_file(($path = "{$current->getPathname()}/") . static::PLUGIN_VERSION_FILE)) {
|
|
return $onExtracted($path);
|
|
}
|
|
}
|
|
throw new Exception(__('manager.plugins.invalidPluginArchive'));
|
|
} finally {
|
|
// Cleanup the extracted folder on failure and rethrow
|
|
if ($extractPath) {
|
|
$fileManager->rmtree($extractPath);
|
|
}
|
|
// Cleanup the temporary archive file in case it was created
|
|
if ($filePathWithExtension) {
|
|
unlink($filePathWithExtension);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Installs an extracted plugin
|
|
*
|
|
* @param string $path path to plugin archive
|
|
* @param string $originalFileName Original filename of plugin archive
|
|
*
|
|
* @return Version Version of installed plugin on success
|
|
*/
|
|
public function installPlugin(string $path, string $originalFileName): Version
|
|
{
|
|
return $this->extractPlugin($path, $originalFileName, function (string $pluginFolder): Version {
|
|
$fileManager = new FileManager();
|
|
$versionFile = $pluginFolder . static::PLUGIN_VERSION_FILE;
|
|
$pluginVersion = VersionCheck::getValidPluginVersionInfo($versionFile);
|
|
/** @var VersionDAO */
|
|
$versionDao = DAORegistry::getDAO('VersionDAO');
|
|
$installedPlugin = $versionDao->getCurrentVersion($pluginVersion->getProductType(), $pluginVersion->getProduct());
|
|
$baseDir = Core::getBaseDir() . '/';
|
|
$destinyPath = $baseDir . strtr($pluginVersion->getProductType(), '.', '/') . "/{$pluginVersion->getProduct()}";
|
|
|
|
if ($installedPlugin && is_dir($destinyPath)) {
|
|
throw new Exception(
|
|
$installedPlugin->compare($pluginVersion) < 0
|
|
? __('manager.plugins.pleaseUpgrade')
|
|
: __('manager.plugins.installedVersionNewest')
|
|
);
|
|
}
|
|
|
|
// Copy the plug-in from the temporary folder to the target folder.
|
|
$fileManager->copyDir($pluginFolder, $destinyPath) || throw new Exception('Failed to copy plugin to destination folder');
|
|
|
|
try {
|
|
// Upgrade the database with the new plug-in.
|
|
$installFile = Arr::first(
|
|
["{$destinyPath}/" . static::PLUGIN_INSTALL_FILE, $baseDir . PKP_LIB_PATH . '/xml/defaultPluginInstall.xml'],
|
|
fn (string $path) => is_file($path)
|
|
)
|
|
?? throw new Exception('Missing installation file');
|
|
|
|
$siteDao = DAORegistry::getDAO('SiteDAO'); /** @var SiteDAO $siteDao */
|
|
$site = $siteDao->getSite();
|
|
$params = $this->_getConnectionParams();
|
|
$params['locale'] = $site->getPrimaryLocale();
|
|
$params['additionalLocales'] = $site->getSupportedLocales();
|
|
$installer = new Install($params, $installFile, true);
|
|
$installer->setCurrentVersion($pluginVersion);
|
|
$installer->execute() || throw new Exception(__('manager.plugins.installFailed', ['errorString' => $installer->getErrorString()]));
|
|
$versionDao->insertVersion($pluginVersion, true);
|
|
return $pluginVersion;
|
|
} catch (Throwable $e) {
|
|
// Delete the plugin files on failure
|
|
$fileManager->rmtree($destinyPath);
|
|
throw $e;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load database connection parameters into an array (needed for upgrade).
|
|
*/
|
|
protected function _getConnectionParams(): array
|
|
{
|
|
return [
|
|
'connectionCharset' => Config::getVar('i18n', 'connection_charset'),
|
|
'databaseDriver' => Config::getVar('database', 'driver'),
|
|
'databaseHost' => Config::getVar('database', 'host'),
|
|
'databasePort' => Config::getVar('database', 'port'),
|
|
'unixSocket' => Config::getVar('database', 'unix_socket'),
|
|
'databaseUsername' => Config::getVar('database', 'username'),
|
|
'databasePassword' => Config::getVar('database', 'password'),
|
|
'databaseName' => Config::getVar('database', 'name')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Upgrade a plugin to a newer version from the user's filesystem
|
|
*
|
|
* @param string $path path to plugin archive
|
|
* @param string $originalFileName Original filename of plugin archive
|
|
*
|
|
*/
|
|
public function upgradePlugin(string $category, string $plugin, string $path, string $originalFileName): Version
|
|
{
|
|
return $this->extractPlugin($path, $originalFileName, function (string $pluginFolder) use ($category, $plugin): Version {
|
|
$fileManager = new FileManager();
|
|
$versionFile = $pluginFolder . static::PLUGIN_VERSION_FILE;
|
|
$pluginVersion = VersionCheck::getValidPluginVersionInfo($versionFile);
|
|
|
|
// Check whether the uploaded plug-in fits the original plug-in.
|
|
if ("plugins.{$category}" !== $pluginVersion->getProductType()) {
|
|
throw new Exception(__('manager.plugins.wrongCategory'));
|
|
}
|
|
|
|
if ($plugin !== $pluginVersion->getProduct()) {
|
|
throw new Exception(__('manager.plugins.wrongName'));
|
|
}
|
|
|
|
$versionDao = DAORegistry::getDAO('VersionDAO'); /** @var VersionDAO $versionDao */
|
|
$installedPlugin = $versionDao->getCurrentVersion($pluginVersion->getProductType(), $pluginVersion->getProduct());
|
|
if (!$installedPlugin) {
|
|
throw new Exception(__('manager.plugins.pleaseInstall'));
|
|
}
|
|
|
|
if ($installedPlugin->compare($pluginVersion) >= 0) {
|
|
throw new Exception(__('manager.plugins.installedVersionNewer'));
|
|
}
|
|
|
|
$destinyPath = Core::getBaseDir() . "/plugins/{$category}/{$plugin}";
|
|
|
|
// Delete existing files.
|
|
$fileManager->rmtree($destinyPath);
|
|
|
|
// Check whether deleting has worked.
|
|
if (is_dir($destinyPath)) {
|
|
throw new Exception(__('manager.plugins.deleteError', ['pluginName' => $pluginVersion->getProduct()]));
|
|
}
|
|
|
|
// Copy the plug-in from the temporary folder to the target folder.
|
|
$fileManager->copyDir($pluginFolder, $destinyPath) || throw new Exception('Could not copy plugin to destination!');
|
|
|
|
try {
|
|
$upgradeFile = "{$destinyPath}/" . static::PLUGIN_UPGRADE_FILE;
|
|
if ($fileManager->fileExists($upgradeFile)) {
|
|
/** @var SiteDAO */
|
|
$siteDao = DAORegistry::getDAO('SiteDAO');
|
|
$site = $siteDao->getSite();
|
|
$params = $this->_getConnectionParams();
|
|
$params['locale'] = $site->getPrimaryLocale();
|
|
$params['additionalLocales'] = $site->getSupportedLocales();
|
|
$installer = new Upgrade($params, $upgradeFile, true);
|
|
// Run the upgrade/migration
|
|
$installer->execute() || throw new Exception(__('manager.plugins.upgradeFailed', ['errorString' => $installer->getErrorString()]));
|
|
}
|
|
|
|
// Add the new version to the database
|
|
$pluginVersion->setCurrent(1);
|
|
$versionDao->insertVersion($pluginVersion, true);
|
|
return $pluginVersion;
|
|
} catch (Throwable $e) {
|
|
// Delete the plugin files on failure
|
|
$fileManager->rmtree($destinyPath);
|
|
throw $e;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Drops risky characters from a filename
|
|
*/
|
|
private function sanitizeFilename(string $filename): string
|
|
{
|
|
return preg_replace('/[^\w.-]/', '', $filename);
|
|
}
|
|
}
|
|
|
|
if (!PKP_STRICT_MODE) {
|
|
class_alias('\PKP\plugins\PluginHelper', '\PluginHelper');
|
|
foreach ([
|
|
'PLUGIN_ACTION_UPLOAD',
|
|
'PLUGIN_ACTION_UPGRADE',
|
|
'PLUGIN_VERSION_FILE',
|
|
'PLUGIN_INSTALL_FILE',
|
|
'PLUGIN_UPGRADE_FILE',
|
|
] as $constantName) {
|
|
define($constantName, constant('\PluginHelper::' . $constantName));
|
|
}
|
|
}
|