2507 lines
76 KiB
PHP
2507 lines
76 KiB
PHP
<?php
|
|
/**
|
|
* @package ShackInstaller
|
|
* @contact www.joomlashack.com, help@joomlashack.com
|
|
* @copyright 2016-2023 Joomlashack.com. All rights reserved
|
|
* @license https://www.gnu.org/licenses/gpl.html GNU/GPL
|
|
*
|
|
* This file is part of ShackInstaller.
|
|
*
|
|
* ShackInstaller is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* ShackInstaller is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with ShackInstaller. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
namespace Alledia\Installer;
|
|
|
|
use Alledia\Installer\Extension\Licensed;
|
|
use JEventDispatcher;
|
|
use JFormFieldCustomFooter;
|
|
use Joomla\CMS\Application\CMSApplication;
|
|
use Joomla\CMS\Application\CMSApplicationInterface;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Filesystem\File;
|
|
use Joomla\CMS\Filesystem\Folder;
|
|
use Joomla\CMS\Installer\Installer;
|
|
use Joomla\CMS\Installer\InstallerAdapter;
|
|
use Joomla\CMS\Language\Text;
|
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
use Joomla\CMS\Table\Extension;
|
|
use Joomla\CMS\Table\Table;
|
|
use Joomla\CMS\Uri\Uri;
|
|
use Joomla\CMS\Version;
|
|
use Joomla\Component\Plugins\Administrator\Model\PluginModel;
|
|
use Joomla\Database\DatabaseDriver;
|
|
use Joomla\Event\DispatcherInterface;
|
|
use Joomla\Registry\Registry;
|
|
use SimpleXMLElement;
|
|
use Throwable;
|
|
|
|
// phpcs:disable PSR1.Files.SideEffects
|
|
defined('_JEXEC') or die();
|
|
|
|
require_once 'include.php';
|
|
|
|
// phpcs:enable PSR1.Files.SideEffects
|
|
|
|
abstract class AbstractScript
|
|
{
|
|
public const VERSION = '2.4.4';
|
|
|
|
/**
|
|
* Recognized installation types
|
|
*/
|
|
protected const TYPE_INSTALL = 'install';
|
|
protected const TYPE_DISCOVER_INSTALL = 'discover_install';
|
|
protected const TYPE_UPDATE = 'update';
|
|
protected const TYPE_UNINSTALL = 'uninstall';
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $outputAllowed = true;
|
|
|
|
/**
|
|
* @var CMSApplication
|
|
*/
|
|
protected $app = null;
|
|
|
|
/**
|
|
* @var DatabaseDriver
|
|
*/
|
|
protected $dbo = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $schemaVersion = null;
|
|
|
|
/**
|
|
* @var JEventDispatcher|DispatcherInterface
|
|
*/
|
|
protected $dispatcher = null;
|
|
|
|
/**
|
|
* @var Installer
|
|
*/
|
|
protected $installer = null;
|
|
|
|
/**
|
|
* @var SimpleXMLElement
|
|
*/
|
|
protected $manifest = null;
|
|
|
|
/**
|
|
* @var SimpleXMLElement
|
|
*/
|
|
protected $previousManifest = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $mediaFolder = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $element = null;
|
|
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
protected $systemExtensions = [
|
|
'library..allediaframework',
|
|
'plugin.system.osmylicensesmanager',
|
|
];
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $isLicensesManagerInstalled = false;
|
|
|
|
/**
|
|
* @var Licensed
|
|
*/
|
|
protected $license = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $licenseKey = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $footer = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $mediaURL = null;
|
|
|
|
/**
|
|
* @var string[]
|
|
* @deprecated v2.0.0
|
|
*/
|
|
protected $messages = [];
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $type = null;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $group = null;
|
|
|
|
/**
|
|
* List of tables and respective columns
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $columns = null;
|
|
|
|
/**
|
|
* List of tables and respective indexes
|
|
*
|
|
* @var array
|
|
* @deprecated v2.1.0
|
|
*/
|
|
protected $indexes = null;
|
|
|
|
/**
|
|
* @var object[]
|
|
*/
|
|
protected $tableColumns = [];
|
|
|
|
/**
|
|
* @var object[]
|
|
*/
|
|
protected $tableIndexes = [];
|
|
|
|
/**
|
|
* @var object[]
|
|
*/
|
|
protected $tableConstraints = [];
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $tables = null;
|
|
|
|
/**
|
|
* Flag to cancel the installation
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $cancelInstallation = false;
|
|
|
|
/**
|
|
* Feedback of the install by related extension
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $relatedExtensionFeedback = [];
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $welcomeMessage = null;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $debug = false;
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
public function __construct(InstallerAdapter $parent)
|
|
{
|
|
$this->sendDebugMessage('ShackInstaller v' . static::VERSION);
|
|
$this->sendDebugMessage('Base v' . SHACK_INSTALLER_VERSION);
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
$this->initProperties($parent);
|
|
}
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
protected function checkInheritance(InstallerAdapter $parent): bool
|
|
{
|
|
$parentClasses = class_parents($this);
|
|
$scriptClassName = array_pop($parentClasses);
|
|
$scriptClass = new \ReflectionClass($scriptClassName);
|
|
|
|
$sourcePath = dirname($scriptClass->getFileName());
|
|
$sourceBase = strpos($sourcePath, JPATH_PLUGINS) === 0 ? 3 : 2;
|
|
$sourceVersion = AbstractScript::VERSION ?? '0.0.0';
|
|
|
|
$sourcePath = $this->cleanPath($sourcePath);
|
|
$targetPath = $this->cleanPath(SHACK_INSTALLER_BASE);
|
|
|
|
if ($sourcePath != $targetPath && version_compare($sourceVersion, SHACK_INSTALLER_COMPATIBLE, 'lt')) {
|
|
$source = join('/', array_slice(explode('/', $sourcePath), 0, $sourceBase));
|
|
|
|
$errorMessage = 'LIB_SHACKINSTALLER_ABORT_'
|
|
. ($parent->getRoute() == 'uninstall' ? 'UNINSTALL' : 'INSTALL');
|
|
|
|
Factory::getApplication()->enqueueMessage(Text::sprintf($errorMessage, $source), 'error');
|
|
|
|
$this->cancelInstallation = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param string $path
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function cleanPath(string $path): string
|
|
{
|
|
return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(JPATH_ROOT . '/', '', $path));
|
|
}
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
protected function initProperties(InstallerAdapter $parent): void
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
$this->app = Factory::getApplication();
|
|
$this->outputAllowed = JPATH_BASE == JPATH_ADMINISTRATOR;
|
|
|
|
$language = Factory::getLanguage();
|
|
$language->load('lib_shackinstaller.sys', realpath(__DIR__ . '/../..'));
|
|
|
|
if ($this->checkInheritance($parent) == false) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$this->dbo = Factory::getDbo();
|
|
$this->installer = $parent->getParent();
|
|
$this->manifest = $this->installer->getManifest();
|
|
$this->schemaVersion = $this->getSchemaVersion();
|
|
|
|
if ($media = $this->manifest->media) {
|
|
$this->mediaFolder = JPATH_SITE . '/' . $media['folder'] . '/' . $media['destination'];
|
|
}
|
|
|
|
$attributes = $this->manifest->attributes();
|
|
$this->type = (string)$attributes['type'];
|
|
$this->group = (string)$attributes['group'];
|
|
|
|
// Get the previous manifest for use in upgrades
|
|
$targetPath = $this->installer->getPath('extension_administrator')
|
|
?: $this->installer->getPath('extension_root');
|
|
$manifestPath = $targetPath . '/' . basename($this->installer->getPath('manifest'));
|
|
|
|
if (is_file($manifestPath)) {
|
|
$this->previousManifest = simplexml_load_file($manifestPath);
|
|
}
|
|
|
|
// Determine basepath for localized files
|
|
$basePath = $this->installer->getPath('source');
|
|
if (is_dir($basePath)) {
|
|
if ($this->type == 'component' && $basePath != $targetPath) {
|
|
// For components sourced by manifest, need to find the admin folder
|
|
if ($files = $this->manifest->administration->files) {
|
|
if ($files = (string)$files['folder']) {
|
|
$basePath .= '/' . $files;
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
$basePath = $this->getExtensionPath(
|
|
$this->type,
|
|
(string)$this->manifest->alledia->element,
|
|
$this->group
|
|
);
|
|
}
|
|
|
|
// All the files we want to load
|
|
$languageFiles = [
|
|
$this->getFullElement(),
|
|
];
|
|
|
|
// Load from localized or core language folder
|
|
foreach ($languageFiles as $languageFile) {
|
|
$language->load($languageFile, $basePath) || $language->load($languageFile, JPATH_ADMINISTRATOR);
|
|
}
|
|
|
|
} catch (Throwable $error) {
|
|
$this->cancelInstallation = true;
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return JEventDispatcher|DispatcherInterface
|
|
*/
|
|
protected function getDispatcher()
|
|
{
|
|
if ($this->dispatcher === null) {
|
|
if (Version::MAJOR_VERSION < 4) {
|
|
$this->dispatcher = JEventDispatcher::getInstance();
|
|
|
|
} else {
|
|
$this->dispatcher = $this->app->getDispatcher();
|
|
}
|
|
}
|
|
|
|
return $this->dispatcher;
|
|
}
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
*/
|
|
final public function install(InstallerAdapter $parent): bool
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
try {
|
|
return $this->customInstall($parent);
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
*/
|
|
final public function discover_install(InstallerAdapter $parent): bool
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
try {
|
|
if ($this->install($parent)) {
|
|
return $this->customDiscoverInstall($parent);
|
|
}
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
*/
|
|
final public function update(InstallerAdapter $parent): bool
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
try {
|
|
return $this->customUpdate($parent);
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
final public function preFlight(string $type, InstallerAdapter $parent): bool
|
|
{
|
|
if ($this->cancelInstallation) {
|
|
$this->sendDebugMessage('CANCEL: ' . __METHOD__);
|
|
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$this->sendDebugMessage(__METHOD__);
|
|
$success = true;
|
|
|
|
if ($type === 'update') {
|
|
$this->clearUpdateServers();
|
|
}
|
|
|
|
if (in_array($type, [static::TYPE_INSTALL, static::TYPE_UPDATE])) {
|
|
// Check minimum target Joomla Platform
|
|
if (isset($this->manifest->alledia->targetplatform)) {
|
|
$targetPlatform = (string)$this->manifest->alledia->targetplatform;
|
|
|
|
if ($this->validateTargetVersion(JVERSION, $targetPlatform) == false) {
|
|
// Platform version is invalid. Displays a warning and cancel the install
|
|
$targetPlatform = str_replace('*', 'x', $targetPlatform);
|
|
|
|
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_PLATFORM', $this->getName(), $targetPlatform);
|
|
|
|
$this->sendMessage($msg, 'warning');
|
|
$success = false;
|
|
}
|
|
}
|
|
|
|
// Check for minimum mysql version
|
|
if ($targetMySqlVersion = $this->manifest->alledia->mysqlminimum) {
|
|
$targetMySqlVersion = (string)$targetMySqlVersion;
|
|
|
|
if ($this->dbo->getServerType() == 'mysql') {
|
|
$dbVersion = $this->dbo->getVersion();
|
|
if (stripos($dbVersion, 'maria') !== false) {
|
|
// For MariaDB this is a bit of a punt. We'll assume any version of Maria will do
|
|
$dbVersion = $targetMySqlVersion;
|
|
}
|
|
|
|
if ($this->validateTargetVersion($dbVersion, $targetMySqlVersion) == false) {
|
|
// mySQL version too low
|
|
$minimumMySql = str_replace('*', 'x', $targetMySqlVersion);
|
|
|
|
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_MYSQL', $this->getName(), $minimumMySql);
|
|
$this->sendMessage($msg, 'warning');
|
|
$success = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for minimum php version
|
|
if (isset($this->manifest->alledia->phpminimum)) {
|
|
$targetPhpVersion = (string)$this->manifest->alledia->phpminimum;
|
|
|
|
if ($this->validateTargetVersion(phpversion(), $targetPhpVersion) == false) {
|
|
// php version is too low
|
|
$minimumPhp = str_replace('*', 'x', $targetPhpVersion);
|
|
|
|
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_PHP', $this->getName(), $minimumPhp);
|
|
$this->sendMessage($msg, 'warning');
|
|
$success = false;
|
|
}
|
|
}
|
|
|
|
// Check for minimum previous version
|
|
$targetVersion = (string)$this->manifest->alledia->previousminimum;
|
|
if ($type == static::TYPE_UPDATE && $targetVersion) {
|
|
if (!$this->validatePreviousVersion($targetVersion)) {
|
|
// Previous minimum is not installed
|
|
$minimumVersion = str_replace('*', 'x', $targetVersion);
|
|
|
|
$msg = Text::sprintf('LIB_SHACKINSTALLER_WRONG_PREVIOUS', $this->getName(), $minimumVersion);
|
|
$this->sendMessage($msg, 'warning');
|
|
$success = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($success) {
|
|
$success = $this->customPreFlight($type, $parent);
|
|
}
|
|
|
|
if ($success) {
|
|
if (
|
|
$type !== static::TYPE_UNINSTALL
|
|
&& empty($this->manifest->alledia->obsolete->preflight) == false
|
|
) {
|
|
$this->clearObsolete($this->manifest->alledia->obsolete->preflight);
|
|
}
|
|
}
|
|
|
|
$this->cancelInstallation = $success == false;
|
|
|
|
return $success;
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final public function postFlight(string $type, InstallerAdapter $parent): void
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
if ($this->cancelInstallation) {
|
|
$this->sendMessage('LIB_SHACKINSTALLER_INSTALL_CANCELLED', 'warning');
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
/*
|
|
* Joomla 4 now calls postFlight on uninstalls. Which is kinda cool actually.
|
|
* But this code is problematic in that scenario
|
|
*/
|
|
if ($type != static::TYPE_UNINSTALL) {
|
|
$this->clearObsolete();
|
|
$this->installRelated();
|
|
$this->addAllediaAuthorshipToExtension();
|
|
|
|
$this->element = (string)$this->manifest->alledia->element;
|
|
|
|
// Check and publish/reorder the plugin, if required
|
|
if (
|
|
$this->type === 'plugin'
|
|
&& in_array($type, [static::TYPE_INSTALL, static::TYPE_DISCOVER_INSTALL])
|
|
) {
|
|
$this->publishThisPlugin();
|
|
$this->reorderThisPlugin();
|
|
}
|
|
|
|
// If Free, remove any Pro library
|
|
$license = $this->getLicense();
|
|
if (!$license->isPro()) {
|
|
$proLibraryPath = $license->getProLibraryPath();
|
|
if (is_dir($proLibraryPath)) {
|
|
Folder::delete($proLibraryPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->customPostFlight($type, $parent);
|
|
|
|
if ($type != static::TYPE_UNINSTALL) {
|
|
$this->displayWelcome($type);
|
|
}
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final public function uninstall(InstallerAdapter $parent): void
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
try {
|
|
$this->uninstallRelated();
|
|
$this->customUninstall($parent);
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $number
|
|
* @param string $error
|
|
* @param ?string $file
|
|
* @param ?int $line
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function errorHandler(int $number, string $error, ?string $file = null, ?int $line = null): void
|
|
{
|
|
try {
|
|
$codes = get_defined_constants(true);
|
|
$codes = $codes['Core'];
|
|
|
|
$codes = array_filter(
|
|
$codes,
|
|
function ($key) {
|
|
return strpos($key, 'E_') === 0;
|
|
},
|
|
ARRAY_FILTER_USE_KEY
|
|
);
|
|
|
|
$name = array_search($number, $codes);
|
|
|
|
Factory::getApplication()->enqueueMessage(
|
|
sprintf('%s: %s<br>(%s) %s', $name, $error, $line ?: 'NA', $file ?: 'NA'),
|
|
'warning'
|
|
);
|
|
|
|
} catch (Throwable $error) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For use in subclasses
|
|
*
|
|
* @param string $type
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
* @throws Throwable
|
|
*/
|
|
protected function customPreFlight(string $type, InstallerAdapter $parent): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* For use in subclasses
|
|
*
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
* @throws Throwable
|
|
*/
|
|
protected function customInstall(InstallerAdapter $parent): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* For use in subclasses
|
|
*
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
* @throws Throwable
|
|
*/
|
|
protected function customDiscoverInstall(InstallerAdapter $parent): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* For use in subclasses
|
|
*
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return bool
|
|
* @throws Throwable
|
|
*/
|
|
protected function customUpdate(InstallerAdapter $parent): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* For use in subclassses
|
|
*
|
|
* @param string $type
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return void
|
|
* @throws Throwable
|
|
*/
|
|
protected function customPostFlight(string $type, InstallerAdapter $parent): void
|
|
{
|
|
}
|
|
|
|
/**
|
|
* For use in subclasses
|
|
*
|
|
* @param InstallerAdapter $parent
|
|
*
|
|
* @return void
|
|
* @throws Throwable
|
|
*/
|
|
protected function customUninstall(InstallerAdapter $parent): void
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function installRelated(): void
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
if ($this->manifest->alledia->relatedExtensions) {
|
|
$source = $this->installer->getPath('source');
|
|
$extensionsPath = $source . '/extensions';
|
|
|
|
$defaultAttributes = $this->manifest->alledia->relatedExtensions->attributes();
|
|
$defaultDowngrade = $this->getXmlValue($defaultAttributes['downgrade'], 'bool');
|
|
$defaultPublish = $this->getXmlValue($defaultAttributes['publish'], 'bool');
|
|
|
|
foreach ($this->manifest->alledia->relatedExtensions->extension as $extension) {
|
|
$path = $extensionsPath . '/' . $this->getXmlValue($extension);
|
|
|
|
if (is_dir($path)) {
|
|
$type = $this->getXmlValue($extension['type']);
|
|
$element = $this->getXmlValue($extension['element']);
|
|
$group = $this->getXmlValue($extension['group']);
|
|
$key = md5(join(':', [$type, $element, $group]));
|
|
|
|
$this->sendDebugMessage(
|
|
sprintf('Related: %s%s/%s', $type, $group ? ($group . '/') : '', $element)
|
|
);
|
|
|
|
try {
|
|
if ($type == 'plugin' && in_array($group, ['search', 'finder'])) {
|
|
if (is_dir(JPATH_ADMINISTRATOR . '/components/com_' . $group) == false) {
|
|
// skip search/finder plugins based on installed components
|
|
$this->sendDebugMessage(
|
|
sprintf(
|
|
'Skipped/Uninstalled plugin %s',
|
|
ucwords($group . ' ' . $element)
|
|
)
|
|
);
|
|
|
|
$this->uninstallExtension($type, $element, $group);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$current = $this->findExtension($type, $element, $group);
|
|
$isNew = empty($current);
|
|
|
|
$typeName = ucwords(trim($group . ' ' . $type));
|
|
|
|
// Get data from the manifest
|
|
$tmpInstaller = new Installer();
|
|
$tmpInstaller->setPath('source', $path);
|
|
$tmpInstaller->setPath('parent', $this->installer->getPath('source'));
|
|
|
|
$newManifest = $tmpInstaller->getManifest();
|
|
$newVersion = (string)$newManifest->version;
|
|
|
|
$this->storeFeedbackForRelatedExtension($key, 'name', (string)$newManifest->name);
|
|
|
|
$downgrade = $this->getXmlValue($extension['downgrade'], 'bool', $defaultDowngrade);
|
|
|
|
if (($isNew || $downgrade) == false) {
|
|
$currentManifestPath = $this->getManifestPath($type, $element, $group);
|
|
$currentManifest = $this->getInfoFromManifest($currentManifestPath);
|
|
|
|
// Avoid to update for an outdated version
|
|
$currentVersion = $currentManifest->get('version');
|
|
|
|
if (version_compare($currentVersion, $newVersion, '>')) {
|
|
// Store the state of the install/update
|
|
$this->storeFeedbackForRelatedExtension(
|
|
$key,
|
|
'message',
|
|
Text::sprintf(
|
|
'LIB_SHACKINSTALLER_RELATED_UPDATE_STATE_SKIPED',
|
|
$newVersion,
|
|
$currentVersion
|
|
)
|
|
);
|
|
|
|
// Skip the installation for this extension
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$text = 'LIB_SHACKINSTALLER_RELATED_' . ($isNew ? 'INSTALL' : 'UPDATE');
|
|
|
|
if ($tmpInstaller->install($path)) {
|
|
$this->sendMessage(Text::sprintf($text, $typeName, $element));
|
|
if ($isNew) {
|
|
$current = $this->findExtension($type, $element, $group);
|
|
|
|
if (is_object($current)) {
|
|
if ($type === 'plugin') {
|
|
if ($this->getXmlValue($extension['publish'], 'bool', $defaultPublish)) {
|
|
$current->publish();
|
|
|
|
$this->storeFeedbackForRelatedExtension($key, 'publish', true);
|
|
}
|
|
|
|
if ($ordering = $this->getXmlValue($extension['ordering'])) {
|
|
$this->setPluginOrder($current, $ordering);
|
|
|
|
$this->storeFeedbackForRelatedExtension($key, 'ordering', $ordering);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->storeFeedbackForRelatedExtension(
|
|
$key,
|
|
'message',
|
|
Text::sprintf('LIB_SHACKINSTALLER_RELATED_UPDATE_STATE_INSTALLED', $newVersion)
|
|
);
|
|
|
|
} else {
|
|
$this->sendMessage(Text::sprintf($text . '_FAIL', $typeName, $element), 'error');
|
|
|
|
$this->storeFeedbackForRelatedExtension(
|
|
$key,
|
|
'message',
|
|
Text::sprintf(
|
|
'LIB_SHACKINSTALLER_RELATED_UPDATE_STATE_FAILED',
|
|
$newVersion
|
|
)
|
|
);
|
|
}
|
|
unset($tmpInstaller);
|
|
|
|
} catch (Throwable $error) {
|
|
$this->sendErrorMessage($error, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uninstall the related extensions that are useless without the component
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function uninstallRelated(): void
|
|
{
|
|
if ($this->manifest->alledia->relatedExtensions) {
|
|
$defaultAttributes = $this->manifest->alledia->relatedExtensions->attributes();
|
|
$defaultUninstall = $this->getXmlValue($defaultAttributes['uninstall'], 'bool');
|
|
|
|
foreach ($this->manifest->alledia->relatedExtensions->extension as $extension) {
|
|
$type = $this->getXmlValue($extension['type']);
|
|
$element = $this->getXmlValue($extension['element']);
|
|
$group = $this->getXmlValue($extension['group']);
|
|
|
|
$uninstall = $this->getXmlValue($extension['uninstall'], 'bool', $defaultUninstall);
|
|
$systemExtension = in_array(join('.', [$type, $group, $element]), $this->systemExtensions);
|
|
if ($uninstall && $systemExtension == false) {
|
|
$this->uninstallExtension($type, $element, $group);
|
|
|
|
} else {
|
|
$message = 'LIB_SHACKINSTALLER_RELATED_NOT_UNINSTALLED'
|
|
. ($systemExtension ? '_SYSTEM' : '');
|
|
|
|
if ($type == 'plugin') {
|
|
$type = $group . ' ' . $type;
|
|
}
|
|
|
|
$this->sendDebugMessage(Text::sprintf($message, ucwords($type), $element));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param string $element
|
|
* @param ?string $group
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function uninstallExtension(string $type, string $element, ?string $group = null): void
|
|
{
|
|
if ($extension = $this->findExtension($type, $element, $group)) {
|
|
$installer = new Installer();
|
|
|
|
$success = $installer->uninstall($extension->get('type'), $extension->get('extension_id'));
|
|
$msg = 'LIB_SHACKINSTALLER_RELATED_UNINSTALL' . ($success ? '' : '_FAIL');
|
|
if ($type == 'plugin') {
|
|
$type = $group . ' ' . $type;
|
|
}
|
|
|
|
$this->sendMessage(
|
|
Text::sprintf($msg, ucwords($type), $element),
|
|
$success ? 'message' : 'error'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param ?string $type
|
|
* @param ?string $element
|
|
* @param ?string $group
|
|
*
|
|
* @return ?Extension
|
|
* @throws \Exception
|
|
*/
|
|
final protected function findExtension(?string $type, ?string $element, ?string $group = null): ?Extension
|
|
{
|
|
// @TODO: Why do we need to use JTable?
|
|
/** @var Extension $row */
|
|
$row = Table::getInstance('extension');
|
|
|
|
$prefixes = [
|
|
'component' => 'com_',
|
|
'module' => 'mod_',
|
|
];
|
|
|
|
// Fix the element, if the prefix is not found
|
|
if (array_key_exists($type, $prefixes)) {
|
|
if (substr_count($element, $prefixes[$type]) === 0) {
|
|
$element = $prefixes[$type] . $element;
|
|
}
|
|
}
|
|
|
|
$terms = [
|
|
'type' => $type,
|
|
'element' => $element,
|
|
];
|
|
|
|
if ($type === 'plugin') {
|
|
$terms['folder'] = $group;
|
|
}
|
|
|
|
$eid = $row->find($terms);
|
|
|
|
if ($eid) {
|
|
if ($row->load($eid) == false) {
|
|
throw new \Exception($row->getError());
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set requested ordering for selected plugin extension
|
|
* Accepted ordering arguments:
|
|
* (n<=1 | first) First within folder
|
|
* (* | last) Last within folder
|
|
* (before:element) Before the named plugin
|
|
* (after:element) After the named plugin
|
|
*
|
|
* @param Extension $extension
|
|
* @param string $order
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function setPluginOrder(Extension $extension, string $order): void
|
|
{
|
|
if ($extension->get('type') == 'plugin' && empty($order) == false) {
|
|
$db = $this->dbo;
|
|
$query = $db->getQuery(true);
|
|
|
|
$query->select('extension_id, element');
|
|
$query->from('#__extensions');
|
|
$query->where([
|
|
$db->quoteName('folder') . ' = ' . $db->quote($extension->get('folder')),
|
|
$db->quoteName('type') . ' = ' . $db->quote($extension->get('type')),
|
|
]);
|
|
$query->order($db->quoteName('ordering'));
|
|
|
|
$plugins = $db->setQuery($query)->loadObjectList('element');
|
|
|
|
// Set the order only if plugin already successfully installed
|
|
if (array_key_exists($extension->get('element'), $plugins)) {
|
|
$target = [
|
|
$extension->get('element') => $plugins[$extension->get('element')],
|
|
];
|
|
$others = array_diff_key($plugins, $target);
|
|
|
|
if ((is_numeric($order) && $order <= 1) || $order == 'first') {
|
|
// First in order
|
|
$neworder = array_merge($target, $others);
|
|
|
|
} elseif (($order == '*') || ($order == 'last')) {
|
|
// Last in order
|
|
$neworder = array_merge($others, $target);
|
|
|
|
} elseif (preg_match('/^(before|after):(\S+)$/', $order, $match)) {
|
|
// place before or after named plugin
|
|
$place = $match[1];
|
|
$element = $match[2];
|
|
$neworder = [];
|
|
$previous = '';
|
|
|
|
foreach ($others as $plugin) {
|
|
if (
|
|
(($place == 'before') && ($plugin->element == $element))
|
|
|| (($place == 'after') && ($previous == $element))
|
|
) {
|
|
$neworder = array_merge($neworder, $target);
|
|
}
|
|
$neworder[$plugin->element] = $plugin;
|
|
$previous = $plugin->element;
|
|
}
|
|
|
|
if (count($neworder) < count($plugins)) {
|
|
// Make it last if the requested plugin isn't installed
|
|
$neworder = array_merge($neworder, $target);
|
|
}
|
|
|
|
} else {
|
|
$neworder = [];
|
|
}
|
|
|
|
if (count($neworder) == count($plugins)) {
|
|
// Only reorder if have a validated new order
|
|
BaseDatabaseModel::addIncludePath(
|
|
JPATH_ADMINISTRATOR . '/components/com_plugins/models',
|
|
'PluginsModels'
|
|
);
|
|
|
|
// @TODO: Model class is (\PluginsModelPlugin) in J3 but this works either way
|
|
/** @var PluginModel $model */
|
|
$model = BaseDatabaseModel::getInstance('Plugin', 'PluginsModel');
|
|
|
|
$ids = [];
|
|
foreach ($neworder as $plugin) {
|
|
$ids[] = $plugin->extension_id;
|
|
}
|
|
$order = range(1, count($ids));
|
|
$model->saveorder($ids, $order);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a message to the message list
|
|
*
|
|
* @param string $message
|
|
* @param ?string $type
|
|
*
|
|
* @return void
|
|
* @deprecated v2.0.0: use $this->sendMessage()
|
|
*/
|
|
final protected function setMessage(string $message, ?string $type = 'message'): void
|
|
{
|
|
$this->sendMessage($message, $type);
|
|
}
|
|
|
|
/**
|
|
* Delete obsolete files, folders and extensions.
|
|
* Files and folders are identified from the site
|
|
* root path.
|
|
*
|
|
* @param ?SimpleXMLElement $obsolete
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function clearObsolete(?SimpleXMLElement $obsolete = null): void
|
|
{
|
|
$obsolete = $obsolete ?: $this->manifest->alledia->obsolete;
|
|
|
|
$this->sendDebugMessage(__METHOD__ . '<pre>' . print_r($obsolete, 1) . '</pre>');
|
|
|
|
$this->clearOldSystemPlugin();
|
|
if ($obsolete) {
|
|
if ($obsolete->extension) {
|
|
foreach ($obsolete->extension as $extension) {
|
|
$type = $this->getXmlValue($extension['type']);
|
|
$element = $this->getXmlValue($extension['element']);
|
|
$group = $this->getXmlValue($extension['group']);
|
|
|
|
$current = $this->findExtension($type, $element, $group);
|
|
if (empty($current) == false) {
|
|
// Try to uninstall
|
|
$tmpInstaller = new Installer();
|
|
$uninstalled = $tmpInstaller->uninstall($type, $current->get('extension_id'));
|
|
|
|
$typeName = ucfirst(trim(($group ?: '') . ' ' . $type));
|
|
|
|
if ($uninstalled) {
|
|
$this->sendMessage(
|
|
Text::sprintf(
|
|
'LIB_SHACKINSTALLER_OBSOLETE_UNINSTALLED_SUCCESS',
|
|
strtolower($typeName),
|
|
$element
|
|
)
|
|
);
|
|
} else {
|
|
$this->sendMessage(
|
|
Text::sprintf(
|
|
'LIB_SHACKINSTALLER_OBSOLETE_UNINSTALLED_FAIL',
|
|
strtolower($typeName),
|
|
$element
|
|
),
|
|
'error'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($obsolete->file) {
|
|
foreach ($obsolete->file as $file) {
|
|
$path = JPATH_ROOT . '/' . trim((string)$file, '/');
|
|
if (is_file($path)) {
|
|
File::delete($path);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($obsolete->folder) {
|
|
foreach ($obsolete->folder as $folder) {
|
|
$path = JPATH_ROOT . '/' . trim((string)$folder, '/');
|
|
if (is_dir($path)) {
|
|
Folder::delete($path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$oldLanguageFiles = Folder::files(JPATH_ADMINISTRATOR . '/language', '\.lib_allediainstaller\.', true, true);
|
|
foreach ($oldLanguageFiles as $oldLanguageFile) {
|
|
File::delete($oldLanguageFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the extension row for the main extension
|
|
*
|
|
* @return ?Extension
|
|
* @throws \Exception
|
|
*/
|
|
final protected function findThisExtension(): ?Extension
|
|
{
|
|
return $this->findExtension(
|
|
$this->getXmlValue($this->manifest['type']),
|
|
$this->getXmlValue($this->manifest->alledia->element),
|
|
$this->getXmlValue($this->manifest['group'])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Use this in preflight to clear out obsolete update servers when the url has changed.
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function clearUpdateServers(): void
|
|
{
|
|
if ($extension = $this->findThisExtension()) {
|
|
$db = $this->dbo;
|
|
|
|
$query = $db->getQuery(true)
|
|
->select($db->quoteName('update_site_id'))
|
|
->from($db->quoteName('#__update_sites_extensions'))
|
|
->where($db->quoteName('extension_id') . '=' . (int)$extension->get('extension_id'));
|
|
|
|
if ($list = $db->setQuery($query)->loadColumn()) {
|
|
$query = $db->getQuery(true)
|
|
->delete($db->quoteName('#__update_sites_extensions'))
|
|
->where($db->quoteName('extension_id') . '=' . (int)$extension->get('extension_id'));
|
|
$db->setQuery($query)->execute();
|
|
|
|
array_walk($list, 'intval');
|
|
$query = $db->getQuery(true)
|
|
->delete($db->quoteName('#__update_sites'))
|
|
->where($db->quoteName('update_site_id') . ' IN (' . join(',', $list) . ')');
|
|
$db->setQuery($query)->execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the full element, like com_myextension, lib_extension
|
|
*
|
|
* @param ?string $type
|
|
* @param ?string $element
|
|
* @param ?string $group
|
|
*
|
|
* @return string
|
|
*/
|
|
final protected function getFullElement(
|
|
?string $type = null,
|
|
?string $element = null,
|
|
?string $group = null
|
|
): string {
|
|
$prefixes = [
|
|
'component' => 'com',
|
|
'plugin' => 'plg',
|
|
'template' => 'tpl',
|
|
'library' => 'lib',
|
|
'cli' => 'cli',
|
|
'module' => 'mod',
|
|
'file' => 'file',
|
|
];
|
|
|
|
$type = $type ?: $this->type;
|
|
$element = $element ?: (string)$this->manifest->alledia->element;
|
|
$group = $group ?: $this->group;
|
|
|
|
$fullElement = $prefixes[$type] . '_';
|
|
|
|
if ($type === 'plugin') {
|
|
$fullElement .= $group . '_';
|
|
}
|
|
|
|
return $fullElement . $element;
|
|
}
|
|
|
|
/**
|
|
* @return Licensed
|
|
*/
|
|
final protected function getLicense(): Licensed
|
|
{
|
|
if ($this->license === null) {
|
|
$this->license = new Licensed(
|
|
(string)$this->manifest->alledia->namespace,
|
|
$this->type,
|
|
$this->group
|
|
);
|
|
}
|
|
|
|
return $this->license;
|
|
}
|
|
|
|
/**
|
|
* @param string $manifestPath
|
|
*
|
|
* @return Registry
|
|
*/
|
|
final protected function getInfoFromManifest(string $manifestPath): Registry
|
|
{
|
|
$info = new Registry();
|
|
|
|
if (is_file($manifestPath)) {
|
|
$xml = simplexml_load_file($manifestPath);
|
|
|
|
$attributes = (array)$xml->attributes();
|
|
$attributes = $attributes['@attributes'];
|
|
foreach ($attributes as $attribute => $value) {
|
|
$info->set($attribute, $value);
|
|
}
|
|
|
|
foreach ($xml->children() as $e) {
|
|
if (!$e->children()) {
|
|
$info->set($e->getName(), (string)$e);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
$relativePath = str_replace(JPATH_SITE . '/', '', $manifestPath);
|
|
$this->sendMessage(
|
|
Text::sprintf('LIB_SHACKINSTALLER_MANIFEST_NOT_FOUND', $relativePath),
|
|
'error'
|
|
);
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param string $element
|
|
* @param ?string $group
|
|
*
|
|
* @return string
|
|
*/
|
|
final protected function getExtensionPath(string $type, string $element, ?string $group = ''): string
|
|
{
|
|
$folders = [
|
|
'component' => 'administrator/components/',
|
|
'plugin' => 'plugins/',
|
|
'template' => 'templates/',
|
|
'library' => 'libraries/',
|
|
'cli' => 'cli/',
|
|
'module' => 'modules/',
|
|
'file' => 'administrator/manifests/files/',
|
|
];
|
|
|
|
$basePath = JPATH_SITE . '/' . $folders[$type];
|
|
|
|
switch ($type) {
|
|
case 'plugin':
|
|
$basePath .= $group . '/';
|
|
break;
|
|
|
|
case 'module':
|
|
if (!preg_match('/^mod_/', $element)) {
|
|
$basePath .= 'mod_';
|
|
}
|
|
break;
|
|
|
|
case 'component':
|
|
if (!preg_match('/^com_/', $element)) {
|
|
$basePath .= 'com_';
|
|
}
|
|
break;
|
|
|
|
case 'template':
|
|
if (preg_match('/^tpl_/', $element)) {
|
|
$element = str_replace('tpl_', '', $element);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ($type !== 'file') {
|
|
$basePath .= $element;
|
|
}
|
|
|
|
return $basePath;
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param string $element
|
|
* @param ?string $group
|
|
*
|
|
* @return int
|
|
*/
|
|
final protected function getExtensionId(string $type, string $element, ?string $group = ''): int
|
|
{
|
|
$db = $this->dbo;
|
|
$query = $db->getQuery(true)
|
|
->select('extension_id')
|
|
->from('#__extensions')
|
|
->where([
|
|
$db->quoteName('element') . ' = ' . $db->quote($element),
|
|
$db->quoteName('folder') . ' = ' . $db->quote($group),
|
|
$db->quoteName('type') . ' = ' . $db->quote($type),
|
|
]);
|
|
$db->setQuery($query);
|
|
|
|
return (int)$db->loadResult();
|
|
}
|
|
|
|
/**
|
|
* Get the path for the manifest file
|
|
*
|
|
* @return string The path
|
|
*/
|
|
final protected function getManifestPath($type, $element, $group = ''): string
|
|
{
|
|
$installer = new Installer();
|
|
|
|
switch ($type) {
|
|
case 'library':
|
|
case 'file':
|
|
$folders = [
|
|
'library' => 'libraries',
|
|
'file' => 'files',
|
|
];
|
|
|
|
$manifestPath = JPATH_SITE . '/administrator/manifests/' . $folders[$type] . '/' . $element . '.xml';
|
|
|
|
if (!file_exists($manifestPath) || !$installer->isManifest($manifestPath)) {
|
|
$manifestPath = false;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
$basePath = $this->getExtensionPath($type, $element, $group);
|
|
|
|
$installer->setPath('source', $basePath);
|
|
$installer->getManifest();
|
|
|
|
$manifestPath = $installer->getPath('manifest');
|
|
break;
|
|
}
|
|
|
|
return $manifestPath;
|
|
}
|
|
|
|
/**
|
|
* Check if it needs to publish the extension
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function publishThisPlugin(): void
|
|
{
|
|
$attributes = $this->manifest->alledia->element->attributes();
|
|
$publish = (string)$attributes['publish'];
|
|
|
|
if ($publish === 'true' || $publish === '1') {
|
|
$extension = $this->findThisExtension();
|
|
$extension->publish();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if it needs to reorder the extension
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function reorderThisPlugin(): void
|
|
{
|
|
$attributes = $this->manifest->alledia->element->attributes();
|
|
$ordering = (string)$attributes['ordering'];
|
|
|
|
if ($ordering !== '') {
|
|
$extension = $this->findThisExtension();
|
|
$this->setPluginOrder($extension, $ordering);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stores feedback data for related extensions to display after install
|
|
*
|
|
* @param string $key
|
|
* @param string $property
|
|
* @param string $value
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function storeFeedbackForRelatedExtension(string $key, string $property, string $value): void
|
|
{
|
|
$this->sendDebugMessage(sprintf(
|
|
'%s<br>**** %s-%s-%s<br><br>',
|
|
__METHOD__,
|
|
$key,
|
|
$property,
|
|
$value
|
|
));
|
|
|
|
if (empty($this->relatedExtensionFeedback[$key])) {
|
|
$this->relatedExtensionFeedback[$key] = [];
|
|
}
|
|
|
|
$this->relatedExtensionFeedback[$key][$property] = $value;
|
|
}
|
|
|
|
/**
|
|
* This method add a mark to the extensions, allowing to detect our extensions
|
|
* on the extensions table.
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function addAllediaAuthorshipToExtension(): void
|
|
{
|
|
if ($extension = $this->findThisExtension()) {
|
|
$db = $this->dbo;
|
|
|
|
// Update the extension
|
|
$customData = json_decode($extension->get('custom_data')) ?: (object)[];
|
|
$customData->author = 'Joomlashack';
|
|
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('custom_data') . '=' . $db->quote(json_encode($customData)))
|
|
->where($db->quoteName('extension_id') . '=' . (int)$extension->get('extension_id'));
|
|
$db->setQuery($query)->execute();
|
|
|
|
// Update the Alledia framework
|
|
// @TODO: remove this after libraries be able to have a custom install script
|
|
$query = $db->getQuery(true)
|
|
->update($db->quoteName('#__extensions'))
|
|
->set($db->quoteName('custom_data') . '=' . $db->quote('{"author":"Joomlashack"}'))
|
|
->where([
|
|
$db->quoteName('type') . '=' . $db->quote('library'),
|
|
$db->quoteName('element') . '=' . $db->quote('allediaframework'),
|
|
]);
|
|
$db->setQuery($query)->execute();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add styles to the output. Used because when the postFlight
|
|
* method is called, we can't add stylesheets to the head.
|
|
*
|
|
* @param mixed $stylesheets
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function addStyle($stylesheets): void
|
|
{
|
|
if (is_string($stylesheets)) {
|
|
$stylesheets = [$stylesheets];
|
|
}
|
|
|
|
foreach ($stylesheets as $path) {
|
|
if (file_exists($path)) {
|
|
$style = file_get_contents($path);
|
|
|
|
echo '<style>' . $style . '</style>';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On new component install, this will check and fix any menus
|
|
* that may have been created in a previous installation.
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function fixMenus(): void
|
|
{
|
|
if ($this->type == 'component') {
|
|
$db = $this->dbo;
|
|
|
|
if ($extension = $this->findThisExtension()) {
|
|
$id = $extension->get('extension_id');
|
|
$option = $extension->get('name');
|
|
|
|
$query = $db->getQuery(true)
|
|
->update('#__menu')
|
|
->set('component_id = ' . $db->quote($id))
|
|
->where([
|
|
'type = ' . $db->quote('component'),
|
|
'link LIKE ' . $db->quote("%option={$option}%"),
|
|
]);
|
|
$db->setQuery($query)->execute();
|
|
|
|
// Check hidden admin menu option
|
|
// @TODO: Remove after Joomla! incorporates this natively
|
|
$menuElement = $this->manifest->administration->menu;
|
|
if (in_array((string)$menuElement['hidden'], ['true', 'hidden'])) {
|
|
$menu = Table::getInstance('Menu');
|
|
$menu->load(['component_id' => $id, 'client_id' => 1]);
|
|
if ($menu->id) {
|
|
$menu->delete();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get and store a cache of columns of a table
|
|
*
|
|
* @param string $table The table name
|
|
*
|
|
* @return string[]
|
|
* @deprecated v2.1.0: Use $this->findColumn()
|
|
*/
|
|
final protected function getColumnsFromTable(string $table): array
|
|
{
|
|
if (!isset($this->columns[$table])) {
|
|
$db = $this->dbo;
|
|
$db->setQuery('SHOW COLUMNS FROM ' . $db->quoteName($table));
|
|
$rows = $db->loadObjectList();
|
|
|
|
$columns = [];
|
|
foreach ($rows as $row) {
|
|
$columns[] = $row->Field;
|
|
}
|
|
|
|
$this->columns[$table] = $columns;
|
|
}
|
|
|
|
return $this->columns[$table];
|
|
}
|
|
|
|
/**
|
|
* Get and store a cache of indexes of a table
|
|
*
|
|
* @param string $table The table name
|
|
*
|
|
* @return string[]
|
|
*/
|
|
final protected function getIndexesFromTable(string $table): array
|
|
{
|
|
if (!isset($this->indexes[$table])) {
|
|
$db = $this->dbo;
|
|
$db->setQuery('SHOW INDEX FROM ' . $db->quoteName($table));
|
|
$rows = $db->loadObjectList();
|
|
|
|
$indexes = [];
|
|
foreach ($rows as $row) {
|
|
$indexes[] = $row->Key_name;
|
|
}
|
|
|
|
$this->indexes[$table] = $indexes;
|
|
}
|
|
|
|
return $this->indexes[$table];
|
|
}
|
|
|
|
/**
|
|
* Add columns to a table if they doesn't exists
|
|
*
|
|
* @param string $table The table name
|
|
* @param string[] $columns Assoc array of columnNames => definition
|
|
*
|
|
* @return void
|
|
* @deprecated v2.1.0: Use $this->addColumns()
|
|
*/
|
|
final protected function addColumnsIfNotExists(string $table, array $columns): void
|
|
{
|
|
$columnSpecs = [];
|
|
foreach ($columns as $columnName => $columnData) {
|
|
$columnId = $table . '.' . $columnName;
|
|
$columnSpecs[$columnId] = $columnData;
|
|
}
|
|
|
|
$this->addColumns($columnSpecs);
|
|
}
|
|
|
|
/**
|
|
* Add indexes to a table if they doesn't exists
|
|
*
|
|
* @param string $table The table name
|
|
* @param array $indexes Assoc array of indexName => definition
|
|
*
|
|
* @return void
|
|
* @deprecated v2.1.0: use $this->addIndexes()
|
|
*/
|
|
final protected function addIndexesIfNotExists(string $table, array $indexes): void
|
|
{
|
|
$db = $this->dbo;
|
|
|
|
$existentIndexes = $this->getIndexesFromTable($table);
|
|
|
|
foreach ($indexes as $index => $specification) {
|
|
if (!in_array($index, $existentIndexes)) {
|
|
$db->setQuery(
|
|
"ALTER TABLE {$db->quoteName($table)} CREATE INDEX {$specification} ON {$index}"
|
|
)
|
|
->execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drop columns from a table if they exists
|
|
*
|
|
* @param string $table The table name
|
|
* @param string[] $columns The column names that needed to be checked and added
|
|
*
|
|
* @return void
|
|
* @deprecated v2.1.0: Use $this->dropColumns()
|
|
*/
|
|
final protected function dropColumnsIfExists(string $table, array $columns): void
|
|
{
|
|
$columnIds = [];
|
|
foreach ($columns as $column) {
|
|
$columnIds[] = $table . '.' . $column;
|
|
}
|
|
|
|
$this->dropColumns($columnIds);
|
|
}
|
|
|
|
/**
|
|
* Check if a table exists
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return bool
|
|
* @deprecated v2.1.0: Use $this->findTable()
|
|
*/
|
|
final protected function tableExists(string $name): bool
|
|
{
|
|
return $this->findTable($name);
|
|
}
|
|
|
|
/**
|
|
* Parses a conditional string, returning a Boolean value (default: false).
|
|
* For now it only supports an extension name and * as version.
|
|
*
|
|
* @param string $expression
|
|
*
|
|
* @return bool
|
|
* @throws \Exception
|
|
*/
|
|
final protected function parseConditionalExpression(string $expression): bool
|
|
{
|
|
$expression = strtolower($expression);
|
|
$terms = explode('=', $expression);
|
|
$firstTerm = array_shift($terms);
|
|
|
|
if (count($terms) == 0) {
|
|
return $firstTerm == 'true' || $firstTerm == '1';
|
|
|
|
} elseif (preg_match('/^(com_|plg_|mod_|lib_|tpl_|cli_)/', $firstTerm)) {
|
|
// The first term is the name of an extension
|
|
|
|
$info = $this->getExtensionInfoFromElement($firstTerm);
|
|
|
|
$extension = $this->findExtension($info['type'], $firstTerm, $info['group']);
|
|
|
|
// @TODO: compare the version, if specified, or different than *
|
|
// @TODO: Check if the extension is enabled, not just installed
|
|
|
|
if (empty($extension) == false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get extension's info from element string, or extension name
|
|
*
|
|
* @param string $element The extension name, as element
|
|
*
|
|
* @return string[] An associative array with information about the extension
|
|
*/
|
|
final protected function getExtensionInfoFromElement(string $element): array
|
|
{
|
|
$result = array_fill_keys(
|
|
['type', 'name', 'group', 'prefix', 'namespace'],
|
|
null
|
|
);
|
|
|
|
$types = [
|
|
'com' => 'component',
|
|
'plg' => 'plugin',
|
|
'mod' => 'module',
|
|
'lib' => 'library',
|
|
'tpl' => 'template',
|
|
'cli' => 'cli',
|
|
];
|
|
|
|
$element = explode('_', $element, 3);
|
|
|
|
$prefix = $result['prefix'] = array_shift($element);
|
|
$name = array_pop($element);
|
|
$group = array_pop($element);
|
|
|
|
if (array_key_exists($prefix, $types)) {
|
|
$result = array_merge(
|
|
$result,
|
|
[
|
|
'type' => $types[$prefix],
|
|
'group' => $group,
|
|
'name' => $name,
|
|
]
|
|
);
|
|
}
|
|
|
|
$result['namespace'] = preg_replace_callback(
|
|
'/^(os[a-z])(.*)/i',
|
|
function ($matches) {
|
|
return strtoupper($matches[1]) . $matches[2];
|
|
},
|
|
$name ?? ''
|
|
);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check if the actual version is at least the minimum target version.
|
|
*
|
|
* @param string $actualVersion
|
|
* @param string $targetVersion
|
|
* @param ?string $compare
|
|
*
|
|
* @return bool True, if the target version is greater than or equal to actual version
|
|
*/
|
|
final protected function validateTargetVersion(
|
|
string $actualVersion,
|
|
string $targetVersion,
|
|
?string $compare = null
|
|
): bool {
|
|
if ($targetVersion === '.*') {
|
|
// Any version is valid
|
|
return true;
|
|
}
|
|
|
|
$targetVersion = str_replace('*', '0', $targetVersion);
|
|
|
|
return version_compare($actualVersion, $targetVersion, $compare ?: 'ge');
|
|
}
|
|
|
|
/**
|
|
* @param string $targetVersion
|
|
* @param ?string $compare
|
|
*
|
|
* @return bool
|
|
*/
|
|
final protected function validatePreviousVersion(string $targetVersion, ?string $compare = null): bool
|
|
{
|
|
if ($this->previousManifest) {
|
|
$lastVersion = (string)$this->previousManifest->version;
|
|
|
|
return $this->validateTargetVersion($lastVersion, $targetVersion, $compare);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the extension name. If no custom name is set, uses the namespace
|
|
*
|
|
* @return string
|
|
*/
|
|
final protected function getName(): string
|
|
{
|
|
return (string)($this->manifest->alledia->name ?? $this->manifest->alledia->namespace);
|
|
}
|
|
|
|
/**
|
|
* @param ?bool $force Force to get a fresh list of tables
|
|
*
|
|
* @return string[] List of tables
|
|
*/
|
|
final protected function getTables(?bool $force = false): array
|
|
{
|
|
if ($force || $this->tables === null) {
|
|
$this->tables = $this->dbo->setQuery('SHOW TABLES')->loadColumn();
|
|
}
|
|
|
|
return $this->tables;
|
|
}
|
|
|
|
/**
|
|
* @param string $table
|
|
*
|
|
* @return bool
|
|
*/
|
|
final protected function findTable(string $table): bool
|
|
{
|
|
return in_array($this->dbo->replacePrefix($table), $this->getTables());
|
|
}
|
|
|
|
/**
|
|
* @param string[] $columnSpecs
|
|
*
|
|
* @return void
|
|
* @TODO: allow use of specification array
|
|
*/
|
|
final protected function addColumns(array $columnSpecs): void
|
|
{
|
|
$db = $this->dbo;
|
|
|
|
foreach ($columnSpecs as $columnId => $specification) {
|
|
if (strpos($columnId, '.') !== false && empty($this->findColumn($columnId))) {
|
|
[$table, $columnName] = explode('.', $columnId);
|
|
|
|
$db->setQuery(
|
|
sprintf(
|
|
'ALTER TABLE %s ADD COLUMN %s %s',
|
|
$db->quoteName($table),
|
|
$db->quoteName($columnName),
|
|
$specification
|
|
)
|
|
)
|
|
->execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string[] $columnIds
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function dropColumns(array $columnIds): void
|
|
{
|
|
$db = $this->dbo;
|
|
foreach ($columnIds as $columnId) {
|
|
if (strpos($columnId, '.') !== false) {
|
|
[$table, $column] = explode('.', $columnId);
|
|
|
|
$db->setQuery(
|
|
sprintf(
|
|
'ALTER TABLE %s DROP COLUMN %s',
|
|
$db->quoteName($table),
|
|
$column
|
|
)
|
|
)
|
|
->execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $columnId
|
|
*
|
|
* @return ?object
|
|
*/
|
|
final protected function findColumn(string $columnId): ?object
|
|
{
|
|
if (strpos($columnId, '.') !== false) {
|
|
$db = $this->dbo;
|
|
|
|
[$table, $field] = explode('.', $columnId, 2);
|
|
|
|
if (isset($this->tableColumns[$table]) == false) {
|
|
$this->tableColumns[$table] = $db->setQuery('SHOW COLUMNS FROM ' . $db->quoteName($table))
|
|
->loadObjectList();
|
|
}
|
|
|
|
foreach ($this->tableColumns[$table] as $column) {
|
|
if ($column->Field == $field) {
|
|
return $column;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array $indexes
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function addIndexes(array $indexes): void
|
|
{
|
|
$db = $this->dbo;
|
|
|
|
foreach ($indexes as $indexId => $ordering) {
|
|
if (strpos($indexId, '.') !== false) {
|
|
$index = explode('.', $indexId);
|
|
$indexTable = array_shift($index) ?: '';
|
|
$indexName = array_shift($index) ?: '';
|
|
$indexType = array_shift($index) ?: '';
|
|
|
|
if ($this->findIndex($indexTable . '.' . $indexName) == false) {
|
|
$db->setQuery(
|
|
sprintf(
|
|
'ALTER TABLE %s ADD %s INDEX %s(%s)',
|
|
$db->quoteName($indexTable),
|
|
$indexType,
|
|
$db->quoteName($indexName),
|
|
join(',', $ordering)
|
|
)
|
|
)
|
|
->execute();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string[] $indexIds
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function dropIndexes(array $indexIds): void
|
|
{
|
|
$db = $this->dbo;
|
|
|
|
foreach ($indexIds as $indexId) {
|
|
if (strpos($indexId, '.') !== false) {
|
|
if ($this->findIndex($indexId)) {
|
|
[$table, $indexName] = explode('.', $indexId);
|
|
|
|
$db->setQuery(
|
|
sprintf(
|
|
'ALTER TABLE %s DROP INDEX %s',
|
|
$db->quoteName($table),
|
|
$db->quoteName($indexName)
|
|
)
|
|
)
|
|
->execute();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $indexId
|
|
*
|
|
* @return object[]
|
|
*/
|
|
final protected function findIndex(string $indexId): array
|
|
{
|
|
if (strpos($indexId, '.') !== false) {
|
|
$db = $this->dbo;
|
|
|
|
[$table, $indexName] = explode('.', $indexId);
|
|
|
|
if (isset($this->tableIndexes[$table]) == false) {
|
|
$this->tableIndexes[$table] = $db->setQuery('SHOW INDEX FROM ' . $db->quoteName($table))
|
|
->loadObjectList();
|
|
}
|
|
|
|
$indexes = [];
|
|
foreach ($this->tableIndexes[$table] as $index) {
|
|
if ($index->Key_name == $indexName) {
|
|
$indexes[] = $index;
|
|
}
|
|
}
|
|
|
|
return $indexes;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param string[] $constraintIds
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function dropConstraints(array $constraintIds): void
|
|
{
|
|
$db = $this->dbo;
|
|
|
|
foreach ($constraintIds as $constraintId) {
|
|
if (strpos($constraintId, '.') !== false && $this->findConstraint($constraintId)) {
|
|
[$table, $constraintName] = explode('.', $constraintId);
|
|
|
|
$db->setQuery(
|
|
sprintf(
|
|
'ALTER TABLE %s DROP FOREIGN KEY %s',
|
|
$db->quoteName($table),
|
|
$db->quoteName($constraintName)
|
|
)
|
|
)
|
|
->execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $constraintId
|
|
*
|
|
* @return object[]
|
|
*/
|
|
final protected function findConstraint(string $constraintId): array
|
|
{
|
|
if (strpos($constraintId, '.') !== false) {
|
|
$db = $this->dbo;
|
|
|
|
[$table, $constraint] = explode('.', $constraintId);
|
|
|
|
if (isset($this->tableConstraints[$table]) == false) {
|
|
$query = $db->getQuery(true)
|
|
->select('*')
|
|
->from('information_schema.KEY_COLUMN_USAGE')
|
|
->where('TABLE_NAME = ' . $db->quote($db->replacePrefix($table)));
|
|
|
|
$this->tableConstraints[$table] = $db->setQuery($query)->loadObjectList();
|
|
}
|
|
|
|
$items = [];
|
|
foreach ($this->tableConstraints[$table] as $item) {
|
|
if ($item->CONSTRAINT_NAME == $constraint) {
|
|
$items[] = $item;
|
|
}
|
|
}
|
|
|
|
return $items ?: [];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return ?string
|
|
* @throws \Exception
|
|
*/
|
|
final protected function getSchemaVersion(): ?string
|
|
{
|
|
if ($extension = $this->findThisExtension()) {
|
|
$query = $this->dbo->getQuery(true)
|
|
->select('version_id')
|
|
->from('#__schemas')
|
|
->where('extension_id = ' . $extension->get('extension_id'));
|
|
|
|
return $this->dbo->setQuery($query)->loadResult();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param string|string[] $queries
|
|
*
|
|
* @return bool|Throwable
|
|
*/
|
|
final protected function executeQuery($schemaVersion, $queries)
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
$this->sendDebugMessage($this->schemaVersion . ' / ' . $schemaVersion);
|
|
|
|
if ($this->schemaVersion && version_compare($this->schemaVersion, $schemaVersion, 'lt')) {
|
|
$this->sendDebugMessage(sprintf('Running v%s Schema Updates', $schemaVersion));
|
|
|
|
$db = $this->dbo;
|
|
try {
|
|
foreach ((array)$queries as $query) {
|
|
$this->sendDebugMessage($query);
|
|
if ($db->setQuery($query)->execute() == false) {
|
|
return new \Exception('Query Error: ' . $query);
|
|
}
|
|
}
|
|
|
|
} catch (Throwable $error) {
|
|
return $error;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Joomla 4 does a database check that has lots of problems with standard sql syntax
|
|
* causing it to declare the database tables as not up to date and in some cases
|
|
* generates various sql errors. This can optionally be called during Post Install to
|
|
* clear out all update files and still maintain the latest schema version correctly.
|
|
*
|
|
* @param string $basePath
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function clearDBUpdateFiles(string $basePath): void
|
|
{
|
|
$this->sendDebugMessage(__METHOD__);
|
|
|
|
$updatePath = $basePath . '/sql/updates';
|
|
if (is_dir($updatePath) && $files = Folder::files($updatePath, '\.sql$', true, true)) {
|
|
$this->sendDebugMessage('Removing:<pre>' . print_r($files, 1) . '</pre>');
|
|
$final = reset($files);
|
|
foreach ($files as $file) {
|
|
$version = basename($file, '.sql');
|
|
$lastVersion = basename($final, '.sql');
|
|
|
|
if (version_compare($version, $lastVersion, 'gt')) {
|
|
$final = $file;
|
|
}
|
|
File::delete($file);
|
|
}
|
|
|
|
if ($final) {
|
|
File::write($final, '');
|
|
$this->sendDebugMessage('Wrote blank: ' . $final);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param SimpleXMLElement|string $element
|
|
* @param ?string $type
|
|
* @param mixed $default
|
|
*
|
|
* @return bool|string
|
|
*/
|
|
final protected function getXmlValue($element, ?string $type = 'string', $default = null)
|
|
{
|
|
$value = $element ? (string)$element : $default;
|
|
|
|
switch ($type) {
|
|
case 'bool':
|
|
case 'boolean':
|
|
$value = $element
|
|
? $value == 'true' || $value == '1'
|
|
: (bool)$default;
|
|
break;
|
|
|
|
case 'string':
|
|
default:
|
|
if ($value) {
|
|
$value = trim($value);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param string $text
|
|
* @param string $type
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function sendMessage(string $text, string $type = 'message'): void
|
|
{
|
|
if ($this->outputAllowed) {
|
|
try {
|
|
$this->app = $this->app ?: Factory::getApplication();
|
|
$this->app->enqueueMessage($text, $type);
|
|
|
|
} catch (Throwable $error) {
|
|
// Give up trying to send a message normally
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Throwable $error
|
|
* @param bool $cancel
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function sendErrorMessage(Throwable $error, bool $cancel = true): void
|
|
{
|
|
if ($cancel) {
|
|
$this->cancelInstallation = true;
|
|
}
|
|
|
|
if ($this->outputAllowed) {
|
|
$trace = $error->getTrace();
|
|
$trace = array_shift($trace);
|
|
|
|
if (empty($trace['class'])) {
|
|
$caller = basename($trace['file']);
|
|
|
|
} else {
|
|
$className = explode('\\', $trace['class']);
|
|
$caller = array_pop($className);
|
|
}
|
|
$line = $trace['line'];
|
|
$function = $trace['function'] ?? null;
|
|
$file = $trace['file'];
|
|
|
|
if ($function) {
|
|
$message = sprintf('%s: %s<br>%s::%s() - %s', $line, $file, $caller, $function, $error->getMessage());
|
|
} else {
|
|
$message = sprintf('%s:%s (%s) - %s', $line, $caller, $file, $error->getMessage());
|
|
}
|
|
|
|
$this->sendMessage($message, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $text
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function sendDebugMessage(string $text): void
|
|
{
|
|
if ($this->debug) {
|
|
$type = Version::MAJOR_VERSION == 3 ? 'Debug-' . get_class($this) : CMSApplicationInterface::MSG_DEBUG;
|
|
$this->sendMessage($text, $type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
*
|
|
* @return void
|
|
*/
|
|
final protected function displayWelcome(string $type): void
|
|
{
|
|
if ($this->outputAllowed == false) {
|
|
return;
|
|
}
|
|
|
|
$this->sendDebugMessage(
|
|
sprintf(
|
|
'%s<br>Parent: %s<br>Current: %s',
|
|
__METHOD__,
|
|
$this->installer->getPath('parent'),
|
|
$this->installer->getPath('source')
|
|
)
|
|
);
|
|
|
|
$license = $this->getLicense();
|
|
$name = $this->getName() . ($license->isPro() ? ' Pro' : '');
|
|
|
|
// Get the footer content
|
|
$this->footer = '';
|
|
|
|
// Check if we have a dedicated config.xml file
|
|
$configPath = $license->getExtensionPath() . '/config.xml';
|
|
if (is_file($configPath)) {
|
|
$config = $license->getConfig();
|
|
|
|
if (empty($config) == false) {
|
|
$footerElement = $config->xpath('//field[@type="customfooter"]');
|
|
}
|
|
} else {
|
|
$footerElement = $this->manifest->xpath('//field[@type="customfooter"]');
|
|
}
|
|
|
|
if (empty($footerElement) == false) {
|
|
if (class_exists('\\JFormFieldCustomFooter') == false) {
|
|
// Custom footer field is not (and should not be) automatically loaded
|
|
$customFooterPath = $license->getExtensionPath() . '/form/fields/customfooter.php';
|
|
|
|
if (is_file($customFooterPath)) {
|
|
include_once $customFooterPath;
|
|
}
|
|
}
|
|
|
|
if (class_exists('\\JFormFieldCustomFooter')) {
|
|
$field = new JFormFieldCustomFooter();
|
|
$field->fromInstaller = true;
|
|
$this->footer = $field->getInputUsingCustomElement($footerElement[0]);
|
|
|
|
unset($field, $footerElement);
|
|
}
|
|
} else {
|
|
$this->sendDebugMessage('No Footer element was found');
|
|
}
|
|
|
|
// Show additional installation messages
|
|
$extensionPath = $this->getExtensionPath(
|
|
$this->type,
|
|
(string)$this->manifest->alledia->element,
|
|
$this->group
|
|
);
|
|
|
|
// If Pro extension, includes the license form view
|
|
if ($license->isPro()) {
|
|
// Get the OSMyLicensesManager extension to handle the license key
|
|
if ($licensesManagerExtension = new Licensed('osmylicensesmanager', 'plugin', 'system')) {
|
|
if (isset($licensesManagerExtension->params)) {
|
|
$this->licenseKey = $licensesManagerExtension->params->get('license-keys', '');
|
|
} else {
|
|
$this->licenseKey = '';
|
|
}
|
|
|
|
$this->isLicensesManagerInstalled = true;
|
|
}
|
|
|
|
$this->sendDebugMessage('License Manager plugin: ' . (int)$this->isLicensesManagerInstalled);
|
|
}
|
|
|
|
// Welcome message
|
|
if (in_array($type, [static::TYPE_INSTALL, static::TYPE_DISCOVER_INSTALL])) {
|
|
$string = 'LIB_SHACKINSTALLER_THANKS_INSTALL';
|
|
} else {
|
|
$string = 'LIB_SHACKINSTALLER_THANKS_UPDATE';
|
|
}
|
|
|
|
// Variables for the included template
|
|
$this->welcomeMessage = Text::sprintf($string, $name);
|
|
$this->mediaURL = Uri::root() . 'media/' . $license->getFullElement();
|
|
|
|
$this->addStyle($this->mediaFolder . '/css/installer.css');
|
|
|
|
/*
|
|
* Include the template
|
|
* Try to find the template in an alternative folder, since some extensions
|
|
* which uses FOF will display the "Installers" view on admin, errouniously.
|
|
* FOF look for views automatically reading the views folder. So on that
|
|
* case we move the installer view to another folder.
|
|
*/
|
|
$path = $extensionPath . '/views/installer/tmpl/default.php';
|
|
|
|
if (is_file($path) == false) {
|
|
$path = $extensionPath . '/alledia_views/installer/tmpl/default.php';
|
|
}
|
|
|
|
$this->sendDebugMessage(sprintf('Welcome View (%s): %s', (int)is_file($path), $path));
|
|
if (is_file($path)) {
|
|
include $path;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WARNIMG! This is duplicated from the Joomlashack Framework
|
|
*
|
|
* @param string $name
|
|
* @param string $prefix
|
|
* @param string $component
|
|
* @param ?string $appName
|
|
* @param ?array $options
|
|
*
|
|
* @return mixed
|
|
* @throws \Exception
|
|
*/
|
|
protected function getJoomlaModel(
|
|
string $name,
|
|
string $prefix,
|
|
string $component,
|
|
?string $appName = null,
|
|
?array $options = []
|
|
) {
|
|
$defaultApp = 'Site';
|
|
$appNames = [$defaultApp, 'Administrator'];
|
|
|
|
$appName = ucfirst($appName ?: $defaultApp);
|
|
$appName = in_array($appName, $appNames) ? $appName : $defaultApp;
|
|
|
|
if (Version::MAJOR_VERSION < 4) {
|
|
$basePath = $appName == 'Administrator' ? JPATH_ADMINISTRATOR : JPATH_SITE;
|
|
|
|
$path = $basePath . '/components/' . $component;
|
|
BaseDatabaseModel::addIncludePath($path . '/models');
|
|
Table::addIncludePath($path . '/tables');
|
|
|
|
$model = BaseDatabaseModel::getInstance($name, $prefix, $options);
|
|
|
|
} else {
|
|
$model = Factory::getApplication()->bootComponent($component)
|
|
->getMVCFactory()->createModel($name, $appName, $options);
|
|
}
|
|
|
|
return $model;
|
|
}
|
|
|
|
/**
|
|
* Utility function to setting extension states
|
|
* @param array $extensions
|
|
* @param int $state
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
final protected function setExtensionState(array $extensions, int $state = 0): array
|
|
{
|
|
$states = [];
|
|
if (in_array($state, [0, 1])) {
|
|
foreach ($extensions as $extension) {
|
|
$parts = explode('.', $extension);
|
|
|
|
$element = array_pop($parts);
|
|
$folder = null;
|
|
|
|
switch (count($parts)) {
|
|
case 1:
|
|
$type = array_pop($parts);
|
|
break;
|
|
|
|
case 2:
|
|
$folder = array_pop($parts);
|
|
$type = array_pop($parts);
|
|
break;
|
|
|
|
default:
|
|
// Badly structured extension identifier
|
|
break 2;
|
|
}
|
|
|
|
if ($object = $this->findExtension($type, $element, $folder)) {
|
|
$states[$extension] = (int)$object->get('enabled');
|
|
if ($states[$extension] != $state) {
|
|
$this->sendDebugMessage(
|
|
sprintf(
|
|
'%s: %s',
|
|
$extension,
|
|
$state ? 'Enabled' : 'Disabled'
|
|
)
|
|
);
|
|
|
|
$object->set('enabled', $state);
|
|
$object->store();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $states;
|
|
}
|
|
|
|
/**
|
|
* If the old system plugin is installed, it requires special handling to avoid
|
|
* fatal conflicts with its install script
|
|
*
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
final protected function clearOldSystemPlugin()
|
|
{
|
|
if ($this->findExtension('plugin', 'ossystem', 'system')) {
|
|
if (class_exists('PlgSystemOSSystemInstallerScript') == false) {
|
|
class_alias(static::class, 'PlgSystemOSSystemInstallerScript');
|
|
}
|
|
}
|
|
}
|
|
}
|