$value) {
$next = $key + 1;
if (0 === strpos($value, $opt)) {
if ($optLength === strlen($value) && isset($argv[$next])) {
return trim($argv[$next]);
} else {
return trim(substr($value, $optLength + 1));
}
}
}
return $default;
}
/**
* Checks that user-supplied params are valid
*
* @param mixed $installDir The required istallation directory
* @param mixed $cafile Certificate Authority file
*
* @return bool True if the supplied params are okay
*/
function checkParams($installDir, $cafile)
{
$result = true;
if (false !== $installDir && !is_dir($installDir)) {
out("The defined install dir ({$installDir}) does not exist.", 'info');
$result = false;
}
if (false !== $cafile && (!file_exists($cafile) || !is_readable($cafile))) {
out("The defined Certificate Authority (CA) cert file ({$cafile}) does not exist or is not readable.", 'info');
$result = false;
}
return $result;
}
/**
* Checks the platform for possible issues running the installer
*
* Errors are written to the output, warnings are saved for later display.
*
* @param array $warnings Populated by method, to be shown later
* @param bool $quiet Quiet mode
* @param bool $disableTls Bypass tls
*
* @return bool True if there are no errors
*/
function checkPlatform(&$warnings, $quiet, $disableTls)
{
getPlatformIssues($errors, $warnings);
// Make openssl warning an error if tls has not been specifically disabled
if (isset($warnings['openssl']) && !$disableTls) {
$errors['openssl'] = $warnings['openssl'];
unset($warnings['openssl']);
}
if (!empty($errors)) {
out('Some settings on your machine make OctoberCMS unable to install properly.', 'error');
out('Make sure that you fix the issues listed below and run this script again:', 'error');
outputIssues($errors);
return false;
}
if (empty($warnings) && !$quiet) {
out('All settings correct for installing OctoberCMS', 'success');
}
return true;
}
/**
* Checks platform configuration for common incompatibility issues
*
* @param array $errors Populated by method
* @param array $warnings Populated by method
*
* @return bool If any errors or warnings have been found
*/
function getPlatformIssues(&$errors, &$warnings)
{
$errors = array();
$warnings = array();
if ($iniPath = php_ini_loaded_file()) {
$iniMessage = PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath;
} else {
$iniMessage = PHP_EOL.'A php.ini file does not exist. You will have to create one.';
}
$iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.';
if (ini_get('detect_unicode')) {
$errors['unicode'] = array(
'The detect_unicode setting must be disabled.',
'Add the following to the end of your `php.ini`:',
' detect_unicode = Off',
$iniMessage
);
}
if (!function_exists('json_decode')) {
$errors['json'] = array(
'The json extension is missing.',
'Install it or recompile php without --disable-json'
);
}
if (!class_exists('ZipArchive')) {
$errors['zip'] = array(
'The ZipArchive extension is missing.',
'Install it to continue'
);
}
if (!extension_loaded('filter')) {
$errors['filter'] = array(
'The filter extension is missing.',
'Install it or recompile php without --disable-filter'
);
}
if (!extension_loaded('hash')) {
$errors['hash'] = array(
'The hash extension is missing.',
'Install it or recompile php without --disable-hash'
);
}
if (!extension_loaded('iconv') && !extension_loaded('mbstring')) {
$errors['iconv_mbstring'] = array(
'The iconv OR mbstring extension is required and both are missing.',
'Install either of them or recompile php without --disable-iconv'
);
}
if (!ini_get('allow_url_fopen')) {
$errors['allow_url_fopen'] = array(
'The allow_url_fopen setting is incorrect.',
'Add the following to the end of your `php.ini`:',
' allow_url_fopen = On',
$iniMessage
);
}
if (extension_loaded('ionCube Loader') && ioncube_loader_iversion() < 40009) {
$ioncube = ioncube_loader_version();
$errors['ioncube'] = array(
'Your ionCube Loader extension ('.$ioncube.') is incompatible with Phar files.',
'Upgrade to ionCube 4.0.9 or higher or remove this line (path may be different) from your `php.ini` to disable it:',
' zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so',
$iniMessage
);
}
if (version_compare(PHP_VERSION, '5.3.2', '<')) {
$errors['php'] = array(
'Your PHP ('.PHP_VERSION.') is too old, you must upgrade to PHP 5.3.2 or higher.'
);
}
if (version_compare(PHP_VERSION, '5.3.4', '<')) {
$warnings['php'] = array(
'Your PHP ('.PHP_VERSION.') is quite old, upgrading to PHP 5.3.4 or higher is recommended.',
'This installer works with 5.3.2+ for most people, but there might be edge case issues.'
);
}
if (!extension_loaded('openssl')) {
$warnings['openssl'] = array(
'The openssl extension is missing, which means that secure HTTPS transfers are impossible.',
'If possible you should enable it or recompile php with --with-openssl'
);
}
if (extension_loaded('openssl') && OPENSSL_VERSION_NUMBER < 0x1000100f) {
// Attempt to parse version number out, fallback to whole string value.
$opensslVersion = trim(strstr(OPENSSL_VERSION_TEXT, ' '));
$opensslVersion = substr($opensslVersion, 0, strpos($opensslVersion, ' '));
$opensslVersion = $opensslVersion ? $opensslVersion : OPENSSL_VERSION_TEXT;
$warnings['openssl_version'] = array(
'The OpenSSL library ('.$opensslVersion.') used by PHP does not support TLSv1.2 or TLSv1.1.',
'If possible you should upgrade OpenSSL to version 1.0.1 or above.'
);
}
if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) {
$warnings['apc_cli'] = array(
'The apc.enable_cli setting is incorrect.',
'Add the following to the end of your `php.ini`:',
' apc.enable_cli = Off',
$iniMessage
);
}
ob_start();
phpinfo(INFO_GENERAL);
$phpinfo = ob_get_clean();
if (preg_match('{Configure Command(?: *
| *=> *)(.*?)(?: | |$)}m', $phpinfo, $match)) {
$configure = $match[1];
if (false !== strpos($configure, '--enable-sigchild')) {
$warnings['sigchild'] = array(
'PHP was compiled with --enable-sigchild which can cause issues on some platforms.',
'Recompile it without this flag if possible, see also:',
' https://bugs.php.net/bug.php?id=22999'
);
}
if (false !== strpos($configure, '--with-curlwrappers')) {
$warnings['curlwrappers'] = array(
'PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.',
'Recompile it without this flag if possible'
);
}
}
// Stringify the message arrays
foreach ($errors as $key => $value) {
$errors[$key] = PHP_EOL.implode(PHP_EOL, $value);
}
foreach ($warnings as $key => $value) {
$warnings[$key] = PHP_EOL.implode(PHP_EOL, $value);
}
return !empty($errors) || !empty($warnings);
}
/**
* Outputs an array of issues
*
* @param array $issues
*/
function outputIssues($issues)
{
foreach ($issues as $issue) {
out($issue, 'info');
}
out('');
}
/**
* Outputs any warnings found
*
* @param array $warnings
*/
function showWarnings($warnings)
{
if (!empty($warnings)) {
out('Some settings on your machine may cause stability issues with OctoberCMS.', 'error');
out('If you encounter issues, try to change the following:', 'error');
outputIssues($warnings);
}
}
/**
* Outputs an end of process warning if tls has been bypassed
*
* @param bool $disableTls Bypass tls
*/
function showSecurityWarning($disableTls)
{
if ($disableTls) {
out('You have instructed the Installer not to enforce SSL/TLS security on remote HTTPS requests.', 'info');
out('This will leave all downloads during installation vulnerable to Man-In-The-Middle (MITM) attacks', 'info');
}
}
/**
* Installs October to the current working directory
*/
function installOctober($installDir, $quiet, $disableTls, $cafile, $channel)
{
$installDir = realpath($installDir) ? realpath($installDir) : getcwd();
$file = $installDir.DIRECTORY_SEPARATOR.'oc-core-installer-temp.pak';
if (is_readable($file)) {
@unlink($file);
}
$home = getHomeDir();
if (!is_dir($home)) {
@mkdir($home, 0777, true);
}
if (false === $disableTls && empty($cafile) && !HttpClient::getSystemCaRootBundlePath()) {
$errorHandler = new ErrorHandler();
set_error_handler(array($errorHandler, 'handleError'));
$target = $home . '/cacert.pem';
$write = file_put_contents($target, HttpClient::getPackagedCaFile(), LOCK_EX);
@chmod($target, 0644);
restore_error_handler();
if (!$write) {
throw new RuntimeException('Unable to write bundled cacert.pem to: '.$target);
}
$cafile = $target;
}
$httpClient = new HttpClient($disableTls, $cafile);
$uriScheme = false === $disableTls ? 'https' : 'http';
$downloadUrl = "/api/installer/".$channel;
$retries = 3;
while ($retries--) {
if (!$quiet) {
out("Downloading OctoberCMS...", 'info');
}
$url = "{$uriScheme}://octobercms.com{$downloadUrl}";
$errorHandler = new ErrorHandler();
set_error_handler(array($errorHandler, 'handleError'));
$fh = fopen($file, 'w');
if (!$fh) {
out('Could not create file '.$file.': '.$errorHandler->message, 'error');
}
if (!fwrite($fh, $httpClient->get($url))) {
out('Download failed: '.$errorHandler->message, 'error');
}
fclose($fh);
restore_error_handler();
if ($errorHandler->message) {
continue;
}
$zip = new ZipArchive;
if ($zip->open($file) === true) {
$zip->extractTo($installDir);
$zip->close();
unlink($file);
break;
} else {
unlink($file);
if ($retries) {
if (!$quiet) {
out('Unable to extract zip file, retrying...', 'error');
}
} else {
out('Unable to extract zip file, aborting.', 'error');
exit(1);
}
}
}
if ($errorHandler->message) {
out('The download failed repeatedly, aborting.', 'error');
exit(1);
}
if (!$quiet) {
out(PHP_EOL."OctoberCMS successfully installed to: " . $installDir, 'success');
}
}
/**
* colorize output
*/
function out($text, $color = null, $newLine = true)
{
$styles = array(
'success' => "\033[0;32m%s\033[0m",
'error' => "\033[31;31m%s\033[0m",
'info' => "\033[33;33m%s\033[0m"
);
$format = '%s';
if (isset($styles[$color]) && USE_ANSI) {
$format = $styles[$color];
}
if ($newLine) {
$format .= PHP_EOL;
}
printf($format, $text);
}
/**
* Returns the system-dependent Composer home location, which may not exist.
* We piggy back on Composer here, for OctoberCMS.
*
* @return string
*/
function getHomeDir()
{
$home = getenv('COMPOSER_HOME');
if (!$home) {
$userDir = getUserDir();
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
$home = $userDir.'/Composer';
} else {
$home = $userDir.'/.composer';
if (!is_dir($home) && useXdg()) {
// XDG Base Directory Specifications
if (!($xdgConfig = getenv('XDG_CONFIG_HOME'))) {
$xdgConfig = $userDir.'/.config';
}
$home = $xdgConfig.'/composer';
}
}
}
return $home;
}
/**
* Returns the location of the user directory from the environment
* @throws Runtime Exception If the environment value does not exists
*
* @return string
*/
function getUserDir()
{
$userEnv = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'APPDATA' : 'HOME';
$userDir = getenv($userEnv);
if (!$userDir) {
throw new RuntimeException('The '.$userEnv.' or COMPOSER_HOME environment variable must be set for composer to run correctly');
}
return rtrim(strtr($userDir, '\\', '/'), '/');
}
/**
* @return bool
*/
function useXdg()
{
foreach (array_keys($_SERVER) as $key) {
if (substr($key, 0, 4) === 'XDG_') {
return true;
}
}
return false;
}
function validateCaFile($contents)
{
// assume the CA is valid if php is vulnerable to
// https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
if (
PHP_VERSION_ID <= 50327
|| (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422)
|| (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506)
) {
return !empty($contents);
}
return (bool) openssl_x509_parse($contents);
}
class ErrorHandler
{
public $message = '';
public function handleError($code, $msg)
{
if ($this->message) {
$this->message .= "\n";
}
$this->message .= preg_replace('{^copy\(.*?\): }', '', $msg);
}
}
class HttpClient {
private $options = array('http' => array());
private $disableTls = false;
public function __construct($disableTls = false, $cafile = false)
{
$this->disableTls = $disableTls;
if ($this->disableTls === false) {
if (!empty($cafile) && !is_dir($cafile)) {
if (!is_readable($cafile) || !validateCaFile(file_get_contents($cafile))) {
throw new RuntimeException('The configured cafile (' .$cafile. ') was not valid or could not be read.');
}
}
$options = $this->getTlsStreamContextDefaults($cafile);
$this->options = array_replace_recursive($this->options, $options);
}
}
public function get($url)
{
$context = $this->getStreamContext($url);
$result = file_get_contents($url, null, $context);
if ($result && extension_loaded('zlib')) {
$decode = false;
foreach ($http_response_header as $header) {
if (preg_match('{^content-encoding: *gzip *$}i', $header)) {
$decode = true;
continue;
} elseif (preg_match('{^HTTP/}i', $header)) {
$decode = false;
}
}
if ($decode) {
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
$result = zlib_decode($result);
} else {
// work around issue with gzuncompress & co that do not work with all gzip checksums
$result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result));
}
if (!$result) {
throw new RuntimeException('Failed to decode zlib stream');
}
}
}
return $result;
}
protected function getStreamContext($url)
{
if ($this->disableTls === false) {
$host = parse_url($url, PHP_URL_HOST);
if (PHP_VERSION_ID < 50600) {
$this->options['ssl']['CN_match'] = $host;
$this->options['ssl']['SNI_server_name'] = $host;
}
}
// Keeping the above mostly isolated from the code copied from Composer.
return $this->getMergedStreamContext($url);
}
protected function getTlsStreamContextDefaults($cafile)
{
$ciphers = implode(':', array(
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-DSS-AES128-GCM-SHA256',
'kEDH+AESGCM',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-RSA-AES128-SHA',
'ECDHE-ECDSA-AES128-SHA',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA',
'ECDHE-ECDSA-AES256-SHA',
'DHE-RSA-AES128-SHA256',
'DHE-RSA-AES128-SHA',
'DHE-DSS-AES128-SHA256',
'DHE-RSA-AES256-SHA256',
'DHE-DSS-AES256-SHA',
'DHE-RSA-AES256-SHA',
'AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'ECDHE-RSA-RC4-SHA',
'ECDHE-ECDSA-RC4-SHA',
'AES128',
'AES256',
'RC4-SHA',
'HIGH',
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!3DES',
'!MD5',
'!PSK'
));
/**
* CN_match and SNI_server_name are only known once a URL is passed.
* They will be set in the getOptionsForUrl() method which receives a URL.
*
* cafile or capath can be overridden by passing in those options to constructor.
*/
$options = array(
'ssl' => array(
'ciphers' => $ciphers,
'verify_peer' => true,
'verify_depth' => 7,
'SNI_enabled' => true,
)
);
/**
* Attempt to find a local cafile or throw an exception.
* The user may go download one if this occurs.
*/
if (!$cafile) {
$cafile = self::getSystemCaRootBundlePath();
}
if (is_dir($cafile)) {
$options['ssl']['capath'] = $cafile;
} elseif ($cafile) {
$options['ssl']['cafile'] = $cafile;
} else {
throw new RuntimeException('A valid cafile could not be located automatically.');
}
/**
* Disable TLS compression to prevent CRIME attacks where supported.
*/
if (version_compare(PHP_VERSION, '5.4.13') >= 0) {
$options['ssl']['disable_compression'] = true;
}
return $options;
}
/**
* function copied from Composer\Util\StreamContextFactory::getContext
*
* Any changes should be applied there as well, or backported here.
*
* @param string $url URL the context is to be used for
* @return resource Default context
* @throws \RuntimeException if https proxy required and OpenSSL uninstalled
*/
protected function getMergedStreamContext($url)
{
$options = $this->options;
// Handle system proxy
if (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy'])) {
// Some systems seem to rely on a lowercased version instead...
$proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']);
}
if (!empty($proxy)) {
$proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : '';
$proxyURL .= isset($proxy['host']) ? $proxy['host'] : '';
if (isset($proxy['port'])) {
$proxyURL .= ":" . $proxy['port'];
} elseif ('http://' == substr($proxyURL, 0, 7)) {
$proxyURL .= ":80";
} elseif ('https://' == substr($proxyURL, 0, 8)) {
$proxyURL .= ":443";
}
// http(s):// is not supported in proxy
$proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL);
if (0 === strpos($proxyURL, 'ssl:') && !extension_loaded('openssl')) {
throw new RuntimeException('You must enable the openssl extension to use a proxy over https');
}
$options['http'] = array(
'proxy' => $proxyURL,
);
// enabled request_fulluri unless it is explicitly disabled
switch (parse_url($url, PHP_URL_SCHEME)) {
case 'http': // default request_fulluri to true
$reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI');
if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
$options['http']['request_fulluri'] = true;
}
break;
case 'https': // default request_fulluri to true
$reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI');
if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
$options['http']['request_fulluri'] = true;
}
break;
}
if (isset($proxy['user'])) {
$auth = urldecode($proxy['user']);
if (isset($proxy['pass'])) {
$auth .= ':' . urldecode($proxy['pass']);
}
$auth = base64_encode($auth);
$options['http']['header'] = "Proxy-Authorization: Basic {$auth}\r\n";
}
}
if (isset($options['http']['header'])) {
$options['http']['header'] .= "Connection: close\r\n";
} else {
$options['http']['header'] = "Connection: close\r\n";
}
if (extension_loaded('zlib')) {
$options['http']['header'] .= "Accept-Encoding: gzip\r\n";
}
$options['http']['header'] .= "User-Agent: OctoberCMS Installer\r\n";
$options['http']['protocol_version'] = 1.1;
return stream_context_create($options);
}
/**
* This method was adapted from Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury
*
* For the full copyright and license information, please see below:
*
* Copyright (c) 2013, Evan Coury
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public static function getSystemCaRootBundlePath()
{
static $caPath = null;
if ($caPath !== null) {
return $caPath;
}
// If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$envCertFile = getenv('SSL_CERT_FILE');
if ($envCertFile && is_readable($envCertFile) && validateCaFile(file_get_contents($envCertFile))) {
return $caPath = $envCertFile;
}
// If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$envCertDir = getenv('SSL_CERT_DIR');
if ($envCertDir && is_dir($envCertDir) && is_readable($envCertDir)) {
return $caPath = $envCertDir;
}
$configured = ini_get('openssl.cafile');
if ($configured && strlen($configured) > 0 && is_readable($configured) && validateCaFile(file_get_contents($configured))) {
return $caPath = $configured;
}
$configured = ini_get('openssl.capath');
if ($configured && is_dir($configured) && is_readable($configured)) {
return $caPath = $configured;
}
$caBundlePaths = array(
'/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package)
'/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
'/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package)
'/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package)
'/usr/ssl/certs/ca-bundle.crt', // Cygwin
'/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package
'/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option)
'/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
'/etc/ssl/cert.pem', // OpenBSD
'/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x
);
foreach ($caBundlePaths as $caBundle) {
if (@is_readable($caBundle) && validateCaFile(file_get_contents($caBundle))) {
return $caPath = $caBundle;
}
}
foreach ($caBundlePaths as $caBundle) {
$caBundle = dirname($caBundle);
if (is_dir($caBundle) && glob($caBundle.'/*')) {
return $caPath = $caBundle;
}
}
return $caPath = false;
}
public static function getPackagedCaFile()
{
return <<