* @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\File; 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\DatabaseDriver; use Joomla\Database\DatabaseInterface; use Joomla\Database\Exception\ExecutionFailureException; use Joomla\Database\Exception\PrepareStatementFailureException; use Joomla\Database\ParameterType; use Joomla\DI\ContainerAwareInterface; // 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 = array(); /** * 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 = array(); /** * 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 $stepval = File::delete($step['path']); break; case 'folder': // Remove the folder $stepval = Folder::delete($step['path']); 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', array( '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', array('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 = array('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', array( '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', array('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 = array(); 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', array('type' => $this->manifest->attributes()->type, 'manifest' => $this->manifest) ); // Run the update $result = $adapter->update(); // Fire the onExtensionAfterUpdate Factory::getApplication()->triggerEvent( 'onExtensionAfterUpdate', array('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 = array('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', array('eid' => $identifier) ); // Run the uninstall $result = $adapter->uninstall($identifier); // Fire the onExtensionAfterInstall Factory::getApplication()->triggerEvent( 'onExtensionAfterUninstall', array('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 = array('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(array($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 (ExecutionFailureException | PrepareStatementFailureException $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 { /** @var DatabaseDriver $db */ $db = Factory::getContainer()->get('DatabaseDriver'); $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 = array(); // 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::delete($destination . '/' . $deleted_folder); } foreach ($deletions['files'] as $deleted_file) { File::delete($destination . '/' . $deleted_file); } } } $path = array(); // 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 = array(); // 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. * en-US.mycomponent.ini * 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['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['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 = array(); // 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['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 = array(); // 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 = array('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 = array('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. * en_US.mycomponent.ini * 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(array($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 = array(); // The folders that are new $folders = array(); // The folders of the files that are new $containers = array(); // A list of files to delete $files_deleted = array(); // A list of folders to delete $folders_deleted = array(); 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 array('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 = array(); 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 = array(); $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; } 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(), array $custom = array()) { $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); // 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 = array()) { $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; } }