.
 */
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