759 lines
20 KiB
PHP
759 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* @package FOF
|
|
* @copyright Copyright (c)2010-2021 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU General Public License version 2, or later
|
|
*/
|
|
|
|
namespace FOF30\Utils\InstallScript;
|
|
|
|
defined('_JEXEC') || die;
|
|
|
|
use DirectoryIterator;
|
|
use Exception;
|
|
use FOFTemplateUtils;
|
|
use JLoader;
|
|
use Joomla\CMS\Factory;
|
|
use Joomla\CMS\Filesystem\File;
|
|
use Joomla\CMS\Filesystem\Folder;
|
|
use Joomla\CMS\Log\Log;
|
|
|
|
class BaseInstaller
|
|
{
|
|
/**
|
|
* The minimum PHP version required to install this extension
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $minimumPHPVersion = '7.2.0';
|
|
|
|
/**
|
|
* The minimum Joomla! version required to install this extension
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $minimumJoomlaVersion = '3.3.0';
|
|
|
|
/**
|
|
* The maximum Joomla! version this extension can be installed on
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $maximumJoomlaVersion = '4.0.99';
|
|
|
|
/**
|
|
* Post-installation message definitions for Joomla! 3.2 or later.
|
|
*
|
|
* This array contains the message definitions for the Post-installation Messages component added in Joomla! 3.2 and
|
|
* later versions. Each element is also a hashed array. For the keys used in these message definitions please see
|
|
* addPostInstallationMessage
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $postInstallationMessages = [];
|
|
|
|
/**
|
|
* Recursively copy a bunch of files, but only if the source and target file have a different size.
|
|
*
|
|
* @param string $source Path to copy FROM
|
|
* @param string $dest Path to copy TO
|
|
* @param array $ignored List of entries to ignore (first level entries are taken into account)
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function recursiveConditionalCopy($source, $dest, $ignored = [])
|
|
{
|
|
// Make sure source and destination exist
|
|
if (!@is_dir($source))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!@is_dir($dest))
|
|
{
|
|
if (!@mkdir($dest, 0755))
|
|
{
|
|
Folder::create($dest, 0755);
|
|
}
|
|
}
|
|
|
|
if (!@is_dir($dest))
|
|
{
|
|
$this->log(__CLASS__ . ": Cannot create folder $dest");
|
|
|
|
return;
|
|
}
|
|
|
|
// List the contents of the source folder
|
|
try
|
|
{
|
|
$di = new DirectoryIterator($source);
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Process each entry
|
|
foreach ($di as $entry)
|
|
{
|
|
// Ignore dot dirs (. and ..)
|
|
if ($entry->isDot())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$sourcePath = $entry->getPathname();
|
|
$fileName = $entry->getFilename();
|
|
|
|
// Do not copy ignored files
|
|
if (!empty($ignored) && in_array($fileName, $ignored))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If it's a directory do a recursive copy
|
|
if ($entry->isDir())
|
|
{
|
|
$this->recursiveConditionalCopy($sourcePath, $dest . DIRECTORY_SEPARATOR . $fileName);
|
|
|
|
continue;
|
|
}
|
|
|
|
// If it's a file check if it's missing or identical
|
|
$mustCopy = false;
|
|
$targetPath = $dest . DIRECTORY_SEPARATOR . $fileName;
|
|
|
|
if (!@is_file($targetPath))
|
|
{
|
|
$mustCopy = true;
|
|
}
|
|
else
|
|
{
|
|
$sourceSize = @filesize($sourcePath);
|
|
$targetSize = @filesize($targetPath);
|
|
|
|
$mustCopy = $sourceSize != $targetSize;
|
|
}
|
|
|
|
if (!$mustCopy)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!@copy($sourcePath, $targetPath))
|
|
{
|
|
if (!File::copy($sourcePath, $targetPath))
|
|
{
|
|
$this->log(__CLASS__ . ": Cannot copy $sourcePath to $targetPath");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to log a warning / error with Joomla
|
|
*
|
|
* @param string $message The message to write to the log
|
|
* @param bool $error Is this an error? If not, it's a warning. (default: false)
|
|
* @param string $category Log category, default jerror
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function log($message, $error = false, $category = 'jerror')
|
|
{
|
|
// Just in case...
|
|
if (!class_exists('JLog', true))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$priority = $error ? Log::ERROR : Log::WARNING;
|
|
|
|
try
|
|
{
|
|
Log::add($message, $priority, $category);
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
// Swallow the exception.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that the server meets the minimum PHP version requirements.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function checkPHPVersion()
|
|
{
|
|
if (!empty($this->minimumPHPVersion))
|
|
{
|
|
if (defined('PHP_VERSION'))
|
|
{
|
|
$version = PHP_VERSION;
|
|
}
|
|
elseif (function_exists('phpversion'))
|
|
{
|
|
$version = phpversion();
|
|
}
|
|
else
|
|
{
|
|
$version = '5.0.0'; // all bets are off!
|
|
}
|
|
|
|
if (!version_compare($version, $this->minimumPHPVersion, 'ge'))
|
|
{
|
|
$msg = "<p>You need PHP $this->minimumPHPVersion or later to install this extension</p>";
|
|
|
|
$this->log($msg);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check the minimum and maximum Joomla! versions for this extension
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function checkJoomlaVersion()
|
|
{
|
|
if (!empty($this->minimumJoomlaVersion) && !version_compare(JVERSION, $this->minimumJoomlaVersion, 'ge'))
|
|
{
|
|
$msg = "<p>You need Joomla! $this->minimumJoomlaVersion or later to install this extension</p>";
|
|
|
|
$this->log($msg);
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check the maximum Joomla! version
|
|
if (!empty($this->maximumJoomlaVersion) && !version_compare(JVERSION, $this->maximumJoomlaVersion, 'le'))
|
|
{
|
|
$msg = "<p>You need Joomla! $this->maximumJoomlaVersion or earlier to install this extension</p>";
|
|
|
|
$this->log($msg);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Clear PHP opcode caches
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function clearOpcodeCaches()
|
|
{
|
|
// Always reset the OPcache if it's enabled. Otherwise there's a good chance the server will not know we are
|
|
// replacing .php scripts. This is a major concern since PHP 5.5 included and enabled OPcache by default.
|
|
if (function_exists('opcache_reset'))
|
|
{
|
|
opcache_reset();
|
|
}
|
|
// Also do that for APC cache
|
|
elseif (function_exists('apc_clear_cache'))
|
|
{
|
|
@apc_clear_cache();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the dependencies for a package from the #__akeeba_common table
|
|
*
|
|
* @param string $package The package
|
|
*
|
|
* @return array The dependencies
|
|
*/
|
|
protected function getDependencies($package)
|
|
{
|
|
$db = Factory::getDbo();
|
|
|
|
$query = $db->getQuery(true)
|
|
->select($db->qn('value'))
|
|
->from($db->qn('#__akeeba_common'))
|
|
->where($db->qn('key') . ' = ' . $db->q($package));
|
|
|
|
try
|
|
{
|
|
$dependencies = $db->setQuery($query)->loadResult();
|
|
$dependencies = json_decode($dependencies, true);
|
|
|
|
if (empty($dependencies))
|
|
{
|
|
$dependencies = [];
|
|
}
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
$dependencies = [];
|
|
}
|
|
|
|
return $dependencies;
|
|
}
|
|
|
|
/**
|
|
* Sets the dependencies for a package into the #__akeeba_common table
|
|
*
|
|
* @param string $package The package
|
|
* @param array $dependencies The dependencies list
|
|
*/
|
|
protected function setDependencies($package, array $dependencies)
|
|
{
|
|
$db = Factory::getDbo();
|
|
|
|
$query = $db->getQuery(true)
|
|
->delete('#__akeeba_common')
|
|
->where($db->qn('key') . ' = ' . $db->q($package));
|
|
|
|
try
|
|
{
|
|
$db->setQuery($query)->execute();
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
// Do nothing if the old key wasn't found
|
|
}
|
|
|
|
$object = (object) [
|
|
'key' => $package,
|
|
'value' => json_encode($dependencies),
|
|
];
|
|
|
|
try
|
|
{
|
|
$db->insertObject('#__akeeba_common', $object, 'key');
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
// Do nothing if the old key wasn't found
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a package dependency to #__akeeba_common
|
|
*
|
|
* @param string $package The package
|
|
* @param string $dependency The dependency to add
|
|
*/
|
|
protected function addDependency($package, $dependency)
|
|
{
|
|
$dependencies = $this->getDependencies($package);
|
|
|
|
if (!in_array($dependency, $dependencies))
|
|
{
|
|
$dependencies[] = $dependency;
|
|
|
|
$this->setDependencies($package, $dependencies);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes a package dependency from #__akeeba_common
|
|
*
|
|
* @param string $package The package
|
|
* @param string $dependency The dependency to remove
|
|
*/
|
|
protected function removeDependency($package, $dependency)
|
|
{
|
|
$dependencies = $this->getDependencies($package);
|
|
|
|
if (in_array($dependency, $dependencies))
|
|
{
|
|
$index = array_search($dependency, $dependencies);
|
|
unset($dependencies[$index]);
|
|
|
|
$this->setDependencies($package, $dependencies);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Do I have a dependency for a package in #__akeeba_common
|
|
*
|
|
* @param string $package The package
|
|
* @param string $dependency The dependency to check for
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function hasDependency($package, $dependency)
|
|
{
|
|
$dependencies = $this->getDependencies($package);
|
|
|
|
return in_array($dependency, $dependencies);
|
|
}
|
|
|
|
/**
|
|
* Adds or updates a post-installation message (PIM) definition for Joomla! 3.2 or later. You can use this in your
|
|
* post-installation script using this code:
|
|
*
|
|
* The $options array contains the following mandatory keys:
|
|
*
|
|
* extension_id The numeric ID of the extension this message is for (see the #__extensions table)
|
|
*
|
|
* type One of message, link or action. Their meaning is:
|
|
* message Informative message. The user can dismiss it.
|
|
* link The action button links to a URL. The URL is defined in the action parameter.
|
|
* action A PHP action takes place when the action button is clicked. You need to specify the
|
|
* action_file (RAD path to the PHP file) and action (PHP function name) keys. See
|
|
* below for more information.
|
|
*
|
|
* title_key The JText language key for the title of this PIM
|
|
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE
|
|
*
|
|
* description_key The JText language key for the main body (description) of this PIM
|
|
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION
|
|
*
|
|
* action_key The JText language key for the action button. Ignored and not required when type=message
|
|
* Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION
|
|
*
|
|
* language_extension The extension name which holds the language keys used above. For example, com_foobar,
|
|
* mod_something, plg_system_whatever, tpl_mytemplate
|
|
*
|
|
* language_client_id Should we load the front-end (0) or back-end (1) language keys?
|
|
*
|
|
* version_introduced Which was the version of your extension where this message appeared for the first time?
|
|
* Example: 3.2.1
|
|
*
|
|
* enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1.
|
|
*
|
|
* condition_file The RAD path to a PHP file containing a PHP function which determines whether this message
|
|
* should be shown to the user. @param array $options See description
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws Exception
|
|
* @see Template::parsePath() for RAD path format. Joomla! will include this file
|
|
* before calling the function defined in the action key below.
|
|
* Example: admin://components/com_foobar/helpers/postinstall.php
|
|
*
|
|
* action The name of a PHP function which will be used to run the action of this PIM. This must be
|
|
* a
|
|
* simple PHP user function (not a class method, static method etc) which returns no result.
|
|
* Example: com_foobar_postinstall_messageone_action
|
|
*
|
|
* @see Template::parsePath() for RAD path format. Joomla!
|
|
* will include this file before calling the condition_method.
|
|
* Example: admin://components/com_foobar/helpers/postinstall.php
|
|
*
|
|
* condition_method The name of a PHP function which will be used to determine whether to show this message to
|
|
* the user. This must be a simple PHP user function (not a class method, static method etc)
|
|
* which returns true to show the message and false to hide it. This function is defined in
|
|
* the condition_file. Example: com_foobar_postinstall_messageone_condition
|
|
*
|
|
* When type=message no additional keys are required.
|
|
*
|
|
* When type=link the following additional keys are required:
|
|
*
|
|
* action The URL which will open when the user clicks on the PIM's action button
|
|
* Example: index.php?option=com_foobar&view=tools&task=installSampleData
|
|
*
|
|
* Then type=action the following additional keys are required:
|
|
*
|
|
* action_file The RAD path to a PHP file containing a PHP function which performs the action of this
|
|
* PIM.
|
|
*
|
|
*/
|
|
protected function addPostInstallationMessage(array $options)
|
|
{
|
|
// Make sure there are options set
|
|
if (!is_array($options))
|
|
{
|
|
throw new Exception('Post-installation message definitions must be of type array', 500);
|
|
}
|
|
|
|
// Initialise array keys
|
|
$defaultOptions = [
|
|
'extension_id' => '',
|
|
'type' => '',
|
|
'title_key' => '',
|
|
'description_key' => '',
|
|
'action_key' => '',
|
|
'language_extension' => '',
|
|
'language_client_id' => '',
|
|
'action_file' => '',
|
|
'action' => '',
|
|
'condition_file' => '',
|
|
'condition_method' => '',
|
|
'version_introduced' => '',
|
|
'enabled' => '1',
|
|
];
|
|
|
|
$options = array_merge($defaultOptions, $options);
|
|
|
|
// Array normalisation. Removes array keys not belonging to a definition.
|
|
$defaultKeys = array_keys($defaultOptions);
|
|
$allKeys = array_keys($options);
|
|
$extraKeys = array_diff($allKeys, $defaultKeys);
|
|
|
|
if (!empty($extraKeys))
|
|
{
|
|
foreach ($extraKeys as $key)
|
|
{
|
|
unset($options[$key]);
|
|
}
|
|
}
|
|
|
|
// Normalisation of integer values
|
|
$options['extension_id'] = (int) $options['extension_id'];
|
|
$options['language_client_id'] = (int) $options['language_client_id'];
|
|
$options['enabled'] = (int) $options['enabled'];
|
|
|
|
// Normalisation of 0/1 values
|
|
foreach (['language_client_id', 'enabled'] as $key)
|
|
{
|
|
$options[$key] = $options[$key] ? 1 : 0;
|
|
}
|
|
|
|
// Make sure there's an extension_id
|
|
if (!(int) $options['extension_id'])
|
|
{
|
|
throw new Exception('Post-installation message definitions need an extension_id', 500);
|
|
}
|
|
|
|
// Make sure there's a valid type
|
|
if (!in_array($options['type'], ['message', 'link', 'action']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need to declare a type of message, link or action', 500);
|
|
}
|
|
|
|
// Make sure there's a title key
|
|
if (empty($options['title_key']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need a title key', 500);
|
|
}
|
|
|
|
// Make sure there's a description key
|
|
if (empty($options['description_key']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need a description key', 500);
|
|
}
|
|
|
|
// If the type is anything other than message you need an action key
|
|
if (($options['type'] != 'message') && empty($options['action_key']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500);
|
|
}
|
|
|
|
// You must specify the language extension
|
|
if (empty($options['language_extension']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need to specify which extension contains their language keys', 500);
|
|
}
|
|
|
|
// The action file and method are only required for the "action" type
|
|
if ($options['type'] == 'action')
|
|
{
|
|
if (empty($options['action_file']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need an action file when they are of type "action"', 500);
|
|
}
|
|
|
|
$file_path = FOFTemplateUtils::parsePath($options['action_file'], true);
|
|
|
|
if (!@is_file($file_path))
|
|
{
|
|
throw new Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500);
|
|
}
|
|
|
|
if (empty($options['action']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500);
|
|
}
|
|
}
|
|
|
|
if ($options['type'] == 'link')
|
|
{
|
|
if (empty($options['link']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500);
|
|
}
|
|
}
|
|
|
|
// The condition file and method are only required when the type is not "message"
|
|
if ($options['type'] != 'message')
|
|
{
|
|
if (empty($options['condition_file']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500);
|
|
}
|
|
|
|
$file_path = FOFTemplateUtils::parsePath($options['condition_file'], true);
|
|
|
|
if (!@is_file($file_path))
|
|
{
|
|
throw new Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500);
|
|
}
|
|
|
|
if (empty($options['condition_method']))
|
|
{
|
|
throw new Exception('Post-installation message definitions need a condition method (function name) when they are of type "' . $options['type'] . '"', 500);
|
|
}
|
|
}
|
|
|
|
// Check if the definition exists
|
|
$tableName = '#__postinstall_messages';
|
|
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true)
|
|
->select('*')
|
|
->from($db->qn($tableName))
|
|
->where($db->qn('extension_id') . ' = ' . $db->q($options['extension_id']))
|
|
->where($db->qn('type') . ' = ' . $db->q($options['type']))
|
|
->where($db->qn('title_key') . ' = ' . $db->q($options['title_key']));
|
|
$existingRow = $db->setQuery($query)->loadAssoc();
|
|
|
|
// Is the existing definition the same as the one we're trying to save (ignore the enabled flag)?
|
|
if (!empty($existingRow))
|
|
{
|
|
$same = true;
|
|
|
|
foreach ($options as $k => $v)
|
|
{
|
|
if ($k == 'enabled')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ($existingRow[$k] != $v)
|
|
{
|
|
$same = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Trying to add the same row as the existing one; quit
|
|
if ($same)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Otherwise it's not the same row. Remove the old row before insert a new one.
|
|
$query = $db->getQuery(true)
|
|
->delete($db->qn($tableName))
|
|
->where($db->q('extension_id') . ' = ' . $db->q($options['extension_id']))
|
|
->where($db->q('type') . ' = ' . $db->q($options['type']))
|
|
->where($db->q('title_key') . ' = ' . $db->q($options['title_key']));
|
|
$db->setQuery($query)->execute();
|
|
}
|
|
|
|
// Insert the new row
|
|
$options = (object) $options;
|
|
$db->insertObject($tableName, $options);
|
|
}
|
|
|
|
/**
|
|
* Applies the post-installation messages for Joomla! 3.2 or later
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function _applyPostInstallationMessages()
|
|
{
|
|
// Make sure it's Joomla! 3.2.0 or later
|
|
if (!version_compare(JVERSION, '3.2.0', 'ge'))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure there are post-installation messages
|
|
if (empty($this->postInstallationMessages))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the extension ID for our component
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true);
|
|
$query->select('extension_id')
|
|
->from('#__extensions')
|
|
->where($db->qn('type') . ' = ' . $db->q('component'))
|
|
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
|
|
$db->setQuery($query);
|
|
|
|
try
|
|
{
|
|
$ids = $db->loadColumn();
|
|
}
|
|
catch (Exception $exc)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (empty($ids))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$extension_id = array_shift($ids);
|
|
|
|
foreach ($this->postInstallationMessages as $message)
|
|
{
|
|
$message['extension_id'] = $extension_id;
|
|
$this->addPostInstallationMessage($message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uninstalls the post-installation messages for Joomla! 3.2 or later
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function uninstallPostInstallationMessages()
|
|
{
|
|
// Make sure it's Joomla! 3.2.0 or later
|
|
if (!version_compare(JVERSION, '3.2.0', 'ge'))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Make sure there are post-installation messages
|
|
if (empty($this->postInstallationMessages))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the extension ID for our component
|
|
$db = Factory::getDbo();
|
|
$query = $db->getQuery(true);
|
|
$query->select('extension_id')
|
|
->from('#__extensions')
|
|
->where($db->qn('type') . ' = ' . $db->q('component'))
|
|
->where($db->qn('element') . ' = ' . $db->q($this->componentName));
|
|
$db->setQuery($query);
|
|
|
|
try
|
|
{
|
|
$ids = $db->loadColumn();
|
|
}
|
|
catch (Exception $exc)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (empty($ids))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$extension_id = array_shift($ids);
|
|
|
|
$query = $db->getQuery(true)
|
|
->delete($db->qn('#__postinstall_messages'))
|
|
->where($db->qn('extension_id') . ' = ' . $db->q($extension_id));
|
|
|
|
try
|
|
{
|
|
$db->setQuery($query)->execute();
|
|
}
|
|
catch (Exception $e)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|