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

', __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 ''; } } } /** * 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:
' . 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
%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
Parent: %s
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'); } } } }