File manager - Edit - /home/opticamezl/www/newok/Installer.tar
Back
Manifest.php 0000644 00000005264 15172720021 0007027 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\CMS\Language\Text; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Package Manifest File * * @since 3.1 */ abstract class Manifest { /** * The error messages * * @var array * @since 4.3.0 */ public $_errors; /** * Path to the manifest file * * @var string * @since 3.1 */ public $manifest_file = ''; /** * Name of the extension * * @var string * @since 3.1 */ public $name = ''; /** * Version of the extension * * @var string * @since 3.1 */ public $version = ''; /** * Description of the extension * * @var string * @since 3.1 */ public $description = ''; /** * Packager of the extension * * @var string * @since 3.1 */ public $packager = ''; /** * Packager's URL of the extension * * @var string * @since 3.1 */ public $packagerurl = ''; /** * Update site for the extension * * @var string * @since 3.1 */ public $update = ''; /** * List of files in the extension * * @var array * @since 3.1 */ public $filelist = []; /** * Constructor * * @param string $xmlpath Path to XML manifest file. * * @since 3.1 */ public function __construct($xmlpath = '') { if ($xmlpath !== '') { $this->loadManifestFromXml($xmlpath); } } /** * Load a manifest from a file * * @param string $xmlfile Path to file to load * * @return boolean * * @since 3.1 */ public function loadManifestFromXml($xmlfile) { $this->manifest_file = basename($xmlfile, '.xml'); $xml = simplexml_load_file($xmlfile); if (!$xml) { $this->_errors[] = Text::sprintf('JLIB_INSTALLER_ERROR_LOAD_XML', $xmlfile); return false; } else { $this->loadManifestFromData($xml); return true; } } /** * Apply manifest data from a \SimpleXMLElement to the object. * * @param \SimpleXMLElement $xml Data to load * * @return void * * @since 3.1 */ abstract protected function loadManifestFromData(\SimpleXMLElement $xml); } Installer.php 0000644 00000231477 15172720021 0007225 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\CMS\Adapter\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Table\Extension; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseAwareInterface; use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\DatabaseInterface; use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\ParameterType; use Joomla\DI\ContainerAwareInterface; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla base installer class * * @since 3.1 */ class Installer extends Adapter implements DatabaseAwareInterface { use DatabaseAwareTrait; /** * Array of paths needed by the installer * * @var array * @since 3.1 */ protected $paths = []; /** * True if package is an upgrade * * @var boolean * @since 3.1 */ protected $upgrade = null; /** * The manifest trigger class * * @var object * @since 3.1 */ public $manifestClass = null; /** * True if existing files can be overwritten * * @var boolean * @since 3.0.0 */ protected $overwrite = false; /** * Stack of installation steps * - Used for installation rollback * * @var array * @since 3.1 */ protected $stepStack = []; /** * Extension Table Entry * * @var Extension * @since 3.1 */ public $extension = null; /** * The output from the install/uninstall scripts * * @var string * @since 3.1 * */ public $message = null; /** * The installation manifest XML object * * @var object * @since 3.1 */ public $manifest = null; /** * The extension message that appears * * @var string * @since 3.1 */ protected $extension_message = null; /** * The redirect URL if this extension (can be null if no redirect) * * @var string * @since 3.1 */ protected $redirect_url = null; /** * Flag if the uninstall process was triggered by uninstalling a package * * @var boolean * @since 3.7.0 */ protected $packageUninstall = false; /** * Backup extra_query during update_sites rebuild * * @var string * @since 3.9.26 */ public $extraQuery = ''; /** * JInstaller instances container. * * @var Installer[] * @since 3.4 */ protected static $instances; /** * A comment marker to indicate that an update SQL query may fail without triggering an update error. * * @since 4.2.0 */ protected const CAN_FAIL_MARKER = '/** CAN FAIL **/'; /** * The length of the CAN_FAIL_MARKER string * * @since 4.2.0 */ protected const CAN_FAIL_MARKER_LENGTH = 16; /** * Constructor * * @param string $basepath Base Path of the adapters * @param string $classprefix Class prefix of adapters * @param string $adapterfolder Name of folder to append to base path * * @since 3.1 */ public function __construct($basepath = __DIR__, $classprefix = '\\Joomla\\CMS\\Installer\\Adapter', $adapterfolder = 'Adapter') { parent::__construct($basepath, $classprefix, $adapterfolder); $this->extension = Table::getInstance('extension'); } /** * Returns the global Installer object, only creating it if it doesn't already exist. * * @param string $basepath Base Path of the adapters * @param string $classprefix Class prefix of adapters * @param string $adapterfolder Name of folder to append to base path * * @return Installer An installer object * * @since 3.1 */ public static function getInstance($basepath = __DIR__, $classprefix = '\\Joomla\\CMS\\Installer\\Adapter', $adapterfolder = 'Adapter') { if (!isset(self::$instances[$basepath])) { self::$instances[$basepath] = new static($basepath, $classprefix, $adapterfolder); self::$instances[$basepath]->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); } return self::$instances[$basepath]; } /** * Splits a string of multiple queries into an array of individual queries. * * This is different than DatabaseDriver::splitSql. It supports the special CAN FAIL comment * marker which indicates that a SQL statement could fail without raising an error during the * installation. * * @param string|null $sql Input SQL string with which to split into individual queries. * * @return array * * @since 4.2.0 */ public static function splitSql(?string $sql): array { if (empty($sql)) { return []; } $start = 0; $open = false; $comment = false; $endString = ''; $end = \strlen($sql); $queries = []; $query = ''; for ($i = 0; $i < $end; $i++) { $current = substr($sql, $i, 1); $current2 = substr($sql, $i, 2); $current3 = substr($sql, $i, 3); $lenEndString = \strlen($endString); $testEnd = substr($sql, $i, $lenEndString); if ( $current === '"' || $current === "'" || $current2 === '--' || ($current2 === '/*' && $current3 !== '/*!' && $current3 !== '/*+') || ($current === '#' && $current3 !== '#__') || ($comment && $testEnd === $endString) ) { // Check if quoted with previous backslash $n = 2; while (substr($sql, $i - $n + 1, 1) === '\\' && $n < $i) { $n++; } // Not quoted if ($n % 2 === 0) { if ($open) { if ($testEnd === $endString) { if ($comment) { $comment = false; if ($lenEndString > 1) { $i += ($lenEndString - 1); $current = substr($sql, $i, 1); } $start = $i + 1; } $open = false; $endString = ''; } } else { $open = true; if ($current2 === '--') { $endString = "\n"; $comment = true; } elseif ($current2 === '/*') { $endString = '*/'; $comment = true; } elseif ($current === '#') { $endString = "\n"; $comment = true; } else { $endString = $current; } if ($comment && $start < $i) { $query .= substr($sql, $start, $i - $start); } } } } if ($comment) { $start = $i + 1; } if (($current === ';' && !$open) || $i === $end - 1) { if ($current === ';' && !$open && $start <= $i && $start > self::CAN_FAIL_MARKER_LENGTH) { $possibleMarker = substr($sql, $start - self::CAN_FAIL_MARKER_LENGTH, $i - $start + self::CAN_FAIL_MARKER_LENGTH); if (strtoupper($possibleMarker) === self::CAN_FAIL_MARKER) { $start -= self::CAN_FAIL_MARKER_LENGTH; } } if ($start <= $i) { $query .= substr($sql, $start, $i - $start + 1); } $query = trim($query); if ($query) { if (($i === $end - 1) && ($current !== ';')) { $query .= ';'; } $queries[] = $query; } $query = ''; $start = $i + 1; } $endComment = false; } return $queries; } /** * Get the allow overwrite switch * * @return boolean Allow overwrite switch * * @since 3.1 */ public function isOverwrite() { return $this->overwrite; } /** * Set the allow overwrite switch * * @param boolean $state Overwrite switch state * * @return boolean True it state is set, false if it is not * * @since 3.1 */ public function setOverwrite($state = false) { $tmp = $this->overwrite; if ($state) { $this->overwrite = true; } else { $this->overwrite = false; } return $tmp; } /** * Get the redirect location * * @return string Redirect location (or null) * * @since 3.1 */ public function getRedirectUrl() { return $this->redirect_url; } /** * Set the redirect location * * @param string $newurl New redirect location * * @return void * * @since 3.1 */ public function setRedirectUrl($newurl) { $this->redirect_url = $newurl; } /** * Get whether this installer is uninstalling extensions which are part of a package * * @return boolean * * @since 3.7.0 */ public function isPackageUninstall() { return $this->packageUninstall; } /** * Set whether this installer is uninstalling extensions which are part of a package * * @param boolean $uninstall True if a package triggered the uninstall, false otherwise * * @return void * * @since 3.7.0 */ public function setPackageUninstall($uninstall) { $this->packageUninstall = $uninstall; } /** * Get the upgrade switch * * @return boolean * * @since 3.1 */ public function isUpgrade() { return $this->upgrade; } /** * Set the upgrade switch * * @param boolean $state Upgrade switch state * * @return boolean True if upgrade, false otherwise * * @since 3.1 */ public function setUpgrade($state = false) { $tmp = $this->upgrade; if ($state) { $this->upgrade = true; } else { $this->upgrade = false; } return $tmp; } /** * Get the installation manifest object * * @return \SimpleXMLElement Manifest object * * @since 3.1 */ public function getManifest() { if (!\is_object($this->manifest)) { $this->findManifest(); } return $this->manifest; } /** * Get an installer path by name * * @param string $name Path name * @param string $default Default value * * @return string Path * * @since 3.1 */ public function getPath($name, $default = null) { return (!empty($this->paths[$name])) ? $this->paths[$name] : $default; } /** * Sets an installer path by name * * @param string $name Path name * @param string $value Path * * @return void * * @since 3.1 */ public function setPath($name, $value) { $this->paths[$name] = $value; } /** * Pushes a step onto the installer stack for rolling back steps * * @param array $step Installer step * * @return void * * @since 3.1 */ public function pushStep($step) { $this->stepStack[] = $step; } /** * Installation abort method * * @param string $msg Abort message from the installer * @param string $type Package type if defined * * @return boolean True if successful * * @since 3.1 */ public function abort($msg = null, $type = null) { $retval = true; $step = array_pop($this->stepStack); // Raise abort warning if ($msg) { Log::add($msg, Log::WARNING, 'jerror'); } while ($step != null) { switch ($step['type']) { case 'file': // Remove the file if (is_file($step['path']) && !($stepval = File::delete($step['path']))) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FILE_FOLDER', $step['path']), Log::WARNING, 'jerror'); } break; case 'folder': // Remove the folder if (Folder::exists($step['path']) && !($stepval = Folder::delete($step['path']))) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FILE_FOLDER', $step['path']), Log::WARNING, 'jerror'); } break; case 'query': // Execute the query. $stepval = $this->parseSQLFiles($step['script']); break; case 'extension': // Get database connector object $db = $this->getDatabase(); $query = $db->getQuery(true); $stepId = (int) $step['id']; // Remove the entry from the #__extensions table $query->delete($db->quoteName('#__extensions')) ->where($db->quoteName('extension_id') . ' = :step_id') ->bind(':step_id', $stepId, ParameterType::INTEGER); $db->setQuery($query); try { $db->execute(); $stepval = true; } catch (ExecutionFailureException $e) { // The database API will have already logged the error it caught, we just need to alert the user to the issue Log::add(Text::_('JLIB_INSTALLER_ABORT_ERROR_DELETING_EXTENSIONS_RECORD'), Log::WARNING, 'jerror'); $stepval = false; } break; default: if ($type && \is_object($this->_adapters[$type])) { // Build the name of the custom rollback method for the type $method = '_rollback_' . $step['type']; // Custom rollback method handler if (method_exists($this->_adapters[$type], $method)) { $stepval = $this->_adapters[$type]->$method($step); } } else { // Set it to false $stepval = false; } break; } // Only set the return value if it is false if ($stepval === false) { $retval = false; } // Get the next step and continue $step = array_pop($this->stepStack); } return $retval; } // Adapter functions /** * Package installation method * * @param string $path Path to package source folder * * @return boolean True if successful * * @since 3.1 */ public function install($path = null) { if ($path && Folder::exists($path)) { $this->setPath('source', $path); } else { $this->abort(Text::_('JLIB_INSTALLER_ABORT_NOINSTALLPATH')); return false; } if (!$adapter = $this->setupInstall('install', true)) { $this->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); return false; } if (!\is_object($adapter)) { return false; } // Add the languages from the package itself if (method_exists($adapter, 'loadLanguage')) { $adapter->loadLanguage($path); } // Fire the onExtensionBeforeInstall event. PluginHelper::importPlugin('extension'); Factory::getApplication()->triggerEvent( 'onExtensionBeforeInstall', [ 'method' => 'install', 'type' => $this->manifest->attributes()->type, 'manifest' => $this->manifest, 'extension' => 0, ] ); // Run the install $result = $adapter->install(); // Make sure Joomla can figure out what has changed clearstatcache(); // Fire the onExtensionAfterInstall Factory::getApplication()->triggerEvent( 'onExtensionAfterInstall', ['installer' => clone $this, 'eid' => $result] ); if ($result !== false) { // Refresh versionable assets cache Factory::getApplication()->flushAssets(); return true; } return false; } /** * Discovered package installation method * * @param integer $eid Extension ID * * @return boolean True if successful * * @since 3.1 */ public function discover_install($eid = null) { if (!$eid) { $this->abort(Text::_('JLIB_INSTALLER_ABORT_EXTENSIONNOTVALID')); return false; } if (!$this->extension->load($eid)) { $this->abort(Text::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS')); return false; } if ($this->extension->state != -1) { $this->abort(Text::_('JLIB_INSTALLER_ABORT_ALREADYINSTALLED')); return false; } // Load the adapter(s) for the install manifest $type = $this->extension->type; $params = ['extension' => $this->extension, 'route' => 'discover_install']; $adapter = $this->loadAdapter($type, $params); if (!\is_object($adapter)) { return false; } if (!method_exists($adapter, 'discover_install') || !$adapter->getDiscoverInstallSupported()) { $this->abort(Text::sprintf('JLIB_INSTALLER_ERROR_DISCOVER_INSTALL_UNSUPPORTED', $type)); return false; } // The adapter needs to prepare itself if (method_exists($adapter, 'prepareDiscoverInstall')) { try { $adapter->prepareDiscoverInstall(); } catch (\RuntimeException $e) { $this->abort($e->getMessage()); return false; } } // Add the languages from the package itself if (method_exists($adapter, 'loadLanguage')) { $adapter->loadLanguage(); } // Fire the onExtensionBeforeInstall event. PluginHelper::importPlugin('extension'); Factory::getApplication()->triggerEvent( 'onExtensionBeforeInstall', [ 'method' => 'discover_install', 'type' => $this->extension->get('type'), 'manifest' => null, 'extension' => $this->extension->get('extension_id'), ] ); // Run the install $result = $adapter->discover_install(); // Fire the onExtensionAfterInstall Factory::getApplication()->triggerEvent( 'onExtensionAfterInstall', ['installer' => clone $this, 'eid' => $result] ); if ($result !== false) { // Refresh versionable assets cache Factory::getApplication()->flushAssets(); return true; } return false; } /** * Extension discover method * * Asks each adapter to find extensions * * @return InstallerExtension[] * * @since 3.1 */ public function discover() { $results = []; foreach ($this->getAdapters() as $adapter) { $instance = $this->loadAdapter($adapter); // Joomla! 1.5 installation adapter legacy support if (method_exists($instance, 'discover')) { $tmp = $instance->discover(); // If its an array and has entries if (\is_array($tmp) && \count($tmp)) { // Merge it into the system $results = array_merge($results, $tmp); } } } return $results; } /** * Package update method * * @param string $path Path to package source folder * * @return boolean True if successful * * @since 3.1 */ public function update($path = null) { if ($path && Folder::exists($path)) { $this->setPath('source', $path); } else { $this->abort(Text::_('JLIB_INSTALLER_ABORT_NOUPDATEPATH')); return false; } if (!$adapter = $this->setupInstall('update', true)) { $this->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); return false; } if (!\is_object($adapter)) { return false; } // Add the languages from the package itself if (method_exists($adapter, 'loadLanguage')) { $adapter->loadLanguage($path); } // Fire the onExtensionBeforeUpdate event. PluginHelper::importPlugin('extension'); Factory::getApplication()->triggerEvent( 'onExtensionBeforeUpdate', ['type' => $this->manifest->attributes()->type, 'manifest' => $this->manifest] ); // Run the update $result = $adapter->update(); // Fire the onExtensionAfterUpdate Factory::getApplication()->triggerEvent( 'onExtensionAfterUpdate', ['installer' => clone $this, 'eid' => $result] ); if ($result !== false) { return true; } return false; } /** * Package uninstallation method * * @param string $type Package type * @param mixed $identifier Package identifier for adapter * * @return boolean True if successful * * @since 3.1 */ public function uninstall($type, $identifier) { $params = ['extension' => $this->extension, 'route' => 'uninstall']; $adapter = $this->loadAdapter($type, $params); if (!\is_object($adapter)) { return false; } // We don't load languages here, we get the extension adapter to work it out // Fire the onExtensionBeforeUninstall event. PluginHelper::importPlugin('extension'); Factory::getApplication()->triggerEvent( 'onExtensionBeforeUninstall', ['eid' => $identifier] ); // Run the uninstall $result = $adapter->uninstall($identifier); // Fire the onExtensionAfterInstall Factory::getApplication()->triggerEvent( 'onExtensionAfterUninstall', ['installer' => clone $this, 'eid' => $identifier, 'removed' => $result] ); // Refresh versionable assets cache Factory::getApplication()->flushAssets(); return $result; } /** * Refreshes the manifest cache stored in #__extensions * * @param integer $eid Extension ID * * @return boolean * * @since 3.1 */ public function refreshManifestCache($eid) { if ($eid) { if (!$this->extension->load($eid)) { $this->abort(Text::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS')); return false; } if ($this->extension->state == -1) { $this->abort(Text::sprintf('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE', $this->extension->name)); return false; } // Fetch the adapter $adapter = $this->loadAdapter($this->extension->type); if (!\is_object($adapter)) { return false; } if (!method_exists($adapter, 'refreshManifestCache')) { $this->abort(Text::sprintf('JLIB_INSTALLER_ABORT_METHODNOTSUPPORTED_TYPE', $this->extension->type)); return false; } $result = $adapter->refreshManifestCache(); if ($result !== false) { return true; } else { return false; } } $this->abort(Text::_('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE_VALID')); return false; } // Utility functions /** * Prepare for installation: this method sets the installation directory, finds * and checks the installation file and verifies the installation type. * * @param string $route The install route being followed * @param boolean $returnAdapter Flag to return the instantiated adapter * * @return boolean|InstallerAdapter InstallerAdapter object if explicitly requested otherwise boolean * * @since 3.1 */ public function setupInstall($route = 'install', $returnAdapter = false) { // We need to find the installation manifest file if (!$this->findManifest()) { return false; } // Load the adapter(s) for the install manifest $type = (string) $this->manifest->attributes()->type; $params = ['route' => $route, 'manifest' => $this->getManifest()]; // Load the adapter $adapter = $this->loadAdapter($type, $params); if ($returnAdapter) { return $adapter; } return true; } /** * Backward compatible method to parse through a queries element of the * installation manifest file and take appropriate action. * * @param \SimpleXMLElement $element The XML node to process * * @return mixed Number of queries processed or False on error * * @since 3.1 */ public function parseQueries(\SimpleXMLElement $element) { // Get the database connector object $db = & $this->_db; if (!$element || !\count($element->children())) { // Either the tag does not exist or has no children therefore we return zero files processed. return 0; } // Get the array of query nodes to process $queries = $element->children(); if (\count($queries) === 0) { // No queries to process return 0; } $update_count = 0; // Process each query in the $queries array (children of $tagName). foreach ($queries as $query) { try { $db->setQuery($query)->execute(); } catch (ExecutionFailureException $e) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), Log::WARNING, 'jerror'); return false; } $update_count++; } return $update_count; } /** * Method to extract the name of a discreet installation sql file from the installation manifest file. * * @param object $element The XML node to process * * @return mixed Number of queries processed or False on error * * @since 3.1 */ public function parseSQLFiles($element) { if (!$element || !\count($element->children())) { // The tag does not exist. return 0; } $db = &$this->_db; $dbDriver = $db->getServerType(); $updateCount = 0; // Get the name of the sql file to process foreach ($element->children() as $file) { $fCharset = strtolower($file->attributes()->charset) === 'utf8' ? 'utf8' : ''; $fDriver = strtolower($file->attributes()->driver); if ($fDriver === 'mysqli' || $fDriver === 'pdomysql') { $fDriver = 'mysql'; } elseif ($fDriver === 'pgsql') { $fDriver = 'postgresql'; } if ($fCharset !== 'utf8' || $fDriver != $dbDriver) { continue; } $sqlfile = $this->getPath('extension_root') . '/' . trim($file); // Check that sql files exists before reading. Otherwise raise error for rollback if (!file_exists($sqlfile)) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_FILENOTFOUND', $sqlfile), Log::WARNING, 'jerror'); return false; } $buffer = file_get_contents($sqlfile); // Graceful exit and rollback if read not successful if ($buffer === false) { Log::add(Text::_('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), Log::WARNING, 'jerror'); return false; } // Create an array of queries from the sql file $queries = self::splitSql($buffer); if (\count($queries) === 0) { // No queries to process continue; } // Process each query in the $queries array (split out of sql file). foreach ($queries as $query) { $canFail = strlen($query) > self::CAN_FAIL_MARKER_LENGTH + 1 && strtoupper(substr($query, -self::CAN_FAIL_MARKER_LENGTH - 1)) === (self::CAN_FAIL_MARKER . ';'); $query = $canFail ? (substr($query, 0, -self::CAN_FAIL_MARKER_LENGTH - 1) . ';') : $query; try { $db->setQuery($query)->execute(); } catch (ExecutionFailureException $e) { if (!$canFail) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), Log::WARNING, 'jerror'); return false; } } $updateCount++; } } return $updateCount; } /** * Set the schema version for an extension by looking at its latest update * * @param \SimpleXMLElement $schema Schema Tag * @param integer $eid Extension ID * * @return void * * @since 3.1 */ public function setSchemaVersion(\SimpleXMLElement $schema, $eid) { if ($eid && $schema) { $db = $this->getDatabase(); $schemapaths = $schema->children(); if (!$schemapaths) { return; } if (\count($schemapaths)) { $dbDriver = $db->getServerType(); $schemapath = ''; foreach ($schemapaths as $entry) { $attrs = $entry->attributes(); if ($attrs['type'] == $dbDriver) { $schemapath = $entry; break; } } if ($schemapath !== '') { $files = str_replace('.sql', '', Folder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$')); usort($files, 'version_compare'); // Update the database $query = $db->getQuery(true) ->delete('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $eid, ParameterType::INTEGER); $db->setQuery($query); if ($db->execute()) { $schemaVersion = end($files); $query->clear() ->insert($db->quoteName('#__schemas')) ->columns([$db->quoteName('extension_id'), $db->quoteName('version_id')]) ->values(':extension_id, :version_id') ->bind(':extension_id', $eid, ParameterType::INTEGER) ->bind(':version_id', $schemaVersion); $db->setQuery($query); $db->execute(); } } } } } /** * Method to process the updates for an item * * @param \SimpleXMLElement $schema The XML node to process * @param integer $eid Extension Identifier * * @return boolean|int Number of SQL updates executed; false on failure. * * @since 3.1 */ public function parseSchemaUpdates(\SimpleXMLElement $schema, $eid) { $updateCount = 0; // Ensure we have an XML element and a valid extension id if (!$eid || !$schema) { return $updateCount; } $db = $this->getDatabase(); $schemapaths = $schema->children(); if (!\count($schemapaths)) { return $updateCount; } $dbDriver = $db->getServerType(); $schemapath = ''; foreach ($schemapaths as $entry) { $attrs = $entry->attributes(); // Assuming that the type is a mandatory attribute but if it is not mandatory then there should be a discussion for it. $uDriver = strtolower($attrs['type']); if ($uDriver === 'mysqli' || $uDriver === 'pdomysql') { $uDriver = 'mysql'; } elseif ($uDriver === 'pgsql') { $uDriver = 'postgresql'; } if ($uDriver == $dbDriver) { $schemapath = $entry; break; } } if ($schemapath === '') { return $updateCount; } $files = Folder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$'); if (empty($files)) { return $updateCount; } Log::add(Text::_('JLIB_INSTALLER_SQL_BEGIN'), Log::INFO, 'Update'); $files = str_replace('.sql', '', $files); usort($files, 'version_compare'); $query = $db->getQuery(true) ->select('version_id') ->from('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $eid, ParameterType::INTEGER); $db->setQuery($query); $hasVersion = true; try { $version = $db->loadResult(); // No version - use initial version. if (!$version) { $version = '0.0.0'; $hasVersion = false; } } catch (ExecutionFailureException $e) { $version = '0.0.0'; } Log::add(Text::sprintf('JLIB_INSTALLER_SQL_BEGIN_SCHEMA', $version), Log::INFO, 'Update'); foreach ($files as $file) { // Skip over files earlier or equal to the latest schema version recorded for this extension. if (version_compare($file, $version) <= 0) { continue; } $buffer = file_get_contents(sprintf("%s/%s/%s.sql", $this->getPath('extension_root'), $schemapath, $file)); // Graceful exit and rollback if read not successful if ($buffer === false) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), Log::WARNING, 'jerror'); return false; } // Create an array of queries from the sql file $queries = self::splitSql($buffer); // Process each query in the $queries array (split out of sql file). foreach ($queries as $query) { $canFail = strlen($query) > self::CAN_FAIL_MARKER_LENGTH + 1 && strtoupper(substr($query, -self::CAN_FAIL_MARKER_LENGTH - 1)) === (self::CAN_FAIL_MARKER . ';'); $query = $canFail ? (substr($query, 0, -self::CAN_FAIL_MARKER_LENGTH - 1) . ';') : $query; $queryString = (string) $query; $queryString = str_replace(["\r", "\n"], ['', ' '], substr($queryString, 0, 80)); try { $db->setQuery($query)->execute(); } catch (\RuntimeException $e) { if (!$canFail) { $errorMessage = Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()); // Log the error in the update log file Log::add(Text::sprintf('JLIB_INSTALLER_UPDATE_LOG_QUERY', $file, $queryString), Log::INFO, 'Update'); Log::add($errorMessage, Log::INFO, 'Update'); Log::add(Text::_('JLIB_INSTALLER_SQL_END_NOT_COMPLETE'), Log::INFO, 'Update'); // Show the error message to the user Log::add($errorMessage, Log::WARNING, 'jerror'); return false; } } Log::add(Text::sprintf('JLIB_INSTALLER_UPDATE_LOG_QUERY', $file, $queryString), Log::INFO, 'Update'); $updateCount++; } // Update the schema version for this extension try { $this->updateSchemaTable($eid, $file, $hasVersion); $hasVersion = true; } catch (ExecutionFailureException $e) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $e->getMessage()), Log::WARNING, 'jerror'); return false; } } Log::add(Text::_('JLIB_INSTALLER_SQL_END'), Log::INFO, 'Update'); return $updateCount; } /** * Update the schema table with the latest version * * @param int $eid Extension ID. * @param string $version Latest schema version ID. * @param boolean $update Should I run an update against an existing record or insert a new one? * * @return void * * @since 4.2.0 */ protected function updateSchemaTable(int $eid, string $version, bool $update = false): void { $db = $this->getDatabase(); $o = (object) [ 'extension_id' => $eid, 'version_id' => $version, ]; try { if ($update) { $db->updateObject('#__schemas', $o, 'extension_id'); } else { $db->insertObject('#__schemas', $o); } } catch (ExecutionFailureException $e) { /** * Safe fallback: delete any existing record and insert afresh. * * It is possible that the schema version may be populated after we detected it does not * exist (or removed after we detected it exists) and before we finish executing the SQL * update script. This could happen e.g. if the update SQL script messes with it, or if * another process is also tinkering with the #__schemas table. * * The safe fallback below even runs inside a transaction to prevent interference from * another process. */ $db->transactionStart(); $query = $db->getQuery(true) ->delete('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $eid, ParameterType::INTEGER); $db->setQuery($query)->execute(); $db->insertObject('#__schemas', $o); $db->transactionCommit(); } } /** * Method to parse through a files element of the installation manifest and take appropriate * action. * * @param \SimpleXMLElement $element The XML node to process * @param integer $cid Application ID of application to install to * @param array $oldFiles List of old files (SimpleXMLElement's) * @param array $oldMD5 List of old MD5 sums (indexed by filename with value as MD5) * * @return boolean True on success * * @since 3.1 */ public function parseFiles(\SimpleXMLElement $element, $cid = 0, $oldFiles = null, $oldMD5 = null) { // Get the array of file nodes to process; we checked whether this had children above. if (!$element || !\count($element->children())) { // Either the tag does not exist or has no children (hence no files to process) therefore we return zero files processed. return 0; } $copyfiles = []; // Get the client info $client = ApplicationHelper::getClientInfo($cid); /* * Here we set the folder we are going to remove the files from. */ if ($client) { $pathname = 'extension_' . $client->name; $destination = $this->getPath($pathname); } else { $pathname = 'extension_root'; $destination = $this->getPath($pathname); } /* * Here we set the folder we are going to copy the files from. * * Does the element have a folder attribute? * * If so this indicates that the files are in a subdirectory of the source * folder and we should append the folder attribute to the source path when * copying files. */ $folder = (string) $element->attributes()->folder; if ($folder && file_exists($this->getPath('source') . '/' . $folder)) { $source = $this->getPath('source') . '/' . $folder; } else { $source = $this->getPath('source'); } // Work out what files have been deleted if ($oldFiles && ($oldFiles instanceof \SimpleXMLElement)) { $oldEntries = $oldFiles->children(); if (\count($oldEntries)) { $deletions = $this->findDeletedFiles($oldEntries, $element->children()); foreach ($deletions['folders'] as $deleted_folder) { $folder = $destination . '/' . $deleted_folder; if (Folder::exists($folder) && !Folder::delete($folder)) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FILE_FOLDER', $folder), Log::WARNING, 'jerror'); } } foreach ($deletions['files'] as $deleted_file) { $file = $destination . '/' . $deleted_file; if (is_file($file) && !File::delete($file)) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FILE_FOLDER', $file), Log::WARNING, 'jerror'); } } } } $path = []; // Copy the MD5SUMS file if it exists if (file_exists($source . '/MD5SUMS')) { $path['src'] = $source . '/MD5SUMS'; $path['dest'] = $destination . '/MD5SUMS'; $path['type'] = 'file'; $copyfiles[] = $path; } // Process each file in the $files array (children of $tagName). foreach ($element->children() as $file) { $path['src'] = $source . '/' . $file; $path['dest'] = $destination . '/' . $file; // Is this path a file or folder? $path['type'] = $file->getName() === 'folder' ? 'folder' : 'file'; /* * Before we can add a file to the copyfiles array we need to ensure * that the folder we are copying our file to exists and if it doesn't, * we need to create it. */ if (basename($path['dest']) !== $path['dest']) { $newdir = \dirname($path['dest']); if (!Folder::create($newdir)) { Log::add( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_INSTALL'), $newdir ), Log::WARNING, 'jerror' ); return false; } } // Add the file to the copyfiles array $copyfiles[] = $path; } return $this->copyFiles($copyfiles); } /** * Method to parse through a languages element of the installation manifest and take appropriate * action. * * @param \SimpleXMLElement $element The XML node to process * @param integer $cid Application ID of application to install to * * @return boolean True on success * * @since 3.1 */ public function parseLanguages(\SimpleXMLElement $element, $cid = 0) { // TODO: work out why the below line triggers 'node no longer exists' errors with files if (!$element || !\count($element->children())) { // Either the tag does not exist or has no children therefore we return zero files processed. return 0; } $copyfiles = []; // Get the client info $client = ApplicationHelper::getClientInfo($cid); // Here we set the folder we are going to copy the files to. // 'languages' Files are copied to JPATH_BASE/language/ folder $destination = $client->path . '/language'; /* * Here we set the folder we are going to copy the files from. * * Does the element have a folder attribute? * * If so this indicates that the files are in a subdirectory of the source * folder and we should append the folder attribute to the source path when * copying files. */ $folder = (string) $element->attributes()->folder; if ($folder && file_exists($this->getPath('source') . '/' . $folder)) { $source = $this->getPath('source') . '/' . $folder; } else { $source = $this->getPath('source'); } // Process each file in the $files array (children of $tagName). foreach ($element->children() as $file) { /* * Language files go in a subfolder based on the language code, ie. * <language tag="en-US">en-US.mycomponent.ini</language> * would go in the en-US subdirectory of the language folder. */ // We will only install language files where a core language pack // already exists. if ((string) $file->attributes()->tag !== '') { $path = []; $path['src'] = $source . '/' . $file; if ((string) $file->attributes()->client !== '') { // Override the client $langclient = ApplicationHelper::getClientInfo((string) $file->attributes()->client, true); $path['dest'] = $langclient->path . '/language/' . $file->attributes()->tag . '/' . basename((string) $file); } else { // Use the default client $path['dest'] = $destination . '/' . $file->attributes()->tag . '/' . basename((string) $file); } // If the language folder is not present, then the core pack hasn't been installed... ignore if (!Folder::exists(\dirname($path['dest']))) { continue; } } else { $path = []; $path['src'] = $source . '/' . $file; $path['dest'] = $destination . '/' . $file; } /* * Before we can add a file to the copyfiles array we need to ensure * that the folder we are copying our file to exists and if it doesn't, * we need to create it. */ if (basename($path['dest']) !== $path['dest']) { $newdir = \dirname($path['dest']); if (!Folder::create($newdir)) { Log::add( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_INSTALL'), $newdir ), Log::WARNING, 'jerror' ); return false; } } // Add the file to the copyfiles array $copyfiles[] = $path; } return $this->copyFiles($copyfiles); } /** * Method to parse through a media element of the installation manifest and take appropriate * action. * * @param \SimpleXMLElement $element The XML node to process * @param integer $cid Application ID of application to install to * * @return boolean True on success * * @since 3.1 */ public function parseMedia(\SimpleXMLElement $element, $cid = 0) { if (!$element || !\count($element->children())) { // Either the tag does not exist or has no children therefore we return zero files processed. return 0; } $copyfiles = []; // Here we set the folder we are going to copy the files to. // Default 'media' Files are copied to the JPATH_BASE/media folder $folder = ((string) $element->attributes()->destination) ? '/' . $element->attributes()->destination : null; $destination = Path::clean(JPATH_ROOT . '/media' . $folder); // Here we set the folder we are going to copy the files from. /* * Does the element have a folder attribute? * If so this indicates that the files are in a subdirectory of the source * folder and we should append the folder attribute to the source path when * copying files. */ $folder = (string) $element->attributes()->folder; if ($folder && file_exists($this->getPath('source') . '/' . $folder)) { $source = $this->getPath('source') . '/' . $folder; } else { $source = $this->getPath('source'); } // Process each file in the $files array (children of $tagName). foreach ($element->children() as $file) { $path = []; $path['src'] = $source . '/' . $file; $path['dest'] = $destination . '/' . $file; // Is this path a file or folder? $path['type'] = $file->getName() === 'folder' ? 'folder' : 'file'; /* * Before we can add a file to the copyfiles array we need to ensure * that the folder we are copying our file to exists and if it doesn't, * we need to create it. */ if (basename($path['dest']) !== $path['dest']) { $newdir = \dirname($path['dest']); if (!Folder::create($newdir)) { Log::add( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_INSTALL'), $newdir ), Log::WARNING, 'jerror' ); return false; } } // Add the file to the copyfiles array $copyfiles[] = $path; } return $this->copyFiles($copyfiles); } /** * Method to parse the parameters of an extension, build the JSON string for its default parameters, and return the JSON string. * * @return string JSON string of parameter values * * @since 3.1 * @note This method must always return a JSON compliant string */ public function getParams() { // Validate that we have a fieldset to use if (!isset($this->manifest->config->fields->fieldset)) { return '{}'; } // Getting the fieldset tags $fieldsets = $this->manifest->config->fields->fieldset; // Creating the data collection variable: $ini = []; // Iterating through the fieldsets: foreach ($fieldsets as $fieldset) { if (!\count($fieldset->children())) { // Either the tag does not exist or has no children therefore we return zero files processed. return '{}'; } // Iterating through the fields and collecting the name/default values: foreach ($fieldset as $field) { // Check against the null value since otherwise default values like "0" // cause entire parameters to be skipped. if (($name = $field->attributes()->name) === null) { continue; } if (($value = $field->attributes()->default) === null) { continue; } $ini[(string) $name] = (string) $value; } } return json_encode($ini); } /** * Copyfiles * * Copy files from source directory to the target directory * * @param array $files Array with filenames * @param boolean $overwrite True if existing files can be replaced * * @return boolean True on success * * @since 3.1 */ public function copyFiles($files, $overwrite = null) { /* * To allow for manual override on the overwriting flag, we check to see if * the $overwrite flag was set and is a boolean value. If not, use the object * allowOverwrite flag. */ if ($overwrite === null || !\is_bool($overwrite)) { $overwrite = $this->overwrite; } /* * $files must be an array of filenames. Verify that it is an array with * at least one file to copy. */ if (\is_array($files) && \count($files) > 0) { foreach ($files as $file) { // Get the source and destination paths $filesource = Path::clean($file['src']); $filedest = Path::clean($file['dest']); $filetype = \array_key_exists('type', $file) ? $file['type'] : 'file'; if (!file_exists($filesource)) { /* * The source file does not exist. Nothing to copy so set an error * and return false. */ Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_NO_FILE', $filesource), Log::WARNING, 'jerror'); return false; } elseif (($exists = file_exists($filedest)) && !$overwrite) { // It's okay if the manifest already exists if ($this->getPath('manifest') === $filesource) { continue; } // The destination file already exists and the overwrite flag is false. // Set an error and return false. Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FILE_EXISTS', $filedest), Log::WARNING, 'jerror'); return false; } else { // Copy the folder or file to the new location. if ($filetype === 'folder') { if (!Folder::copy($filesource, $filedest, null, $overwrite)) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER', $filesource, $filedest), Log::WARNING, 'jerror'); return false; } $step = ['type' => 'folder', 'path' => $filedest]; } else { if (!File::copy($filesource, $filedest, null)) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FILE', $filesource, $filedest), Log::WARNING, 'jerror'); // In 3.2, TinyMCE language handling changed. Display a special notice in case an older language pack is installed. if (strpos($filedest, 'media/editors/tinymce/jscripts/tiny_mce/langs')) { Log::add(Text::_('JLIB_INSTALLER_NOT_ERROR'), Log::WARNING, 'jerror'); } return false; } $step = ['type' => 'file', 'path' => $filedest]; } /* * Since we copied a file/folder, we want to add it to the installation step stack so that * in case we have to roll back the installation we can remove the files copied. */ if (!$exists) { $this->stepStack[] = $step; } } } } else { // The $files variable was either not an array or an empty array return false; } return \count($files); } /** * Method to parse through a files element of the installation manifest and remove * the files that were installed * * @param object $element The XML node to process * @param integer $cid Application ID of application to remove from * * @return boolean True on success * * @since 3.1 */ public function removeFiles($element, $cid = 0) { if (!$element || !\count($element->children())) { // Either the tag does not exist or has no children therefore we return zero files processed. return true; } $retval = true; // Get the client info if we're using a specific client if ($cid > -1) { $client = ApplicationHelper::getClientInfo($cid); } else { $client = null; } // Get the array of file nodes to process $files = $element->children(); if (\count($files) === 0) { // No files to process return true; } $folder = ''; /* * Here we set the folder we are going to remove the files from. There are a few * special cases that need to be considered for certain reserved tags. */ switch ($element->getName()) { case 'media': if ((string) $element->attributes()->destination) { $folder = (string) $element->attributes()->destination; } else { $folder = ''; } $source = $client->path . '/media/' . $folder; break; case 'languages': $lang_client = (string) $element->attributes()->client; if ($lang_client) { $client = ApplicationHelper::getClientInfo($lang_client, true); $source = $client->path . '/language'; } else { if ($client) { $source = $client->path . '/language'; } else { $source = ''; } } break; default: if ($client) { $pathname = 'extension_' . $client->name; $source = $this->getPath($pathname); } else { $pathname = 'extension_root'; $source = $this->getPath($pathname); } break; } // Process each file in the $files array (children of $tagName). foreach ($files as $file) { /* * If the file is a language, we must handle it differently. Language files * go in a subdirectory based on the language code, ie. * <language tag="en_US">en_US.mycomponent.ini</language> * would go in the en_US subdirectory of the languages directory. */ if ($file->getName() === 'language' && (string) $file->attributes()->tag !== '') { if ($source) { $path = $source . '/' . $file->attributes()->tag . '/' . basename((string) $file); } else { $target_client = ApplicationHelper::getClientInfo((string) $file->attributes()->client, true); $path = $target_client->path . '/language/' . $file->attributes()->tag . '/' . basename((string) $file); } // If the language folder is not present, then the core pack hasn't been installed... ignore if (!Folder::exists(\dirname($path))) { continue; } } else { $path = $source . '/' . $file; } // Actually delete the files/folders if (is_dir($path)) { $val = Folder::delete($path); } else { $val = File::delete($path); } if ($val === false) { Log::add('Failed to delete ' . $path, Log::WARNING, 'jerror'); $retval = false; } } if (!empty($folder)) { Folder::delete($source); } return $retval; } /** * Copies the installation manifest file to the extension folder in the given client * * @param integer $cid Where to copy the installfile [optional: defaults to 1 (admin)] * * @return boolean True on success, False on error * * @since 3.1 */ public function copyManifest($cid = 1) { // Get the client info $client = ApplicationHelper::getClientInfo($cid); $path = ['src' => $this->getPath('manifest')]; if ($client) { $pathname = 'extension_' . $client->name; $path['dest'] = $this->getPath($pathname) . '/' . basename($this->getPath('manifest')); } else { $pathname = 'extension_root'; $path['dest'] = $this->getPath($pathname) . '/' . basename($this->getPath('manifest')); } return $this->copyFiles([$path], true); } /** * Tries to find the package manifest file * * @return boolean True on success, False on error * * @since 3.1 */ public function findManifest() { // Do nothing if folder does not exist for some reason if (!Folder::exists($this->getPath('source'))) { return false; } // Main folder manifests (higher priority) $parentXmlfiles = Folder::files($this->getPath('source'), '.xml$', false, true); // Search for children manifests (lower priority) $allXmlFiles = Folder::files($this->getPath('source'), '.xml$', 1, true); // Create an unique array of files ordered by priority $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); // If at least one XML file exists if (!empty($xmlfiles)) { foreach ($xmlfiles as $file) { // Is it a valid Joomla installation manifest file? $manifest = $this->isManifest($file); if ($manifest !== null) { // If the root method attribute is set to upgrade, allow file overwrite if ((string) $manifest->attributes()->method === 'upgrade') { $this->upgrade = true; $this->overwrite = true; } // If the overwrite option is set, allow file overwriting if ((string) $manifest->attributes()->overwrite === 'true') { $this->overwrite = true; } // Set the manifest object and path $this->manifest = $manifest; $this->setPath('manifest', $file); // Set the installation source path to that of the manifest file $this->setPath('source', \dirname($file)); return true; } } // None of the XML files found were valid install files Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); return false; } else { // No XML files were found in the install folder Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); return false; } } /** * Is the XML file a valid Joomla installation manifest file. * * @param string $file An xmlfile path to check * * @return \SimpleXMLElement|null A \SimpleXMLElement, or null if the file failed to parse * * @since 3.1 */ public function isManifest($file) { $xml = simplexml_load_file($file); // If we cannot load the XML file return null if (!$xml) { return; } // Check for a valid XML root tag. if ($xml->getName() !== 'extension') { return; } // Valid manifest file return the object return $xml; } /** * Generates a manifest cache * * @return string serialised manifest data * * @since 3.1 */ public function generateManifestCache() { return json_encode(self::parseXMLInstallFile($this->getPath('manifest'))); } /** * Cleans up discovered extensions if they're being installed some other way * * @param string $type The type of extension (component, etc) * @param string $element Unique element identifier (e.g. com_content) * @param string $folder The folder of the extension (plugins; e.g. system) * @param integer $client The client application (administrator or site) * * @return object Result of query * * @since 3.1 */ public function cleanDiscoveredExtension($type, $element, $folder = '', $client = 0) { $db = $this->getDatabase(); $query = $db->getQuery(true) ->delete($db->quoteName('#__extensions')) ->where('type = :type') ->where('element = :element') ->where('folder = :folder') ->where('client_id = :client_id') ->where('state = -1') ->bind(':type', $type) ->bind(':element', $element) ->bind(':folder', $folder) ->bind(':client_id', $client, ParameterType::INTEGER); $db->setQuery($query); return $db->execute(); } /** * Compares two "files" entries to find deleted files/folders * * @param array $oldFiles An array of \SimpleXMLElement objects that are the old files * @param array $newFiles An array of \SimpleXMLElement objects that are the new files * * @return array An array with the delete files and folders in findDeletedFiles[files] and findDeletedFiles[folders] respectively * * @since 3.1 */ public function findDeletedFiles($oldFiles, $newFiles) { // The magic find deleted files function! // The files that are new $files = []; // The folders that are new $folders = []; // The folders of the files that are new $containers = []; // A list of files to delete $files_deleted = []; // A list of folders to delete $folders_deleted = []; foreach ($newFiles as $file) { switch ($file->getName()) { case 'folder': // Add any folders to the list $folders[] = (string) $file; break; case 'file': default: // Add any files to the list $files[] = (string) $file; // Now handle the folder part of the file to ensure we get any containers // Break up the parts of the directory $container_parts = explode('/', \dirname((string) $file)); // Make sure this is clean and empty $container = ''; foreach ($container_parts as $part) { // Iterate through each part // Add a slash if its not empty if (!empty($container)) { $container .= '/'; } // Append the folder part $container .= $part; if (!\in_array($container, $containers)) { // Add the container if it doesn't already exist $containers[] = $container; } } break; } } foreach ($oldFiles as $file) { switch ($file->getName()) { case 'folder': if (!\in_array((string) $file, $folders)) { // See whether the folder exists in the new list if (!\in_array((string) $file, $containers)) { // Check if the folder exists as a container in the new list // If it's not in the new list or a container then delete it $folders_deleted[] = (string) $file; } } break; case 'file': default: if (!\in_array((string) $file, $files)) { // Look if the file exists in the new list if (!\in_array(\dirname((string) $file), $folders)) { // Look if the file is now potentially in a folder $files_deleted[] = (string) $file; } } break; } } return ['files' => $files_deleted, 'folders' => $folders_deleted]; } /** * Loads an MD5SUMS file into an associative array * * @param string $filename Filename to load * * @return array Associative array with filenames as the index and the MD5 as the value * * @since 3.1 */ public function loadMD5Sum($filename) { if (!file_exists($filename)) { // Bail if the file doesn't exist return false; } $data = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $retval = []; foreach ($data as $row) { // Split up the data $results = explode(' ', $row); // Cull any potential prefix $results[1] = str_replace('./', '', $results[1]); // Throw into the array $retval[$results[1]] = $results[0]; } return $retval; } /** * Parse a XML install manifest file. * * XML Root tag should be 'install' except for languages which use meta file. * * @param string $path Full path to XML file. * * @return array XML metadata. * * @since 3.0.0 */ public static function parseXMLInstallFile($path) { // Check if xml file exists. if (!file_exists($path)) { return false; } // Read the file to see if it's a valid component XML file $xml = simplexml_load_file($path); if (!$xml) { return false; } // Check for a valid XML root tag. // Extensions use 'extension' as the root tag. Languages use 'metafile' instead $name = $xml->getName(); if ($name !== 'extension' && $name !== 'metafile') { unset($xml); return false; } $data = []; $data['name'] = (string) $xml->name; // Check if we're a language. If so use metafile. $data['type'] = $xml->getName() === 'metafile' ? 'language' : (string) $xml->attributes()->type; $data['creationDate'] = ((string) $xml->creationDate) ?: Text::_('JLIB_UNKNOWN'); $data['author'] = ((string) $xml->author) ?: Text::_('JLIB_UNKNOWN'); $data['copyright'] = (string) $xml->copyright; $data['authorEmail'] = (string) $xml->authorEmail; $data['authorUrl'] = (string) $xml->authorUrl; $data['version'] = (string) $xml->version; $data['description'] = (string) $xml->description; $data['group'] = (string) $xml->group; // Child template specific fields. if (isset($xml->inheritable)) { $data['inheritable'] = (string) $xml->inheritable === '0' ? false : true; } // Child template specific fields. if (isset($xml->namespace) && (string) $xml->namespace !== '') { $data['namespace'] = (string) $xml->namespace; } if (isset($xml->parent) && (string) $xml->parent !== '') { $data['parent'] = (string) $xml->parent; } if ($xml->files && \count($xml->files->children())) { $filename = basename($path); $data['filename'] = File::stripExt($filename); foreach ($xml->files->children() as $oneFile) { if ((string) $oneFile->attributes()->plugin) { $data['filename'] = (string) $oneFile->attributes()->plugin; break; } } } return $data; } /** * Gets a list of available install adapters. * * @param array $options An array of options to inject into the adapter * @param array $custom Array of custom install adapters * * @return string[] An array of the class names of available install adapters. * * @since 3.4 */ public function getAdapters($options = [], array $custom = []) { $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); $adapters = []; // Process the core adapters foreach ($files as $file) { $fileName = $file->getFilename(); // Only load for php files. if (!$file->isFile() || $file->getExtension() !== 'php') { continue; } // Derive the class name from the filename. $name = str_ireplace('.php', '', trim($fileName)); $name = str_ireplace('adapter', '', trim($name)); $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; if (!class_exists($class)) { // Not namespaced $class = $this->_classprefix . ucfirst($name); } // Core adapters should autoload based on classname, keep this fallback just in case if (!class_exists($class)) { // Try to load the adapter object \JLoader::register($class, $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName); if (!class_exists($class)) { // Skip to next one continue; } } $adapters[] = $name; } // Add any custom adapters if specified if (\count($custom) >= 1) { foreach ($custom as $adapter) { // Setup the class name // TODO - Can we abstract this to not depend on the Joomla class namespace without PHP namespaces? $class = $this->_classprefix . ucfirst(trim($adapter)); // If the class doesn't exist we have nothing left to do but look at the next type. We did our best. if (!class_exists($class)) { continue; } $adapters[] = str_ireplace('.php', '', $fileName); } } return $adapters; } /** * Method to load an adapter instance * * @param string $adapter Adapter name * @param array $options Adapter options * * @return InstallerAdapter * * @since 3.4 * @throws \InvalidArgumentException */ public function loadAdapter($adapter, $options = []) { $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($adapter) . 'Adapter'; if (!class_exists($class)) { // Not namespaced $class = $this->_classprefix . ucfirst($adapter); } if (!class_exists($class)) { throw new \InvalidArgumentException(sprintf('The %s install adapter does not exist.', $adapter)); } // Ensure the adapter type is part of the options array $options['type'] = $adapter; // Check for a possible service from the container otherwise manually instantiate the class if (Factory::getContainer()->has($class)) { return Factory::getContainer()->get($class); } $adapter = new $class($this, $this->getDatabase(), $options); if ($adapter instanceof ContainerAwareInterface) { $adapter->setContainer(Factory::getContainer()); } return $adapter; } } Manifest/LibraryManifest.php 0000644 00000005123 15172720021 0012114 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Manifest; use Joomla\CMS\Installer\Manifest; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Library Manifest File * * @since 3.1 */ class LibraryManifest extends Manifest { /** * Creation date of the library * * @var string * @since 4.3.0 */ public $creationdate; /** * File system name of the library * * @var string * @since 3.1 */ public $libraryname = ''; /** * Creation Date of the library * * @var string * @since 3.1 */ public $creationDate = ''; /** * Copyright notice for the library * * @var string * @since 3.1 */ public $copyright = ''; /** * License for the library * * @var string * @since 3.1 */ public $license = ''; /** * Author for the library * * @var string * @since 3.1 */ public $author = ''; /** * Author email for the library * * @var string * @since 3.1 */ public $authoremail = ''; /** * Author URL for the library * * @var string * @since 3.1 */ public $authorurl = ''; /** * Apply manifest data from a \SimpleXMLElement to the object. * * @param \SimpleXMLElement $xml Data to load * * @return void * * @since 3.1 */ protected function loadManifestFromData(\SimpleXMLElement $xml) { $this->name = (string) $xml->name; $this->libraryname = (string) $xml->libraryname; $this->version = (string) $xml->version; $this->description = (string) $xml->description; $this->creationdate = (string) $xml->creationDate; $this->author = (string) $xml->author; $this->authoremail = (string) $xml->authorEmail; $this->authorurl = (string) $xml->authorUrl; $this->packager = (string) $xml->packager; $this->packagerurl = (string) $xml->packagerurl; $this->update = (string) $xml->update; if (isset($xml->files) && isset($xml->files->file) && \count($xml->files->file)) { foreach ($xml->files->file as $file) { $this->filelist[] = (string) $file; } } } } Manifest/PackageManifest.php 0000644 00000006351 15172720021 0012047 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Manifest; use Joomla\CMS\Installer\InstallerExtension; use Joomla\CMS\Installer\Manifest; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Joomla! Package Manifest File * * @since 3.1 */ class PackageManifest extends Manifest { /** * Author url of the package * * @var string * @since 4.3.0 */ public $authorurl; /** * Author of the package * * @var string * @since 4.3.0 */ public $author; /** * Author email of the package * * @var string * @since 4.3.0 */ public $authoremail; /** * Unique name of the package * * @var string * @since 3.1 */ public $packagename = ''; /** * Website for the package * * @var string * @since 3.1 */ public $url = ''; /** * Scriptfile for the package * * @var string * @since 3.1 */ public $scriptfile = ''; /** * Flag if the package blocks individual child extensions from being uninstalled * * @var boolean * @since 3.7.0 */ public $blockChildUninstall = false; /** * Apply manifest data from a \SimpleXMLElement to the object. * * @param \SimpleXMLElement $xml Data to load * * @return void * * @since 3.1 */ protected function loadManifestFromData(\SimpleXMLElement $xml) { $this->name = (string) $xml->name; $this->packagename = (string) $xml->packagename; $this->update = (string) $xml->update; $this->authorurl = (string) $xml->authorUrl; $this->author = (string) $xml->author; $this->authoremail = (string) $xml->authorEmail; $this->description = (string) $xml->description; $this->packager = (string) $xml->packager; $this->packagerurl = (string) $xml->packagerurl; $this->scriptfile = (string) $xml->scriptfile; $this->version = (string) $xml->version; if (isset($xml->blockChildUninstall)) { $value = (string) $xml->blockChildUninstall; if ($value === '1' || $value === 'true') { $this->blockChildUninstall = true; } } if (isset($xml->files->file) && \count($xml->files->file)) { foreach ($xml->files->file as $file) { // NOTE: JInstallerExtension doesn't expect a string. // DO NOT CAST $file $this->filelist[] = new InstallerExtension($file); } } // Handle cases where package contains folders if (isset($xml->files->folder) && \count($xml->files->folder)) { foreach ($xml->files->folder as $folder) { // NOTE: JInstallerExtension doesn't expect a string. // DO NOT CAST $folder $this->filelist[] = new InstallerExtension($folder); } } } } LegacyInstallerScript.php 0000644 00000012606 15172720021 0011526 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\CMS\Factory; use Joomla\Database\DatabaseAwareInterface; use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\Exception\DatabaseNotFoundException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Legacy installer script which delegates the methods to the internal instance when possible. * * @since 4.2.0 */ class LegacyInstallerScript implements InstallerScriptInterface, DatabaseAwareInterface { use DatabaseAwareTrait; /** * @var \stdClass * @since 4.2.0 */ private $installerScript; /** * @param \stdClass $installerScript The script instance */ public function __construct($installerScript) { $this->installerScript = $installerScript; } /** * Function called after the extension is installed. * * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function install(InstallerAdapter $adapter): bool { return $this->callOnScript('install', [$adapter]); } /** * Function called after the extension is updated. * * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function update(InstallerAdapter $adapter): bool { return $this->callOnScript('update', [$adapter]); } /** * Function called after the extension is uninstalled. * * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function uninstall(InstallerAdapter $adapter): bool { return $this->callOnScript('uninstall', [$adapter]); } /** * Function called before extension installation/update/removal procedure commences. * * @param string $type The type of change (install or discover_install, update, uninstall) * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function preflight(string $type, InstallerAdapter $adapter): bool { return $this->callOnScript('preflight', [$type, $adapter]); } /** * Function called after extension installation/update/removal procedure commences. * * @param string $type The type of change (install or discover_install, update, uninstall) * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function postflight(string $type, InstallerAdapter $adapter): bool { return $this->callOnScript('postflight', [$type, $adapter]); } /** * Sets the variable to the internal script. * * @param string $name The name of the variable * @param mixed $value The value of the variable * * @return void * * @since 4.2.0 */ public function __set(string $name, $value) { $this->installerScript->$name = $value; } /** * Returns the variable from the internal script. * * @param string $name The name of the variable * * @return mixed * * @since 4.2.0 */ public function __get(string $name) { return $this->installerScript->$name; } /** * Calls the function with the given name on the internal script with * the given name and arguments. * * @param string $name The name of the function * @param array $arguments The arguments * * @return mixed * * @since 4.2.0 */ public function __call(string $name, array $arguments) { return call_user_func_array([$this->installerScript, $name], $arguments); } /** * Calls the function with the given name on the internal script with * some condition checking. * * @param string $name The name of the function * @param array $arguments The arguments * * @return bool * * @since 4.2.0 */ private function callOnScript(string $name, array $arguments): bool { if (!method_exists($this->installerScript, $name)) { return true; } if ($this->installerScript instanceof DatabaseAwareInterface) { try { $this->installerScript->setDatabase($this->getDatabase()); } catch (DatabaseNotFoundException $e) { @trigger_error(sprintf('Database must be set, this will not be caught anymore in 6.0 in %s.', __METHOD__), E_USER_DEPRECATED); $this->installerScript->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); } } $return = $this->__call($name, $arguments); // When function doesn't have a return value, assume it succeeded if ($return === null) { return true; } return (bool) $return; } } InstallerAdapter.php 0000644 00000113376 15172720021 0010523 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Installer\Manifest\PackageManifest; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Extension; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\TableInterface; use Joomla\Database\DatabaseAwareInterface; use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseInterface; use Joomla\DI\Container; use Joomla\DI\ContainerAwareInterface; use Joomla\DI\ContainerAwareTrait; use Joomla\DI\Exception\ContainerNotFoundException; use Joomla\DI\ServiceProviderInterface; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Abstract adapter for the installer. * * @since 3.4 */ abstract class InstallerAdapter implements ContainerAwareInterface, DatabaseAwareInterface { use ContainerAwareTrait; use DatabaseAwareTrait; /** * Changelog URL of extensions * * @var string * @since 4.0.0 * */ protected $changelogurl = null; /** * ID for the currently installed extension if present * * @var integer * @since 3.4 */ protected $currentExtensionId = null; /** * The unique identifier for the extension (e.g. mod_login) * * @var string * @since 3.4 * */ protected $element = null; /** * Extension object. * * @var Extension * @since 3.4 * */ protected $extension = null; /** * Messages rendered by custom scripts * * @var string * @since 3.4 */ protected $extensionMessage = ''; /** * Copy of the XML manifest file. * * Making this object public allows extensions to customize the manifest in custom scripts. * * @var \SimpleXMLElement * @since 3.4 */ public $manifest = null; /** * A path to the PHP file that the scriptfile declaration in the manifest refers to. * * @var string * @since 3.4 */ protected $manifest_script = null; /** * Name of the extension * * @var string * @since 3.4 */ protected $name = null; /** * Installer used with this adapter * * @var Installer * @since 4.0.0 */ protected $parent = null; /** * Install function routing * * @var string * @since 3.4 */ protected $route = 'install'; /** * Flag if the adapter supports discover installs * * Adapters should override this and set to false if discover install is unsupported * * @var boolean * @since 3.4 */ protected $supportsDiscoverInstall = true; /** * The type of adapter in use * * @var string * @since 3.4 */ protected $type; /** * Constructor * * @param Installer $parent Parent object * @param DatabaseDriver $db Database object * @param array $options Configuration Options * * @since 3.4 */ public function __construct(Installer $parent, DatabaseDriver $db, array $options = []) { $this->parent = $parent; $this->setDatabase($db); foreach ($options as $key => $value) { if (property_exists($this, $key)) { $this->$key = $value; } } // Get a generic TableExtension instance for use if not already loaded if (!($this->extension instanceof TableInterface)) { $this->extension = Table::getInstance('extension'); } // Sanity check, make sure the type is set by taking the adapter name from the class name if (!$this->type) { // This assumes the adapter short class name in its namespace is `<foo>Adapter`, replace this logic in subclasses if needed $reflection = new \ReflectionClass(\get_called_class()); $this->type = str_replace('Adapter', '', $reflection->getShortName()); } // Extension type is stored as lowercase in the database $this->type = strtolower($this->type); } /** * Check if a package extension allows its child extensions to be uninstalled individually * * @param integer $packageId The extension ID of the package to check * * @return boolean * * @since 3.7.0 * @note This method defaults to true to emulate the behavior of 3.6 and earlier which did not support this lookup */ protected function canUninstallPackageChild($packageId) { $package = Table::getInstance('extension'); // If we can't load this package ID, we have a corrupt database if (!$package->load((int) $packageId)) { return true; } $manifestFile = JPATH_MANIFESTS . '/packages/' . $package->element . '.xml'; $xml = $this->parent->isManifest($manifestFile); // If the manifest doesn't exist, we've got some major issues if (!$xml) { return true; } $manifest = new PackageManifest($manifestFile); return $manifest->blockChildUninstall === false; } /** * Method to check if the extension is already present in the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExistingExtension() { try { $this->currentExtensionId = $this->extension->find( ['element' => $this->element, 'type' => $this->type] ); // If it does exist, load it if ($this->currentExtensionId) { $this->extension->load(['element' => $this->element, 'type' => $this->type]); } } catch (\RuntimeException $e) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $e->getMessage() ), $e->getCode(), $e ); } } /** * Method to check if the extension is present in the filesystem, flags the route as update if so * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExtensionInFilesystem() { if (file_exists($this->parent->getPath('extension_root')) && (!$this->parent->isOverwrite() || $this->parent->isUpgrade())) { // Look for an update function or update tag $updateElement = $this->getManifest()->update; // Upgrade manually set or update function available or update tag detected if ( $updateElement || $this->parent->isUpgrade() || ($this->parent->manifestClass && method_exists($this->parent->manifestClass, 'update')) ) { // Force this one $this->parent->setOverwrite(true); $this->parent->setUpgrade(true); if ($this->currentExtensionId) { // If there is a matching extension mark this as an update $this->setRoute('update'); } } elseif (!$this->parent->isOverwrite()) { // We didn't have overwrite set, find an update function or find an update tag so lets call it safe throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_DIRECTORY', Text::_('JLIB_INSTALLER_' . $this->route), $this->type, $this->parent->getPath('extension_root') ) ); } } } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ abstract protected function copyBaseFiles(); /** * Method to create the extension root path if necessary * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function createExtensionRoot() { // If the extension directory does not exist, lets create it $created = false; if (!file_exists($this->parent->getPath('extension_root'))) { if (!$created = Folder::create($this->parent->getPath('extension_root'))) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_' . $this->route), $this->parent->getPath('extension_root') ) ); } } /* * Since we created the extension directory and will want to remove it if * we have to roll back the installation, let's add it to the * installation step stack */ if ($created) { $this->parent->pushStep( [ 'type' => 'folder', 'path' => $this->parent->getPath('extension_root'), ] ); } } /** * Generic discover_install method for extensions * * @return boolean True on success * * @since 3.4 */ public function discover_install() { // Get the extension's description $description = (string) $this->getManifest()->description; if ($description) { $this->parent->message = Text::_($description); } else { $this->parent->message = ''; } // Set the extension's name and element $this->name = $this->getName(); $this->element = $this->getElement(); /* * --------------------------------------------------------------------------------------------- * Extension Precheck and Setup Section * --------------------------------------------------------------------------------------------- */ // Setup the install paths and perform other prechecks as necessary try { $this->setupInstallPaths(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } /* * --------------------------------------------------------------------------------------------- * Installer Trigger Loading * --------------------------------------------------------------------------------------------- */ $this->setupScriptfile(); try { $this->triggerManifestScript('preflight'); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } /* * --------------------------------------------------------------------------------------------- * Database Processing Section * --------------------------------------------------------------------------------------------- */ try { $this->storeExtension(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } try { $this->parseQueries(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // Run the custom install method try { $this->triggerManifestScript('install'); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } /* * --------------------------------------------------------------------------------------------- * Finalization and Cleanup Section * --------------------------------------------------------------------------------------------- */ try { $this->finaliseInstall(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // And now we run the postflight try { $this->triggerManifestScript('postflight'); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } return $this->extension->extension_id; } /** * Method to handle database transactions for the installer * * @return boolean True on success * * @since 3.4 * @throws \RuntimeException */ protected function doDatabaseTransactions() { $route = $this->route === 'discover_install' ? 'install' : $this->route; // Let's run the install queries for the component if (isset($this->getManifest()->{$route}->sql)) { $result = $this->parent->parseSQLFiles($this->getManifest()->{$route}->sql); if ($result === false) { // Only rollback if installing if ($route === 'install') { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_INSTALL_ABORTED')); } return false; } // If installing with success and there is an uninstall script, add an installer rollback step to rollback if needed if ($route === 'install' && isset($this->getManifest()->uninstall->sql)) { $this->parent->pushStep(['type' => 'query', 'script' => $this->getManifest()->uninstall->sql]); } } return true; } /** * Load language files * * @param string $extension The name of the extension * @param string $source Path to the extension * @param string $base Base path for the extension language * * @return void * * @since 3.4 */ protected function doLoadLanguage($extension, $source, $base = JPATH_ADMINISTRATOR) { $lang = Factory::getLanguage(); $lang->load($extension . '.sys', $source) || $lang->load($extension . '.sys', $base); } /** * Method to finalise the installation processing * * @return void * * @since 4.0.0 * @throws \RuntimeException */ abstract protected function finaliseInstall(); /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ abstract protected function finaliseUninstall(): bool; /** * Checks if the adapter supports discover_install * * @return boolean * * @since 3.4 */ public function getDiscoverInstallSupported() { return $this->supportsDiscoverInstall; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string The filtered element * * @since 3.4 */ public function getElement($element = null) { if (!$element) { // Ensure the element is a string $element = (string) $this->getManifest()->element; } if (!$element) { $element = $this->getName(); } // Filter the name for illegal characters return strtolower(InputFilter::getInstance()->clean($element, 'cmd')); } /** * Get the manifest object. * * @return \SimpleXMLElement Manifest object * * @since 3.4 */ public function getManifest() { return $this->manifest; } /** * Get the filtered component name from the manifest * * @return string The filtered name * * @since 3.4 */ public function getName() { // Ensure the name is a string $name = (string) $this->getManifest()->name; // Filter the name for illegal characters $name = InputFilter::getInstance()->clean($name, 'string'); return $name; } /** * Retrieves the parent installer * * @return Installer * * @since 4.0.0 */ public function getParent() { return $this->parent; } /** * Get the install route being followed * * @return string The install route * * @since 3.4 */ public function getRoute() { return $this->route; } /** * Get the class name for the install adapter script. * * @return string The class name. * * @since 3.4 */ protected function getScriptClassName() { // Support element names like 'en-GB' $className = InputFilter::getInstance()->clean($this->element, 'cmd') . 'InstallerScript'; // Cannot have - in class names $className = str_replace('-', '', $className); return $className; } /** * Generic install method for extensions * * @return boolean|integer The extension ID on success, boolean false on failure * * @since 3.4 */ public function install() { // Get the extension's description $description = (string) $this->getManifest()->description; $this->parent->message = ''; if ($description) { $this->parent->message = Text::_($description); } // Set the extension's name and element $this->name = $this->getName(); $this->element = $this->getElement(); $this->changelogurl = (string) $this->getManifest()->changelogurl; /* * --------------------------------------------------------------------------------------------- * Extension Precheck and Setup Section * --------------------------------------------------------------------------------------------- */ // Setup the install paths and perform other prechecks as necessary try { $this->setupInstallPaths(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // Check to see if an extension by the same name is already installed. try { $this->checkExistingExtension(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // Check if the extension is present in the filesystem try { $this->checkExtensionInFilesystem(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // If we are on the update route, run any custom setup routines if ($this->route === 'update') { try { $this->setupUpdates(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } } /* * --------------------------------------------------------------------------------------------- * Installer Trigger Loading * --------------------------------------------------------------------------------------------- */ $this->setupScriptfile(); try { $this->triggerManifestScript('preflight'); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } /* * --------------------------------------------------------------------------------------------- * Filesystem Processing Section * --------------------------------------------------------------------------------------------- */ // If the extension directory does not exist, lets create it try { $this->createExtensionRoot(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // Copy all necessary files try { $this->copyBaseFiles(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // Parse optional tags $this->parseOptionalTags(); /* * --------------------------------------------------------------------------------------------- * Database Processing Section * --------------------------------------------------------------------------------------------- */ try { $this->storeExtension(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } try { $this->parseQueries(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // Run the custom method based on the route try { $this->triggerManifestScript($this->route); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } /* * --------------------------------------------------------------------------------------------- * Finalization and Cleanup Section * --------------------------------------------------------------------------------------------- */ try { $this->finaliseInstall(); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } // And now we run the postflight try { $this->triggerManifestScript('postflight'); } catch (\RuntimeException $e) { // Install failed, roll back changes $this->parent->abort($e->getMessage()); return false; } return $this->extension->extension_id; } /** * Method to parse the queries specified in the `<sql>` tags * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function parseQueries() { // Let's run the queries for the extension if (\in_array($this->route, ['install', 'discover_install', 'uninstall'])) { // This method may throw an exception, but it is caught by the parent caller if (!$this->doDatabaseTransactions()) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_INSTALL_ABORTED')); } // Set the schema version to be the latest update version if ($this->getManifest()->update) { $this->parent->setSchemaVersion($this->getManifest()->update->schemas, $this->extension->extension_id); } } elseif ($this->route === 'update') { if ($this->getManifest()->update) { $result = $this->parent->parseSchemaUpdates($this->getManifest()->update->schemas, $this->extension->extension_id); if ($result === false) { // Install failed, rollback changes throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_INSTALL_ABORTED')); } } } } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.1 */ protected function parseOptionalTags() { // Some extensions may not have optional tags } /** * Prepares the adapter for a discover_install task * * @return void * * @since 3.4 */ public function prepareDiscoverInstall() { // Adapters may not support discover install or may have overridden the default task and aren't using this } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ abstract protected function removeExtensionFiles(); /** * Set the manifest object. * * @param object $manifest The manifest object * * @return InstallerAdapter Instance of this class to support chaining * * @since 3.4 */ public function setManifest($manifest) { $this->manifest = $manifest; return $this; } /** * Set the install route being followed * * @param string $route The install route being followed * * @return InstallerAdapter Instance of this class to support chaining * * @since 3.4 */ public function setRoute($route) { $this->route = $route; return $this; } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 */ abstract protected function setupInstallPaths(); /** * Setup the manifest script file for those adapters that use it. * * @return void * * @since 3.4 */ protected function setupScriptfile() { // If there is a manifest class file, lets load it; we'll copy it later (don't have dest yet) $manifestScript = (string) $this->getManifest()->scriptfile; // When no script file, do nothing if (!$manifestScript) { return; } // Build a child container, so we do not overwrite the global one // and start from scratch when multiple extensions are installed try { $container = new Container($this->getContainer()); } catch (ContainerNotFoundException $e) { @trigger_error('Container must be set.', E_USER_DEPRECATED); // Fallback to the global container $container = new Container(Factory::getContainer()); } // The real location of the file $manifestScriptFile = $this->parent->getPath('source') . '/' . $manifestScript; // Load the installer from the file if (!file_exists($manifestScriptFile)) { @trigger_error( 'Installer file must exist when defined. In version 5.0 this will crash.', E_USER_DEPRECATED ); return; } $installer = require_once $manifestScriptFile; // When the instance is a service provider, then register the container with it if ($installer instanceof ServiceProviderInterface) { $installer->register($container); } // When the returned object is an installer instance, use it directly if ($installer instanceof InstallerScriptInterface) { $container->set(InstallerScriptInterface::class, $installer); } // When none is set, then use the legacy way if (!$container->has(InstallerScriptInterface::class)) { @trigger_error( 'Legacy installer files are deprecated and will be removed in 6.0. Use a service provider instead.', E_USER_DEPRECATED ); $classname = $this->getScriptClassName(); \JLoader::register($classname, $manifestScriptFile); if (!class_exists($classname)) { return; } $container->set( InstallerScriptInterface::class, function (Container $container) use ($classname) { return new LegacyInstallerScript(new $classname($this)); } ); } // Create a new instance $this->parent->manifestClass = $container->get(InstallerScriptInterface::class); // Set the database if ($this->parent->manifestClass instanceof DatabaseAwareInterface) { $this->parent->manifestClass->setDatabase($container->get(DatabaseInterface::class)); } // And set this so we can copy it later $this->manifest_script = $manifestScript; } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ abstract protected function setupUninstall(); /** * Method to setup the update routine for the adapter * * @return void * * @since 3.4 */ protected function setupUpdates() { // Some extensions may not have custom setup routines for updates } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ abstract protected function storeExtension(); /** * Executes a custom install script method * * @param string $method The install method to execute * * @return boolean True on success * * @since 3.4 * @throws \RuntimeException */ protected function triggerManifestScript($method) { ob_start(); ob_implicit_flush(false); if ($this->parent->manifestClass && method_exists($this->parent->manifestClass, $method)) { switch ($method) { // The preflight and postflight take the route as a param case 'preflight': case 'postflight': if ($this->parent->manifestClass->$method($this->route, $this) === false) { if ($method !== 'postflight') { // Clean and close the output buffer ob_end_clean(); // The script failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } } break; // The install, uninstall, and update methods only pass this object as a param case 'install': case 'uninstall': case 'update': if ($this->parent->manifestClass->$method($this) === false) { if ($method !== 'uninstall') { // Clean and close the output buffer ob_end_clean(); // The script failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } } break; } } // Append to the message object $this->extensionMessage .= ob_get_clean(); // If in postflight or uninstall, set the message for display if (($method === 'uninstall' || $method === 'postflight') && $this->extensionMessage !== '') { $this->parent->set('extension_message', $this->extensionMessage); } return true; } /** * Generic update method for extensions * * @param integer $id The extension ID * * @return boolean True on success * * @since 4.0.0 */ public function uninstall($id) { if (!$this->extension->load((int) $id)) { Log::add(Text::_('JLIB_INSTALLER_ERROR_UNKNOWN_EXTENSION'), Log::WARNING, 'jerror'); return false; } // Joomla 4: Locked extensions cannot be removed. if (isset($this->extension->locked) && $this->extension->locked) { Log::add(Text::_('JLIB_INSTALLER_ERROR_UNINSTALL_LOCKED_EXTENSION'), Log::WARNING, 'jerror'); return false; } elseif (!isset($this->extension->locked) && $this->extension->protected) { // Joomla 3 ('locked' property does not exist yet): Protected extensions cannot be removed. Log::add(Text::_('JLIB_INSTALLER_ERROR_UNINSTALL_PROTECTED_EXTENSION'), Log::WARNING, 'jerror'); return false; } /* * Does this extension have a parent package? * If so, check if the package disallows individual extensions being uninstalled if the package is not being uninstalled */ if ($this->extension->package_id && !$this->parent->isPackageUninstall() && !$this->canUninstallPackageChild($this->extension->package_id)) { Log::add( Text::sprintf('JLIB_INSTALLER_ERROR_CANNOT_UNINSTALL_CHILD_OF_PACKAGE', $this->extension->name), Log::WARNING, 'jerror' ); return false; } // Setup the uninstall job as required try { $this->setupUninstall(); } catch (\RuntimeException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); return false; } // Set the extension's name and element $this->name = $this->getName(); $this->element = $this->getElement(); /* * --------------------------------------------------------------------------------------------- * Installer Trigger Loading and Uninstall * --------------------------------------------------------------------------------------------- */ $this->setupScriptfile(); try { $this->triggerManifestScript('preflight'); } catch (\RuntimeException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); return false; } try { $this->triggerManifestScript('uninstall'); } catch (\RuntimeException $e) { // Ignore errors for now } // Tasks from here may fail but we will still attempt to finish the uninstall process $retval = true; /* * --------------------------------------------------------------------------------------------- * Database Processing Section * --------------------------------------------------------------------------------------------- */ try { $this->parseQueries(); } catch (\RuntimeException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); $retval = false; } /* * --------------------------------------------------------------------------------------------- * Filesystem Processing Section * --------------------------------------------------------------------------------------------- */ try { $this->removeExtensionFiles(); } catch (\RuntimeException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); $retval = false; } /* * --------------------------------------------------------------------------------------------- * Finalization and Cleanup Section * --------------------------------------------------------------------------------------------- */ try { $retval |= $this->finaliseUninstall(); } catch (\RuntimeException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); $retval = false; } // And now we run the postflight try { $this->triggerManifestScript('postflight'); } catch (\RuntimeException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); $retval = false; } return $retval; } /** * Generic update method for extensions * * @return boolean|integer The extension ID on success, boolean false on failure * * @since 3.4 */ public function update() { // Set the overwrite setting $this->parent->setOverwrite(true); $this->parent->setUpgrade(true); // And make sure the route is set correctly $this->setRoute('update'); // Now jump into the install method to run the update return $this->install(); } /** * Proxy for db variable. * * @param string $name The name of the element * * @return mixed The value of the element if set, null otherwise * * @since 4.2.0 * * @deprecated 4.3 will be removed in 6.0 * Use getDatabase() instead of directly accessing _db */ public function __get($name) { if ($name === 'db') { return $this->getDatabase(); } // Default the variable if (!isset($this->$name)) { $this->$name = null; } return $this->$name; } } InstallerExtension.php 0000644 00000007577 15172720021 0011124 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Object\CMSObject; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Extension object * * @since 3.1 */ class InstallerExtension extends CMSObject { /** * Client ID of the extension * * @var int * @since 4.3.0 */ public $client_id; /** * Filename of the extension * * @var string * @since 3.1 */ public $filename = ''; /** * Type of the extension * * @var string * @since 3.1 */ public $type = ''; /** * Unique Identifier for the extension * * @var string * @since 3.1 */ public $id = ''; /** * The status of the extension * * @var boolean * @since 3.1 */ public $published = false; /** * String representation of client. Valid for modules, templates and languages. * Set by default to site. * * @var string * @since 3.1 */ public $client = 'site'; /** * The group name of the plugin. Not used for other known extension types (only plugins) * * @var string * @since 3.1 */ public $group = ''; /** * An object representation of the manifest file stored metadata * * @var object * @since 3.1 */ public $manifest_cache = null; /** * An object representation of the extension params * * @var object * @since 3.1 */ public $params = null; /** * The namespace of the extension * * @var string * @since 4.0.0 */ public $namespace = null; /** * Constructor * * @param \SimpleXMLElement $element A SimpleXMLElement from which to load data from * * @since 3.1 */ public function __construct(\SimpleXMLElement $element = null) { if ($element) { $this->type = (string) $element->attributes()->type; $this->id = (string) $element->attributes()->id; switch ($this->type) { case 'component': // By default a component doesn't have anything break; case 'module': case 'template': case 'language': $this->client = (string) $element->attributes()->client; $tmp_client_id = ApplicationHelper::getClientInfo($this->client, 1); if ($tmp_client_id == null) { Log::add(Text::_('JLIB_INSTALLER_ERROR_EXTENSION_INVALID_CLIENT_IDENTIFIER'), Log::WARNING, 'jerror'); } else { $this->client_id = $tmp_client_id->id; } break; case 'plugin': $this->group = (string) $element->attributes()->group; break; default: // Catch all // Get and set client and group if we don't recognise the extension if ($element->attributes()->client) { $this->client_id = ApplicationHelper::getClientInfo($this->client, 1); $this->client_id = $this->client_id->id; } if ($element->attributes()->group) { $this->group = (string) $element->attributes()->group; } break; } $this->filename = (string) $element; } } } InstallerScriptInterface.php 0000644 00000004417 15172720021 0012223 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Base install script interface for use by extensions providing helper methods for common behaviours. * * @since 4.2.0 */ interface InstallerScriptInterface { /** * Function called after the extension is installed. * * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function install(InstallerAdapter $adapter): bool; /** * Function called after the extension is updated. * * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function update(InstallerAdapter $adapter): bool; /** * Function called after the extension is uninstalled. * * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function uninstall(InstallerAdapter $adapter): bool; /** * Function called before extension installation/update/removal procedure commences. * * @param string $type The type of change (install or discover_install, update, uninstall) * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function preflight(string $type, InstallerAdapter $adapter): bool; /** * Function called after extension installation/update/removal procedure commences. * * @param string $type The type of change (install or discover_install, update, uninstall) * @param InstallerAdapter $adapter The adapter calling this method * * @return boolean True on success * * @since 4.2.0 */ public function postflight(string $type, InstallerAdapter $adapter): bool; } InstallerHelper.php 0000644 00000026345 15172720021 0010361 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2007 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\Archive\Archive; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Updater\Update; use Joomla\CMS\Version; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Installer helper class * * @since 3.1 */ abstract class InstallerHelper { /** * Hash not validated identifier. * * @var integer * @since 3.9.0 */ public const HASH_NOT_VALIDATED = 0; /** * Hash validated identifier. * * @var integer * @since 3.9.0 */ public const HASH_VALIDATED = 1; /** * Hash not provided identifier. * * @var integer * @since 3.9.0 */ public const HASH_NOT_PROVIDED = 2; /** * Downloads a package * * @param string $url URL of file to download * @param string|bool $target Download target filename or false to get the filename from the URL * * @return string|boolean Path to downloaded package or boolean false on failure * * @since 3.1 */ public static function downloadPackage($url, $target = false) { // Capture PHP errors $track_errors = ini_get('track_errors'); ini_set('track_errors', true); // Set user agent $version = new Version(); ini_set('user_agent', $version->getUserAgent('Installer')); // Load installer plugins, and allow URL and headers modification $headers = []; PluginHelper::importPlugin('installer'); Factory::getApplication()->triggerEvent('onInstallerBeforePackageDownload', [&$url, &$headers]); try { $response = HttpFactory::getHttp()->get($url, $headers); } catch (\RuntimeException $exception) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $exception->getMessage()), Log::WARNING, 'jerror'); return false; } // Convert keys of headers to lowercase, to accommodate for case variations $headers = array_change_key_case($response->headers, CASE_LOWER); if (302 == $response->code && !empty($headers['location'])) { return self::downloadPackage($headers['location']); } elseif (200 != $response->code) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_DOWNLOAD_SERVER_CONNECT', $response->code), Log::WARNING, 'jerror'); return false; } // Parse the Content-Disposition header to get the file name if ( !empty($headers['content-disposition']) && preg_match("/\s*filename\s?=\s?(.*)/", $headers['content-disposition'][0], $parts) ) { $flds = explode(';', $parts[1]); $target = trim($flds[0], '"'); } $tmpPath = Factory::getApplication()->get('tmp_path'); // Set the target path if not given if (!$target) { $target = $tmpPath . '/' . self::getFilenameFromUrl($url); } else { $target = $tmpPath . '/' . basename($target); } // Fix Indirect Modification of Overloaded Property $body = $response->body; // Write buffer to file File::write($target, $body); // Restore error tracking to what it was before ini_set('track_errors', $track_errors); // Bump the max execution time because not using built in php zip libs are slow if (\function_exists('set_time_limit')) { set_time_limit(ini_get('max_execution_time')); } // Return the name of the downloaded package return basename($target); } /** * Unpacks a file and verifies it as a Joomla element package * Supports .gz .tar .tar.gz and .zip * * @param string $packageFilename The uploaded package filename or install directory * @param boolean $alwaysReturnArray If should return false (and leave garbage behind) or return $retval['type']=false * * @return array|boolean Array on success or boolean false on failure * * @since 3.1 */ public static function unpack($packageFilename, $alwaysReturnArray = false) { // Path to the archive $archivename = $packageFilename; // Temporary folder to extract the archive into $tmpdir = uniqid('install_'); // Clean the paths to use for archive extraction $extractdir = Path::clean(\dirname($packageFilename) . '/' . $tmpdir); $archivename = Path::clean($archivename); // Do the unpacking of the archive try { $archive = new Archive(['tmp_path' => Factory::getApplication()->get('tmp_path')]); $extract = $archive->extract($archivename, $extractdir); } catch (\Exception $e) { if ($alwaysReturnArray) { return [ 'extractdir' => null, 'packagefile' => $archivename, 'type' => false, ]; } return false; } if (!$extract) { if ($alwaysReturnArray) { return [ 'extractdir' => null, 'packagefile' => $archivename, 'type' => false, ]; } return false; } /* * Let's set the extraction directory and package file in the result array so we can * cleanup everything properly later on. */ $retval = []; $retval['extractdir'] = $extractdir; $retval['packagefile'] = $archivename; /* * Try to find the correct install directory. In case the package is inside a * subdirectory detect this and set the install directory to the correct path. * * List all the items in the installation directory. If there is only one, and * it is a folder, then we will set that folder to be the installation folder. */ $dirList = array_merge((array) Folder::files($extractdir, ''), (array) Folder::folders($extractdir, '')); if (\count($dirList) === 1) { if (Folder::exists($extractdir . '/' . $dirList[0])) { $extractdir = Path::clean($extractdir . '/' . $dirList[0]); } } /* * We have found the install directory so lets set it and then move on * to detecting the extension type. */ $retval['dir'] = $extractdir; /* * Get the extension type and return the directory/type array on success or * false on fail. */ $retval['type'] = self::detectType($extractdir); if ($alwaysReturnArray || $retval['type']) { return $retval; } else { return false; } } /** * Method to detect the extension type from a package directory * * @param string $packageDirectory Path to package directory * * @return mixed Extension type string or boolean false on fail * * @since 3.1 */ public static function detectType($packageDirectory) { // Search the install dir for an XML file $files = Folder::files($packageDirectory, '\.xml$', 1, true); if (!$files || !\count($files)) { Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), Log::WARNING, 'jerror'); return false; } foreach ($files as $file) { $xml = simplexml_load_file($file); if (!$xml) { continue; } if ($xml->getName() !== 'extension') { unset($xml); continue; } $type = (string) $xml->attributes()->type; // Free up memory unset($xml); return $type; } Log::add(Text::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), Log::WARNING, 'jerror'); return false; } /** * Gets a file name out of a url * * @param string $url URL to get name from * * @return string Clean version of the filename or a unique id * * @since 3.1 */ public static function getFilenameFromUrl($url) { $default = uniqid(); if (!\is_string($url) || strpos($url, '/') === false) { return $default; } // Get last part of the url (after the last slash). $parts = explode('/', $url); $filename = array_pop($parts); // Replace special characters with underscores. $filename = preg_replace('/[^a-z0-9\_\-\.]/i', '_', $filename); // Replace multiple underscores with just one. $filename = preg_replace('/__+/', '_', trim($filename, '_')); // Return the cleaned filename or, if it is empty, a unique id. return $filename ?: $default; } /** * Clean up temporary uploaded package and unpacked extension * * @param string $package Path to the uploaded package file * @param string $resultdir Path to the unpacked extension * * @return boolean True on success * * @since 3.1 */ public static function cleanupInstall($package, $resultdir) { // Does the unpacked extension directory exist? if ($resultdir && is_dir($resultdir)) { Folder::delete($resultdir); } // Is the package file a valid file? if (is_file($package)) { File::delete($package); } elseif (is_file(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package))) { // It might also be just a base filename File::delete(Path::clean(Factory::getApplication()->get('tmp_path') . '/' . $package)); } } /** * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest * * @param string $packagefile Location of the package to be installed * @param Update $updateObject The Update Object * * @return integer one if the hashes match, zero if hashes doesn't match, two if hashes not found * * @since 3.9.0 */ public static function isChecksumValid($packagefile, $updateObject) { $hashes = ['sha256', 'sha384', 'sha512']; $hashOnFile = false; foreach ($hashes as $hash) { if ($updateObject->get($hash, false)) { $hashPackage = hash_file($hash, $packagefile); $hashRemote = $updateObject->$hash->_data; $hashOnFile = true; if ($hashPackage !== strtolower($hashRemote)) { return self::HASH_NOT_VALIDATED; } } } if ($hashOnFile) { return self::HASH_VALIDATED; } return self::HASH_NOT_PROVIDED; } } Adapter/ComponentAdapter.php 0000644 00000147327 15172720021 0012113 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Asset; use Joomla\CMS\Table\Extension; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Update; use Joomla\Database\ParameterType; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Component installer * * @since 3.1 */ class ComponentAdapter extends InstallerAdapter { /** * The list of current files for the Joomla! CMS administrator that are installed and is read * from the manifest on disk in the update area to handle doing a diff * and deleting files that are in the old files list and not in the new * files list. * * @var array * @since 3.1 * */ protected $oldAdminFiles = null; /** * The list of current files for the Joomla! CMS API that are installed and is read * from the manifest on disk in the update area to handle doing a diff * and deleting files that are in the old files list and not in the new * files list. * * @var array * @since 4.0.0 * */ protected $oldApiFiles = null; /** * The list of current files that are installed and is read * from the manifest on disk in the update area to handle doing a diff * and deleting files that are in the old files list and not in the new * files list. * * @var array * @since 3.1 * */ protected $oldFiles = null; /** * A path to the PHP file that the scriptfile declaration in * the manifest refers to. * * @var string * @since 3.1 * */ protected $manifest_script = null; /** * For legacy installations this is a path to the PHP file that the scriptfile declaration in the * manifest refers to. * * @var string * @since 3.1 * */ protected $install_script = null; /** * Method to check if the extension is present in the filesystem * * @return boolean * * @since 3.4 * @throws \RuntimeException */ protected function checkExtensionInFilesystem() { /* * If the component site or admin directory already exists, then we will assume that the component is already * installed or another component is using that directory. */ if ( file_exists($this->parent->getPath('extension_site')) || file_exists($this->parent->getPath('extension_administrator')) || file_exists($this->parent->getPath('extension_api')) ) { // Look for an update function or update tag $updateElement = $this->getManifest()->update; // Upgrade manually set or update function available or update tag detected if ( $updateElement || $this->parent->isUpgrade() || ($this->parent->manifestClass && method_exists($this->parent->manifestClass, 'update')) ) { // If there is a matching extension mark this as an update $this->setRoute('update'); } elseif (!$this->parent->isOverwrite()) { // We didn't have overwrite set, find an update function or find an update tag so lets call it safe if (file_exists($this->parent->getPath('extension_site'))) { // If the site exists say so. throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_SITE', $this->parent->getPath('extension_site') ) ); } if (file_exists($this->parent->getPath('extension_administrator'))) { // If the admin exists say so throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_ADMIN', $this->parent->getPath('extension_administrator') ) ); } // If the API exists say so throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_API', $this->parent->getPath('extension_api') ) ); } } return false; } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { // Copy site files if ($this->getManifest()->files) { if ($this->route === 'update') { $result = $this->parent->parseFiles($this->getManifest()->files, 0, $this->oldFiles); } else { $result = $this->parent->parseFiles($this->getManifest()->files); } if ($result === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COMP_FAIL_SITE_FILES', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } // Copy admin files if ($this->getManifest()->administration->files) { if ($this->route === 'update') { $result = $this->parent->parseFiles($this->getManifest()->administration->files, 1, $this->oldAdminFiles); } else { $result = $this->parent->parseFiles($this->getManifest()->administration->files, 1); } if ($result === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COMP_FAIL_ADMIN_FILES', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } // Copy API files if ($this->getManifest()->api->files) { if ($this->route === 'update') { $result = $this->parent->parseFiles($this->getManifest()->api->files, 3, $this->oldApiFiles); } else { $result = $this->parent->parseFiles($this->getManifest()->api->files, 3); } if ($result === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COMP_FAIL_API_FILES', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } // If there is a manifest script, let's copy it. if ($this->manifest_script) { $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_administrator') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } } /** * Method to create the extension root path if necessary * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function createExtensionRoot() { // If the component directory does not exist, let's create it $created = false; if (!file_exists($this->parent->getPath('extension_site'))) { if (!$created = Folder::create($this->parent->getPath('extension_site'))) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->parent->getPath('extension_site') ) ); } } /* * Since we created the component directory and we will want to remove it if we have to roll back * the installation, let's add it to the installation step stack */ if ($created) { $this->parent->pushStep( [ 'type' => 'folder', 'path' => $this->parent->getPath('extension_site'), ] ); } // If the component admin directory does not exist, let's create it $created = false; if (!file_exists($this->parent->getPath('extension_administrator'))) { if (!$created = Folder::create($this->parent->getPath('extension_administrator'))) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->parent->getPath('extension_administrator') ) ); } } /* * Since we created the component admin directory and we will want to remove it if we have to roll * back the installation, let's add it to the installation step stack */ if ($created) { $this->parent->pushStep( [ 'type' => 'folder', 'path' => $this->parent->getPath('extension_administrator'), ] ); } // If the component API directory does not exist, let's create it $created = false; if (!file_exists($this->parent->getPath('extension_api'))) { if (!$created = Folder::create($this->parent->getPath('extension_api'))) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->parent->getPath('extension_api') ) ); } } /* * Since we created the component API directory and we will want to remove it if we have to roll * back the installation, let's add it to the installation step stack */ if ($created) { $this->parent->pushStep( [ 'type' => 'folder', 'path' => $this->parent->getPath('extension_api'), ] ); } } /** * Method to finalise the installation processing * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function finaliseInstall() { /** @var Update $update */ $update = Table::getInstance('update'); // Clobber any possible pending updates $uid = $update->find( [ 'element' => $this->element, 'type' => $this->extension->type, 'client_id' => 1, ] ); if ($uid) { $update->delete($uid); } // We will copy the manifest file to its appropriate place. if ($this->route !== 'discover_install') { if (!$this->parent->copyManifest()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } // Time to build the admin menus if (!$this->_buildAdminMenus($this->extension->extension_id)) { Log::add(Text::_('JLIB_INSTALLER_ABORT_COMP_BUILDADMINMENUS_FAILED'), Log::WARNING, 'jerror'); } // Make sure that menu items pointing to the component have correct component id assigned to them. // Prevents message "Component 'com_extension' does not exist." after uninstalling / re-installing component. if (!$this->_updateMenus($this->extension->extension_id)) { Log::add(Text::_('JLIB_INSTALLER_ABORT_COMP_UPDATESITEMENUS_FAILED'), Log::WARNING, 'jerror'); } /** @var Asset $asset */ $asset = Table::getInstance('Asset'); // Check if an asset already exists for this extension and create it if not if (!$asset->loadByName($this->extension->element)) { // Register the component container just under root in the assets table. $asset->name = $this->extension->element; $asset->parent_id = 1; $asset->rules = '{}'; $asset->title = $this->extension->name; $asset->setLocation(1, 'last-child'); if (!$asset->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->extension->getError() ) ); } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $extensionId = $this->extension->extension_id; $db = $this->getDatabase(); // Remove the schema version $query = $db->getQuery(true) ->delete($db->quoteName('#__schemas')) ->where($db->quoteName('extension_id') . ' = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Remove the component container in the assets table. $asset = Table::getInstance('Asset'); if ($asset->loadByName($this->getElement())) { $asset->delete(); } $extensionName = $this->element; $extensionNameWithWildcard = $extensionName . '.%'; // Remove categories for this component $query = $db->getQuery(true) ->delete($db->quoteName('#__categories')) ->where( [ $db->quoteName('extension') . ' = :extension', $db->quoteName('extension') . ' LIKE :wildcard', ], 'OR' ) ->bind(':extension', $extensionName) ->bind(':wildcard', $extensionNameWithWildcard); $db->setQuery($query); $db->execute(); // Rebuild the categories for correct lft/rgt Table::getInstance('category')->rebuild(); // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->extension->element, 'type' => 'component', 'client_id' => 1, 'folder' => '', ] ); if ($uid) { $update->delete($uid); } // Now we need to delete the installation directories. This is the final step in uninstalling the component. if (trim($this->extension->element)) { $retval = true; // Delete the component site directory if (is_dir($this->parent->getPath('extension_site'))) { if (!Folder::delete($this->parent->getPath('extension_site'))) { Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_SITE'), Log::WARNING, 'jerror'); $retval = false; } } // Delete the component admin directory if (is_dir($this->parent->getPath('extension_administrator'))) { if (!Folder::delete($this->parent->getPath('extension_administrator'))) { Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_ADMIN'), Log::WARNING, 'jerror'); $retval = false; } } // Delete the component API directory if (is_dir($this->parent->getPath('extension_api'))) { if (!Folder::delete($this->parent->getPath('extension_api'))) { Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_API'), Log::WARNING, 'jerror'); $retval = false; } } // Now we will no longer need the extension object, so let's delete it $this->extension->delete($this->extension->extension_id); return $retval; } // No component option defined... cannot delete what we don't know about Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_NO_OPTION'), Log::WARNING, 'jerror'); return false; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string The filtered element * * @since 3.4 */ public function getElement($element = null) { $element = parent::getElement($element); if (strpos($element, 'com_') !== 0) { $element = 'com_' . $element; } return $element; } /** * Custom loadLanguage method * * @param string $path The path language files are on. * * @return void * * @since 3.1 */ public function loadLanguage($path = null) { $source = $this->parent->getPath('source'); switch ($this->parent->extension->client_id) { case 0: $client = JPATH_SITE; break; case 1: $client = JPATH_ADMINISTRATOR; break; case 3: $client = JPATH_API; break; default: throw new \InvalidArgumentException( sprintf( 'Unsupported client ID %d for component %s', $this->parent->extension->client_id, $this->parent->extension->element ) ); } if (!$source) { $this->parent->setPath('source', $client . '/components/' . $this->parent->extension->element); } $extension = $this->getElement(); $source = $path ?: $client . '/components/' . $extension; if ($this->getManifest()->administration->files) { $element = $this->getManifest()->administration->files; } elseif ($this->getManifest()->api->files) { $element = $this->getManifest()->api->files; } elseif ($this->getManifest()->files) { $element = $this->getManifest()->files; } else { $element = null; } if ($element) { $folder = (string) $element->attributes()->folder; if ($folder && file_exists($path . '/' . $folder)) { $source = $path . '/' . $folder; } } $this->doLoadLanguage($extension, $source); } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { // Parse optional tags $this->parent->parseMedia($this->getManifest()->media); $this->parent->parseLanguages($this->getManifest()->languages); $this->parent->parseLanguages($this->getManifest()->administration->languages, 1); } /** * Method to parse the queries specified in the `<sql>` tags * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function parseQueries() { parent::parseQueries(); // We have extra tasks to run for the uninstall path if ($this->route === 'uninstall') { $this->_removeAdminMenus($this->extension->extension_id); } } /** * Prepares the adapter for a discover_install task * * @return void * * @since 3.4 * @throws \RuntimeException */ public function prepareDiscoverInstall() { // Need to find to find where the XML file is since we don't store this normally $client = ApplicationHelper::getClientInfo($this->extension->client_id); $short_element = str_replace('com_', '', $this->extension->element); $manifestPath = $client->path . '/components/' . $this->extension->element . '/' . $short_element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $this->parent->setPath('source', $client->path . '/components/' . $this->extension->element); $this->parent->setPath('extension_root', $this->parent->getPath('source')); $this->setManifest($this->parent->getManifest()); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->extension->manifest_cache = json_encode($manifest_details); $this->extension->state = 0; $this->extension->name = $manifest_details['name']; $this->extension->enabled = 1; $this->extension->params = $this->parent->getParams(); $stored = false; try { $this->extension->store(); $stored = true; } catch (\RuntimeException $e) { $name = $this->extension->name; $type = $this->extension->type; $element = $this->extension->element; // Try to delete existing failed records before retrying $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where( [ $db->quoteName('name') . ' = :name', $db->quoteName('type') . ' = :type', $db->quoteName('element') . ' = :element', ] ) ->bind(':name', $name) ->bind(':type', $type) ->bind(':element', $element); $db->setQuery($query); $extension_ids = $db->loadColumn(); if (!empty($extension_ids)) { foreach ($extension_ids as $eid) { // Remove leftover admin menus for this extension ID $this->_removeAdminMenus($eid); // Remove the extension record itself /** @var Extension $extensionTable */ $extensionTable = Table::getInstance('extension'); $extensionTable->delete($eid); } } } if (!$stored) { try { $this->extension->store(); } catch (\RuntimeException $e) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_COMP_DISCOVER_STORE_DETAILS'), $e->getCode(), $e); } } } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { // Let's remove those language files and media in the JROOT/images/ folder that are associated with the component we are uninstalling $this->parent->removeFiles($this->getManifest()->media); $this->parent->removeFiles($this->getManifest()->languages); $this->parent->removeFiles($this->getManifest()->administration->languages, 1); } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function setupInstallPaths() { // Set the installation target paths $this->parent->setPath('extension_site', Path::clean(JPATH_SITE . '/components/' . $this->element)); $this->parent->setPath('extension_administrator', Path::clean(JPATH_ADMINISTRATOR . '/components/' . $this->element)); $this->parent->setPath('extension_api', Path::clean(JPATH_API . '/components/' . $this->element)); // Copy the admin path as it's used as a common base $this->parent->setPath('extension_root', $this->parent->getPath('extension_administrator')); // Make sure that we have an admin element if (!$this->getManifest()->administration) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_COMP_INSTALL_ADMIN_ELEMENT')); } } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { // Get the admin and site paths for the component $this->parent->setPath('extension_administrator', Path::clean(JPATH_ADMINISTRATOR . '/components/' . $this->extension->element)); $this->parent->setPath('extension_api', Path::clean(JPATH_API . '/components/' . $this->extension->element)); $this->parent->setPath('extension_site', Path::clean(JPATH_SITE . '/components/' . $this->extension->element)); // Copy the admin path as it's used as a common base $this->parent->setPath('extension_root', $this->parent->getPath('extension_administrator')); // Find and load the XML install file for the component $this->parent->setPath('source', $this->parent->getPath('extension_administrator')); // Get the package manifest object // We do findManifest to avoid problem when uninstalling a list of extension: getManifest cache its manifest file $this->parent->findManifest(); $this->setManifest($this->parent->getManifest()); if (!$this->getManifest()) { // Make sure we delete the folders if no manifest exists Folder::delete($this->parent->getPath('extension_administrator')); Folder::delete($this->parent->getPath('extension_api')); Folder::delete($this->parent->getPath('extension_site')); // Remove the menu $this->_removeAdminMenus($this->extension->extension_id); // Raise a warning throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_ERRORREMOVEMANUALLY')); } // Attempt to load the admin language file; might have uninstall strings $this->loadLanguage(JPATH_ADMINISTRATOR . '/components/' . $this->extension->element); } /** * Method to setup the update routine for the adapter * * @return void * * @since 3.4 */ protected function setupUpdates() { // Hunt for the original XML file $old_manifest = null; // Use a temporary instance due to side effects; start in the administrator first $tmpInstaller = new Installer(); $tmpInstaller->setDatabase($this->getDatabase()); $tmpInstaller->setPath('source', $this->parent->getPath('extension_administrator')); if (!$tmpInstaller->findManifest()) { // Then the site $tmpInstaller->setPath('source', $this->parent->getPath('extension_site')); if ($tmpInstaller->findManifest()) { $old_manifest = $tmpInstaller->getManifest(); } } else { $old_manifest = $tmpInstaller->getManifest(); } if ($old_manifest) { $this->oldAdminFiles = $old_manifest->administration->files; $this->oldApiFiles = $old_manifest->api->files; $this->oldFiles = $old_manifest->files; } } /** * Method to store the extension to the database * * @param bool $deleteExisting Should I try to delete existing records of the same component? * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension($deleteExisting = false) { // The extension is stored during prepareDiscoverInstall for discover installs if ($this->route === 'discover_install') { return; } // Add or update an entry to the extension table $this->extension->name = $this->name; $this->extension->type = 'component'; $this->extension->element = $this->element; $this->extension->changelogurl = $this->changelogurl; // If we are told to delete existing extension entries then do so. if ($deleteExisting) { $name = $this->extension->name; $type = $this->extension->type; $element = $this->extension->element; // Try to delete existing failed records before retrying $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where( [ $db->quoteName('name') . ' = :name', $db->quoteName('type') . ' = :type', $db->quoteName('element') . ' = :element', ] ) ->bind(':name', $name) ->bind(':type', $type) ->bind(':element', $element); $db->setQuery($query); $extension_ids = $db->loadColumn(); if (!empty($extension_ids)) { foreach ($extension_ids as $eid) { // Remove leftover admin menus for this extension ID $this->_removeAdminMenus($eid); // Remove the extension record itself /** @var Extension $extensionTable */ $extensionTable = Table::getInstance('extension'); $extensionTable->delete($eid); } } } // Namespace is optional if (isset($this->manifest->namespace)) { $this->extension->namespace = (string) $this->manifest->namespace; } // If there is not already a row, generate a heap of defaults if (!$this->currentExtensionId) { $this->extension->folder = ''; $this->extension->enabled = 1; $this->extension->protected = 0; $this->extension->access = 0; $this->extension->client_id = 1; $this->extension->params = $this->parent->getParams(); } $this->extension->manifest_cache = $this->parent->generateManifestCache(); $couldStore = $this->extension->store(); if (!$couldStore && $deleteExisting) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $this->extension->getError() ) ); } if (!$couldStore && !$deleteExisting) { // Maybe we have a failed installation (e.g. timeout). Let's retry after deleting old records. $this->storeExtension(true); } } /** * Method to build menu database entries for a component * * @param int|null $componentId The component ID for which I'm building menus * * @return boolean True if successful * * @since 3.1 */ protected function _buildAdminMenus($componentId = null) { $db = $this->getDatabase(); $option = $this->element; // If a component exists with this option in the table within the protected menutype 'main' then we don't need to add menus $query = $db->getQuery(true) ->select( [ $db->quoteName('m.id'), $db->quoteName('e.extension_id'), ] ) ->from($db->quoteName('#__menu', 'm')) ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) ->where( [ $db->quoteName('m.parent_id') . ' = 1', $db->quoteName('m.client_id') . ' = 1', $db->quoteName('m.menutype') . ' = ' . $db->quote('main'), $db->quoteName('e.element') . ' = :element', ] ) ->bind(':element', $option); $db->setQuery($query); // In case of a failed installation (e.g. timeout error) we may have duplicate menu item and extension records. $componentrows = $db->loadObjectList(); // Check if menu items exist if (!empty($componentrows)) { // Don't do anything if overwrite has not been enabled if (!$this->parent->isOverwrite()) { return true; } // Remove all menu items foreach ($componentrows as $componentrow) { // Remove existing menu items if overwrite has been enabled if ($option) { // If something goes wrong, there's no way to rollback @todo: Search for better solution $this->_removeAdminMenus($componentrow->extension_id); } } } // Only try to detect the component ID if it's not provided if (empty($componentId)) { // Lets find the extension id $query->clear() ->select($db->quoteName('e.extension_id')) ->from($db->quoteName('#__extensions', 'e')) ->where( [ $db->quoteName('e.type') . ' = ' . $db->quote('component'), $db->quoteName('e.element') . ' = :element', ] ) ->bind(':element', $option); $db->setQuery($query); $componentId = $db->loadResult(); } // Ok, now its time to handle the menus. Start with the component root menu, then handle submenus. $menuElement = $this->getManifest()->administration->menu; // Just do not create the menu if $menuElement not exist if (!$menuElement) { return true; } // If the menu item is hidden do nothing more, just return if (\in_array((string) $menuElement['hidden'], ['true', 'hidden'])) { return true; } // Let's figure out what the menu item data should look like $data = []; // I have a menu element, use this information $data['menutype'] = 'main'; $data['client_id'] = 1; $data['title'] = (string) trim($menuElement); $data['alias'] = (string) $menuElement; $data['type'] = 'component'; $data['published'] = 1; $data['parent_id'] = 1; $data['component_id'] = $componentId; $data['img'] = ((string) $menuElement->attributes()->img) ?: 'class:component'; $data['home'] = 0; $data['path'] = ''; $data['params'] = ''; if ($params = $menuElement->params) { // Pass $params through Registry to convert to JSON. $params = new Registry($params); $data['params'] = $params->toString(); } // Set the menu link $request = []; if ((string) $menuElement->attributes()->task) { $request[] = 'task=' . $menuElement->attributes()->task; } if ((string) $menuElement->attributes()->view) { $request[] = 'view=' . $menuElement->attributes()->view; } $qstring = \count($request) ? '&' . implode('&', $request) : ''; $data['link'] = 'index.php?option=' . $option . $qstring; // Try to create the menu item in the database $parent_id = $this->_createAdminMenuItem($data, 1); if ($parent_id === false) { return false; } /* * Process SubMenus */ if (!$this->getManifest()->administration->submenu) { // No submenu? We're done. return true; } foreach ($this->getManifest()->administration->submenu->menu as $child) { $data = []; $data['menutype'] = 'main'; $data['client_id'] = 1; $data['title'] = (string) trim($child); $data['alias'] = ((string) $child->attributes()->alias) ?: (string) $child; $data['type'] = ((string) $child->attributes()->type) ?: 'component'; $data['published'] = 1; $data['parent_id'] = $parent_id; $data['component_id'] = $componentId; $data['img'] = ((string) $child->attributes()->img) ?: 'class:component'; $data['home'] = 0; $data['params'] = ''; if ($params = $child->params) { // Pass $params through Registry to convert to JSON. $params = new Registry($params); $data['params'] = $params->toString(); } // Set the sub menu link if ((string) $child->attributes()->link) { $data['link'] = 'index.php?' . $child->attributes()->link; } else { $request = []; if ((string) $child->attributes()->act) { $request[] = 'act=' . $child->attributes()->act; } if ((string) $child->attributes()->task) { $request[] = 'task=' . $child->attributes()->task; } if ((string) $child->attributes()->controller) { $request[] = 'controller=' . $child->attributes()->controller; } if ((string) $child->attributes()->view) { $request[] = 'view=' . $child->attributes()->view; } if ((string) $child->attributes()->layout) { $request[] = 'layout=' . $child->attributes()->layout; } if ((string) $child->attributes()->sub) { $request[] = 'sub=' . $child->attributes()->sub; } $qstring = \count($request) ? '&' . implode('&', $request) : ''; $data['link'] = 'index.php?option=' . $option . $qstring; } $submenuId = $this->_createAdminMenuItem($data, $parent_id); if ($submenuId === false) { return false; } /* * Since we have created a menu item, we add it to the installation step stack * so that if we have to rollback the changes we can undo it. */ $this->parent->pushStep(['type' => 'menu', 'id' => $componentId]); } return true; } /** * Method to remove admin menu references to a component * * @param int $id The ID of the extension whose admin menus will be removed * * @return boolean True if successful. * * @throws \Exception * * @since 3.1 */ protected function _removeAdminMenus($id) { $db = $this->getDatabase(); /** @var \Joomla\CMS\Table\Menu $table */ $table = Table::getInstance('menu'); // Get the ids of the menu items $query = $db->getQuery(true) ->select($db->quoteName('id')) ->from($db->quoteName('#__menu')) ->where( [ $db->quoteName('client_id') . ' = 1', $db->quoteName('menutype') . ' = ' . $db->quote('main'), $db->quoteName('component_id') . ' = :id', ] ) ->bind(':id', $id, ParameterType::INTEGER); $db->setQuery($query); $ids = $db->loadColumn(); $result = true; // Check for error if (!empty($ids)) { // Iterate the items to delete each one. foreach ($ids as $menuid) { if (!$table->delete((int) $menuid, false)) { Factory::getApplication()->enqueueMessage($table->getError(), 'error'); $result = false; } } // Rebuild the whole tree $table->rebuild(); } return $result; } /** * Method to update menu database entries for a component in case the component has been uninstalled before. * NOTE: This will not update admin menus. Use _updateMenus() instead to update admin menus ase well. * * @param int|null $componentId The component ID. * * @return boolean True if successful * * @since 3.4.2 */ protected function _updateSiteMenus($componentId = null) { return $this->_updateMenus($componentId, 0); } /** * Method to update menu database entries for a component in case if the component has been uninstalled before. * * @param int|null $componentId The component ID. * @param int $clientId The client id * * @return boolean True if successful * * @since 3.7.0 */ protected function _updateMenus($componentId, $clientId = null) { $db = $this->getDatabase(); $option = $this->element; $link = 'index.php?option=' . $option; $linkMatch = 'index.php?option=' . $option . '&%'; // Update all menu items which contain 'index.php?option=com_extension' or 'index.php?option=com_extension&...' // to use the new component id. $query = $db->getQuery(true) ->update($db->quoteName('#__menu')) ->set($db->quoteName('component_id') . ' = :componentId') ->where($db->quoteName('type') . ' = ' . $db->quote('component')) ->extendWhere( 'AND', [ $db->quoteName('link') . ' LIKE :link', $db->quoteName('link') . ' LIKE :linkMatch', ], 'OR' ) ->bind(':componentId', $componentId, ParameterType::INTEGER) ->bind(':link', $link) ->bind(':linkMatch', $linkMatch); if (isset($clientId)) { $query->where($db->quoteName('client_id') . ' = :clientId') ->bind(':clientId', $clientId, ParameterType::INTEGER); } try { $db->setQuery($query); $db->execute(); } catch (\RuntimeException $e) { return false; } return true; } /** * Custom rollback method * - Roll back the component menu item * * @param array $step Installation step to rollback. * * @return boolean True on success * * @throws \Exception * * @since 3.1 */ protected function _rollback_menu($step) { return $this->_removeAdminMenus($step['id']); } /** * Discover unregistered extensions. * * @return array A list of extensions. * * @since 3.1 */ public function discover() { $results = []; $site_components = Folder::folders(JPATH_SITE . '/components'); $admin_components = Folder::folders(JPATH_ADMINISTRATOR . '/components'); $api_components = Folder::folders(JPATH_API . '/components'); foreach ($site_components as $component) { if (file_exists(JPATH_SITE . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml')) { $manifest_details = Installer::parseXMLInstallFile( JPATH_SITE . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml' ); $extension = Table::getInstance('extension'); $extension->set('type', 'component'); $extension->set('client_id', 0); $extension->set('element', $component); $extension->set('folder', ''); $extension->set('name', $component); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } foreach ($admin_components as $component) { if (file_exists(JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml')) { $manifest_details = Installer::parseXMLInstallFile( JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml' ); $extension = Table::getInstance('extension'); $extension->set('type', 'component'); $extension->set('client_id', 1); $extension->set('element', $component); $extension->set('folder', ''); $extension->set('name', $component); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } foreach ($api_components as $component) { if (file_exists(JPATH_API . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml')) { $manifest_details = Installer::parseXMLInstallFile( JPATH_API . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml' ); $extension = Table::getInstance('extension'); $extension->set('type', 'component'); $extension->set('client_id', 3); $extension->set('element', $component); $extension->set('folder', ''); $extension->set('name', $component); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } return $results; } /** * Refreshes the extension table cache * * @return boolean Result of operation, true if updated, false on failure * * @since 3.1 */ public function refreshManifestCache() { // Need to find to find where the XML file is since we don't store this normally $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $short_element = str_replace('com_', '', $this->parent->extension->element); $manifestPath = $client->path . '/components/' . $this->parent->extension->element . '/' . $short_element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; // Namespace is optional if (isset($manifest_details['namespace'])) { $this->parent->extension->namespace = $manifest_details['namespace']; } try { return $this->parent->extension->store(); } catch (\RuntimeException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } /** * Creates the menu item in the database. If the item already exists it tries to remove it and create it afresh. * * @param array &$data The menu item data to create * @param integer $parentId The parent menu item ID * * @return boolean|integer Menu item ID on success, false on failure * * @throws \Exception * * @since 3.1 */ protected function _createAdminMenuItem(array &$data, $parentId) { $db = $this->getDatabase(); /** @var \Joomla\CMS\Table\Menu $table */ $table = Table::getInstance('menu'); try { $table->setLocation($parentId, 'last-child'); } catch (\InvalidArgumentException $e) { Log::add($e->getMessage(), Log::WARNING, 'jerror'); return false; } if (!$table->bind($data) || !$table->check() || !$table->store()) { $menutype = $data['menutype']; $link = $data['link']; $type = $data['type']; $menuParentId = $data['parent_id']; $home = $data['home']; // The menu item already exists. Delete it and retry instead of throwing an error. $query = $db->getQuery(true) ->select($db->quoteName('id')) ->from($db->quoteName('#__menu')) ->where( [ $db->quoteName('menutype') . ' = :menutype', $db->quoteName('client_id') . ' = 1', $db->quoteName('link') . ' = :link', $db->quoteName('type') . ' = :type', $db->quoteName('parent_id') . ' = :parent_id', $db->quoteName('home') . ' = :home', ] ) ->bind(':menutype', $menutype) ->bind(':link', $link) ->bind(':type', $type) ->bind(':parent_id', $menuParentId, ParameterType::INTEGER) ->bind(':home', $home, ParameterType::BOOLEAN); $db->setQuery($query); $menu_id = $db->loadResult(); if (!$menu_id) { // Oops! Could not get the menu ID. Go back and rollback changes. Factory::getApplication()->enqueueMessage($table->getError(), 'error'); return false; } else { /** @var \Joomla\CMS\Table\Menu $temporaryTable */ $temporaryTable = Table::getInstance('menu'); $temporaryTable->delete($menu_id, true); $temporaryTable->load($parentId); $temporaryTable->rebuild($parentId, $temporaryTable->lft, $temporaryTable->level, $temporaryTable->path); // Retry creating the menu item $table->setLocation($parentId, 'last-child'); if (!$table->bind($data) || !$table->check() || !$table->store()) { // Install failed, warn user and rollback changes Factory::getApplication()->enqueueMessage($table->getError(), 'error'); return false; } } } return $table->id; } } Adapter/ModuleAdapter.php 0000644 00000057017 15172720021 0011372 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\Database\ParameterType; use Joomla\Utilities\ArrayHelper; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Module installer * * @since 3.1 */ class ModuleAdapter extends InstallerAdapter { /** * The install client ID * * @var integer * @since 3.4 */ protected $clientId; /** * `<scriptfile>` element of the extension manifest * * @var object * @since 3.1 */ protected $scriptElement = null; /** * Method to check if the extension is already present in the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExistingExtension() { try { $this->currentExtensionId = $this->extension->find( [ 'element' => $this->element, 'type' => $this->type, 'client_id' => $this->clientId, ] ); } catch (\RuntimeException $e) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $e->getMessage() ), $e->getCode(), $e ); } } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { // Copy all necessary files if ($this->parent->parseFiles($this->getManifest()->files, -1) === false) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_MOD_COPY_FILES')); } // If there is a manifest script, let's copy it. if ($this->manifest_script) { $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } } /** * Custom discover method * * @return array Extension list of extensions available * * @since 3.1 */ public function discover() { $results = []; $site_list = Folder::folders(JPATH_SITE . '/modules'); $admin_list = Folder::folders(JPATH_ADMINISTRATOR . '/modules'); $site_info = ApplicationHelper::getClientInfo('site', true); $admin_info = ApplicationHelper::getClientInfo('administrator', true); foreach ($site_list as $module) { if (file_exists(JPATH_SITE . "/modules/$module/$module.xml")) { $manifest_details = Installer::parseXMLInstallFile(JPATH_SITE . "/modules/$module/$module.xml"); $extension = Table::getInstance('extension'); $extension->set('type', 'module'); $extension->set('client_id', $site_info->id); $extension->set('element', $module); $extension->set('folder', ''); $extension->set('name', $module); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = clone $extension; } } foreach ($admin_list as $module) { if (file_exists(JPATH_ADMINISTRATOR . "/modules/$module/$module.xml")) { $manifest_details = Installer::parseXMLInstallFile(JPATH_ADMINISTRATOR . "/modules/$module/$module.xml"); $extension = Table::getInstance('extension'); $extension->set('type', 'module'); $extension->set('client_id', $admin_info->id); $extension->set('element', $module); $extension->set('folder', ''); $extension->set('name', $module); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = clone $extension; } } return $results; } /** * Method to finalise the installation processing * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function finaliseInstall() { // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->element, 'type' => 'module', 'client_id' => $this->clientId, ] ); if ($uid) { $update->delete($uid); } // Lastly, we will copy the manifest file to its appropriate place. if ($this->route !== 'discover_install') { if (!$this->parent->copyManifest(-1)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $extensionId = $this->extension->extension_id; $db = $this->getDatabase(); $retval = true; // Remove the schema version $query = $db->getQuery(true) ->delete('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); $element = $this->extension->element; $clientId = $this->extension->client_id; // Let's delete all the module copies for the type we are uninstalling $query->clear() ->select($db->quoteName('id')) ->from($db->quoteName('#__modules')) ->where($db->quoteName('module') . ' = :element') ->where($db->quoteName('client_id') . ' = :client_id') ->bind(':element', $element) ->bind(':client_id', $clientId, ParameterType::INTEGER); $db->setQuery($query); try { $modules = $db->loadColumn(); } catch (\RuntimeException $e) { $modules = []; } // Do we have any module copies? if (\count($modules)) { // Ensure the list is sane $modules = ArrayHelper::toInteger($modules); // Wipe out any items assigned to menus $query = $db->getQuery(true) ->delete($db->quoteName('#__modules_menu')) ->whereIn($db->quoteName('moduleid'), $modules); $db->setQuery($query); try { $db->execute(); } catch (\RuntimeException $e) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_MOD_UNINSTALL_EXCEPTION', $e->getMessage()), Log::WARNING, 'jerror'); $retval = false; } // Wipe out any instances in the modules table /** @var \Joomla\CMS\Table\Module $module */ $module = Table::getInstance('Module'); foreach ($modules as $modInstanceId) { $module->load($modInstanceId); if (!$module->delete()) { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_MOD_UNINSTALL_EXCEPTION', $module->getError()), Log::WARNING, 'jerror'); $retval = false; } } } // Now we will no longer need the module object, so let's delete it and free up memory $this->extension->delete($this->extension->extension_id); $query = $db->getQuery(true) ->delete($db->quoteName('#__modules')) ->where($db->quoteName('module') . ' = :element') ->where($db->quoteName('client_id') . ' = :client_id') ->bind(':element', $element) ->bind(':client_id', $clientId, ParameterType::INTEGER); $db->setQuery($query); try { // Clean up any other ones that might exist as well $db->execute(); } catch (\RuntimeException $e) { // Ignore the error... } // Remove the installation folder if (!Folder::delete($this->parent->getPath('extension_root'))) { // Folder should raise an error $retval = false; } return $retval; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string|null The filtered element * * @since 3.4 */ public function getElement($element = null) { if ($element) { return $element; } // Joomla 4 Module. if ((string) $this->getManifest()->element) { return (string) $this->getManifest()->element; } if (!\count($this->getManifest()->files->children())) { return $element; } foreach ($this->getManifest()->files->children() as $file) { if ((string) $file->attributes()->module) { // Joomla 3 (legacy) Module. return strtolower((string) $file->attributes()->module); } } return $element; } /** * Custom loadLanguage method * * @param string $path The path where we find language files * * @return void * * @since 3.4 */ public function loadLanguage($path = null) { $source = $this->parent->getPath('source'); $client = $this->parent->extension->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; if (!$source) { $this->parent->setPath('source', $client . '/modules/' . $this->parent->extension->element); } $this->setManifest($this->parent->getManifest()); if ($this->getManifest()->files) { $extension = $this->getElement(); if ($extension) { $source = $path ?: ($this->parent->extension->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $extension; $folder = (string) $this->getManifest()->files->attributes()->folder; if ($folder && file_exists($path . '/' . $folder)) { $source = $path . '/' . $folder; } $client = (string) $this->getManifest()->attributes()->client ?: 'site'; $this->doLoadLanguage($extension, $source, \constant('JPATH_' . strtoupper($client))); } } } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { // Parse optional tags $this->parent->parseMedia($this->getManifest()->media, $this->clientId); $this->parent->parseLanguages($this->getManifest()->languages, $this->clientId); } /** * Prepares the adapter for a discover_install task * * @return void * * @since 3.4 */ public function prepareDiscoverInstall() { $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $manifestPath = $client->path . '/modules/' . $this->parent->extension->element . '/' . $this->parent->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $this->setManifest($this->parent->getManifest()); } /** * Refreshes the extension table cache * * @return boolean Result of operation, true if updated, false on failure. * * @since 3.1 */ public function refreshManifestCache() { $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $manifestPath = $client->path . '/modules/' . $this->parent->extension->element . '/' . $this->parent->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; if ($this->parent->extension->store()) { return true; } else { Log::add(Text::_('JLIB_INSTALLER_ERROR_MOD_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { $this->parent->removeFiles($this->getManifest()->media); $this->parent->removeFiles($this->getManifest()->languages, $this->extension->client_id); } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function setupInstallPaths() { // Get the target application $cname = (string) $this->getManifest()->attributes()->client; if ($cname) { // Attempt to map the client to a base path $client = ApplicationHelper::getClientInfo($cname, true); if ($client === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MOD_UNKNOWN_CLIENT', Text::_('JLIB_INSTALLER_' . $this->route), $client->name ) ); } $basePath = $client->path; $this->clientId = $client->id; } else { // No client attribute was found so we assume the site as the client $basePath = JPATH_SITE; $this->clientId = 0; } // Set the installation path if (empty($this->element)) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MOD_INSTALL_NOFILE', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } $this->parent->setPath('extension_root', $basePath . '/modules/' . $this->element); } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { // Get the extension root path $element = $this->extension->element; $client = ApplicationHelper::getClientInfo($this->extension->client_id); if ($client === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ERROR_MOD_UNINSTALL_UNKNOWN_CLIENT', $this->extension->client_id ) ); } $this->parent->setPath('extension_root', $client->path . '/modules/' . $element); $this->parent->setPath('source', $this->parent->getPath('extension_root')); // Get the module's manifest object // We do findManifest to avoid problem when uninstalling a list of extensions: getManifest cache its manifest file. $this->parent->findManifest(); $this->setManifest($this->parent->getManifest()); // Attempt to load the language file; might have uninstall strings $this->loadLanguage(($this->extension->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $element); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { // Discover installs are stored a little differently if ($this->route === 'discover_install') { $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->extension->manifest_cache = json_encode($manifest_details); $this->extension->state = 0; $this->extension->name = $manifest_details['name']; $this->extension->enabled = 1; $this->extension->params = $this->parent->getParams(); $this->extension->changelogurl = (string) $this->manifest->changelogurl; if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_MOD_DISCOVER_STORE_DETAILS')); } return; } // Was there a module already installed with the same name? if ($this->currentExtensionId) { if (!$this->parent->isOverwrite()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ALREADY_EXISTS', Text::_('JLIB_INSTALLER_' . $this->route), $this->name ) ); } // Load the entry and update the manifest_cache $this->extension->load($this->currentExtensionId); // Update name $this->extension->name = $this->name; // Update namespace $this->extension->namespace = (string) $this->manifest->namespace; // Update changelogurl $this->extension->changelogurl = (string) $this->manifest->changelogurl; // Update manifest $this->extension->manifest_cache = $this->parent->generateManifestCache(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MOD_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $this->extension->getError() ) ); } } else { $this->extension->name = $this->name; $this->extension->type = 'module'; $this->extension->element = $this->element; $this->extension->namespace = (string) $this->manifest->namespace; $this->extension->changelogurl = $this->changelogurl; // There is no folder for modules $this->extension->folder = ''; $this->extension->enabled = 1; $this->extension->protected = 0; $this->extension->access = $this->clientId == 1 ? 2 : 0; $this->extension->client_id = $this->clientId; $this->extension->params = $this->parent->getParams(); // Update the manifest cache for the entry $this->extension->manifest_cache = $this->parent->generateManifestCache(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MOD_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $this->extension->getError() ) ); } // Since we have created a module item, we add it to the installation step stack // so that if we have to rollback the changes we can undo it. $this->parent->pushStep( [ 'type' => 'extension', 'extension_id' => $this->extension->extension_id, ] ); // Create unpublished module $name = preg_replace('#[\*?]#', '', Text::_($this->name)); /** @var \Joomla\CMS\Table\Module $module */ $module = Table::getInstance('module'); $module->title = $name; $module->content = ''; $module->module = $this->element; $module->access = '1'; $module->showtitle = '1'; $module->params = ''; $module->client_id = $this->clientId; $module->language = '*'; $module->position = ''; $module->store(); } } /** * Custom rollback method * - Roll back the menu item * * @param array $arg Installation step to rollback * * @return boolean True on success * * @since 3.1 */ protected function _rollback_menu($arg) { // Get database connector object $db = $this->getDatabase(); $moduleId = $arg['id']; // Remove the entry from the #__modules_menu table $query = $db->getQuery(true) ->delete($db->quoteName('#__modules_menu')) ->where($db->quoteName('moduleid') . ' = :module_id') ->bind(':module_id', $moduleId, ParameterType::INTEGER); $db->setQuery($query); try { return $db->execute(); } catch (\RuntimeException $e) { return false; } } /** * Custom rollback method * - Roll back the module item * * @param array $arg Installation step to rollback * * @return boolean True on success * * @since 3.1 */ protected function _rollback_module($arg) { // Get database connector object $db = $this->getDatabase(); $moduleId = $arg['id']; // Remove the entry from the #__modules table $query = $db->getQuery(true) ->delete($db->quoteName('#__modules')) ->where($db->quoteName('id') . ' = :module_id') ->bind(':module_id', $moduleId, ParameterType::INTEGER); $db->setQuery($query); try { return $db->execute(); } catch (\RuntimeException $e) { return false; } } } Adapter/LanguageAdapter.php 0000644 00000074616 15172720021 0011674 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Language; use Joomla\CMS\Language\LanguageHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Update; use Joomla\Database\ParameterType; use Joomla\Registry\Registry; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Language installer * * @since 3.1 */ class LanguageAdapter extends InstallerAdapter { /** * Core language pack flag * * @var boolean * @since 3.0.0 */ protected $core = false; /** * The language tag for the package * * @var string * @since 4.0.0 */ protected $tag; /** * Flag indicating the uninstall process should not run SQL queries * * @var boolean * @since 4.0.0 */ protected $ignoreUninstallQueries = false; /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { // @todo - Refactor adapter to use common code } /** * Method to finalise the installation processing * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseInstall() { // @todo - Refactor adapter to use common code } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { if ($this->ignoreUninstallQueries) { return false; } $this->resetUserLanguage(); $extensionId = $this->extension->extension_id; // Remove the schema version $db = $this->getDatabase(); $query = $db->getQuery(true) ->delete($db->quoteName('#__schemas')) ->where($db->quoteName('extension_id') . ' = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->extension->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } // Clean installed languages cache. Factory::getCache()->clean('com_languages'); // Remove the extension table entry $this->extension->delete(); return true; } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { $this->parent->removeFiles($this->getManifest()->media); // Construct the path from the client, the language and the extension element name $path = ApplicationHelper::getClientInfo($this->extension->client_id)->path . '/language/' . $this->extension->element; if (!Folder::delete($path)) { // If deleting failed we'll leave the extension entry in tact just in case Log::add(Text::_('JLIB_INSTALLER_ERROR_LANG_UNINSTALL_DIRECTORY'), Log::WARNING, 'jerror'); $this->ignoreUninstallQueries = true; } } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 */ protected function setupInstallPaths() { // @todo - Refactor adapter to use common code } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { // Grab a copy of the client details $client = ApplicationHelper::getClientInfo($this->extension->client_id); // Check the element isn't blank to prevent nuking the languages directory...just in case if (empty($this->extension->element)) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LANG_UNINSTALL_ELEMENT_EMPTY')); } // Verify that it's not the default language for that client $params = ComponentHelper::getParams('com_languages'); if ($params->get($client->name) === $this->extension->element) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LANG_UNINSTALL_DEFAULT')); } // Construct the path from the client, the language and the extension element name $path = $client->path . '/language/' . $this->extension->element; // Get the package manifest object and remove media $this->parent->setPath('source', $path); // Check it exists if (!Folder::exists($path)) { // If the folder doesn't exist lets just nuke the row as well and presume the user killed it for us $this->extension->delete(); throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LANG_UNINSTALL_PATH_EMPTY')); } // We do findManifest to avoid problem when uninstalling a list of extension: getManifest cache its manifest file $this->parent->findManifest(); $this->setManifest($this->parent->getManifest()); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { // @todo - Refactor adapter to use common code } /** * Custom install method * * Note: This behaves badly due to hacks made in the middle of 1.5.x to add * the ability to install multiple distinct packs in one install. The * preferred method is to use a package to install multiple language packs. * * @return boolean|integer The extension ID on success, boolean false on failure * * @since 3.1 */ public function install() { $source = $this->parent->getPath('source'); if (!$source) { $this->parent ->setPath( 'source', ApplicationHelper::getClientInfo($this->parent->extension->client_id)->path . '/language/' . $this->parent->extension->element ); } $this->setManifest($this->parent->getManifest()); // Get the client application target if ($cname = (string) $this->getManifest()->attributes()->client) { // Attempt to map the client to a base path $client = ApplicationHelper::getClientInfo($cname, true); if ($client === null) { $this->parent->abort(Text::sprintf('JLIB_INSTALLER_ABORT', Text::sprintf('JLIB_INSTALLER_ERROR_UNKNOWN_CLIENT_TYPE', $cname))); return false; } $basePath = $client->path; $clientId = $client->id; $element = $this->getManifest()->files; return $this->_install($cname, $basePath, $clientId, $element); } else { // No client attribute was found so we assume the site as the client $cname = 'site'; $basePath = JPATH_SITE; $clientId = 0; $element = $this->getManifest()->files; return $this->_install($cname, $basePath, $clientId, $element); } } /** * Install function that is designed to handle individual clients * * @param string $cname Cname @todo: not used * @param string $basePath The base name. * @param integer $clientId The client id. * @param object &$element The XML element. * * @return boolean|integer The extension ID on success, boolean false on failure * * @since 3.1 */ protected function _install($cname, $basePath, $clientId, &$element) { $this->setManifest($this->parent->getManifest()); // Get the language name // Set the extensions name $this->name = InputFilter::getInstance()->clean((string) $this->getManifest()->name, 'string'); // Get the Language tag [ISO tag, eg. en-GB] $tag = (string) $this->getManifest()->tag; // Check if we found the tag - if we didn't, we may be trying to install from an older language package if (!$tag) { $this->parent->abort(Text::sprintf('JLIB_INSTALLER_ABORT', Text::_('JLIB_INSTALLER_ERROR_NO_LANGUAGE_TAG'))); return false; } $this->tag = $tag; // Set the language installation path $this->parent->setPath('extension_site', $basePath . '/language/' . $tag); // Do we have a meta file in the file list? In other words... is this a core language pack? if ($element && \count($element->children())) { $files = $element->children(); foreach ($files as $file) { if ((string) $file->attributes()->file === 'meta') { $this->core = true; break; } } } // If the language directory does not exist, let's create it $created = false; if (!file_exists($this->parent->getPath('extension_site'))) { if (!$created = Folder::create($this->parent->getPath('extension_site'))) { $this->parent ->abort( Text::sprintf( 'JLIB_INSTALLER_ABORT', Text::sprintf('JLIB_INSTALLER_ERROR_CREATE_FOLDER_FAILED', $this->parent->getPath('extension_site')) ) ); return false; } } else { // Look for an update function or update tag $updateElement = $this->getManifest()->update; // Upgrade manually set or update tag detected if ($updateElement || $this->parent->isUpgrade()) { // Transfer control to the update function return $this->update(); } elseif (!$this->parent->isOverwrite()) { // Overwrite is set // We didn't have overwrite set, find an update function or find an update tag so lets call it safe if (file_exists($this->parent->getPath('extension_site'))) { // If the site exists say so. Log::add( Text::sprintf('JLIB_INSTALLER_ABORT', Text::sprintf('JLIB_INSTALLER_ERROR_FOLDER_IN_USE', $this->parent->getPath('extension_site'))), Log::WARNING, 'jerror' ); } elseif (file_exists($this->parent->getPath('extension_administrator'))) { // If the admin exists say so. Log::add( Text::sprintf( 'JLIB_INSTALLER_ABORT', Text::sprintf('JLIB_INSTALLER_ERROR_FOLDER_IN_USE', $this->parent->getPath('extension_administrator')) ), Log::WARNING, 'jerror' ); } else { // If the api exists say so. Log::add( Text::sprintf( 'JLIB_INSTALLER_ABORT', Text::sprintf('JLIB_INSTALLER_ERROR_FOLDER_IN_USE', $this->parent->getPath('extension_api')) ), Log::WARNING, 'jerror' ); } return false; } } /* * If we created the language directory we will want to remove it if we * have to roll back the installation, so let's add it to the installation * step stack */ if ($created) { $this->parent->pushStep(['type' => 'folder', 'path' => $this->parent->getPath('extension_site')]); } // Copy all the necessary files if ($this->parent->parseFiles($element) === false) { // Install failed, rollback changes $this->parent->abort(); return false; } // Parse optional tags $this->parent->parseMedia($this->getManifest()->media); // Get the language description $description = (string) $this->getManifest()->description; if ($description) { $this->parent->set('message', Text::_($description)); } else { $this->parent->set('message', ''); } // Add an entry to the extension table with a whole heap of defaults $row = Table::getInstance('extension'); $row->set('name', $this->name); $row->set('type', 'language'); $row->set('element', $this->tag); $row->set('changelogurl', (string) $this->getManifest()->changelogurl); // There is no folder for languages $row->set('folder', ''); $row->set('enabled', 1); $row->set('protected', 0); $row->set('access', 0); $row->set('client_id', $clientId); $row->set('params', $this->parent->getParams()); $row->set('manifest_cache', $this->parent->generateManifestCache()); if (!$row->check() || !$row->store()) { // Install failed, roll back changes $this->parent->abort(Text::sprintf('JLIB_INSTALLER_ABORT', $row->getError())); return false; } if ((int) $clientId === 0) { $this->createContentLanguage($this->tag); } // Clobber any possible pending updates /** @var Update $update */ $update = Table::getInstance('update'); $uid = $update->find(['element' => $this->tag, 'type' => 'language', 'folder' => '']); if ($uid) { $update->delete($uid); } // Clean installed languages cache. Factory::getCache()->clean('com_languages'); return $row->get('extension_id'); } /** * Gets a unique language SEF string. * * This function checks other existing language with the same code, if they exist provides a unique SEF name. * For instance: en-GB, en-US and en-AU will share the same SEF code by default: www.mywebsite.com/en/ * To avoid this conflict, this function creates a specific SEF in case of existing conflict: * For example: www.mywebsite.com/en-au/ * * @param string $itemLanguageTag Language Tag. * * @return string * * @since 3.7.0 */ protected function getSefString($itemLanguageTag) { $langs = explode('-', $itemLanguageTag); $prefixToFind = $langs[0]; $numberPrefixesFound = 0; // Get the sef value of all current content languages. $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('sef')) ->from($db->quoteName('#__languages')); $db->setQuery($query); $siteLanguages = $db->loadObjectList(); foreach ($siteLanguages as $siteLang) { if ($siteLang->sef === $prefixToFind) { $numberPrefixesFound++; } } return $numberPrefixesFound === 0 ? $prefixToFind : strtolower($itemLanguageTag); } /** * Custom update method * * @return boolean True on success, false on failure * * @since 3.1 */ public function update() { $xml = $this->parent->getManifest(); $this->setManifest($xml); $cname = $xml->attributes()->client; // Attempt to map the client to a base path $client = ApplicationHelper::getClientInfo($cname, true); if ($client === null || (empty($cname) && $cname !== 0)) { $this->parent->abort(Text::sprintf('JLIB_INSTALLER_ABORT', Text::sprintf('JLIB_INSTALLER_ERROR_UNKNOWN_CLIENT_TYPE', $cname))); return false; } $basePath = $client->path; $clientId = $client->id; // Get the language name // Set the extensions name $name = (string) $this->getManifest()->name; $name = InputFilter::getInstance()->clean($name, 'string'); $this->name = $name; // Get the Language tag [ISO tag, eg. en-GB] $tag = (string) $xml->tag; // Check if we found the tag - if we didn't, we may be trying to install from an older language package if (!$tag) { $this->parent->abort(Text::sprintf('JLIB_INSTALLER_ABORT', Text::_('JLIB_INSTALLER_ERROR_NO_LANGUAGE_TAG'))); return false; } $this->tag = $tag; // Set the language installation path $this->parent->setPath('extension_site', $basePath . '/language/' . $tag); // Do we have a meta file in the file list? In other words... is this a core language pack? if (\count($xml->files->children())) { foreach ($xml->files->children() as $file) { if ((string) $file->attributes()->file === 'meta') { $this->core = true; break; } } } // Copy all the necessary files if ($this->parent->parseFiles($xml->files) === false) { // Install failed, rollback changes $this->parent->abort(); return false; } // Parse optional tags $this->parent->parseMedia($xml->media); // Get the language description and set it as message $this->parent->set('message', (string) $xml->description); /** * --------------------------------------------------------------------------------------------- * Finalization and Cleanup Section * --------------------------------------------------------------------------------------------- */ // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find(['element' => $this->tag, 'type' => 'language', 'client_id' => $clientId]); if ($uid) { $update->delete($uid); } // Update an entry to the extension table $row = Table::getInstance('extension'); $eid = $row->find(['element' => $this->tag, 'type' => 'language', 'client_id' => $clientId]); if ($eid) { $row->load($eid); } else { // Set the defaults // There is no folder for language $row->set('folder', ''); $row->set('enabled', 1); $row->set('protected', 0); $row->set('access', 0); $row->set('client_id', $clientId); $row->set('params', $this->parent->getParams()); } $row->set('name', $this->name); $row->set('type', 'language'); $row->set('element', $this->tag); $row->set('manifest_cache', $this->parent->generateManifestCache()); $row->set('changelogurl', (string) $this->getManifest()->changelogurl); // Clean installed languages cache. Factory::getCache()->clean('com_languages'); if (!$row->check() || !$row->store()) { // Install failed, roll back changes $this->parent->abort(Text::sprintf('JLIB_INSTALLER_ABORT', $row->getError())); return false; } if ($clientId === 0) { $this->createContentLanguage($this->tag); } return $row->get('extension_id'); } /** * Custom discover method * Finds language files * * @return \Joomla\CMS\Table\Extension[] Array of discovered extensions. * * @since 3.1 */ public function discover() { $results = []; $clients = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API]; foreach ($clients as $clientId => $basePath) { $languages = Folder::folders($basePath . '/language'); foreach ($languages as $language) { $manifestfile = $basePath . '/language/' . $language . '/langmetadata.xml'; if (!is_file($manifestfile)) { $manifestfile = $basePath . '/language/' . $language . '/' . $language . '.xml'; if (!is_file($manifestfile)) { continue; } } $manifest_details = Installer::parseXMLInstallFile($manifestfile); $extension = Table::getInstance('extension'); $extension->set('type', 'language'); $extension->set('client_id', $clientId); $extension->set('element', $language); $extension->set('folder', ''); $extension->set('name', $language); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } return $results; } /** * Custom discover install method * Basically updates the manifest cache and leaves everything alone * * @return integer The extension id * * @since 3.1 */ public function discover_install() { // Need to find to find where the XML file is since we don't store this normally $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $short_element = $this->parent->extension->element; $manifestPath = $client->path . '/language/' . $short_element . '/langmetadata.xml'; if (!is_file($manifestPath)) { $manifestPath = $client->path . '/language/' . $short_element . '/' . $short_element . '.xml'; } $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $this->parent->setPath('source', $client->path . '/language/' . $short_element); $this->parent->setPath('extension_root', $this->parent->getPath('source')); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->state = 0; $this->parent->extension->name = $manifest_details['name']; $this->parent->extension->enabled = 1; // @todo remove code: $this->parent->extension->params = $this->parent->getParams(); try { $this->parent->extension->check(); $this->parent->extension->store(); } catch (\RuntimeException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_LANG_DISCOVER_STORE_DETAILS'), Log::WARNING, 'jerror'); return false; } if ($client->id === 0) { $this->createContentLanguage($short_element); } // Clean installed languages cache. Factory::getCache()->clean('com_languages'); return $this->parent->extension->get('extension_id'); } /** * Refreshes the extension table cache * * @return boolean result of operation, true if updated, false on failure * * @since 3.1 */ public function refreshManifestCache() { $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $manifestPath = $client->path . '/language/' . $this->parent->extension->element . '/langmetadata.xml'; if (!is_file($manifestPath)) { $manifestPath = $client->path . '/language/' . $this->parent->extension->element . '/' . $this->parent->extension->element . '.xml'; } $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; if ($this->parent->extension->store()) { return true; } Log::add(Text::_('JLIB_INSTALLER_ERROR_MOD_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } /** * Resets user language to default language * * @return void * * @since 4.0.0 */ private function resetUserLanguage(): void { $client = ApplicationHelper::getClientInfo($this->extension->client_id); if ($client->name !== 'site' && $client->name !== 'administrator') { return; } // Setting the language of users which have this language as the default language $db = $this->getDatabase(); $query = $db->getQuery(true) ->select( [ $db->quoteName('id'), $db->quoteName('params'), ] ) ->from($db->quoteName('#__users')); $db->setQuery($query); $users = $db->loadObjectList(); if ($client->name === 'administrator') { $param_name = 'admin_language'; } else { $param_name = 'language'; } $count = 0; // Prepare the query. $query = $db->getQuery(true) ->update($db->quoteName('#__users')) ->set($db->quoteName('params') . ' = :registry') ->where($db->quoteName('id') . ' = :userId') ->bind(':registry', $registry) ->bind(':userId', $userId, ParameterType::INTEGER); $db->setQuery($query); foreach ($users as $user) { $registry = new Registry($user->params); if ($registry->get($param_name) === $this->extension->element) { // Update query parameters. $registry->set($param_name, ''); $userId = $user->id; $db->execute(); $count++; } } if (!empty($count)) { Log::add(Text::plural('JLIB_INSTALLER_NOTICE_LANG_RESET_USERS', $count), Log::NOTICE, 'jerror'); } } /** * Create an unpublished content language. * * @param $tag string The language tag * * @throws \Exception * @since 4.0.0 */ protected function createContentLanguage($tag) { $tableLanguage = Table::getInstance('language'); // Check if content language already exists. if ($tableLanguage->load(['lang_code' => $tag])) { return; } $manifestfile = JPATH_SITE . '/language/' . $tag . '/langmetadata.xml'; if (!is_file($manifestfile)) { $manifestfile = JPATH_SITE . '/language/' . $tag . '/' . $tag . '.xml'; } // Load the site language manifest. $siteLanguageManifest = LanguageHelper::parseXMLLanguageFile($manifestfile); // Set the content language title as the language metadata name. $contentLanguageTitle = $siteLanguageManifest['name']; // Set, as fallback, the content language native title to the language metadata name. $contentLanguageNativeTitle = $contentLanguageTitle; // If exist, load the native title from the language xml metadata. if (isset($siteLanguageManifest['nativeName']) && $siteLanguageManifest['nativeName']) { $contentLanguageNativeTitle = $siteLanguageManifest['nativeName']; } // Try to load a language string from the installation language var. Will be removed in 4.0. if ($contentLanguageNativeTitle === $contentLanguageTitle) { $manifestfile = JPATH_INSTALLATION . '/language/' . $tag . '/langmetadata.xml'; if (!is_file($manifestfile)) { $manifestfile = JPATH_INSTALLATION . '/language/' . $tag . '/' . $tag . '.xml'; } if (file_exists($manifestfile)) { $installationLanguage = new Language($tag); $installationLanguage->load('', JPATH_INSTALLATION); if ($installationLanguage->hasKey('INSTL_DEFAULTLANGUAGE_NATIVE_LANGUAGE_NAME')) { // Make sure it will not use the en-GB fallback. $defaultLanguage = new Language('en-GB'); $defaultLanguage->load('', JPATH_INSTALLATION); $defaultLanguageNativeTitle = $defaultLanguage->_('INSTL_DEFAULTLANGUAGE_NATIVE_LANGUAGE_NAME'); $installationLanguageNativeTitle = $installationLanguage->_('INSTL_DEFAULTLANGUAGE_NATIVE_LANGUAGE_NAME'); if ($defaultLanguageNativeTitle !== $installationLanguageNativeTitle) { $contentLanguageNativeTitle = $installationLanguage->_('INSTL_DEFAULTLANGUAGE_NATIVE_LANGUAGE_NAME'); } } } } // Prepare language data for store. $languageData = [ 'lang_id' => 0, 'lang_code' => $tag, 'title' => $contentLanguageTitle, 'title_native' => $contentLanguageNativeTitle, 'sef' => $this->getSefString($tag), 'image' => strtolower(str_replace('-', '_', $tag)), 'published' => 0, 'ordering' => 0, 'access' => (int) Factory::getApplication()->get('access', 1), 'description' => '', 'metadesc' => '', 'sitename' => '', ]; if (!$tableLanguage->bind($languageData) || !$tableLanguage->check() || !$tableLanguage->store() || !$tableLanguage->reorder()) { Log::add( Text::sprintf('JLIB_INSTALLER_WARNING_UNABLE_TO_INSTALL_CONTENT_LANGUAGE', $siteLanguageManifest['name'], $tableLanguage->getError()), Log::NOTICE, 'jerror' ); } } } Adapter/LibraryAdapter.php 0000644 00000040723 15172720021 0011545 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Installer\Manifest\LibraryManifest; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Update; use Joomla\Database\ParameterType; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Library installer * * @since 3.1 */ class LibraryAdapter extends InstallerAdapter { /** * Method to check if the extension is present in the filesystem, flags the route as update if so * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExtensionInFilesystem() { if ($this->currentExtensionId) { // Already installed, can we upgrade? if ($this->parent->isOverwrite() || $this->parent->isUpgrade()) { // We can upgrade, so uninstall the old one // We don't want to compromise this instance! $installer = new Installer(); $installer->setDatabase($this->getDatabase()); $installer->setPackageUninstall(true); $installer->uninstall('library', $this->currentExtensionId); // Clear the cached data $this->currentExtensionId = null; $this->extension = Table::getInstance('Extension', 'JTable', ['dbo' => $this->getDatabase()]); // From this point we'll consider this an update $this->setRoute('update'); } else { // Stop the install, no upgrade possible throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_LIB_INSTALL_ALREADY_INSTALLED')); } } } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { if ($this->parent->parseFiles($this->getManifest()->files, -1) === false) { throw new \RuntimeException(Text::sprintf('JLIB_INSTALLER_ABORT_LIB_COPY_FILES', $this->element)); } } /** * Method to finalise the installation processing * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function finaliseInstall() { // Clobber any possible pending updates /** @var Update $update */ $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } // Lastly, we will copy the manifest file to its appropriate place. if ($this->route !== 'discover_install') { $manifest = []; $manifest['src'] = $this->parent->getPath('manifest'); $manifest['dest'] = JPATH_MANIFESTS . '/libraries/' . $this->element . '.xml'; $destFolder = \dirname($manifest['dest']); if (!is_dir($destFolder) && !@mkdir($destFolder)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } if (!$this->parent->copyFiles([$manifest], true)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } // If there is a manifest script, let's copy it. if ($this->manifest_script) { $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $extensionId = $this->extension->extension_id; $db = $this->getDatabase(); // Remove the schema version $query = $db->getQuery(true) ->delete('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->extension->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } $this->extension->delete(); return true; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string The filtered element * * @since 3.4 */ public function getElement($element = null) { if (!$element) { $element = (string) $this->getManifest()->libraryname; } return $element; } /** * Custom loadLanguage method * * @param string $path The path where to find language files. * * @return void * * @since 3.1 */ public function loadLanguage($path = null) { $source = $this->parent->getPath('source'); if (!$source) { $this->parent->setPath('source', JPATH_PLATFORM . '/' . $this->getElement()); } $extension = 'lib_' . str_replace('/', '_', $this->getElement()); $librarypath = (string) $this->getManifest()->libraryname; $source = $path ?: JPATH_PLATFORM . '/' . $librarypath; $this->doLoadLanguage($extension, $source, JPATH_SITE); } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { $this->parent->parseLanguages($this->getManifest()->languages); $this->parent->parseMedia($this->getManifest()->media); } /** * Prepares the adapter for a discover_install task * * @return void * * @since 3.4 */ public function prepareDiscoverInstall() { $manifestPath = JPATH_MANIFESTS . '/libraries/' . $this->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $this->setManifest($this->parent->getManifest()); } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { $this->parent->removeFiles($this->getManifest()->files, -1); $manifest = JPATH_MANIFESTS . '/libraries/' . $this->extension->element . '.xml'; if (is_file($manifest)) { File::delete($manifest); } // @todo: Change this so it walked up the path backwards so we clobber multiple empties // If the folder is empty, let's delete it if (Folder::exists($this->parent->getPath('extension_root'))) { if (is_dir($this->parent->getPath('extension_root'))) { $files = Folder::files($this->parent->getPath('extension_root')); if (!\count($files)) { Folder::delete($this->parent->getPath('extension_root')); } } } $this->parent->removeFiles($this->getManifest()->media); $this->parent->removeFiles($this->getManifest()->languages); $elementParts = explode('/', $this->extension->element); // Delete empty vendor folders if (2 === \count($elementParts)) { $folders = Folder::folders(JPATH_PLATFORM . '/' . $elementParts[0]); if (empty($folders)) { Folder::delete(JPATH_MANIFESTS . '/libraries/' . $elementParts[0]); Folder::delete(JPATH_PLATFORM . '/' . $elementParts[0]); } } } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function setupInstallPaths() { $group = (string) $this->getManifest()->libraryname; if (!$group) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_LIB_INSTALL_NOFILE')); } // Don't install libraries which would override core folders $restrictedFolders = ['php-encryption', 'phpass', 'src', 'vendor']; if (in_array($group, $restrictedFolders)) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ABORT_LIB_INSTALL_CORE_FOLDER')); } $this->parent->setPath('extension_root', JPATH_PLATFORM . '/' . implode(DIRECTORY_SEPARATOR, explode('/', $group))); } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { $manifestFile = JPATH_MANIFESTS . '/libraries/' . $this->extension->element . '.xml'; // Because libraries may not have their own folders we cannot use the standard method of finding an installation manifest if (!file_exists($manifestFile)) { // Remove this row entry since its invalid $this->extension->delete($this->extension->extension_id); throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LIB_UNINSTALL_INVALID_NOTFOUND_MANIFEST')); } $manifest = new LibraryManifest($manifestFile); // Set the library root path $this->parent->setPath('extension_root', JPATH_PLATFORM . '/' . $manifest->libraryname); // Set the source path to the library root, the manifest script may be found $this->parent->setPath('source', $this->parent->getPath('extension_root')); $xml = simplexml_load_file($manifestFile); // If we cannot load the XML file return null if (!$xml) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LIB_UNINSTALL_LOAD_MANIFEST')); } // Check for a valid XML root tag. if ($xml->getName() !== 'extension') { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LIB_UNINSTALL_INVALID_MANIFEST')); } $this->setManifest($xml); // Attempt to load the language file; might have uninstall strings $this->loadLanguage(); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { // Discover installs are stored a little differently if ($this->route === 'discover_install') { $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->extension->manifest_cache = json_encode($manifest_details); $this->extension->state = 0; $this->extension->name = $manifest_details['name']; $this->extension->enabled = 1; $this->extension->params = $this->parent->getParams(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_LIB_DISCOVER_STORE_DETAILS')); } return; } $this->extension->name = $this->name; $this->extension->type = 'library'; $this->extension->element = $this->element; $this->extension->changelogurl = $this->changelogurl; // There is no folder for libraries $this->extension->folder = ''; $this->extension->enabled = 1; $this->extension->protected = 0; $this->extension->access = 1; $this->extension->client_id = 0; $this->extension->params = $this->parent->getParams(); // Update the manifest cache for the entry $this->extension->manifest_cache = $this->parent->generateManifestCache(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_LIB_INSTALL_ROLLBACK', $this->extension->getError() ) ); } // Since we have created a library item, we add it to the installation step stack // so that if we have to rollback the changes we can undo it. $this->parent->pushStep(['type' => 'extension', 'id' => $this->extension->extension_id]); } /** * Custom discover method * * @return array Extension list of extensions available * * @since 3.1 */ public function discover() { $results = []; $mainFolder = JPATH_MANIFESTS . '/libraries'; $folder = new \RecursiveDirectoryIterator($mainFolder); $iterator = new \RegexIterator( new \RecursiveIteratorIterator($folder), '/\.xml$/i', \RecursiveRegexIterator::GET_MATCH ); foreach ($iterator as $file => $pattern) { $element = str_replace([$mainFolder . DIRECTORY_SEPARATOR, '.xml'], '', $file); $manifestCache = Installer::parseXMLInstallFile($file); $extension = Table::getInstance('extension'); $extension->set('type', 'library'); $extension->set('client_id', 0); $extension->set('element', $element); $extension->set('folder', ''); $extension->set('name', $element); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifestCache)); $extension->set('params', '{}'); $results[] = $extension; } return $results; } /** * Refreshes the extension table cache * * @return boolean Result of operation, true if updated, false on failure * * @since 3.1 */ public function refreshManifestCache() { // Need to find to find where the XML file is since we don't store this normally $manifestPath = JPATH_MANIFESTS . '/libraries/' . $this->parent->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; try { return $this->parent->extension->store(); } catch (\RuntimeException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_LIB_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } } Adapter/FileAdapter.php 0000644 00000045234 15172720021 0011022 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\Database\ParameterType; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * File installer * * @since 3.1 */ class FileAdapter extends InstallerAdapter { /** * `<scriptfile>` element of the extension manifest * * @var object * @since 3.1 */ protected $scriptElement = null; /** * Flag if the adapter supports discover installs * * Adapters should override this and set to false if discover install is unsupported * * @var boolean * @since 3.4 */ protected $supportsDiscoverInstall = false; /** * List of processed folders * * @var array * @since 3.4 */ protected $folderList; /** * List of processed files * * @var array * @since 3.4 */ protected $fileList; /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { // Populate File and Folder List to copy $this->populateFilesAndFolderList(); // Now that we have folder list, lets start creating them foreach ($this->folderList as $folder) { if (!Folder::exists($folder)) { if (!$created = Folder::create($folder)) { throw new \RuntimeException( Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_FAIL_SOURCE_DIRECTORY', $folder) ); } // Since we created a directory and will want to remove it if we have to roll back. // The installation due to some errors, let's add it to the installation step stack. if ($created) { $this->parent->pushStep(['type' => 'folder', 'path' => $folder]); } } } // Now that we have file list, let's start copying them $this->parent->copyFiles($this->fileList); } /** * Method to finalise the installation processing * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function finaliseInstall() { // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } // Lastly, we will copy the manifest file to its appropriate place. $manifest = []; $manifest['src'] = $this->parent->getPath('manifest'); $manifest['dest'] = JPATH_MANIFESTS . '/files/' . basename($this->parent->getPath('manifest')); if (!$this->parent->copyFiles([$manifest], true)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } // If there is a manifest script, let's copy it. if ($this->manifest_script) { // First, we have to create a folder for the script if one isn't present if (!file_exists($this->parent->getPath('extension_root'))) { Folder::create($this->parent->getPath('extension_root')); } $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $manifest = JPATH_MANIFESTS . '/files/' . $this->extension->element . '.xml'; if (is_file($manifest)) { File::delete($manifest); } $extensionId = $this->extension->extension_id; $db = $this->getDatabase(); // Remove the schema version $query = $db->getQuery(true) ->delete('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->extension->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } $this->extension->delete(); return true; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string The filtered element * * @since 3.4 */ public function getElement($element = null) { if (!$element) { $manifestPath = Path::clean($this->parent->getPath('manifest', '')); $element = preg_replace('/\.xml/', '', basename($manifestPath)); } return $element; } /** * Custom loadLanguage method * * @param string $path The path on which to find language files. * * @return void * * @since 3.1 */ public function loadLanguage($path) { $extension = 'files_' . strtolower(str_replace('files_', '', $this->getElement())); $this->doLoadLanguage($extension, $path, JPATH_SITE); } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { // Parse optional tags $this->parent->parseLanguages($this->getManifest()->languages); } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { // Loop through all elements and get list of files and folders foreach ($this->getManifest()->fileset->files as $eFiles) { $target = (string) $eFiles->attributes()->target; // Create folder path if (empty($target)) { $targetFolder = JPATH_ROOT; } else { $targetFolder = JPATH_ROOT . '/' . $target; } $folderList = []; // Check if all children exists if (\count($eFiles->children()) > 0) { // Loop through all filenames elements foreach ($eFiles->children() as $eFileName) { if ($eFileName->getName() === 'folder') { $folderList[] = $targetFolder . '/' . $eFileName; } else { $fileName = $targetFolder . '/' . $eFileName; if (is_file($fileName)) { File::delete($fileName); } } } } // Delete any folders that don't have any content in them. foreach ($folderList as $folder) { $files = Folder::files($folder); if ($files !== false && !\count($files)) { Folder::delete($folder); } } } // Lastly, remove the extension_root $folder = $this->parent->getPath('extension_root'); if (Folder::exists($folder)) { Folder::delete($folder); } $this->parent->removeFiles($this->getManifest()->languages); } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 */ protected function setupInstallPaths() { // Set the file root path if ($this->name === 'files_joomla') { // If we are updating the Joomla core, set the root path to the root of Joomla $this->parent->setPath('extension_root', JPATH_ROOT); } else { $this->parent->setPath('extension_root', JPATH_MANIFESTS . '/files/' . $this->element); } } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { $manifestFile = JPATH_MANIFESTS . '/files/' . $this->extension->element . '.xml'; // Because libraries may not have their own folders we cannot use the standard method of finding an installation manifest if (!file_exists($manifestFile)) { // Remove this row entry since its invalid $this->extension->delete($this->extension->extension_id); throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_FILE_UNINSTALL_INVALID_NOTFOUND_MANIFEST')); } // Set the files root path $this->parent->setPath('extension_root', JPATH_MANIFESTS . '/files/' . $this->extension->element); // Set the source path for compatibility with the API $this->parent->setPath('source', $this->parent->getPath('extension_root')); $xml = simplexml_load_file($manifestFile); // If we cannot load the XML file return null if (!$xml) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_FILE_UNINSTALL_LOAD_MANIFEST')); } // Check for a valid XML root tag. if ($xml->getName() !== 'extension') { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_FILE_UNINSTALL_INVALID_MANIFEST')); } $this->setManifest($xml); // Attempt to load the language file; might have uninstall strings $this->loadLanguage(JPATH_MANIFESTS . '/files'); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { if ($this->currentExtensionId) { // Load the entry and update the manifest_cache $this->extension->load($this->currentExtensionId); // Update name $this->extension->name = $this->name; // Update manifest $this->extension->manifest_cache = $this->parent->generateManifestCache(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->extension->getError() ) ); } } else { // Add an entry to the extension table with a whole heap of defaults $this->extension->name = $this->name; $this->extension->type = 'file'; $this->extension->element = $this->element; $this->extension->changelogurl = $this->changelogurl; // There is no folder for files so leave it blank $this->extension->folder = ''; $this->extension->enabled = 1; $this->extension->protected = 0; $this->extension->access = 0; $this->extension->client_id = 0; $this->extension->params = ''; // Update the manifest cache for the entry $this->extension->manifest_cache = $this->parent->generateManifestCache(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->extension->getError() ) ); } // Since we have created a module item, we add it to the installation step stack // so that if we have to rollback the changes we can undo it. $this->parent->pushStep(['type' => 'extension', 'extension_id' => $this->extension->extension_id]); } } /** * Function used to check if extension is already installed * * @param string $extension The element name of the extension to install * * @return boolean True if extension exists * * @since 3.1 */ protected function extensionExistsInSystem($extension = null) { // Get a database connector object $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('type') . ' = ' . $db->quote('file')) ->where($db->quoteName('element') . ' = :extension') ->bind(':extension', $extension); $db->setQuery($query); try { $db->execute(); } catch (\RuntimeException $e) { // Install failed, rollback changes - error logged by the installer return false; } $id = $db->loadResult(); if (empty($id)) { return false; } return true; } /** * Function used to populate files and folder list * * @return boolean none * * @since 3.1 */ protected function populateFilesAndFolderList() { // Initialise variable $this->folderList = []; $this->fileList = []; // Set root folder names $packagePath = $this->parent->getPath('source'); $jRootPath = Path::clean(JPATH_ROOT); // Loop through all elements and get list of files and folders foreach ($this->getManifest()->fileset->files as $eFiles) { // Check if the element is files element $folder = (string) $eFiles->attributes()->folder; $target = (string) $eFiles->attributes()->target; // Split folder names into array to get folder names. This will help in creating folders $arrList = preg_split("#/|\\/#", $target); $folderName = $jRootPath; foreach ($arrList as $dir) { if (empty($dir)) { continue; } $folderName .= '/' . $dir; // Check if folder exists, if not then add to the array for folder creation if (!Folder::exists($folderName)) { $this->folderList[] = $folderName; } } // Create folder path $sourceFolder = empty($folder) ? $packagePath : $packagePath . '/' . $folder; $targetFolder = empty($target) ? $jRootPath : $jRootPath . '/' . $target; // Check if source folder exists if (!Folder::exists($sourceFolder)) { Log::add(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_FAIL_SOURCE_DIRECTORY', $sourceFolder), Log::WARNING, 'jerror'); // If installation fails, rollback $this->parent->abort(); return false; } // Check if all children exists if (\count($eFiles->children())) { // Loop through all filenames elements foreach ($eFiles->children() as $eFileName) { $path = []; $path['src'] = $sourceFolder . '/' . $eFileName; $path['dest'] = $targetFolder . '/' . $eFileName; $path['type'] = 'file'; if ($eFileName->getName() === 'folder') { $folderName = $targetFolder . '/' . $eFileName; $this->folderList[] = $folderName; $path['type'] = 'folder'; } $this->fileList[] = $path; } } else { $files = Folder::files($sourceFolder); foreach ($files as $file) { $path = []; $path['src'] = $sourceFolder . '/' . $file; $path['dest'] = $targetFolder . '/' . $file; $this->fileList[] = $path; } } } } /** * Refreshes the extension table cache * * @return boolean result of operation, true if updated, false on failure * * @since 3.1 */ public function refreshManifestCache() { // Need to find to find where the XML file is since we don't store this normally $manifestPath = JPATH_MANIFESTS . '/files/' . $this->parent->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; try { return $this->parent->extension->store(); } catch (\RuntimeException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_PACK_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } } Adapter/PluginAdapter.php 0000644 00000050566 15172720021 0011405 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Update; use Joomla\Database\ParameterType; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Plugin installer * * @since 3.1 */ class PluginAdapter extends InstallerAdapter { /** * Group of the plugin * * @var string * @since 4.3.0 */ protected $group; /** * `<scriptfile>` element of the extension manifest * * @var object * @since 3.1 */ protected $scriptElement = null; /** * `<files>` element of the old extension manifest * * @var object * @since 3.1 */ protected $oldFiles = null; /** * Method to check if the extension is already present in the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExistingExtension() { try { $this->currentExtensionId = $this->extension->find( ['type' => $this->type, 'element' => $this->element, 'folder' => $this->group] ); } catch (\RuntimeException $e) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $e->getMessage() ), $e->getCode(), $e ); } } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { // Copy all necessary files if ($this->parent->parseFiles($this->getManifest()->files, -1, $this->oldFiles) === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PLG_COPY_FILES', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } // If there is a manifest script, let's copy it. if ($this->manifest_script) { $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } } } } /** * Method to create the extension root path if necessary * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function createExtensionRoot() { // Run the common create code first parent::createExtensionRoot(); // If we're updating at this point when there is always going to be an extension_root find the old XML files if ($this->route === 'update') { // Create a new installer because findManifest sets stuff; side effects! $tmpInstaller = new Installer(); $tmpInstaller->setDatabase($this->getDatabase()); // Look in the extension root $tmpInstaller->setPath('source', $this->parent->getPath('extension_root')); if ($tmpInstaller->findManifest()) { $old_manifest = $tmpInstaller->getManifest(); $this->oldFiles = $old_manifest->files; } } } /** * Method to finalise the installation processing * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function finaliseInstall() { // Clobber any possible pending updates /** @var Update $update */ $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->element, 'type' => $this->type, 'folder' => $this->group, ] ); if ($uid) { $update->delete($uid); } // Lastly, we will copy the manifest file to its appropriate place. if ($this->route !== 'discover_install') { if (!$this->parent->copyManifest(-1)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $extensionId = $this->extension->extension_id; $db = $this->getDatabase(); // Remove the schema version $query = $db->getQuery(true) ->delete('#__schemas') ->where('extension_id = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Now we will no longer need the plugin object, so let's delete it $this->extension->delete($this->extension->extension_id); // Remove the plugin's folder Folder::delete($this->parent->getPath('extension_root')); return true; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string The filtered element * * @since 3.4 */ public function getElement($element = null) { if ($element || !$this->getManifest()) { return $element; } // Backward Compatibility // @todo Deprecate in future version if (!\count($this->getManifest()->files->children())) { return $element; } $type = (string) $this->getManifest()->attributes()->type; foreach ($this->getManifest()->files->children() as $file) { if ((string) $file->attributes()->$type) { $element = (string) $file->attributes()->$type; break; } } return $element; } /** * Get the class name for the install adapter script. * * @return string The class name. * * @since 3.4 */ protected function getScriptClassName() { return 'Plg' . str_replace('-', '', $this->group) . $this->element . 'InstallerScript'; } /** * Custom loadLanguage method * * @param string $path The path where to find language files. * * @return void * * @since 3.1 */ public function loadLanguage($path = null) { $source = $this->parent->getPath('source'); if (!$source) { $this->parent->setPath( 'source', JPATH_PLUGINS . '/' . $this->parent->extension->folder . '/' . $this->parent->extension->element ); } $element = $this->getManifest()->files; if ($element) { $group = strtolower((string) $this->getManifest()->attributes()->group); $name = ''; if (\count($element->children())) { foreach ($element->children() as $file) { if ((string) $file->attributes()->plugin) { $name = strtolower((string) $file->attributes()->plugin); break; } } } if ($name) { $extension = "plg_{$group}_{$name}"; $source = $path ?: JPATH_PLUGINS . "/$group/$name"; $folder = (string) $element->attributes()->folder; if ($folder && file_exists("$path/$folder")) { $source = "$path/$folder"; } $this->doLoadLanguage($extension, $source, JPATH_ADMINISTRATOR); } } } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { // Parse optional tags -- media and language files for plugins go in admin app $this->parent->parseMedia($this->getManifest()->media, 1); $this->parent->parseLanguages($this->getManifest()->languages, 1); } /** * Prepares the adapter for a discover_install task * * @return void * * @since 3.4 */ public function prepareDiscoverInstall() { $client = ApplicationHelper::getClientInfo($this->extension->client_id); $basePath = $client->path . '/plugins/' . $this->extension->folder; $manifestPath = $basePath . '/' . $this->extension->element . '/' . $this->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $this->setManifest($this->parent->getManifest()); } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { // Remove the plugin files $this->parent->removeFiles($this->getManifest()->files, -1); // Remove all media and languages as well $this->parent->removeFiles($this->getManifest()->media); $this->parent->removeFiles($this->getManifest()->languages, 1); } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function setupInstallPaths() { $this->group = (string) $this->getManifest()->attributes()->group; if (empty($this->element) && empty($this->group)) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PLG_INSTALL_NO_FILE', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } $this->parent->setPath('extension_root', JPATH_PLUGINS . '/' . $this->group . '/' . $this->element); } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { // Get the plugin folder so we can properly build the plugin path if (trim($this->extension->folder) === '') { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_PLG_UNINSTALL_FOLDER_FIELD_EMPTY')); } // Set the plugin root path $this->parent->setPath('extension_root', JPATH_PLUGINS . '/' . $this->extension->folder . '/' . $this->extension->element); $this->parent->setPath('source', $this->parent->getPath('extension_root')); $this->parent->findManifest(); $this->setManifest($this->parent->getManifest()); if ($this->getManifest()) { $this->group = (string) $this->getManifest()->attributes()->group; } // Attempt to load the language file; might have uninstall strings $this->loadLanguage($this->parent->getPath('source')); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { // Discover installs are stored a little differently if ($this->route === 'discover_install') { $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->extension->manifest_cache = json_encode($manifest_details); $this->extension->state = 0; $this->extension->name = $manifest_details['name']; $this->extension->enabled = 'editors' === $this->extension->folder ? 1 : 0; $this->extension->params = $this->parent->getParams(); $this->extension->changelogurl = (string) $this->manifest->changelogurl; if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_PLG_DISCOVER_STORE_DETAILS')); } return; } // Was there a plugin with the same name already installed? if ($this->currentExtensionId) { if (!$this->parent->isOverwrite()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ALREADY_EXISTS', Text::_('JLIB_INSTALLER_' . $this->route), $this->name ) ); } // Load the entry and update the manifest_cache $this->extension->load($this->currentExtensionId); // Update name $this->extension->name = $this->name; // Update namespace $this->extension->namespace = (string) $this->manifest->namespace; // Update changelogurl $this->extension->changelogurl = (string) $this->manifest->changelogurl; // Update manifest $this->extension->manifest_cache = $this->parent->generateManifestCache(); // Update the manifest cache and name $this->extension->store(); } else { // Store in the extensions table (1.6) $this->extension->name = $this->name; $this->extension->type = 'plugin'; $this->extension->ordering = 0; $this->extension->element = $this->element; $this->extension->folder = $this->group; $this->extension->enabled = 0; $this->extension->protected = 0; $this->extension->access = 1; $this->extension->client_id = 0; $this->extension->params = $this->parent->getParams(); $this->extension->changelogurl = $this->changelogurl; // Update the manifest cache for the entry $this->extension->manifest_cache = $this->parent->generateManifestCache(); // Editor plugins are published by default if ($this->group === 'editors') { $this->extension->enabled = 1; } if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PLG_INSTALL_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $this->extension->getError() ) ); } // Since we have created a plugin item, we add it to the installation step stack // so that if we have to rollback the changes we can undo it. $this->parent->pushStep(['type' => 'extension', 'id' => $this->extension->extension_id]); } } /** * Custom discover method * * @return array Extension) list of extensions available * * @since 3.1 */ public function discover() { $results = []; $folder_list = Folder::folders(JPATH_SITE . '/plugins'); foreach ($folder_list as $folder) { $file_list = Folder::files(JPATH_SITE . '/plugins/' . $folder, '\.xml$'); foreach ($file_list as $file) { $manifest_details = Installer::parseXMLInstallFile(JPATH_SITE . '/plugins/' . $folder . '/' . $file); $file = File::stripExt($file); // Ignore example plugins if ($file === 'example' || $manifest_details === false) { continue; } $element = empty($manifest_details['filename']) ? $file : $manifest_details['filename']; $extension = Table::getInstance('extension'); $extension->set('type', 'plugin'); $extension->set('client_id', 0); $extension->set('element', $element); $extension->set('folder', $folder); $extension->set('name', $manifest_details['name']); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } $folder_list = Folder::folders(JPATH_SITE . '/plugins/' . $folder); foreach ($folder_list as $plugin_folder) { $file_list = Folder::files(JPATH_SITE . '/plugins/' . $folder . '/' . $plugin_folder, '\.xml$'); foreach ($file_list as $file) { $manifest_details = Installer::parseXMLInstallFile( JPATH_SITE . '/plugins/' . $folder . '/' . $plugin_folder . '/' . $file ); $file = File::stripExt($file); if ($file === 'example' || $manifest_details === false) { continue; } $element = empty($manifest_details['filename']) ? $file : $manifest_details['filename']; // Ignore example plugins $extension = Table::getInstance('extension'); $extension->set('type', 'plugin'); $extension->set('client_id', 0); $extension->set('element', $element); $extension->set('folder', $folder); $extension->set('name', $manifest_details['name']); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } } return $results; } /** * Refreshes the extension table cache. * * @return boolean Result of operation, true if updated, false on failure. * * @since 3.1 */ public function refreshManifestCache() { /* * Plugins use the extensions table as their primary store * Similar to modules and templates, rather easy * If it's not in the extensions table we just add it */ $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $manifestPath = $client->path . '/plugins/' . $this->parent->extension->folder . '/' . $this->parent->extension->element . '/' . $this->parent->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; if ($this->parent->extension->store()) { return true; } else { Log::add(Text::_('JLIB_INSTALLER_ERROR_PLG_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } } Adapter/TemplateAdapter.php 0000644 00000056031 15172720021 0011713 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Update; use Joomla\Database\ParameterType; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Template installer * * @since 3.1 */ class TemplateAdapter extends InstallerAdapter { /** * The install client ID * * @var integer * @since 3.4 */ protected $clientId; /** * Method to check if the extension is already present in the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExistingExtension() { try { $this->currentExtensionId = $this->extension->find( [ 'element' => $this->element, 'type' => $this->type, 'client_id' => $this->clientId, ] ); } catch (\RuntimeException $e) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . $this->route), $e->getMessage() ), $e->getCode(), $e ); } } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { // Copy all the necessary files if ($this->parent->parseFiles($this->getManifest()->files, -1) === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_TPL_INSTALL_COPY_FILES', 'files' ) ); } if ($this->parent->parseFiles($this->getManifest()->images, -1) === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_TPL_INSTALL_COPY_FILES', 'images' ) ); } if ($this->parent->parseFiles($this->getManifest()->css, -1) === false) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_TPL_INSTALL_COPY_FILES', 'css' ) ); } // If there is a manifest script, let's copy it. if ($this->manifest_script) { $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . strtoupper($this->getRoute())) ) ); } } } } /** * Method to finalise the installation processing * * @return void * * @since 3.1 * @throws \RuntimeException */ protected function finaliseInstall() { // Clobber any possible pending updates /** @var Update $update */ $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->element, 'type' => $this->type, 'client_id' => $this->clientId, ] ); if ($uid) { $update->delete($uid); } // Lastly, we will copy the manifest file to its appropriate place. if ($this->route !== 'discover_install') { if (!$this->parent->copyManifest(-1)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $db = $this->getDatabase(); $query = $db->getQuery(true); $element = $this->extension->element; $clientId = $this->extension->client_id; $extensionId = $this->extension->extension_id; // Set menu that assigned to the template back to default template $subQuery = $db->getQuery(true) ->select($db->quoteName('s.id')) ->from($db->quoteName('#__template_styles', 's')) ->where( [ $db->quoteName('s.template') . ' = :element', $db->quoteName('s.client_id') . ' = :clientId', ] ); $query->bind(':element', $element) ->bind(':clientId', $clientId, ParameterType::INTEGER); $query->update($db->quoteName('#__menu')) ->set($db->quoteName('template_style_id') . ' = 0') ->where($db->quoteName('template_style_id') . ' IN (' . (string) $subQuery . ')'); $db->setQuery($query); $db->execute(); // Remove the template's styles $query = $db->getQuery(true) ->delete($db->quoteName('#__template_styles')) ->where( [ $db->quoteName('template') . ' = :template', $db->quoteName('client_id') . ' = :client_id', ] ) ->bind(':template', $element) ->bind(':client_id', $clientId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Remove the schema version $query = $db->getQuery(true) ->delete($db->quoteName('#__schemas')) ->where($db->quoteName('extension_id') . ' = :extension_id') ->bind(':extension_id', $extensionId, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Remove any overrides $query = $db->getQuery(true) ->delete($db->quoteName('#__template_overrides')) ->where($db->quoteName('template') . ' = :template') ->bind(':template', $element); $db->setQuery($query); $db->execute(); // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->extension->element, 'type' => $this->type, 'client_id' => $this->extension->client_id, ] ); if ($uid) { $update->delete($uid); } $this->extension->delete(); return true; } /** * Custom loadLanguage method * * @param string $path The path where to find language files. * * @return void * * @since 3.1 */ public function loadLanguage($path = null) { $source = $this->parent->getPath('source'); $basePath = $this->parent->extension->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; if (!$source) { $this->parent->setPath('source', $basePath . '/templates/' . $this->parent->extension->element); } $this->setManifest($this->parent->getManifest()); $client = (string) $this->getManifest()->attributes()->client; // Load administrator language if not set. if (!$client) { $client = 'ADMINISTRATOR'; } $base = \constant('JPATH_' . strtoupper($client)); $extension = 'tpl_' . $this->getName(); $source = $path ?: $base . '/templates/' . $this->getName(); $this->doLoadLanguage($extension, $source, $base); } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { $this->parent->parseMedia($this->getManifest()->media); $this->parent->parseLanguages($this->getManifest()->languages, $this->clientId); } /** * Overloaded method to parse queries for template installations * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function parseQueries() { if (\in_array($this->route, ['install', 'discover_install'])) { $db = $this->getDatabase(); $query = $db->getQuery(true); $lang = Factory::getLanguage(); $debug = $lang->setDebug(false); $columns = [ $db->quoteName('template'), $db->quoteName('client_id'), $db->quoteName('home'), $db->quoteName('title'), $db->quoteName('params'), $db->quoteName('inheritable'), $db->quoteName('parent'), ]; $values = $query->bindArray( [ $this->extension->element, $this->extension->client_id, '0', Text::sprintf('JLIB_INSTALLER_DEFAULT_STYLE', Text::_($this->extension->name)), $this->extension->params, (int) $this->manifest->inheritable, (string) $this->manifest->parent ?: '', ], [ ParameterType::STRING, ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER, ParameterType::STRING, ] ); $lang->setDebug($debug); // Insert record in #__template_styles $query->insert($db->quoteName('#__template_styles')) ->columns($columns) ->values(implode(',', $values)); // There is a chance this could fail but we don't care... $db->setQuery($query)->execute(); } } /** * Prepares the adapter for a discover_install task * * @return void * * @since 3.4 */ public function prepareDiscoverInstall() { $client = ApplicationHelper::getClientInfo($this->extension->client_id); $manifestPath = $client->path . '/templates/' . $this->extension->element . '/templateDetails.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $this->setManifest($this->parent->getManifest()); } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { // Remove files $this->parent->removeFiles($this->getManifest()->media); $this->parent->removeFiles($this->getManifest()->languages, $this->extension->client_id); // Delete the template directory if (Folder::exists($this->parent->getPath('extension_root'))) { Folder::delete($this->parent->getPath('extension_root')); } else { Log::add(Text::_('JLIB_INSTALLER_ERROR_TPL_UNINSTALL_TEMPLATE_DIRECTORY'), Log::WARNING, 'jerror'); } } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function setupInstallPaths() { // Get the client application target $cname = (string) $this->getManifest()->attributes()->client; if ($cname) { // Attempt to map the client to a base path $client = ApplicationHelper::getClientInfo($cname, true); if ($client === false) { throw new \RuntimeException(Text::sprintf('JLIB_INSTALLER_ABORT_TPL_INSTALL_UNKNOWN_CLIENT', $cname)); } $basePath = $client->path; $this->clientId = $client->id; } else { // No client attribute was found so we assume the site as the client $basePath = JPATH_SITE; $this->clientId = 0; } // Set the template root path if (empty($this->element)) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MOD_INSTALL_NOFILE', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } $this->parent->setPath('extension_root', $basePath . '/templates/' . $this->element); } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { $this->parent->extension = $this->extension; $db = $this->getDatabase(); $name = $this->extension->element; $clientId = $this->extension->client_id; // For a template the id will be the template name which represents the subfolder of the templates folder that the template resides in. if (!$name) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_TPL_UNINSTALL_TEMPLATE_ID_EMPTY')); } // Deny removing a parent template if there are children $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__template_styles')) ->where( [ $db->quoteName('parent') . ' = :template', $db->quoteName('client_id') . ' = :client_id', ] ) ->bind(':template', $name) ->bind(':client_id', $clientId); $db->setQuery($query); if ($db->loadResult() != 0) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_TPL_UNINSTALL_PARENT_TEMPLATE')); } // Deny remove default template $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__template_styles')) ->where( [ $db->quoteName('home') . ' = ' . $db->quote('1'), $db->quoteName('template') . ' = :template', $db->quoteName('client_id') . ' = :client_id', ] ) ->bind(':template', $name) ->bind(':client_id', $clientId); $db->setQuery($query); if ($db->loadResult() != 0) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_TPL_UNINSTALL_TEMPLATE_DEFAULT')); } // Get the template root path $client = ApplicationHelper::getClientInfo($clientId); if (!$client) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_TPL_UNINSTALL_INVALID_CLIENT')); } $this->parent->setPath('extension_root', $client->path . '/templates/' . strtolower($name)); $this->parent->setPath('source', $this->parent->getPath('extension_root')); // We do findManifest to avoid problem when uninstalling a list of extensions: getManifest cache its manifest file $this->parent->findManifest(); $manifest = $this->parent->getManifest(); if (!($manifest instanceof \SimpleXMLElement)) { // Kill the extension entry $this->extension->delete($this->extension->extension_id); // Make sure we delete the folders Folder::delete($this->parent->getPath('extension_root')); throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_TPL_UNINSTALL_INVALID_NOTFOUND_MANIFEST')); } // Attempt to load the language file; might have uninstall strings $this->loadLanguage(); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { // Discover installs are stored a little differently if ($this->route === 'discover_install') { $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->extension->manifest_cache = json_encode($manifest_details); $this->extension->state = 0; $this->extension->name = $manifest_details['name']; $this->extension->enabled = 1; $this->extension->params = $this->parent->getParams(); $this->extension->changelogurl = (string) $this->manifest->changelogurl; if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_TPL_DISCOVER_STORE_DETAILS')); } return; } // Was there a template already installed with the same name? if ($this->currentExtensionId) { if (!$this->parent->isOverwrite()) { // Install failed, roll back changes throw new \RuntimeException( Text::_('JLIB_INSTALLER_ABORT_TPL_INSTALL_ALREADY_INSTALLED') ); } // Load the entry and update the manifest_cache $this->extension->load($this->currentExtensionId); } else { $this->extension->type = 'template'; $this->extension->element = $this->element; // There is no folder for templates $this->extension->folder = ''; $this->extension->enabled = 1; $this->extension->protected = 0; $this->extension->access = 1; $this->extension->client_id = $this->clientId; $this->extension->params = $this->parent->getParams(); $this->extension->changelogurl = $this->changelogurl; } // Name might change in an update $this->extension->name = $this->name; // Update the manifest cache for the entry $this->extension->manifest_cache = $this->parent->generateManifestCache(); $this->extension->changelogurl = $this->changelogurl; if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ROLLBACK', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), $this->extension->getError() ) ); } } /** * Discover existing but uninstalled templates * * @return array Extension list */ public function discover() { $results = []; $site_list = Folder::folders(JPATH_SITE . '/templates'); $admin_list = Folder::folders(JPATH_ADMINISTRATOR . '/templates'); $site_info = ApplicationHelper::getClientInfo('site', true); $admin_info = ApplicationHelper::getClientInfo('administrator', true); foreach ($site_list as $template) { if (file_exists(JPATH_SITE . "/templates/$template/templateDetails.xml")) { if ($template === 'system') { // Ignore special system template continue; } $manifest_details = Installer::parseXMLInstallFile(JPATH_SITE . "/templates/$template/templateDetails.xml"); $extension = Table::getInstance('extension'); $extension->set('type', 'template'); $extension->set('client_id', $site_info->id); $extension->set('element', $template); $extension->set('folder', ''); $extension->set('name', $template); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } foreach ($admin_list as $template) { if (file_exists(JPATH_ADMINISTRATOR . "/templates/$template/templateDetails.xml")) { if ($template === 'system') { // Ignore special system template continue; } $manifest_details = Installer::parseXMLInstallFile(JPATH_ADMINISTRATOR . "/templates/$template/templateDetails.xml"); $extension = Table::getInstance('extension'); $extension->set('type', 'template'); $extension->set('client_id', $admin_info->id); $extension->set('element', $template); $extension->set('folder', ''); $extension->set('name', $template); $extension->set('state', -1); $extension->set('manifest_cache', json_encode($manifest_details)); $extension->set('params', '{}'); $results[] = $extension; } } return $results; } /** * Refreshes the extension table cache * * @return boolean Result of operation, true if updated, false on failure * * @since 3.1 */ public function refreshManifestCache() { // Need to find to find where the XML file is since we don't store this normally. $client = ApplicationHelper::getClientInfo($this->parent->extension->client_id); $manifestPath = $client->path . '/templates/' . $this->parent->extension->element . '/templateDetails.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; try { return $this->parent->extension->store(); } catch (\RuntimeException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_TPL_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } } Adapter/PackageAdapter.php 0000644 00000057456 15172720021 0011507 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer\Adapter; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filter\InputFilter; use Joomla\CMS\Installer\Installer; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Installer\InstallerHelper; use Joomla\CMS\Installer\Manifest\PackageManifest; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Update; use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\ParameterType; use Joomla\Event\Event; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('JPATH_PLATFORM') or die; // phpcs:enable PSR1.Files.SideEffects /** * Package installer * * @since 3.1 */ class PackageAdapter extends InstallerAdapter { /** * An array of extension IDs for each installed extension * * @var array * @since 3.7.0 */ protected $installedIds = []; /** * The results of each installed extensions * * @var array * @since 3.1 */ protected $results = []; /** * Flag if the adapter supports discover installs * * Adapters should override this and set to false if discover install is unsupported * * @var boolean * @since 3.4 */ protected $supportsDiscoverInstall = false; /** * Method to check if the extension is present in the filesystem, flags the route as update if so * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function checkExtensionInFilesystem() { // If the package manifest already exists, then we will assume that the package is already installed. if (file_exists(JPATH_MANIFESTS . '/packages/' . basename($this->parent->getPath('manifest')))) { // Look for an update function or update tag $updateElement = $this->manifest->update; // Upgrade manually set or update function available or update tag detected if ( $updateElement || $this->parent->isUpgrade() || ($this->parent->manifestClass && method_exists($this->parent->manifestClass, 'update')) ) { // Force this one $this->parent->setOverwrite(true); $this->parent->setUpgrade(true); if ($this->currentExtensionId) { // If there is a matching extension mark this as an update $this->setRoute('update'); } } elseif (!$this->parent->isOverwrite()) { // We didn't have overwrite set, find an update function or find an update tag so lets call it safe throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_DIRECTORY', Text::_('JLIB_INSTALLER_' . $this->route), $this->type, $this->parent->getPath('extension_root') ) ); } } } /** * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function copyBaseFiles() { $folder = (string) $this->getManifest()->files->attributes()->folder; $source = $this->parent->getPath('source'); if ($folder) { $source .= '/' . $folder; } // Install all necessary files if (!\count($this->getManifest()->files->children())) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PACK_INSTALL_NO_FILES', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } $dispatcher = Factory::getApplication()->getDispatcher(); // Add a callback for the `onExtensionAfterInstall` event so we can receive the installed extension ID if (!$dispatcher->hasListener([$this, 'onExtensionAfterInstall'], 'onExtensionAfterInstall')) { $dispatcher->addListener('onExtensionAfterInstall', [$this, 'onExtensionAfterInstall']); } foreach ($this->getManifest()->files->children() as $child) { $file = $source . '/' . (string) $child; if (is_dir($file)) { // If it's actually a directory then fill it up $package = []; $package['dir'] = $file; $package['type'] = InstallerHelper::detectType($file); } else { // If it's an archive $package = InstallerHelper::unpack($file); } $tmpInstaller = new Installer(); $tmpInstaller->setDatabase($this->getDatabase()); $installResult = $tmpInstaller->install($package['dir']); if (!$installResult) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PACK_INSTALL_ERROR_EXTENSION', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)), basename($file) ) ); } $this->results[] = [ 'name' => (string) $tmpInstaller->manifest->name, 'result' => $installResult, ]; } } /** * Method to create the extension root path if necessary * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function createExtensionRoot() { /* * For packages, we only need the extension root if copying manifest files; this step will be handled * at that point if necessary */ } /** * Method to finalise the installation processing * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function finaliseInstall() { // Clobber any possible pending updates /** @var Update $update */ $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } // Set the package ID for each of the installed extensions to track the relationship if (!empty($this->installedIds)) { $db = $this->getDatabase(); $query = $db->getQuery(true) ->update($db->quoteName('#__extensions')) ->set($db->quoteName('package_id') . ' = :id') ->whereIn($db->quoteName('extension_id'), $this->installedIds) ->bind(':id', $this->extension->extension_id, ParameterType::INTEGER); try { $db->setQuery($query)->execute(); } catch (ExecutionFailureException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_PACK_SETTING_PACKAGE_ID'), Log::WARNING, 'jerror'); } } // Lastly, we will copy the manifest file to its appropriate place. $manifest = []; $manifest['src'] = $this->parent->getPath('manifest'); $manifest['dest'] = JPATH_MANIFESTS . '/packages/' . basename($this->parent->getPath('manifest')); if (!$this->parent->copyFiles([$manifest], true)) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_COPY_SETUP', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } // If there is a manifest script, let's copy it. if ($this->manifest_script) { // First, we have to create a folder for the script if one isn't present if (!file_exists($this->parent->getPath('extension_root'))) { if (!Folder::create($this->parent->getPath('extension_root'))) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_CREATE_DIRECTORY', Text::_('JLIB_INSTALLER_' . $this->route), $this->parent->getPath('extension_root') ) ); } /* * Since we created the extension directory and will want to remove it if * we have to roll back the installation, let's add it to the * installation step stack */ $this->parent->pushStep( [ 'type' => 'folder', 'path' => $this->parent->getPath('extension_root'), ] ); } $path = []; $path['src'] = $this->parent->getPath('source') . '/' . $this->manifest_script; $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script; if ($this->parent->isOverwrite() || !file_exists($path['dest'])) { if (!$this->parent->copyFiles([$path])) { // Install failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_MANIFEST', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } } } } /** * Method to finalise the uninstallation processing * * @return boolean * * @since 4.0.0 * @throws \RuntimeException */ protected function finaliseUninstall(): bool { $db = $this->getDatabase(); // Remove the schema version $query = $db->getQuery(true) ->delete($db->quoteName('#__schemas')) ->where($db->quoteName('extension_id') . ' = :extension_id') ->bind(':extension_id', $this->extension->extension_id, ParameterType::INTEGER); $db->setQuery($query); $db->execute(); // Clobber any possible pending updates $update = Table::getInstance('update'); $uid = $update->find( [ 'element' => $this->extension->element, 'type' => $this->type, ] ); if ($uid) { $update->delete($uid); } $file = JPATH_MANIFESTS . '/packages/' . $this->extension->element . '.xml'; if (is_file($file)) { File::delete($file); } $folder = $this->parent->getPath('extension_root'); if (Folder::exists($folder)) { Folder::delete($folder); } $this->extension->delete(); return true; } /** * Get the filtered extension element from the manifest * * @param string $element Optional element name to be converted * * @return string The filtered element * * @since 3.4 */ public function getElement($element = null) { if (!$element) { // Ensure the element is a string $element = (string) $this->getManifest()->packagename; // Filter the name for illegal characters $element = 'pkg_' . InputFilter::getInstance()->clean($element, 'cmd'); } return $element; } /** * Load language from a path * * @param string $path The path of the language. * * @return void * * @since 3.1 */ public function loadLanguage($path) { $this->doLoadLanguage($this->getElement(), $path); } /** * Handler for the `onExtensionAfterInstall` event * * @param Event $event The event * * @return void * * @since 3.7.0 */ public function onExtensionAfterInstall(Event $event) { if ($event->getArgument('eid', false) !== false) { $this->installedIds[] = $event->getArgument('eid'); } } /** * Method to parse optional tags in the manifest * * @return void * * @since 3.4 */ protected function parseOptionalTags() { $this->parent->parseLanguages($this->getManifest()->languages); } /** * Removes this extension's files * * @return void * * @since 4.0.0 * @throws \RuntimeException */ protected function removeExtensionFiles() { $manifest = new PackageManifest(JPATH_MANIFESTS . '/packages/' . $this->extension->element . '.xml'); $error = false; foreach ($manifest->filelist as $extension) { $tmpInstaller = new Installer(); $tmpInstaller->setDatabase($this->getDatabase()); $tmpInstaller->setPackageUninstall(true); $id = $this->_getExtensionId($extension->type, $extension->id, $extension->client, $extension->group); if ($id) { if (!$tmpInstaller->uninstall($extension->type, $id)) { $error = true; Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_PACK_UNINSTALL_NOT_PROPER', basename($extension->filename)), Log::WARNING, 'jerror'); } } else { Log::add(Text::sprintf('JLIB_INSTALLER_ERROR_PACK_UNINSTALL_MISSING_EXTENSION', basename($extension->filename)), Log::WARNING, 'jerror'); } } // Remove any language files $this->parent->removeFiles($this->getManifest()->languages); // Clean up manifest file after we're done if there were no errors if ($error) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_PACK_UNINSTALL_MANIFEST_NOT_REMOVED')); } } /** * Method to do any prechecks and setup the install paths for the extension * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function setupInstallPaths() { $packagepath = (string) $this->getManifest()->packagename; if (empty($packagepath)) { throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PACK_INSTALL_NO_PACK', Text::_('JLIB_INSTALLER_' . strtoupper($this->route)) ) ); } $this->parent->setPath('extension_root', JPATH_MANIFESTS . '/packages/' . $packagepath); } /** * Method to do any prechecks and setup the uninstall job * * @return void * * @since 4.0.0 */ protected function setupUninstall() { $manifestFile = JPATH_MANIFESTS . '/packages/' . $this->extension->element . '.xml'; $manifest = new PackageManifest($manifestFile); // Set the package root path $this->parent->setPath('extension_root', JPATH_MANIFESTS . '/packages/' . $manifest->packagename); // Set the source path for compatibility with the API $this->parent->setPath('source', $this->parent->getPath('extension_root')); // Because packages may not have their own folders we cannot use the standard method of finding an installation manifest if (!file_exists($manifestFile)) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_PACK_UNINSTALL_MISSINGMANIFEST')); } $xml = simplexml_load_file($manifestFile); if (!$xml) { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_PACK_UNINSTALL_LOAD_MANIFEST')); } // Check for a valid XML root tag. if ($xml->getName() !== 'extension') { throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_PACK_UNINSTALL_INVALID_MANIFEST')); } $this->setManifest($xml); // Attempt to load the language file; might have uninstall strings $this->loadLanguage(JPATH_SITE); } /** * Method to store the extension to the database * * @return void * * @since 3.4 * @throws \RuntimeException */ protected function storeExtension() { if ($this->currentExtensionId) { if (!$this->parent->isOverwrite()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_ALREADY_EXISTS', Text::_('JLIB_INSTALLER_' . $this->route), $this->name ) ); } $this->extension->load($this->currentExtensionId); $this->extension->name = $this->name; } else { $this->extension->name = $this->name; $this->extension->type = 'package'; $this->extension->element = $this->element; $this->extension->changelogurl = $this->changelogurl; // There is no folder for packages $this->extension->folder = ''; $this->extension->enabled = 1; $this->extension->protected = 0; $this->extension->access = 1; $this->extension->client_id = 0; $this->extension->params = $this->parent->getParams(); } // Update the manifest cache for the entry $this->extension->manifest_cache = $this->parent->generateManifestCache(); if (!$this->extension->store()) { // Install failed, roll back changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_PACK_INSTALL_ROLLBACK', $this->extension->getError() ) ); } // Since we have created a package item, we add it to the installation step stack // so that if we have to rollback the changes we can undo it. $this->parent->pushStep(['type' => 'extension', 'id' => $this->extension->extension_id]); } /** * Executes a custom install script method * * @param string $method The install method to execute * * @return boolean True on success * * @since 3.4 */ protected function triggerManifestScript($method) { ob_start(); ob_implicit_flush(false); if ($this->parent->manifestClass && method_exists($this->parent->manifestClass, $method)) { switch ($method) { // The preflight method takes the route as a param case 'preflight': if ($this->parent->manifestClass->$method($this->route, $this) === false) { // The script failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } break; // The postflight method takes the route and a results array as params case 'postflight': $this->parent->manifestClass->$method($this->route, $this, $this->results); break; // The install, uninstall, and update methods only pass this object as a param case 'install': case 'uninstall': case 'update': if ($this->parent->manifestClass->$method($this) === false) { if ($method !== 'uninstall') { // The script failed, rollback changes throw new \RuntimeException( Text::sprintf( 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', Text::_('JLIB_INSTALLER_' . $this->route) ) ); } } break; } } // Append to the message object $this->extensionMessage .= ob_get_clean(); // If in postflight or uninstall, set the message for display if (($method === 'uninstall' || $method === 'postflight') && $this->extensionMessage !== '') { $this->parent->set('extension_message', $this->extensionMessage); } return true; } /** * Gets the extension id. * * @param string $type The extension type. * @param string $id The name of the extension (the element field). * @param integer $client The application id (0: Joomla CMS site; 1: Joomla CMS administrator). * @param string $group The extension group (mainly for plugins). * * @return integer * * @since 3.1 */ protected function _getExtensionId($type, $id, $client, $group) { $db = $this->getDatabase(); $query = $db->getQuery(true) ->select($db->quoteName('extension_id')) ->from($db->quoteName('#__extensions')) ->where( [ $db->quoteName('type') . ' = :type', $db->quoteName('element') . ' = :element', ] ) ->bind(':type', $type) ->bind(':element', $id); switch ($type) { case 'plugin': // Plugins have a folder but not a client $query->where('folder = :folder') ->bind(':folder', $group); break; case 'library': case 'package': case 'component': // Components, packages and libraries don't have a folder or client. // Included for completeness. break; case 'language': case 'module': case 'template': // Languages, modules and templates have a client but not a folder $clientId = ApplicationHelper::getClientInfo($client, true)->id; $query->where('client_id = :client_id') ->bind(':client_id', $clientId, ParameterType::INTEGER); break; } $db->setQuery($query); // Note: For templates, libraries and packages their unique name is their key. // This means they come out the same way they came in. return $db->loadResult(); } /** * Refreshes the extension table cache * * @return boolean Result of operation, true if updated, false on failure * * @since 3.1 */ public function refreshManifestCache() { // Need to find to find where the XML file is since we don't store this normally $manifestPath = JPATH_MANIFESTS . '/packages/' . $this->parent->extension->element . '.xml'; $this->parent->manifest = $this->parent->isManifest($manifestPath); $this->parent->setPath('manifest', $manifestPath); $manifest_details = Installer::parseXMLInstallFile($this->parent->getPath('manifest')); $this->parent->extension->manifest_cache = json_encode($manifest_details); $this->parent->extension->name = $manifest_details['name']; try { return $this->parent->extension->store(); } catch (\RuntimeException $e) { Log::add(Text::_('JLIB_INSTALLER_ERROR_PACK_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror'); return false; } } } InstallerScript.php 0000644 00000030670 15172720021 0010402 0 ustar 00 <?php /** * Joomla! Content Management System * * @copyright (C) 2016 Open Source Matters, Inc. <https://www.joomla.org> * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\CMS\Installer; use Joomla\CMS\Factory; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Language\Text; use Joomla\CMS\Log\Log; use Joomla\Database\ParameterType; use Joomla\Filesystem\File; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * Base install script for use by extensions providing helper methods for common behaviours. * * @since 3.6 */ class InstallerScript { /** * The version number of the extension. * * @var string * @since 3.6 */ protected $release; /** * The table the parameters are stored in. * * @var string * @since 3.6 */ protected $paramTable; /** * The extension name. This should be set in the installer script. * * @var string * @since 3.6 */ protected $extension; /** * A list of files to be deleted * * @var array * @since 3.6 */ protected $deleteFiles = []; /** * A list of folders to be deleted * * @var array * @since 3.6 */ protected $deleteFolders = []; /** * A list of CLI script files to be copied to the cli directory * * @var array * @since 3.6 */ protected $cliScriptFiles = []; /** * Minimum PHP version required to install the extension * * @var string * @since 3.6 */ protected $minimumPhp; /** * Minimum Joomla! version required to install the extension * * @var string * @since 3.6 */ protected $minimumJoomla; /** * Allow downgrades of your extension * * Use at your own risk as if there is a change in functionality people may wish to downgrade. * * @var boolean * @since 3.6 */ protected $allowDowngrades = false; /** * Function called before extension installation/update/removal procedure commences * * @param string $type The type of change (install, update or discover_install, not uninstall) * @param InstallerAdapter $parent The class calling this method * * @return boolean True on success * * @since 3.6 */ public function preflight($type, $parent) { // Check for the minimum PHP version before continuing if (!empty($this->minimumPhp) && version_compare(PHP_VERSION, $this->minimumPhp, '<')) { Log::add(Text::sprintf('JLIB_INSTALLER_MINIMUM_PHP', $this->minimumPhp), Log::WARNING, 'jerror'); return false; } // Check for the minimum Joomla version before continuing if (!empty($this->minimumJoomla) && version_compare(JVERSION, $this->minimumJoomla, '<')) { Log::add(Text::sprintf('JLIB_INSTALLER_MINIMUM_JOOMLA', $this->minimumJoomla), Log::WARNING, 'jerror'); return false; } // Extension manifest file version $this->extension = $parent->getName(); $this->release = $parent->getManifest()->version; $extensionType = substr($this->extension, 0, 3); // Modules parameters are located in the module table - else in the extension table if ($extensionType === 'mod') { $this->paramTable = '#__modules'; } else { $this->paramTable = '#__extensions'; } // Abort if the extension being installed is not newer than the currently installed version if (!$this->allowDowngrades && strtolower($type) === 'update') { $manifest = $this->getItemArray('manifest_cache', '#__extensions', 'name', $this->extension); // Check whether we have an old release installed and skip this check when this here is the initial install. if (!isset($manifest['version'])) { return true; } $oldRelease = $manifest['version']; if (version_compare($this->release, $oldRelease, '<')) { Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_INSTALLER_INCORRECT_SEQUENCE', $oldRelease, $this->release), 'error'); return false; } } return true; } /** * Gets each instance of a module in the #__modules table * * @param boolean $isModule True if the extension is a module as this can have multiple instances * * @return array An array of ID's of the extension * * @since 3.6 */ public function getInstances($isModule) { $extension = $this->extension; $db = Factory::getDbo(); $query = $db->getQuery(true); // Select the item(s) and retrieve the id if ($isModule) { $query->select($db->quoteName('id')) ->from($db->quoteName('#__modules')) ->where($db->quoteName('module') . ' = :extension'); } else { $query->select($db->quoteName('extension_id', 'id')) ->from($db->quoteName('#__extensions')) ->where($db->quoteName('element') . ' = :extension'); } $query->bind(':extension', $extension); // Set the query and obtain an array of id's return $db->setQuery($query)->loadColumn(); } /** * Gets parameter value in the extensions row of the extension table * * @param string $name The name of the parameter to be retrieved * @param integer $id The id of the item in the Param Table * * @return string The parameter desired * * @since 3.6 */ public function getParam($name, $id = 0) { if (!\is_int($id) || $id == 0) { // Return false if there is no item given return false; } $params = $this->getItemArray('params', $this->paramTable, 'id', $id); return $params[$name]; } /** * Sets parameter values in the extensions row of the extension table. Note that the * this must be called separately for deleting and editing. Note if edit is called as a * type then if the param doesn't exist it will be created * * @param array $paramArray The array of parameters to be added/edited/removed * @param string $type The type of change to be made to the param (edit/remove) * @param integer $id The id of the item in the relevant table * * @return boolean True on success * * @since 3.6 */ public function setParams($paramArray = null, $type = 'edit', $id = 0) { if (!\is_int($id) || $id == 0) { // Return false if there is no valid item given return false; } $params = $this->getItemArray('params', $this->paramTable, 'id', $id); if ($paramArray) { foreach ($paramArray as $name => $value) { if ($type === 'edit') { // Add or edit the new variable(s) to the existing params if (\is_array($value)) { // Convert an array into a json encoded string $params[(string) $name] = array_values($value); } else { $params[(string) $name] = (string) $value; } } elseif ($type === 'remove') { // Unset the parameter from the array unset($params[(string) $name]); } } } // Store the combined new and existing values back as a JSON string $paramsString = json_encode($params); $db = Factory::getDbo(); $query = $db->getQuery(true) ->update($db->quoteName($this->paramTable)) ->set('params = :params') ->where('id = :id') ->bind(':params', $paramsString) ->bind(':id', $id, ParameterType::INTEGER); // Update table $db->setQuery($query)->execute(); return true; } /** * Builds a standard select query to produce better DRY code in this script. * This should produce a single unique cell which is json encoded - it will then * return an associated array with this data in. * * @param string $element The element to get from the query * @param string $table The table to search for the data in * @param string $column The column of the database to search from * @param mixed $identifier The integer id or the string * * @return array Associated array containing data from the cell * * @since 3.6 */ public function getItemArray($element, $table, $column, $identifier) { // Get the DB and query objects $db = Factory::getDbo(); $paramType = is_numeric($identifier) ? ParameterType::INTEGER : ParameterType::STRING; // Build the query $query = $db->getQuery(true) ->select($db->quoteName($element)) ->from($db->quoteName($table)) ->where($db->quoteName($column) . ' = :id') ->bind(':id', $identifier, $paramType); $db->setQuery($query); // Load the single cell and json_decode data $result = $db->loadResult(); return $result === null ? [] : json_decode($result, true); } /** * Remove the files and folders in the given array from * * @return void * * @since 3.6 */ public function removeFiles() { if (!empty($this->deleteFiles)) { foreach ($this->deleteFiles as $file) { if (is_file(JPATH_ROOT . $file) && !File::delete(JPATH_ROOT . $file)) { echo Text::sprintf('JLIB_INSTALLER_ERROR_FILE_FOLDER', $file) . '<br>'; } } } if (!empty($this->deleteFolders)) { foreach ($this->deleteFolders as $folder) { if (Folder::exists(JPATH_ROOT . $folder) && !Folder::delete(JPATH_ROOT . $folder)) { echo Text::sprintf('JLIB_INSTALLER_ERROR_FILE_FOLDER', $folder) . '<br>'; } } } } /** * Moves the CLI scripts into the CLI folder in the CMS * * @return void * * @since 3.6 */ public function moveCliFiles() { if (!empty($this->cliScriptFiles)) { foreach ($this->cliScriptFiles as $file) { $name = basename($file); if (file_exists(JPATH_ROOT . $file) && !File::move(JPATH_ROOT . $file, JPATH_ROOT . '/cli/' . $name)) { echo Text::sprintf('JLIB_INSTALLER_FILE_ERROR_MOVE', $name); } } } } /** * Creates the dashboard menu module * * @param string $dashboard The name of the dashboard * @param string $preset The name of the menu preset * * @return void * * @throws \Exception * @since 4.0.0 */ public function addDashboardMenu(string $dashboard, string $preset) { $model = Factory::getApplication()->bootComponent('com_modules')->getMVCFactory()->createModel('Module', 'Administrator', ['ignore_request' => true]); $module = [ 'id' => 0, 'asset_id' => 0, 'language' => '*', 'note' => '', 'published' => 1, 'assignment' => 0, 'client_id' => 1, 'showtitle' => 0, 'content' => '', 'module' => 'mod_submenu', 'position' => 'cpanel-' . $dashboard, ]; // Try to get a translated module title, otherwise fall back to a fixed string. $titleKey = strtoupper('COM_' . $this->extension . '_DASHBOARD_' . $dashboard . '_TITLE'); $title = Text::_($titleKey); $module['title'] = ($title === $titleKey) ? ucfirst($dashboard) . ' Dashboard' : $title; $module['access'] = (int) Factory::getApplication()->get('access', 1); $module['params'] = [ 'menutype' => '*', 'preset' => $preset, 'style' => 'System-none', ]; if (!$model->save($module)) { Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_INSTALLER_ERROR_COMP_INSTALL_FAILED_TO_CREATE_DASHBOARD', $model->getError())); } } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.23 | Generation time: 0.01 |
proxy
|
phpinfo
|
Settings