.
*/
namespace Alledia\Installer;
use Alledia\Installer\Extension\Licensed;
use JEventDispatcher;
use JFormFieldCustomFooter;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Table\Extension;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Version;
use Joomla\Component\Plugins\Administrator\Model\PluginModel;
use Joomla\Database\DatabaseDriver;
use Joomla\Event\DispatcherInterface;
use Joomla\Registry\Registry;
use SimpleXMLElement;
use Throwable;
// phpcs:disable PSR1.Files.SideEffects
defined('_JEXEC') or die();
require_once 'include.php';
// phpcs:enable PSR1.Files.SideEffects
abstract class AbstractScript
{
public const VERSION = '2.4.4';
/**
* Recognized installation types
*/
protected const TYPE_INSTALL = 'install';
protected const TYPE_DISCOVER_INSTALL = 'discover_install';
protected const TYPE_UPDATE = 'update';
protected const TYPE_UNINSTALL = 'uninstall';
/**
* @var bool
*/
protected $outputAllowed = true;
/**
* @var CMSApplication
*/
protected $app = null;
/**
* @var DatabaseDriver
*/
protected $dbo = null;
/**
* @var string
*/
protected $schemaVersion = null;
/**
* @var JEventDispatcher|DispatcherInterface
*/
protected $dispatcher = null;
/**
* @var Installer
*/
protected $installer = null;
/**
* @var SimpleXMLElement
*/
protected $manifest = null;
/**
* @var SimpleXMLElement
*/
protected $previousManifest = null;
/**
* @var string
*/
protected $mediaFolder = null;
/**
* @var string
*/
protected $element = null;
/**
* @var string[]
*/
protected $systemExtensions = [
'library..allediaframework',
'plugin.system.osmylicensesmanager',
];
/**
* @var bool
*/
protected $isLicensesManagerInstalled = false;
/**
* @var Licensed
*/
protected $license = null;
/**
* @var string
*/
protected $licenseKey = null;
/**
* @var string
*/
protected $footer = null;
/**
* @var string
*/
protected $mediaURL = null;
/**
* @var string[]
* @deprecated v2.0.0
*/
protected $messages = [];
/**
* @var string
*/
protected $type = null;
/**
* @var string
*/
protected $group = null;
/**
* List of tables and respective columns
*
* @var array
*/
protected $columns = null;
/**
* List of tables and respective indexes
*
* @var array
* @deprecated v2.1.0
*/
protected $indexes = null;
/**
* @var object[]
*/
protected $tableColumns = [];
/**
* @var object[]
*/
protected $tableIndexes = [];
/**
* @var object[]
*/
protected $tableConstraints = [];
/**
* @var array
*/
protected $tables = null;
/**
* Flag to cancel the installation
*
* @var bool
*/
protected $cancelInstallation = false;
/**
* Feedback of the install by related extension
*
* @var array
*/
protected $relatedExtensionFeedback = [];
/**
* @var string
*/
protected $welcomeMessage = null;
/**
* @var bool
*/
protected $debug = false;
/**
* @param InstallerAdapter $parent
*
* @return void
* @throws \Exception
*/
public function __construct(InstallerAdapter $parent)
{
$this->sendDebugMessage('ShackInstaller v' . static::VERSION);
$this->sendDebugMessage('Base v' . SHACK_INSTALLER_VERSION);
$this->sendDebugMessage(__METHOD__);
$this->initProperties($parent);
}
/**
* @param InstallerAdapter $parent
*
* @return bool
* @throws \Exception
*/
protected function checkInheritance(InstallerAdapter $parent): bool
{
$parentClasses = class_parents($this);
$scriptClassName = array_pop($parentClasses);
$scriptClass = new \ReflectionClass($scriptClassName);
$sourcePath = dirname($scriptClass->getFileName());
$sourceBase = strpos($sourcePath, JPATH_PLUGINS) === 0 ? 3 : 2;
$sourceVersion = AbstractScript::VERSION ?? '0.0.0';
$sourcePath = $this->cleanPath($sourcePath);
$targetPath = $this->cleanPath(SHACK_INSTALLER_BASE);
if ($sourcePath != $targetPath && version_compare($sourceVersion, SHACK_INSTALLER_COMPATIBLE, 'lt')) {
$source = join('/', array_slice(explode('/', $sourcePath), 0, $sourceBase));
$errorMessage = 'LIB_SHACKINSTALLER_ABORT_'
. ($parent->getRoute() == 'uninstall' ? 'UNINSTALL' : 'INSTALL');
Factory::getApplication()->enqueueMessage(Text::sprintf($errorMessage, $source), 'error');
$this->cancelInstallation = true;
return false;
}
return true;
}
/**
* @param string $path
*
* @return string
*/
protected function cleanPath(string $path): string
{
return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(JPATH_ROOT . '/', '', $path));
}
/**
* @param InstallerAdapter $parent
*
* @return void
* @throws \Exception
*/
protected function initProperties(InstallerAdapter $parent): void
{
$this->sendDebugMessage(__METHOD__);
$this->app = Factory::getApplication();
$this->outputAllowed = JPATH_BASE == JPATH_ADMINISTRATOR;
$language = Factory::getLanguage();
$language->load('lib_shackinstaller.sys', realpath(__DIR__ . '/../..'));
if ($this->checkInheritance($parent) == false) {
return;
}
try {
$this->dbo = Factory::getDbo();
$this->installer = $parent->getParent();
$this->manifest = $this->installer->getManifest();
$this->schemaVersion = $this->getSchemaVersion();
if ($media = $this->manifest->media) {
$this->mediaFolder = JPATH_SITE . '/' . $media['folder'] . '/' . $media['destination'];
}
$attributes = $this->manifest->attributes();
$this->type = (string)$attributes['type'];
$this->group = (string)$attributes['group'];
// Get the previous manifest for use in upgrades
$targetPath = $this->installer->getPath('extension_administrator')
?: $this->installer->getPath('extension_root');
$manifestPath = $targetPath . '/' . basename($this->installer->getPath('manifest'));
if (is_file($manifestPath)) {
$this->previousManifest = simplexml_load_file($manifestPath);
}
// Determine basepath for localized files
$basePath = $this->installer->getPath('source');
if (is_dir($basePath)) {
if ($this->type == 'component' && $basePath != $targetPath) {
// For components sourced by manifest, need to find the admin folder
if ($files = $this->manifest->administration->files) {
if ($files = (string)$files['folder']) {
$basePath .= '/' . $files;
}
}
}
} else {
$basePath = $this->getExtensionPath(
$this->type,
(string)$this->manifest->alledia->element,
$this->group
);
}
// All the files we want to load
$languageFiles = [
$this->getFullElement(),
];
// Load from localized or core language folder
foreach ($languageFiles as $languageFile) {
$language->load($languageFile, $basePath) || $language->load($languageFile, JPATH_ADMINISTRATOR);
}
} catch (Throwable $error) {
$this->cancelInstallation = true;
$this->sendErrorMessage($error);
}
}
/**
* @return JEventDispatcher|DispatcherInterface
*/
protected function getDispatcher()
{
if ($this->dispatcher === null) {
if (Version::MAJOR_VERSION < 4) {
$this->dispatcher = JEventDispatcher::getInstance();
} else {
$this->dispatcher = $this->app->getDispatcher();
}
}
return $this->dispatcher;
}
/**
* @param InstallerAdapter $parent
*
* @return bool
*/
final public function install(InstallerAdapter $parent): bool
{
$this->sendDebugMessage(__METHOD__);
try {
return $this->customInstall($parent);
} catch (Throwable $error) {
$this->sendErrorMessage($error);
}
return false;
}
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
/**
* @param InstallerAdapter $parent
*
* @return bool
*/
final public function discover_install(InstallerAdapter $parent): bool
{
$this->sendDebugMessage(__METHOD__);
try {
if ($this->install($parent)) {
return $this->customDiscoverInstall($parent);
}
} catch (Throwable $error) {
$this->sendErrorMessage($error);
}
return false;
}
// phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
/**
* @param InstallerAdapter $parent
*
* @return bool
*/
final public function update(InstallerAdapter $parent): bool
{
$this->sendDebugMessage(__METHOD__);
try {
return $this->customUpdate($parent);
} catch (Throwable $error) {
$this->sendErrorMessage($error);
}
return false;
}
/**
* @param string $type
* @param InstallerAdapter $parent
*
* @return bool
* @throws \Exception
*/
final public function preFlight(string $type, InstallerAdapter $parent): bool
{
if ($this->cancelInstallation) {
$this->sendDebugMessage('CANCEL: ' . __METHOD__);
return false;
}
try {
$this->sendDebugMessage(__METHOD__);
$success = true;
if ($type === 'update') {
$this->clearUpdateServers();
}
if (in_array($type, [static::TYPE_INSTALL, static::TYPE_UPDATE])) {
// Check minimum target Joomla Platform
if (isset($this->manifest->alledia->targetplatform)) {
$targetPlatform = (string)$this->manifest->alledia->targetplatform;
if ($this->validateTargetVersion(JVERSION, $targetPlatform) == false) {
// Platform version is invalid. Displays a warning and cancel the install
$targetPlatform = str_replace('*', 'x', $targetPlatform);
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_PLATFORM', $this->getName(), $targetPlatform);
$this->sendMessage($msg, 'warning');
$success = false;
}
}
// Check for minimum mysql version
if ($targetMySqlVersion = $this->manifest->alledia->mysqlminimum) {
$targetMySqlVersion = (string)$targetMySqlVersion;
if ($this->dbo->getServerType() == 'mysql') {
$dbVersion = $this->dbo->getVersion();
if (stripos($dbVersion, 'maria') !== false) {
// For MariaDB this is a bit of a punt. We'll assume any version of Maria will do
$dbVersion = $targetMySqlVersion;
}
if ($this->validateTargetVersion($dbVersion, $targetMySqlVersion) == false) {
// mySQL version too low
$minimumMySql = str_replace('*', 'x', $targetMySqlVersion);
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_MYSQL', $this->getName(), $minimumMySql);
$this->sendMessage($msg, 'warning');
$success = false;
}
}
}
// Check for minimum php version
if (isset($this->manifest->alledia->phpminimum)) {
$targetPhpVersion = (string)$this->manifest->alledia->phpminimum;
if ($this->validateTargetVersion(phpversion(), $targetPhpVersion) == false) {
// php version is too low
$minimumPhp = str_replace('*', 'x', $targetPhpVersion);
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_PHP', $this->getName(), $minimumPhp);
$this->sendMessage($msg, 'warning');
$success = false;
}
}
// Check for minimum previous version
$targetVersion = (string)$this->manifest->alledia->previousminimum;
if ($type == static::TYPE_UPDATE && $targetVersion) {
if (!$this->validatePreviousVersion($targetVersion)) {
// Previous minimum is not installed
$minimumVersion = str_replace('*', 'x', $targetVersion);
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_PREVIOUS', $this->getName(), $minimumVersion);
$this->sendMessage($msg, 'warning');
$success = false;
}
}
}
if ($success) {
$success = $this->customPreFlight($type, $parent);
}
if ($success) {
if (
$type !== static::TYPE_UNINSTALL
&& empty($this->manifest->alledia->obsolete->preflight) == false
) {
$this->clearObsolete($this->manifest->alledia->obsolete->preflight);
}
}
$this->cancelInstallation = $success == false;
return $success;
} catch (Throwable $error) {
$this->sendErrorMessage($error);
}
return false;
}
/**
* @param string $type
* @param InstallerAdapter $parent
*
* @return void
* @throws \Exception
*/
final public function postFlight(string $type, InstallerAdapter $parent): void
{
$this->sendDebugMessage(__METHOD__);
if ($this->cancelInstallation) {
$this->sendMessage('LIB_SHACKINSTALLER_INSTALL_CANCELLED', 'warning');
return;
}
try {
/*
* Joomla 4 now calls postFlight on uninstalls. Which is kinda cool actually.
* But this code is problematic in that scenario
*/
if ($type != static::TYPE_UNINSTALL) {
$this->clearObsolete();
$this->installRelated();
$this->addAllediaAuthorshipToExtension();
$this->element = (string)$this->manifest->alledia->element;
// Check and publish/reorder the plugin, if required
if (
$this->type === 'plugin'
&& in_array($type, [static::TYPE_INSTALL, static::TYPE_DISCOVER_INSTALL])
) {
$this->publishThisPlugin();
$this->reorderThisPlugin();
}
// If Free, remove any Pro library
$license = $this->getLicense();
if (!$license->isPro()) {
$proLibraryPath = $license->getProLibraryPath();
if (is_dir($proLibraryPath)) {
Folder::delete($proLibraryPath);
}
}
}
$this->customPostFlight($type, $parent);
if ($type != static::TYPE_UNINSTALL) {
$this->displayWelcome($type);
}
} catch (Throwable $error) {
$this->sendErrorMessage($error);
}
}
/**
* @param InstallerAdapter $parent
*
* @return void
* @throws \Exception
*/
final public function uninstall(InstallerAdapter $parent): void
{
$this->sendDebugMessage(__METHOD__);
try {
$this->uninstallRelated();
$this->customUninstall($parent);
} catch (Throwable $error) {
$this->sendErrorMessage($error);
}
}
/**
* @param int $number
* @param string $error
* @param ?string $file
* @param ?int $line
*
* @return void
*/
public static function errorHandler(int $number, string $error, ?string $file = null, ?int $line = null): void
{
try {
$codes = get_defined_constants(true);
$codes = $codes['Core'];
$codes = array_filter(
$codes,
function ($key) {
return strpos($key, 'E_') === 0;
},
ARRAY_FILTER_USE_KEY
);
$name = array_search($number, $codes);
Factory::getApplication()->enqueueMessage(
sprintf('%s: %s
(%s) %s', $name, $error, $line ?: 'NA', $file ?: 'NA'),
'warning'
);
} catch (Throwable $error) {
// ignore
}
}
/**
* For use in subclasses
*
* @param string $type
* @param InstallerAdapter $parent
*
* @return bool
* @throws Throwable
*/
protected function customPreFlight(string $type, InstallerAdapter $parent): bool
{
return true;
}
/**
* For use in subclasses
*
* @param InstallerAdapter $parent
*
* @return bool
* @throws Throwable
*/
protected function customInstall(InstallerAdapter $parent): bool
{
return true;
}
/**
* For use in subclasses
*
* @param InstallerAdapter $parent
*
* @return bool
* @throws Throwable
*/
protected function customDiscoverInstall(InstallerAdapter $parent): bool
{
return true;
}
/**
* For use in subclasses
*
* @param InstallerAdapter $parent
*
* @return bool
* @throws Throwable
*/
protected function customUpdate(InstallerAdapter $parent): bool
{
return true;
}
/**
* For use in subclassses
*
* @param string $type
* @param InstallerAdapter $parent
*
* @return void
* @throws Throwable
*/
protected function customPostFlight(string $type, InstallerAdapter $parent): void
{
}
/**
* For use in subclasses
*
* @param InstallerAdapter $parent
*
* @return void
* @throws Throwable
*/
protected function customUninstall(InstallerAdapter $parent): void
{
}
/**
* @return void
* @throws \Exception
*/
final protected function installRelated(): void
{
$this->sendDebugMessage(__METHOD__);
if ($this->manifest->alledia->relatedExtensions) {
$source = $this->installer->getPath('source');
$extensionsPath = $source . '/extensions';
$defaultAttributes = $this->manifest->alledia->relatedExtensions->attributes();
$defaultDowngrade = $this->getXmlValue($defaultAttributes['downgrade'], 'bool');
$defaultPublish = $this->getXmlValue($defaultAttributes['publish'], 'bool');
foreach ($this->manifest->alledia->relatedExtensions->extension as $extension) {
$path = $extensionsPath . '/' . $this->getXmlValue($extension);
if (is_dir($path)) {
$type = $this->getXmlValue($extension['type']);
$element = $this->getXmlValue($extension['element']);
$group = $this->getXmlValue($extension['group']);
$key = md5(join(':', [$type, $element, $group]));
$this->sendDebugMessage(
sprintf('Related: %s%s/%s', $type, $group ? ($group . '/') : '', $element)
);
try {
if ($type == 'plugin' && in_array($group, ['search', 'finder'])) {
if (is_dir(JPATH_ADMINISTRATOR . '/components/com_' . $group) == false) {
// skip search/finder plugins based on installed components
$this->sendDebugMessage(
sprintf(
'Skipped/Uninstalled plugin %s',
ucwords($group . ' ' . $element)
)
);
$this->uninstallExtension($type, $element, $group);
continue;
}
}
$current = $this->findExtension($type, $element, $group);
$isNew = empty($current);
$typeName = ucwords(trim($group . ' ' . $type));
// Get data from the manifest
$tmpInstaller = new Installer();
$tmpInstaller->setPath('source', $path);
$tmpInstaller->setPath('parent', $this->installer->getPath('source'));
$newManifest = $tmpInstaller->getManifest();
$newVersion = (string)$newManifest->version;
$this->storeFeedbackForRelatedExtension($key, 'name', (string)$newManifest->name);
$downgrade = $this->getXmlValue($extension['downgrade'], 'bool', $defaultDowngrade);
if (($isNew || $downgrade) == false) {
$currentManifestPath = $this->getManifestPath($type, $element, $group);
$currentManifest = $this->getInfoFromManifest($currentManifestPath);
// Avoid to update for an outdated version
$currentVersion = $currentManifest->get('version');
if (version_compare($currentVersion, $newVersion, '>')) {
// Store the state of the install/update
$this->storeFeedbackForRelatedExtension(
$key,
'message',
Text::sprintf(
'LIB_SHACKINSTALLER_RELATED_UPDATE_STATE_SKIPED',
$newVersion,
$currentVersion
)
);
// Skip the installation for this extension
continue;
}
}
$text = 'LIB_SHACKINSTALLER_RELATED_' . ($isNew ? 'INSTALL' : 'UPDATE');
if ($tmpInstaller->install($path)) {
$this->sendMessage(Text::sprintf($text, $typeName, $element));
if ($isNew) {
$current = $this->findExtension($type, $element, $group);
if (is_object($current)) {
if ($type === 'plugin') {
if ($this->getXmlValue($extension['publish'], 'bool', $defaultPublish)) {
$current->publish();
$this->storeFeedbackForRelatedExtension($key, 'publish', true);
}
if ($ordering = $this->getXmlValue($extension['ordering'])) {
$this->setPluginOrder($current, $ordering);
$this->storeFeedbackForRelatedExtension($key, 'ordering', $ordering);
}
}
}
}
$this->storeFeedbackForRelatedExtension(
$key,
'message',
Text::sprintf('LIB_SHACKINSTALLER_RELATED_UPDATE_STATE_INSTALLED', $newVersion)
);
} else {
$this->sendMessage(Text::sprintf($text . '_FAIL', $typeName, $element), 'error');
$this->storeFeedbackForRelatedExtension(
$key,
'message',
Text::sprintf(
'LIB_SHACKINSTALLER_RELATED_UPDATE_STATE_FAILED',
$newVersion
)
);
}
unset($tmpInstaller);
} catch (Throwable $error) {
$this->sendErrorMessage($error, false);
}
}
}
}
}
/**
* Uninstall the related extensions that are useless without the component
*
* @return void
* @throws \Exception
*/
final protected function uninstallRelated(): void
{
if ($this->manifest->alledia->relatedExtensions) {
$defaultAttributes = $this->manifest->alledia->relatedExtensions->attributes();
$defaultUninstall = $this->getXmlValue($defaultAttributes['uninstall'], 'bool');
foreach ($this->manifest->alledia->relatedExtensions->extension as $extension) {
$type = $this->getXmlValue($extension['type']);
$element = $this->getXmlValue($extension['element']);
$group = $this->getXmlValue($extension['group']);
$uninstall = $this->getXmlValue($extension['uninstall'], 'bool', $defaultUninstall);
$systemExtension = in_array(join('.', [$type, $group, $element]), $this->systemExtensions);
if ($uninstall && $systemExtension == false) {
$this->uninstallExtension($type, $element, $group);
} else {
$message = 'LIB_SHACKINSTALLER_RELATED_NOT_UNINSTALLED'
. ($systemExtension ? '_SYSTEM' : '');
if ($type == 'plugin') {
$type = $group . ' ' . $type;
}
$this->sendDebugMessage(Text::sprintf($message, ucwords($type), $element));
}
}
}
}
/**
* @param string $type
* @param string $element
* @param ?string $group
*
* @return void
* @throws \Exception
*/
final protected function uninstallExtension(string $type, string $element, ?string $group = null): void
{
if ($extension = $this->findExtension($type, $element, $group)) {
$installer = new Installer();
$success = $installer->uninstall($extension->get('type'), $extension->get('extension_id'));
$msg = 'LIB_SHACKINSTALLER_RELATED_UNINSTALL' . ($success ? '' : '_FAIL');
if ($type == 'plugin') {
$type = $group . ' ' . $type;
}
$this->sendMessage(
Text::sprintf($msg, ucwords($type), $element),
$success ? 'message' : 'error'
);
}
}
/**
* @param ?string $type
* @param ?string $element
* @param ?string $group
*
* @return ?Extension
* @throws \Exception
*/
final protected function findExtension(?string $type, ?string $element, ?string $group = null): ?Extension
{
// @TODO: Why do we need to use JTable?
/** @var Extension $row */
$row = Table::getInstance('extension');
$prefixes = [
'component' => 'com_',
'module' => 'mod_',
];
// Fix the element, if the prefix is not found
if (array_key_exists($type, $prefixes)) {
if (substr_count($element, $prefixes[$type]) === 0) {
$element = $prefixes[$type] . $element;
}
}
$terms = [
'type' => $type,
'element' => $element,
];
if ($type === 'plugin') {
$terms['folder'] = $group;
}
$eid = $row->find($terms);
if ($eid) {
if ($row->load($eid) == false) {
throw new \Exception($row->getError());
}
return $row;
}
return null;
}
/**
* Set requested ordering for selected plugin extension
* Accepted ordering arguments:
* (n<=1 | first) First within folder
* (* | last) Last within folder
* (before:element) Before the named plugin
* (after:element) After the named plugin
*
* @param Extension $extension
* @param string $order
*
* @return void
*/
final protected function setPluginOrder(Extension $extension, string $order): void
{
if ($extension->get('type') == 'plugin' && empty($order) == false) {
$db = $this->dbo;
$query = $db->getQuery(true);
$query->select('extension_id, element');
$query->from('#__extensions');
$query->where([
$db->quoteName('folder') . ' = ' . $db->quote($extension->get('folder')),
$db->quoteName('type') . ' = ' . $db->quote($extension->get('type')),
]);
$query->order($db->quoteName('ordering'));
$plugins = $db->setQuery($query)->loadObjectList('element');
// Set the order only if plugin already successfully installed
if (array_key_exists($extension->get('element'), $plugins)) {
$target = [
$extension->get('element') => $plugins[$extension->get('element')],
];
$others = array_diff_key($plugins, $target);
if ((is_numeric($order) && $order <= 1) || $order == 'first') {
// First in order
$neworder = array_merge($target, $others);
} elseif (($order == '*') || ($order == 'last')) {
// Last in order
$neworder = array_merge($others, $target);
} elseif (preg_match('/^(before|after):(\S+)$/', $order, $match)) {
// place before or after named plugin
$place = $match[1];
$element = $match[2];
$neworder = [];
$previous = '';
foreach ($others as $plugin) {
if (
(($place == 'before') && ($plugin->element == $element))
|| (($place == 'after') && ($previous == $element))
) {
$neworder = array_merge($neworder, $target);
}
$neworder[$plugin->element] = $plugin;
$previous = $plugin->element;
}
if (count($neworder) < count($plugins)) {
// Make it last if the requested plugin isn't installed
$neworder = array_merge($neworder, $target);
}
} else {
$neworder = [];
}
if (count($neworder) == count($plugins)) {
// Only reorder if have a validated new order
BaseDatabaseModel::addIncludePath(
JPATH_ADMINISTRATOR . '/components/com_plugins/models',
'PluginsModels'
);
// @TODO: Model class is (\PluginsModelPlugin) in J3 but this works either way
/** @var PluginModel $model */
$model = BaseDatabaseModel::getInstance('Plugin', 'PluginsModel');
$ids = [];
foreach ($neworder as $plugin) {
$ids[] = $plugin->extension_id;
}
$order = range(1, count($ids));
$model->saveorder($ids, $order);
}
}
}
}
/**
* Add a message to the message list
*
* @param string $message
* @param ?string $type
*
* @return void
* @deprecated v2.0.0: use $this->sendMessage()
*/
final protected function setMessage(string $message, ?string $type = 'message'): void
{
$this->sendMessage($message, $type);
}
/**
* Delete obsolete files, folders and extensions.
* Files and folders are identified from the site
* root path.
*
* @param ?SimpleXMLElement $obsolete
*
* @return void
* @throws \Exception
*/
final protected function clearObsolete(?SimpleXMLElement $obsolete = null): void
{
$obsolete = $obsolete ?: $this->manifest->alledia->obsolete;
$this->sendDebugMessage(__METHOD__ . '
' . print_r($obsolete, 1) . ''); $this->clearOldSystemPlugin(); if ($obsolete) { if ($obsolete->extension) { foreach ($obsolete->extension as $extension) { $type = $this->getXmlValue($extension['type']); $element = $this->getXmlValue($extension['element']); $group = $this->getXmlValue($extension['group']); $current = $this->findExtension($type, $element, $group); if (empty($current) == false) { // Try to uninstall $tmpInstaller = new Installer(); $uninstalled = $tmpInstaller->uninstall($type, $current->get('extension_id')); $typeName = ucfirst(trim(($group ?: '') . ' ' . $type)); if ($uninstalled) { $this->sendMessage( Text::sprintf( 'LIB_SHACKINSTALLER_OBSOLETE_UNINSTALLED_SUCCESS', strtolower($typeName), $element ) ); } else { $this->sendMessage( Text::sprintf( 'LIB_SHACKINSTALLER_OBSOLETE_UNINSTALLED_FAIL', strtolower($typeName), $element ), 'error' ); } } } } if ($obsolete->file) { foreach ($obsolete->file as $file) { $path = JPATH_ROOT . '/' . trim((string)$file, '/'); if (is_file($path)) { File::delete($path); } } } if ($obsolete->folder) { foreach ($obsolete->folder as $folder) { $path = JPATH_ROOT . '/' . trim((string)$folder, '/'); if (is_dir($path)) { Folder::delete($path); } } } } $oldLanguageFiles = Folder::files(JPATH_ADMINISTRATOR . '/language', '\.lib_allediainstaller\.', true, true); foreach ($oldLanguageFiles as $oldLanguageFile) { File::delete($oldLanguageFile); } } /** * Finds the extension row for the main extension * * @return ?Extension * @throws \Exception */ final protected function findThisExtension(): ?Extension { return $this->findExtension( $this->getXmlValue($this->manifest['type']), $this->getXmlValue($this->manifest->alledia->element), $this->getXmlValue($this->manifest['group']) ); } /** * Use this in preflight to clear out obsolete update servers when the url has changed. * * @return void * @throws \Exception */ final protected function clearUpdateServers(): void { if ($extension = $this->findThisExtension()) { $db = $this->dbo; $query = $db->getQuery(true) ->select($db->quoteName('update_site_id')) ->from($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . '=' . (int)$extension->get('extension_id')); if ($list = $db->setQuery($query)->loadColumn()) { $query = $db->getQuery(true) ->delete($db->quoteName('#__update_sites_extensions')) ->where($db->quoteName('extension_id') . '=' . (int)$extension->get('extension_id')); $db->setQuery($query)->execute(); array_walk($list, 'intval'); $query = $db->getQuery(true) ->delete($db->quoteName('#__update_sites')) ->where($db->quoteName('update_site_id') . ' IN (' . join(',', $list) . ')'); $db->setQuery($query)->execute(); } } } /** * Get the full element, like com_myextension, lib_extension * * @param ?string $type * @param ?string $element * @param ?string $group * * @return string */ final protected function getFullElement( ?string $type = null, ?string $element = null, ?string $group = null ): string { $prefixes = [ 'component' => 'com', 'plugin' => 'plg', 'template' => 'tpl', 'library' => 'lib', 'cli' => 'cli', 'module' => 'mod', 'file' => 'file', ]; $type = $type ?: $this->type; $element = $element ?: (string)$this->manifest->alledia->element; $group = $group ?: $this->group; $fullElement = $prefixes[$type] . '_'; if ($type === 'plugin') { $fullElement .= $group . '_'; } return $fullElement . $element; } /** * @return Licensed */ final protected function getLicense(): Licensed { if ($this->license === null) { $this->license = new Licensed( (string)$this->manifest->alledia->namespace, $this->type, $this->group ); } return $this->license; } /** * @param string $manifestPath * * @return Registry */ final protected function getInfoFromManifest(string $manifestPath): Registry { $info = new Registry(); if (is_file($manifestPath)) { $xml = simplexml_load_file($manifestPath); $attributes = (array)$xml->attributes(); $attributes = $attributes['@attributes']; foreach ($attributes as $attribute => $value) { $info->set($attribute, $value); } foreach ($xml->children() as $e) { if (!$e->children()) { $info->set($e->getName(), (string)$e); } } } else { $relativePath = str_replace(JPATH_SITE . '/', '', $manifestPath); $this->sendMessage( Text::sprintf('LIB_SHACKINSTALLER_MANIFEST_NOT_FOUND', $relativePath), 'error' ); } return $info; } /** * @param string $type * @param string $element * @param ?string $group * * @return string */ final protected function getExtensionPath(string $type, string $element, ?string $group = ''): string { $folders = [ 'component' => 'administrator/components/', 'plugin' => 'plugins/', 'template' => 'templates/', 'library' => 'libraries/', 'cli' => 'cli/', 'module' => 'modules/', 'file' => 'administrator/manifests/files/', ]; $basePath = JPATH_SITE . '/' . $folders[$type]; switch ($type) { case 'plugin': $basePath .= $group . '/'; break; case 'module': if (!preg_match('/^mod_/', $element)) { $basePath .= 'mod_'; } break; case 'component': if (!preg_match('/^com_/', $element)) { $basePath .= 'com_'; } break; case 'template': if (preg_match('/^tpl_/', $element)) { $element = str_replace('tpl_', '', $element); } break; } if ($type !== 'file') { $basePath .= $element; } return $basePath; } /** * @param string $type * @param string $element * @param ?string $group * * @return int */ final protected function getExtensionId(string $type, string $element, ?string $group = ''): int { $db = $this->dbo; $query = $db->getQuery(true) ->select('extension_id') ->from('#__extensions') ->where([ $db->quoteName('element') . ' = ' . $db->quote($element), $db->quoteName('folder') . ' = ' . $db->quote($group), $db->quoteName('type') . ' = ' . $db->quote($type), ]); $db->setQuery($query); return (int)$db->loadResult(); } /** * Get the path for the manifest file * * @return string The path */ final protected function getManifestPath($type, $element, $group = ''): string { $installer = new Installer(); switch ($type) { case 'library': case 'file': $folders = [ 'library' => 'libraries', 'file' => 'files', ]; $manifestPath = JPATH_SITE . '/administrator/manifests/' . $folders[$type] . '/' . $element . '.xml'; if (!file_exists($manifestPath) || !$installer->isManifest($manifestPath)) { $manifestPath = false; } break; default: $basePath = $this->getExtensionPath($type, $element, $group); $installer->setPath('source', $basePath); $installer->getManifest(); $manifestPath = $installer->getPath('manifest'); break; } return $manifestPath; } /** * Check if it needs to publish the extension * * @return void * @throws \Exception */ final protected function publishThisPlugin(): void { $attributes = $this->manifest->alledia->element->attributes(); $publish = (string)$attributes['publish']; if ($publish === 'true' || $publish === '1') { $extension = $this->findThisExtension(); $extension->publish(); } } /** * Check if it needs to reorder the extension * * @return void * @throws \Exception */ final protected function reorderThisPlugin(): void { $attributes = $this->manifest->alledia->element->attributes(); $ordering = (string)$attributes['ordering']; if ($ordering !== '') { $extension = $this->findThisExtension(); $this->setPluginOrder($extension, $ordering); } } /** * Stores feedback data for related extensions to display after install * * @param string $key * @param string $property * @param string $value * * @return void */ final protected function storeFeedbackForRelatedExtension(string $key, string $property, string $value): void { $this->sendDebugMessage(sprintf( '%s
' . print_r($files, 1) . ''); $final = reset($files); foreach ($files as $file) { $version = basename($file, '.sql'); $lastVersion = basename($final, '.sql'); if (version_compare($version, $lastVersion, 'gt')) { $final = $file; } File::delete($file); } if ($final) { File::write($final, ''); $this->sendDebugMessage('Wrote blank: ' . $final); } } } /** * @param SimpleXMLElement|string $element * @param ?string $type * @param mixed $default * * @return bool|string */ final protected function getXmlValue($element, ?string $type = 'string', $default = null) { $value = $element ? (string)$element : $default; switch ($type) { case 'bool': case 'boolean': $value = $element ? $value == 'true' || $value == '1' : (bool)$default; break; case 'string': default: if ($value) { $value = trim($value); } break; } return $value; } /** * @param string $text * @param string $type * * @return void */ final protected function sendMessage(string $text, string $type = 'message'): void { if ($this->outputAllowed) { try { $this->app = $this->app ?: Factory::getApplication(); $this->app->enqueueMessage($text, $type); } catch (Throwable $error) { // Give up trying to send a message normally } } } /** * @param Throwable $error * @param bool $cancel * * @return void */ final protected function sendErrorMessage(Throwable $error, bool $cancel = true): void { if ($cancel) { $this->cancelInstallation = true; } if ($this->outputAllowed) { $trace = $error->getTrace(); $trace = array_shift($trace); if (empty($trace['class'])) { $caller = basename($trace['file']); } else { $className = explode('\\', $trace['class']); $caller = array_pop($className); } $line = $trace['line']; $function = $trace['function'] ?? null; $file = $trace['file']; if ($function) { $message = sprintf('%s: %s