Files
conservatorio-tomadini/plugins/system/osmylicensesmanager/library/Installer/AbstractScript.php
2024-12-17 17:34:10 +01:00

2507 lines
76 KiB
PHP

<?php
/**
* @package ShackInstaller
* @contact www.joomlashack.com, help@joomlashack.com
* @copyright 2016-2023 Joomlashack.com. All rights reserved
* @license https://www.gnu.org/licenses/gpl.html GNU/GPL
*
* This file is part of ShackInstaller.
*
* ShackInstaller is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* ShackInstaller is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ShackInstaller. If not, see <https://www.gnu.org/licenses/>.
*/
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<br>(%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__ . '<pre>' . print_r($obsolete, 1) . '</pre>');
$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<br>**** %s-%s-%s<br><br>',
__METHOD__,
$key,
$property,
$value
));
if (empty($this->relatedExtensionFeedback[$key])) {
$this->relatedExtensionFeedback[$key] = [];
}
$this->relatedExtensionFeedback[$key][$property] = $value;
}
/**
* This method add a mark to the extensions, allowing to detect our extensions
* on the extensions table.
*
* @return void
* @throws \Exception
*/
final protected function addAllediaAuthorshipToExtension(): void
{
if ($extension = $this->findThisExtension()) {
$db = $this->dbo;
// Update the extension
$customData = json_decode($extension->get('custom_data')) ?: (object)[];
$customData->author = 'Joomlashack';
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('custom_data') . '=' . $db->quote(json_encode($customData)))
->where($db->quoteName('extension_id') . '=' . (int)$extension->get('extension_id'));
$db->setQuery($query)->execute();
// Update the Alledia framework
// @TODO: remove this after libraries be able to have a custom install script
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('custom_data') . '=' . $db->quote('{"author":"Joomlashack"}'))
->where([
$db->quoteName('type') . '=' . $db->quote('library'),
$db->quoteName('element') . '=' . $db->quote('allediaframework'),
]);
$db->setQuery($query)->execute();
}
}
/**
* Add styles to the output. Used because when the postFlight
* method is called, we can't add stylesheets to the head.
*
* @param mixed $stylesheets
*
* @return void
*/
final protected function addStyle($stylesheets): void
{
if (is_string($stylesheets)) {
$stylesheets = [$stylesheets];
}
foreach ($stylesheets as $path) {
if (file_exists($path)) {
$style = file_get_contents($path);
echo '<style>' . $style . '</style>';
}
}
}
/**
* On new component install, this will check and fix any menus
* that may have been created in a previous installation.
*
* @return void
* @throws \Exception
*/
final protected function fixMenus(): void
{
if ($this->type == 'component') {
$db = $this->dbo;
if ($extension = $this->findThisExtension()) {
$id = $extension->get('extension_id');
$option = $extension->get('name');
$query = $db->getQuery(true)
->update('#__menu')
->set('component_id = ' . $db->quote($id))
->where([
'type = ' . $db->quote('component'),
'link LIKE ' . $db->quote("%option={$option}%"),
]);
$db->setQuery($query)->execute();
// Check hidden admin menu option
// @TODO: Remove after Joomla! incorporates this natively
$menuElement = $this->manifest->administration->menu;
if (in_array((string)$menuElement['hidden'], ['true', 'hidden'])) {
$menu = Table::getInstance('Menu');
$menu->load(['component_id' => $id, 'client_id' => 1]);
if ($menu->id) {
$menu->delete();
}
}
}
}
}
/**
* Get and store a cache of columns of a table
*
* @param string $table The table name
*
* @return string[]
* @deprecated v2.1.0: Use $this->findColumn()
*/
final protected function getColumnsFromTable(string $table): array
{
if (!isset($this->columns[$table])) {
$db = $this->dbo;
$db->setQuery('SHOW COLUMNS FROM ' . $db->quoteName($table));
$rows = $db->loadObjectList();
$columns = [];
foreach ($rows as $row) {
$columns[] = $row->Field;
}
$this->columns[$table] = $columns;
}
return $this->columns[$table];
}
/**
* Get and store a cache of indexes of a table
*
* @param string $table The table name
*
* @return string[]
*/
final protected function getIndexesFromTable(string $table): array
{
if (!isset($this->indexes[$table])) {
$db = $this->dbo;
$db->setQuery('SHOW INDEX FROM ' . $db->quoteName($table));
$rows = $db->loadObjectList();
$indexes = [];
foreach ($rows as $row) {
$indexes[] = $row->Key_name;
}
$this->indexes[$table] = $indexes;
}
return $this->indexes[$table];
}
/**
* Add columns to a table if they doesn't exists
*
* @param string $table The table name
* @param string[] $columns Assoc array of columnNames => definition
*
* @return void
* @deprecated v2.1.0: Use $this->addColumns()
*/
final protected function addColumnsIfNotExists(string $table, array $columns): void
{
$columnSpecs = [];
foreach ($columns as $columnName => $columnData) {
$columnId = $table . '.' . $columnName;
$columnSpecs[$columnId] = $columnData;
}
$this->addColumns($columnSpecs);
}
/**
* Add indexes to a table if they doesn't exists
*
* @param string $table The table name
* @param array $indexes Assoc array of indexName => definition
*
* @return void
* @deprecated v2.1.0: use $this->addIndexes()
*/
final protected function addIndexesIfNotExists(string $table, array $indexes): void
{
$db = $this->dbo;
$existentIndexes = $this->getIndexesFromTable($table);
foreach ($indexes as $index => $specification) {
if (!in_array($index, $existentIndexes)) {
$db->setQuery(
"ALTER TABLE {$db->quoteName($table)} CREATE INDEX {$specification} ON {$index}"
)
->execute();
}
}
}
/**
* Drop columns from a table if they exists
*
* @param string $table The table name
* @param string[] $columns The column names that needed to be checked and added
*
* @return void
* @deprecated v2.1.0: Use $this->dropColumns()
*/
final protected function dropColumnsIfExists(string $table, array $columns): void
{
$columnIds = [];
foreach ($columns as $column) {
$columnIds[] = $table . '.' . $column;
}
$this->dropColumns($columnIds);
}
/**
* Check if a table exists
*
* @param string $name
*
* @return bool
* @deprecated v2.1.0: Use $this->findTable()
*/
final protected function tableExists(string $name): bool
{
return $this->findTable($name);
}
/**
* Parses a conditional string, returning a Boolean value (default: false).
* For now it only supports an extension name and * as version.
*
* @param string $expression
*
* @return bool
* @throws \Exception
*/
final protected function parseConditionalExpression(string $expression): bool
{
$expression = strtolower($expression);
$terms = explode('=', $expression);
$firstTerm = array_shift($terms);
if (count($terms) == 0) {
return $firstTerm == 'true' || $firstTerm == '1';
} elseif (preg_match('/^(com_|plg_|mod_|lib_|tpl_|cli_)/', $firstTerm)) {
// The first term is the name of an extension
$info = $this->getExtensionInfoFromElement($firstTerm);
$extension = $this->findExtension($info['type'], $firstTerm, $info['group']);
// @TODO: compare the version, if specified, or different than *
// @TODO: Check if the extension is enabled, not just installed
if (empty($extension) == false) {
return true;
}
}
return false;
}
/**
* Get extension's info from element string, or extension name
*
* @param string $element The extension name, as element
*
* @return string[] An associative array with information about the extension
*/
final protected function getExtensionInfoFromElement(string $element): array
{
$result = array_fill_keys(
['type', 'name', 'group', 'prefix', 'namespace'],
null
);
$types = [
'com' => 'component',
'plg' => 'plugin',
'mod' => 'module',
'lib' => 'library',
'tpl' => 'template',
'cli' => 'cli',
];
$element = explode('_', $element, 3);
$prefix = $result['prefix'] = array_shift($element);
$name = array_pop($element);
$group = array_pop($element);
if (array_key_exists($prefix, $types)) {
$result = array_merge(
$result,
[
'type' => $types[$prefix],
'group' => $group,
'name' => $name,
]
);
}
$result['namespace'] = preg_replace_callback(
'/^(os[a-z])(.*)/i',
function ($matches) {
return strtoupper($matches[1]) . $matches[2];
},
$name ?? ''
);
return $result;
}
/**
* Check if the actual version is at least the minimum target version.
*
* @param string $actualVersion
* @param string $targetVersion
* @param ?string $compare
*
* @return bool True, if the target version is greater than or equal to actual version
*/
final protected function validateTargetVersion(
string $actualVersion,
string $targetVersion,
?string $compare = null
): bool {
if ($targetVersion === '.*') {
// Any version is valid
return true;
}
$targetVersion = str_replace('*', '0', $targetVersion);
return version_compare($actualVersion, $targetVersion, $compare ?: 'ge');
}
/**
* @param string $targetVersion
* @param ?string $compare
*
* @return bool
*/
final protected function validatePreviousVersion(string $targetVersion, ?string $compare = null): bool
{
if ($this->previousManifest) {
$lastVersion = (string)$this->previousManifest->version;
return $this->validateTargetVersion($lastVersion, $targetVersion, $compare);
}
return true;
}
/**
* Get the extension name. If no custom name is set, uses the namespace
*
* @return string
*/
final protected function getName(): string
{
return (string)($this->manifest->alledia->name ?? $this->manifest->alledia->namespace);
}
/**
* @param ?bool $force Force to get a fresh list of tables
*
* @return string[] List of tables
*/
final protected function getTables(?bool $force = false): array
{
if ($force || $this->tables === null) {
$this->tables = $this->dbo->setQuery('SHOW TABLES')->loadColumn();
}
return $this->tables;
}
/**
* @param string $table
*
* @return bool
*/
final protected function findTable(string $table): bool
{
return in_array($this->dbo->replacePrefix($table), $this->getTables());
}
/**
* @param string[] $columnSpecs
*
* @return void
* @TODO: allow use of specification array
*/
final protected function addColumns(array $columnSpecs): void
{
$db = $this->dbo;
foreach ($columnSpecs as $columnId => $specification) {
if (strpos($columnId, '.') !== false && empty($this->findColumn($columnId))) {
[$table, $columnName] = explode('.', $columnId);
$db->setQuery(
sprintf(
'ALTER TABLE %s ADD COLUMN %s %s',
$db->quoteName($table),
$db->quoteName($columnName),
$specification
)
)
->execute();
}
}
}
/**
* @param string[] $columnIds
*
* @return void
*/
final protected function dropColumns(array $columnIds): void
{
$db = $this->dbo;
foreach ($columnIds as $columnId) {
if (strpos($columnId, '.') !== false) {
[$table, $column] = explode('.', $columnId);
$db->setQuery(
sprintf(
'ALTER TABLE %s DROP COLUMN %s',
$db->quoteName($table),
$column
)
)
->execute();
}
}
}
/**
* @param string $columnId
*
* @return ?object
*/
final protected function findColumn(string $columnId): ?object
{
if (strpos($columnId, '.') !== false) {
$db = $this->dbo;
[$table, $field] = explode('.', $columnId, 2);
if (isset($this->tableColumns[$table]) == false) {
$this->tableColumns[$table] = $db->setQuery('SHOW COLUMNS FROM ' . $db->quoteName($table))
->loadObjectList();
}
foreach ($this->tableColumns[$table] as $column) {
if ($column->Field == $field) {
return $column;
}
}
}
return null;
}
/**
* @param array $indexes
*
* @return void
*/
final protected function addIndexes(array $indexes): void
{
$db = $this->dbo;
foreach ($indexes as $indexId => $ordering) {
if (strpos($indexId, '.') !== false) {
$index = explode('.', $indexId);
$indexTable = array_shift($index) ?: '';
$indexName = array_shift($index) ?: '';
$indexType = array_shift($index) ?: '';
if ($this->findIndex($indexTable . '.' . $indexName) == false) {
$db->setQuery(
sprintf(
'ALTER TABLE %s ADD %s INDEX %s(%s)',
$db->quoteName($indexTable),
$indexType,
$db->quoteName($indexName),
join(',', $ordering)
)
)
->execute();
}
}
}
}
/**
* @param string[] $indexIds
*
* @return void
*/
final protected function dropIndexes(array $indexIds): void
{
$db = $this->dbo;
foreach ($indexIds as $indexId) {
if (strpos($indexId, '.') !== false) {
if ($this->findIndex($indexId)) {
[$table, $indexName] = explode('.', $indexId);
$db->setQuery(
sprintf(
'ALTER TABLE %s DROP INDEX %s',
$db->quoteName($table),
$db->quoteName($indexName)
)
)
->execute();
}
}
}
}
/**
* @param string $indexId
*
* @return object[]
*/
final protected function findIndex(string $indexId): array
{
if (strpos($indexId, '.') !== false) {
$db = $this->dbo;
[$table, $indexName] = explode('.', $indexId);
if (isset($this->tableIndexes[$table]) == false) {
$this->tableIndexes[$table] = $db->setQuery('SHOW INDEX FROM ' . $db->quoteName($table))
->loadObjectList();
}
$indexes = [];
foreach ($this->tableIndexes[$table] as $index) {
if ($index->Key_name == $indexName) {
$indexes[] = $index;
}
}
return $indexes;
}
return [];
}
/**
* @param string[] $constraintIds
*
* @return void
*/
final protected function dropConstraints(array $constraintIds): void
{
$db = $this->dbo;
foreach ($constraintIds as $constraintId) {
if (strpos($constraintId, '.') !== false && $this->findConstraint($constraintId)) {
[$table, $constraintName] = explode('.', $constraintId);
$db->setQuery(
sprintf(
'ALTER TABLE %s DROP FOREIGN KEY %s',
$db->quoteName($table),
$db->quoteName($constraintName)
)
)
->execute();
}
}
}
/**
* @param string $constraintId
*
* @return object[]
*/
final protected function findConstraint(string $constraintId): array
{
if (strpos($constraintId, '.') !== false) {
$db = $this->dbo;
[$table, $constraint] = explode('.', $constraintId);
if (isset($this->tableConstraints[$table]) == false) {
$query = $db->getQuery(true)
->select('*')
->from('information_schema.KEY_COLUMN_USAGE')
->where('TABLE_NAME = ' . $db->quote($db->replacePrefix($table)));
$this->tableConstraints[$table] = $db->setQuery($query)->loadObjectList();
}
$items = [];
foreach ($this->tableConstraints[$table] as $item) {
if ($item->CONSTRAINT_NAME == $constraint) {
$items[] = $item;
}
}
return $items ?: [];
}
return [];
}
/**
* @return ?string
* @throws \Exception
*/
final protected function getSchemaVersion(): ?string
{
if ($extension = $this->findThisExtension()) {
$query = $this->dbo->getQuery(true)
->select('version_id')
->from('#__schemas')
->where('extension_id = ' . $extension->get('extension_id'));
return $this->dbo->setQuery($query)->loadResult();
}
return null;
}
/**
* @param string|string[] $queries
*
* @return bool|Throwable
*/
final protected function executeQuery($schemaVersion, $queries)
{
$this->sendDebugMessage(__METHOD__);
$this->sendDebugMessage($this->schemaVersion . ' / ' . $schemaVersion);
if ($this->schemaVersion && version_compare($this->schemaVersion, $schemaVersion, 'lt')) {
$this->sendDebugMessage(sprintf('Running v%s Schema Updates', $schemaVersion));
$db = $this->dbo;
try {
foreach ((array)$queries as $query) {
$this->sendDebugMessage($query);
if ($db->setQuery($query)->execute() == false) {
return new \Exception('Query Error: ' . $query);
}
}
} catch (Throwable $error) {
return $error;
}
}
return true;
}
/**
* Joomla 4 does a database check that has lots of problems with standard sql syntax
* causing it to declare the database tables as not up to date and in some cases
* generates various sql errors. This can optionally be called during Post Install to
* clear out all update files and still maintain the latest schema version correctly.
*
* @param string $basePath
*
* @return void
*/
final protected function clearDBUpdateFiles(string $basePath): void
{
$this->sendDebugMessage(__METHOD__);
$updatePath = $basePath . '/sql/updates';
if (is_dir($updatePath) && $files = Folder::files($updatePath, '\.sql$', true, true)) {
$this->sendDebugMessage('Removing:<pre>' . print_r($files, 1) . '</pre>');
$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<br>%s::%s() - %s', $line, $file, $caller, $function, $error->getMessage());
} else {
$message = sprintf('%s:%s (%s) - %s', $line, $caller, $file, $error->getMessage());
}
$this->sendMessage($message, 'error');
}
}
/**
* @param string $text
*
* @return void
*/
final protected function sendDebugMessage(string $text): void
{
if ($this->debug) {
$type = Version::MAJOR_VERSION == 3 ? 'Debug-' . get_class($this) : CMSApplicationInterface::MSG_DEBUG;
$this->sendMessage($text, $type);
}
}
/**
* @param string $type
*
* @return void
*/
final protected function displayWelcome(string $type): void
{
if ($this->outputAllowed == false) {
return;
}
$this->sendDebugMessage(
sprintf(
'%s<br>Parent: %s<br>Current: %s',
__METHOD__,
$this->installer->getPath('parent'),
$this->installer->getPath('source')
)
);
$license = $this->getLicense();
$name = $this->getName() . ($license->isPro() ? ' Pro' : '');
// Get the footer content
$this->footer = '';
// Check if we have a dedicated config.xml file
$configPath = $license->getExtensionPath() . '/config.xml';
if (is_file($configPath)) {
$config = $license->getConfig();
if (empty($config) == false) {
$footerElement = $config->xpath('//field[@type="customfooter"]');
}
} else {
$footerElement = $this->manifest->xpath('//field[@type="customfooter"]');
}
if (empty($footerElement) == false) {
if (class_exists('\\JFormFieldCustomFooter') == false) {
// Custom footer field is not (and should not be) automatically loaded
$customFooterPath = $license->getExtensionPath() . '/form/fields/customfooter.php';
if (is_file($customFooterPath)) {
include_once $customFooterPath;
}
}
if (class_exists('\\JFormFieldCustomFooter')) {
$field = new JFormFieldCustomFooter();
$field->fromInstaller = true;
$this->footer = $field->getInputUsingCustomElement($footerElement[0]);
unset($field, $footerElement);
}
} else {
$this->sendDebugMessage('No Footer element was found');
}
// Show additional installation messages
$extensionPath = $this->getExtensionPath(
$this->type,
(string)$this->manifest->alledia->element,
$this->group
);
// If Pro extension, includes the license form view
if ($license->isPro()) {
// Get the OSMyLicensesManager extension to handle the license key
if ($licensesManagerExtension = new Licensed('osmylicensesmanager', 'plugin', 'system')) {
if (isset($licensesManagerExtension->params)) {
$this->licenseKey = $licensesManagerExtension->params->get('license-keys', '');
} else {
$this->licenseKey = '';
}
$this->isLicensesManagerInstalled = true;
}
$this->sendDebugMessage('License Manager plugin: ' . (int)$this->isLicensesManagerInstalled);
}
// Welcome message
if (in_array($type, [static::TYPE_INSTALL, static::TYPE_DISCOVER_INSTALL])) {
$string = 'LIB_SHACKINSTALLER_THANKS_INSTALL';
} else {
$string = 'LIB_SHACKINSTALLER_THANKS_UPDATE';
}
// Variables for the included template
$this->welcomeMessage = Text::sprintf($string, $name);
$this->mediaURL = Uri::root() . 'media/' . $license->getFullElement();
$this->addStyle($this->mediaFolder . '/css/installer.css');
/*
* Include the template
* Try to find the template in an alternative folder, since some extensions
* which uses FOF will display the "Installers" view on admin, errouniously.
* FOF look for views automatically reading the views folder. So on that
* case we move the installer view to another folder.
*/
$path = $extensionPath . '/views/installer/tmpl/default.php';
if (is_file($path) == false) {
$path = $extensionPath . '/alledia_views/installer/tmpl/default.php';
}
$this->sendDebugMessage(sprintf('Welcome View (%s): %s', (int)is_file($path), $path));
if (is_file($path)) {
include $path;
}
}
/**
* WARNIMG! This is duplicated from the Joomlashack Framework
*
* @param string $name
* @param string $prefix
* @param string $component
* @param ?string $appName
* @param ?array $options
*
* @return mixed
* @throws \Exception
*/
protected function getJoomlaModel(
string $name,
string $prefix,
string $component,
?string $appName = null,
?array $options = []
) {
$defaultApp = 'Site';
$appNames = [$defaultApp, 'Administrator'];
$appName = ucfirst($appName ?: $defaultApp);
$appName = in_array($appName, $appNames) ? $appName : $defaultApp;
if (Version::MAJOR_VERSION < 4) {
$basePath = $appName == 'Administrator' ? JPATH_ADMINISTRATOR : JPATH_SITE;
$path = $basePath . '/components/' . $component;
BaseDatabaseModel::addIncludePath($path . '/models');
Table::addIncludePath($path . '/tables');
$model = BaseDatabaseModel::getInstance($name, $prefix, $options);
} else {
$model = Factory::getApplication()->bootComponent($component)
->getMVCFactory()->createModel($name, $appName, $options);
}
return $model;
}
/**
* Utility function to setting extension states
* @param array $extensions
* @param int $state
* @return array
* @throws \Exception
*/
final protected function setExtensionState(array $extensions, int $state = 0): array
{
$states = [];
if (in_array($state, [0, 1])) {
foreach ($extensions as $extension) {
$parts = explode('.', $extension);
$element = array_pop($parts);
$folder = null;
switch (count($parts)) {
case 1:
$type = array_pop($parts);
break;
case 2:
$folder = array_pop($parts);
$type = array_pop($parts);
break;
default:
// Badly structured extension identifier
break 2;
}
if ($object = $this->findExtension($type, $element, $folder)) {
$states[$extension] = (int)$object->get('enabled');
if ($states[$extension] != $state) {
$this->sendDebugMessage(
sprintf(
'%s: %s',
$extension,
$state ? 'Enabled' : 'Disabled'
)
);
$object->set('enabled', $state);
$object->store();
}
}
}
}
return $states;
}
/**
* If the old system plugin is installed, it requires special handling to avoid
* fatal conflicts with its install script
*
* @return void
* @throws \Exception
*/
final protected function clearOldSystemPlugin()
{
if ($this->findExtension('plugin', 'ossystem', 'system')) {
if (class_exists('PlgSystemOSSystemInstallerScript') == false) {
class_alias(static::class, 'PlgSystemOSSystemInstallerScript');
}
}
}
}