joomla_test/libraries/cms/installer/installer.php

2151 lines
49 KiB
PHP
Raw Permalink Normal View History

2020-01-02 22:20:31 +07:00
<?php
/**
* @package Joomla.Libraries
* @subpackage Installer
*
* @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
defined('JPATH_PLATFORM') or die;
jimport('joomla.filesystem.file');
jimport('joomla.filesystem.folder');
jimport('joomla.filesystem.path');
jimport('joomla.base.adapter');
/**
* Joomla base installer class
*
* @package Joomla.Libraries
* @subpackage Installer
* @since 3.1
*/
class JInstaller extends JAdapter
{
/**
* Array of paths needed by the installer
*
* @var array
* @since 12.1
*/
protected $paths = array();
/**
* True if package is an upgrade
*
* @var boolean
* @since 12.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 12.1
*/
protected $overwrite = false;
/**
* Stack of installation steps
* - Used for installation rollback
*
* @var array
* @since 12.1
*/
protected $stepStack = array();
/**
* Extension Table Entry
*
* @var JTableExtension
* @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;
/**
* @var JInstaller JInstaller instance container.
* @since 3.1
*/
protected static $instance;
/**
* Constructor
*
* @since 3.1
*/
public function __construct()
{
parent::__construct(__DIR__, 'JInstallerAdapter', __DIR__ . '/adapter');
// Override the default adapter folder
$this->_adapterfolder = 'adapter';
}
/**
* Returns the global Installer object, only creating it
* if it doesn't already exist.
*
* @return JInstaller An installer object
*
* @since 3.1
*/
public static function getInstance()
{
if (!isset(self::$instance))
{
self::$instance = new JInstaller;
}
return self::$instance;
}
/**
* 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 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 object 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
* @throws RuntimeException
*/
public function abort($msg = null, $type = null)
{
$retval = true;
$step = array_pop($this->stepStack);
// Raise abort warning
if ($msg)
{
JLog::add($msg, JLog::WARNING, 'jerror');
}
while ($step != null)
{
switch ($step['type'])
{
case 'file':
// Remove the file
$stepval = JFile::delete($step['path']);
break;
case 'folder':
// Remove the folder
$stepval = JFolder::delete($step['path']);
break;
case 'query':
// Placeholder in case this is necessary in the future
// $stepval is always false because if this step was called it invariably failed
$stepval = false;
break;
case 'extension':
// Get database connector object
$db = $this->getDBO();
$query = $db->getQuery(true);
// Remove the entry from the #__extensions table
$query->delete($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . (int) $step['id']);
$db->setQuery($query);
$stepval = $db->execute();
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);
}
$conf = JFactory::getConfig();
$debug = $conf->get('debug');
if ($debug)
{
throw new RuntimeException('Installation unexpectedly terminated: ' . $msg, 500);
}
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 && JFolder::exists($path))
{
$this->setPath('source', $path);
}
else
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_NOINSTALLPATH'));
return false;
}
if (!$this->setupInstall())
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'));
return false;
}
$type = (string) $this->manifest->attributes()->type;
if (is_object($this->_adapters[$type]))
{
// Add the languages from the package itself
if (method_exists($this->_adapters[$type], 'loadLanguage'))
{
$this->_adapters[$type]->loadLanguage($path);
}
// Fire the onExtensionBeforeInstall event.
JPluginHelper::importPlugin('extension');
$dispatcher = JEventDispatcher::getInstance();
$dispatcher->trigger(
'onExtensionBeforeInstall',
array('method' => 'install', 'type' => $type, 'manifest' => $this->manifest, 'extension' => 0)
);
// Run the install
$result = $this->_adapters[$type]->install();
// Fire the onExtensionAfterInstall
$dispatcher->trigger(
'onExtensionAfterInstall',
array('installer' => clone $this, 'eid' => $result)
);
if ($result !== false)
{
return true;
}
else
{
return false;
}
}
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->extension = JTable::getInstance('extension');
if (!$this->extension->load($eid))
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS'));
return false;
}
if ($this->extension->state != -1)
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_ALREADYINSTALLED'));
return false;
}
// Lazy load the adapter
if (!isset($this->_adapters[$this->extension->type]) || !is_object($this->_adapters[$this->extension->type]))
{
if (!$this->setAdapter($this->extension->type))
{
return false;
}
}
if (is_object($this->_adapters[$this->extension->type]))
{
if (method_exists($this->_adapters[$this->extension->type], 'discover_install'))
{
// Add the languages from the package itself
if (method_exists($this->_adapters[$this->extension->type], 'loadLanguage'))
{
$this->_adapters[$this->extension->type]->loadLanguage();
}
// Fire the onExtensionBeforeInstall event.
JPluginHelper::importPlugin('extension');
$dispatcher = JEventDispatcher::getInstance();
$dispatcher->trigger(
'onExtensionBeforeInstall',
array(
'method' => 'discover_install',
'type' => $this->extension->get('type'),
'manifest' => null,
'extension' => $this->extension->get('extension_id')
)
);
// Run the install
$result = $this->_adapters[$this->extension->type]->discover_install();
// Fire the onExtensionAfterInstall
$dispatcher->trigger(
'onExtensionAfterInstall',
array('installer' => clone $this, 'eid' => $result)
);
if ($result !== false)
{
return true;
}
else
{
return false;
}
}
else
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_METHODNOTSUPPORTED'));
return false;
}
}
return false;
}
else
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_EXTENSIONNOTVALID'));
return false;
}
}
/**
* Extension discover method
* Asks each adapter to find extensions
*
* @return array JExtension
*
* @since 3.1
*/
public function discover()
{
$this->loadAllAdapters();
$results = array();
foreach ($this->_adapters as $adapter)
{
// Joomla! 1.5 installation adapter legacy support
if (method_exists($adapter, 'discover'))
{
$tmp = $adapter->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 && JFolder::exists($path))
{
$this->setPath('source', $path);
}
else
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_NOUPDATEPATH'));
return false;
}
if (!$this->setupInstall())
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'));
return false;
}
$type = (string) $this->manifest->attributes()->type;
if (is_object($this->_adapters[$type]))
{
// Add the languages from the package itself
if (method_exists($this->_adapters[$type], 'loadLanguage'))
{
$this->_adapters[$type]->loadLanguage($path);
}
// Fire the onExtensionBeforeUpdate event.
JPluginHelper::importPlugin('extension');
$dispatcher = JEventDispatcher::getInstance();
$dispatcher->trigger('onExtensionBeforeUpdate', array('type' => $type, 'manifest' => $this->manifest));
// Run the update
$result = $this->_adapters[$type]->update();
// Fire the onExtensionAfterUpdate
$dispatcher->trigger(
'onExtensionAfterUpdate',
array('installer' => clone $this, 'eid' => $result)
);
if ($result !== false)
{
return true;
}
else
{
return false;
}
}
return false;
}
/**
* Package uninstallation method
*
* @param string $type Package type
* @param mixed $identifier Package identifier for adapter
* @param integer $cid Application ID; deprecated in 1.6
*
* @return boolean True if successful
*
* @since 3.1
*/
public function uninstall($type, $identifier, $cid = 0)
{
if (!isset($this->_adapters[$type]) || !is_object($this->_adapters[$type]))
{
if (!$this->setAdapter($type))
{
// We failed to get the right adapter
return false;
}
}
if (is_object($this->_adapters[$type]))
{
// We don't load languages here, we get the extension adapter to work it out
// Fire the onExtensionBeforeUninstall event.
JPluginHelper::importPlugin('extension');
$dispatcher = JEventDispatcher::getInstance();
$dispatcher->trigger('onExtensionBeforeUninstall', array('eid' => $identifier));
// Run the uninstall
$result = $this->_adapters[$type]->uninstall($identifier);
// Fire the onExtensionAfterInstall
$dispatcher->trigger(
'onExtensionAfterUninstall',
array('installer' => clone $this, 'eid' => $identifier, 'result' => $result)
);
return $result;
}
return false;
}
/**
* Refreshes the manifest cache stored in #__extensions
*
* @param integer $eid Extension ID
*
* @return mixed void on success, false on error @todo missing return value ?
*
* @since 3.1
*/
public function refreshManifestCache($eid)
{
if ($eid)
{
$this->extension = JTable::getInstance('extension');
if (!$this->extension->load($eid))
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_LOAD_DETAILS'));
return false;
}
if ($this->extension->state == -1)
{
$this->abort(JText::_('JLIB_INSTALLER_ABORT_REFRESH_MANIFEST_CACHE'));
return false;
}
// Lazy load the adapter
if (!isset($this->_adapters[$this->extension->type]) || !is_object($this->_adapters[$this->extension->type]))
{
if (!$this->setAdapter($this->extension->type))
{
return false;
}
}
if (is_object($this->_adapters[$this->extension->type]))
{
if (method_exists($this->_adapters[$this->extension->type], 'refreshManifestCache'))
{
$result = $this->_adapters[$this->extension->type]->refreshManifestCache();
if ($result !== false)
{
return true;
}
else
{
return false;
}
}
else
{
$this->abort(JText::sprintf('JLIB_INSTALLER_ABORT_METHODNOTSUPPORTED_TYPE', $this->extension->type));
return false;
}
}
return false;
}
else
{
$this->abort(JText::_('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.
*
* @return boolean True on success
*
* @since 3.1
*/
public function setupInstall()
{
// 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;
// Lazy load the adapter
if (!isset($this->_adapters[$type]) || !is_object($this->_adapters[$type]))
{
if (!$this->setAdapter($type))
{
return false;
}
}
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;
}
// Process each query in the $queries array (children of $tagName).
foreach ($queries as $query)
{
$db->setQuery($query->data());
if (!$db->execute())
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $db->stderr(true)), JLog::WARNING, 'jerror');
return false;
}
}
return (int) count($queries);
}
/**
* 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;
}
$queries = array();
$db = & $this->_db;
$dbDriver = strtolower($db->name);
if ($dbDriver == 'mysqli')
{
$dbDriver = 'mysql';
}
// 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 = 'mysql';
}
if ($fCharset == 'utf8' && $fDriver == $dbDriver)
{
$sqlfile = $this->getPath('extension_root') . '/' . $file;
// Check that sql files exists before reading. Otherwise raise error for rollback
if (!file_exists($sqlfile))
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $db->stderr(true)), JLog::WARNING, 'jerror');
return false;
}
$buffer = file_get_contents($sqlfile);
// Graceful exit and rollback if read not successful
if ($buffer === false)
{
JLog::add(JText::_('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), JLog::WARNING, 'jerror');
return false;
}
// Create an array of queries from the sql file
$queries = JDatabaseDriver::splitSql($buffer);
if (count($queries) == 0)
{
// No queries to process
return 0;
}
// Process each query in the $queries array (split out of sql file).
foreach ($queries as $query)
{
$query = trim($query);
if ($query != '' && $query{0} != '#')
{
$db->setQuery($query);
if (!$db->execute())
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $db->stderr(true)), JLog::WARNING, 'jerror');
return false;
}
}
}
}
}
return (int) count($queries);
}
/**
* 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 = JFactory::getDbo();
$schemapaths = $schema->children();
if (!$schemapaths)
{
return;
}
if (count($schemapaths))
{
$dbDriver = strtolower($db->name);
if ($dbDriver == 'mysqli')
{
$dbDriver = 'mysql';
}
$schemapath = '';
foreach ($schemapaths as $entry)
{
$attrs = $entry->attributes();
if ($attrs['type'] == $dbDriver)
{
$schemapath = $entry;
break;
}
}
if (strlen($schemapath))
{
$files = str_replace('.sql', '', JFolder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$'));
usort($files, 'version_compare');
// Update the database
$query = $db->getQuery(true)
->delete('#__schemas')
->where('extension_id = ' . $eid);
$db->setQuery($query);
if ($db->execute())
{
$query->clear()
->insert($db->quoteName('#__schemas'))
->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id')))
->values($eid . ', ' . $db->quote(end($files)));
$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 Result of the operations
*
* @since 3.1
*/
public function parseSchemaUpdates(SimpleXMLElement $schema, $eid)
{
$update_count = 0;
// Ensure we have an XML element and a valid extension id
if ($eid && $schema)
{
$db = JFactory::getDbo();
$schemapaths = $schema->children();
if (count($schemapaths))
{
$dbDriver = strtolower($db->name);
if ($dbDriver == 'mysqli')
{
$dbDriver = 'mysql';
}
$schemapath = '';
foreach ($schemapaths as $entry)
{
$attrs = $entry->attributes();
if ($attrs['type'] == $dbDriver)
{
$schemapath = $entry;
break;
}
}
if (strlen($schemapath))
{
$files = str_replace('.sql', '', JFolder::files($this->getPath('extension_root') . '/' . $schemapath, '\.sql$'));
usort($files, 'version_compare');
if (!count($files))
{
return false;
}
$query = $db->getQuery(true)
->select('version_id')
->from('#__schemas')
->where('extension_id = ' . $eid);
$db->setQuery($query);
$version = $db->loadResult();
if ($version)
{
// We have a version!
foreach ($files as $file)
{
if (version_compare($file, $version) > 0)
{
$buffer = file_get_contents($this->getPath('extension_root') . '/' . $schemapath . '/' . $file . '.sql');
// Graceful exit and rollback if read not successful
if ($buffer === false)
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_SQL_READBUFFER'), JLog::WARNING, 'jerror');
return false;
}
// Create an array of queries from the sql file
$queries = JDatabaseDriver::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 $q)
{
$q = trim($q);
if ($q != '' && $q{0} != '#')
{
$db->setQuery($q);
if (!$db->execute())
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_SQL_ERROR', $db->stderr(true)), JLog::WARNING, 'jerror');
return false;
}
$update_count++;
}
}
}
}
}
// Update the database
$query->clear()
->delete('#__schemas')
->where('extension_id = ' . $eid);
$db->setQuery($query);
if ($db->execute())
{
$query->clear()
->insert($db->quoteName('#__schemas'))
->columns(array($db->quoteName('extension_id'), $db->quoteName('version_id')))
->values($eid . ', ' . $db->quote(end($files)));
$db->setQuery($query);
$db->execute();
}
}
}
}
return $update_count;
}
/**
* 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 = JApplicationHelper::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)
{
JFolder::delete($destination . '/' . $deleted_folder);
}
foreach ($deletions['files'] as $deleted_file)
{
JFile::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 exits and if it doesn't,
* we need to create it.
*/
if (basename($path['dest']) != $path['dest'])
{
$newdir = dirname($path['dest']);
if (!JFolder::create($newdir))
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_CREATE_DIRECTORY', $newdir), JLog::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 = JApplicationHelper::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['src'] = $source . '/' . $file;
if ((string) $file->attributes()->client != '')
{
// Override the client
$langclient = JApplicationHelper::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 (!JFolder::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 exits and if it doesn't,
* we need to create it.
*/
if (basename($path['dest']) != $path['dest'])
{
$newdir = dirname($path['dest']);
if (!JFolder::create($newdir))
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_CREATE_DIRECTORY', $newdir), JLog::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 = JPath::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 exits and if it doesn't,
* we need to create it.
*/
if (basename($path['dest']) != $path['dest'])
{
$newdir = dirname($path['dest']);
if (!JFolder::create($newdir))
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_CREATE_DIRECTORY', $newdir), JLog::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 INI
* string for its default parameters, and return the INI string.
*
* @return string INI string of parameter values
*
* @since 3.1
*/
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 null;
}
// 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 (is_null($overwrite) || !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 = JPath::clean($file['src']);
$filedest = JPath::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.
*/
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_NO_FILE', $filesource), JLog::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.
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_FILE_EXISTS', $filedest), JLog::WARNING, 'jerror');
return false;
}
else
{
// Copy the folder or file to the new location.
if ($filetype == 'folder')
{
if (!(JFolder::copy($filesource, $filedest, null, $overwrite)))
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FOLDER', $filesource, $filedest), JLog::WARNING, 'jerror');
return false;
}
$step = array('type' => 'folder', 'path' => $filedest);
}
else
{
if (!(JFile::copy($filesource, $filedest, null)))
{
JLog::add(JText::sprintf('JLIB_INSTALLER_ERROR_FAIL_COPY_FILE', $filesource, $filedest), JLog::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 = JApplicationHelper::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 = JApplicationHelper::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 = JApplicationHelper::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 (!JFolder::exists(dirname($path)))
{
continue;
}
}
else
{
$path = $source . '/' . $file;
}
// Actually delete the files/folders
if (is_dir($path))
{
$val = JFolder::delete($path);
}
else
{
$val = JFile::delete($path);
}
if ($val === false)
{
JLog::add('Failed to delete ' . $path, JLog::WARNING, 'jerror');
$retval = false;
}
}
if (!empty($folder))
{
JFolder::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 = JApplicationHelper::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()
{
// Main folder manifests (higher priority)
$parentXmlfiles = JFolder::files($this->getPath('source'), '.xml$', false, true);
// Search for children manifests (lower priority)
$allXmlFiles = JFolder::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 (!is_null($manifest))
{
// 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
JLog::add(JText::_('JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE'), JLog::WARNING, 'jerror');
return false;
}
else
{
// No XML files were found in the install folder
JLog::add(JText::_('JLIB_INSTALLER_ERROR_NOTFINDXMLSETUPFILE'), JLog::WARNING, 'jerror');
return false;
}
}
/**
* Is the XML file a valid Joomla installation manifest file.
*
* @param string $file An xmlfile path to check
*
* @return mixed 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 null;
}
// Check for a valid XML root tag.
if ($xml->getName() != 'extension')
{
return null;
}
// 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 = JFactory::getDbo();
$query = $db->getQuery(true)
->delete($db->quoteName('#__extensions'))
->where('type = ' . $db->quote($type))
->where('element = ' . $db->quote($element))
->where('folder = ' . $db->quote($folder))
->where('client_id = ' . (int) $client)
->where('state = -1');
$db->setQuery($query);
return $db->execute();
}
/**
* Compares two "files" entries to find deleted files/folders
*
* @param array $old_files An array of SimpleXMLElement objects that are the old files
* @param array $new_files 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($old_files, $new_files)
{
// 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 ($new_files as $file)
{
switch ($file->getName())
{
case 'folder':
// Add any folders to the list
$folders[] = (string) $file; // add any folders to the list
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 .= '/';
}
// Aappend the folder part
$container .= $part;
if (!in_array($container, $containers))
{
// Add the container if it doesn't already exist
$containers[] = $container;
}
}
break;
}
}
foreach ($old_files 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; // not in a folder, doesn't exist, wipe it out!
}
}
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 12.1
*/
public static function parseXMLInstallFile($path)
{
// 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
if ($xml->getName() != 'extension' && $xml->getName() != '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) ? (string) $xml->creationDate : JText::_('Unknown');
$data['author'] = ((string) $xml->author) ? (string) $xml->author : JText::_('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;
return $data;
}
}