<?php
/**
* Code related to the base.lib.php interface.
*
* PHP version 5
*
* @category Library
* @package Sucuri
* @subpackage SucuriScanner
* @author Daniel Cid <dcid@sucuri.net>
* @copyright 2010-2018 Sucuri Inc.
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL2
* @link https://wordpress.org/plugins/sucuri-scanner
*/
if (!defined('SUCURISCAN_INIT') || SUCURISCAN_INIT !== true) {
if (!headers_sent()) {
/* Report invalid access if possible. */
header('HTTP/1.1 403 Forbidden');
}
exit(1);
}
/**
* Miscellaneous library.
*
* Multiple and generic methods that will be used through out the code of other
* libraries extending from this and methods defined in other files, be aware of
* the hierarchy and check the other libraries for duplicated methods.
*
* @category Library
* @package Sucuri
* @subpackage SucuriScanner
* @author Daniel Cid <dcid@sucuri.net>
* @copyright 2010-2018 Sucuri Inc.
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL2
* @link https://wordpress.org/plugins/sucuri-scanner
*/
class SucuriScan
{
/**
* Throw generic exception instead of silent failure for unit-tests.
*
* @throws Exception
*
* @param string $message Error or information message.
* @param string $type Either info or error.
* @return bool False all the time.
*/
public static function throwException($message, $type = 'error')
{
if (defined('SUCURISCAN_THROW_EXCEPTIONS')
&& SUCURISCAN_THROW_EXCEPTIONS === true
&& is_string($message)
) {
$code = ($type === 'error' ? 157 : 333);
$message = str_replace(
SUCURISCAN_ADMIN_NOTICE_PREFIX,
($type === 'error' ? __('Error:', 'sucuri-scanner') : __('Info:', 'sucuri-scanner')),
$message
);
/* throw catchable errors via tests */
throw new Exception($message, $code);
}
return false; /* for backward compatibility */
}
/**
* Return name of a variable with the plugin's prefix (if needed).
*
* To facilitate the development, you can prefix the name of the key in the
* request (when accessing it) with a single colon, this method will auto-
* matically replace that character with the unique plugin ID.
*
* @param string $name Text with optional colon prefix.
* @return string Real variable name.
*/
public static function varPrefix($name = '')
{
if (!empty($name) && $name[0] === ':') {
return sprintf(
'%s_%s',
SUCURISCAN,
substr($name, 1)
);
}
return $name;
}
/**
* Gets the value of a configuration option.
*
* @param string $property The configuration option name.
* @param bool $raw Return the original value from the php.ini file.
* @return string Value of the option as a string on success.
*/
public static function iniGet($property = '', $raw = false)
{
$ini_value = ini_get($property);
if ($raw) {
return $ini_value;
}
$default = array(
'error_log' => 'error_log',
'safe_mode' => 'not active',
'memory_limit' => '128M',
'upload_max_filesize' => '2M',
'post_max_size' => '8M',
'max_execution_time' => '30',
'max_input_time' => '-1',
);
if ($ini_value === false) {
$ini_value = 'unknown';
} elseif (empty($ini_value) || $ini_value === null) {
if (array_key_exists($property, $default)) {
$ini_value = $default[$property];
} else {
$ini_value = 'not active';
}
}
if ($property == 'error_log') {
$ini_value = basename($ini_value);
}
return $ini_value;
}
/**
* Encodes the less-than, greater-than, ampersand, double quote and single
* quote characters, will never double encode entities.
*
* @see https://developer.wordpress.org/reference/functions/esc_attr/
*
* @param string $text The text which is to be encoded.
* @return string The encoded text with HTML entities.
*/
public static function escape($text = '')
{
return esc_attr($text);
}
/**
* Translate a given number in bytes to a human readable file size using the
* a approximate value in Kylo, Mega, Giga, etc.
*
* @see https://www.php.net/manual/en/function.filesize.php#106569
*
* @param int $bytes Integer representing a file size in bytes.
* @param int $decimals How many decimals should be returned.
* @return string Human readable representation of the given number.
*/
public static function humanFileSize($bytes = 0, $decimals = 2)
{
$sz = 'BKMGTP';
$factor = (int) floor((strlen((string) $bytes) - 1) / 3);
$number = $bytes / pow(1024, $factor);
$result = sprintf("%.{$decimals}f", $number) . @$sz[$factor];
$zeroes = '.' . str_repeat('0', $decimals);
$result = str_replace($zeroes, '', $result);
return $result;
}
/**
* Returns the human version of the time difference.
*
* If the timestamp is in the past in comparison with the current time, it
* will return a string in the form of "X time ago". If the timestamp is in
* the future in comparison with the current time, it will return a string
* in the form of "in X time". If the timestamp is the same as the current
* time it will return "right now".
*
* @param int $time Unix timestamp.
* @return string Different between timestamp and current time.
*/
public static function humanTime($time = 0)
{
if (!is_numeric($time)) {
return 'N/A';
}
$now = time();
if ($time === $now) {
return 'right now';
}
$result = '';
$template = '';
$diff = $now - $time;
$groups = array(
31536000 => 'year',
2592000 => 'month',
86400 => 'day',
3600 => 'hour',
60 => 'minute',
1 => 'second',
);
if ($time < $now) {
$template = '%d %s ago';
} else {
$template = 'in %d %s';
}
foreach ($groups as $secs => $label) {
$distance = abs($diff / $secs);
if ($distance >= 1) {
$plural = (round($distance) == 1) ? '' : 's';
$result = sprintf(
$template,
round($distance),
$label . $plural
);
break;
}
}
return $result;
}
/**
* Check if the admin init hook must not be intercepted.
*
* @return bool True if the admin init hook must not be intercepted.
*/
public static function noAdminInit()
{
return (bool) (
defined('SUCURISCAN_ADMIN_INIT')
&& SUCURISCAN_ADMIN_INIT === false
);
}
/**
* Check if the admin init hook must be intercepted.
*
* @return bool True if the admin init hook must be intercepted.
*/
public static function runAdminInit()
{
return (bool) (self::noAdminInit() === false);
}
/**
* Fix the deliminar of a resource path.
*
* In Windows based system the directory separator is a back slash which
* differs from what other file systems use. To keep consistency during the
* unit-tests we have decided to replace any non forward slash with it.
*
* @param string $path Directory path to fix.
* @return string Fixed file path.
*/
public static function fixPath($path = '')
{
$new = str_replace(DIRECTORY_SEPARATOR, '/', $path);
$new = rtrim($new, '/'); /* purge right side. */
return empty($new) ? '/' : $new;
}
/**
* Returns the full path to a specific directory or file.
*
* @param string $path Path that needs to be completed.
* @return string Full path including the base directory.
*/
public static function dataStorePath($path = '')
{
$folder = WP_CONTENT_DIR . '/uploads/sucuri';
/* custom path no matter its existence */
if (defined('SUCURI_DATA_STORAGE')) {
$folder = SUCURI_DATA_STORAGE;
}
$fullpath = self::fixPath($folder . '/' . $path);
$fullpath = str_replace('//', '/', $fullpath);
$fullpath = rtrim($fullpath, '/');
return $fullpath;
}
/**
* Check whether the current site is working as a multi-site instance.
*
* @return bool Either TRUE or FALSE in case WordPress is being used as a multi-site instance.
*/
public static function isMultiSite()
{
return (bool) (function_exists('is_multisite') && is_multisite());
}
/**
* Returns an URL from the admin dashboard.
*
* @param string $url Optional trailing of the URL.
* @return string Full valid URL from the admin dashboard.
*/
public static function adminURL($url = '')
{
if (self::isMultiSite()) {
// @codeCoverageIgnoreStart
return network_admin_url($url);
// @codeCoverageIgnoreEnd
}
return admin_url($url);
}
/**
* Find and retrieve the current version of Wordpress installed.
*
* @return string The version number of Wordpress installed.
*/
public static function siteVersion()
{
if (isset($GLOBALS['wp_version'])) {
return self::escape($GLOBALS['wp_version']);
}
$wp_version = '';
$filename = ABSPATH . '/' . WPINC . '/version.php';
$lines = SucuriScanFileInfo::fileLines($filename);
foreach ($lines as $line) {
if (strpos($line, '$wp_version') === 0) {
$version = str_replace("\x20", '', $line);
$index = strpos($version, "'");
$version = substr($version, $index + 1);
$index = strpos($version, "'");
$version = substr($version, 0, $index);
$wp_version = $version;
break;
}
}
return self::escape($wp_version);
}
/**
* Find and retrieve the absolute path of the WordPress configuration file.
*
* @return string|bool Absolute path of the WordPress configuration file.
*/
public static function getConfigPath()
{
$filename = ABSPATH . '/wp-config.php';
/* check one directory up */
if (!file_exists($filename)) {
$filename = ABSPATH . '/../wp-config.php';
}
return @realpath($filename);
}
/**
* Find and retrieve the absolute path of the main WordPress htaccess file.
*
* @return string Absolute path of the main WordPress htaccess file.
*/
public static function getHtaccessPath()
{
$result = '';
$base_dirs = array(
rtrim(ABSPATH, '/'),
dirname(ABSPATH),
dirname(dirname(ABSPATH)),
);
foreach ($base_dirs as $base_dir) {
$htaccess_path = sprintf('%s/.htaccess', $base_dir);
if (file_exists($htaccess_path)) {
$result = $htaccess_path;
break;
}
}
return $result;
}
/**
* Get the pattern of the definition related with a WordPress secret key.
*
* @return string Secret key definition pattern.
*/
public static function secretKeyPattern()
{
return '/define\(\s*\'([A-Z_]+)\',(\s*)\'(.+)\'\s*\);/';
}
/**
* Execute the plugin' scheduled tasks.
*
* @return void
*/
public static function runScheduledTask()
{
SucuriScanEvent::filesystemScan();
SucuriScanEvent::reportSiteVersion();
SucuriScanIntegrity::getIntegrityStatus(true);
SucuriScanSettingsPosthack::availableUpdatesContent(true);
SucuriScanEvent::sendLogsFromQueue(); /* blocking; keep at the end */
}
/**
* List of allowed HTTP headers to retrieve the real IP.
*
* Once the DNS lookups are enabled to discover the real IP address of the
* visitors the user may choose the HTTP header that will be used by default to
* retrieve the real IP address of each HTTP request, generally they do not need
* to set this but in rare cases the hosting provider may have a load balancer
* that can interfere in the process, in which case they will have to explicitly
* specify the main HTTP header. This is a list of the allowed headers that the
* user can choose.
*
* @param bool $with_keys Return the array with its values are keys.
* @return array Allowed HTTP headers to retrieve real IP.
*/
public static function allowedHttpHeaders($with_keys = false)
{
$allowed = array(
/* Sucuri custom HTTP headers */
'HTTP_X_SUCURI_CLIENTIP',
/* CloudFlare custom HTTP headers */
'HTTP_CF_CONNECTING_IP', /* Real visitor IP. */
'HTTP_CF_IPCOUNTRY', /* Country of visitor. */
'HTTP_CF_RAY', /* https://support.cloudflare.com/entries/23046742-w. */
'HTTP_CF_VISITOR', /* Determine if HTTP or HTTPS. */
/* Possible HTTP headers */
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'SUCURI_RIP',
'REMOTE_ADDR',
);
if ($with_keys === true) {
$verbose = array();
foreach ($allowed as $header) {
$verbose[$header] = $header;
}
return $verbose;
}
return $allowed;
}
/**
* List HTTP headers ordered.
*
* The list of HTTP headers is ordered per relevancy, and having the main
* HTTP header as the first entry, this guarantees that the IP address of
* the visitors will be retrieved from the HTTP header chosen by the user
* first and fallback to the other alternatives if available.
*
* @return array Ordered allowed HTTP headers.
*/
private static function orderedHttpHeaders()
{
$ordered = array();
$allowed = self::allowedHttpHeaders();
$addr_header = SucuriScanOption::getOption(':addr_header');
$ordered[] = $addr_header;
foreach ($allowed as $header) {
if (!in_array($header, $ordered)) {
$ordered[] = $header;
}
}
return $ordered;
}
/**
* Retrieve the real ip address of the user in the current request.
*
* @param bool $with_header Return HTTP header where the IP address was found.
* @return string Real IP address of the user in the current request.
*/
public static function getRemoteAddr($with_header = false)
{
$remote_addr = false;
$header_used = 'unknown';
$headers = self::orderedHttpHeaders();
foreach ($headers as $header) {
if (array_key_exists($header, $_SERVER)
&& self::isValidIP($_SERVER[$header])
) {
$remote_addr = $_SERVER[$header];
$header_used = $header;
break;
}
}
if (!$remote_addr || $remote_addr === '::1') {
$remote_addr = '127.0.0.1';
}
if ($with_header) {
return $header_used;
}
return $remote_addr;
}
/**
* Return the HTTP header used to retrieve the remote address.
*
* @return string The HTTP header used to retrieve the remote address.
*/
public static function getRemoteAddrHeader()
{
return self::getRemoteAddr(true);
}
/**
* Retrieve the user-agent from the current request.
*
* @return string The user-agent from the current request.
*/
public static function getUserAgent()
{
if (isset($_SERVER['HTTP_USER_AGENT'])) {
return self::escape($_SERVER['HTTP_USER_AGENT']);
}
return 'Mozilla/5.0 (KHTML, like Gecko) Safari/537.36';
}
/**
* Get the clean version of the current domain.
*
* @see https://developer.wordpress.org/reference/functions/get_site_url/
*
* @param bool $return_tld Returns the top-level domain instead.
* @return string The domain of the current site.
*/
public static function getDomain($return_tld = false)
{
$site_url = get_site_url();
$pattern = '/([fhtps]+:\/\/)?([^:\/]+)(:[0-9:]+)?(\/.*)?/';
$replacement = ($return_tld === true) ? '$2' : '$2$3$4';
$domain_name = @preg_replace($pattern, $replacement, $site_url);
return $domain_name;
}
/**
* Get top-level domain (TLD) of the website.
*
* @return string Top-level domain (TLD) of the website.
*/
public static function getTopLevelDomain()
{
return self::getDomain(true);
}
/**
* Check whether reverse proxy servers must be supported.
*
* @return bool TRUE if reverse proxies must be supported, FALSE otherwise.
*/
public static function supportReverseProxy()
{
return SucuriScanOption::isEnabled(':revproxy');
}
/**
* Checks the existence of the HTTP_X_SUCURI_CLIENTIP header in the request headers
*
* Once active, the Sucuri Firewall sends custom headers to the server with information
* about the original request.
*
* @return boolean True if the website is being reached with our HTTP_X_SUCURI_CLIENTIP header.
*/
private static function hasSucuriClientIPHeader()
{
return array_key_exists('HTTP_X_SUCURI_CLIENTIP', $_SERVER);
}
/**
* Checks whether the site is behind the firewall network.
*
* @param bool $verbose Return array with HTTP and HOST information.
* @return array|bool True if the firewall is in use, false otherwise.
*/
public static function isBehindFirewall($verbose = false)
{
if (!$verbose) {
return (bool) self::hasSucuriClientIPHeader();
}
$http_host = self::getTopLevelDomain();
$host_by_addr = @gethostbyname($http_host);
$host_by_name = @gethostbyaddr($host_by_addr);
return array(
'status' => self::hasSucuriClientIPHeader(),
'http_host' => self::getTopLevelDomain(),
'host_name' => $host_by_name,
'host_addr' => $host_by_addr,
);
}
/**
* Get the email address set by the administrator to receive the alerts
* sent by the plugin, if the email is missing the WordPress email address is
* chosen by default.
*
* @return string The administrator email address.
*/
public static function getSiteEmail()
{
$email = get_option('admin_email');
if (self::isValidEmail($email)) {
return $email;
}
return 'noreply@example.org';
}
/**
* Get user data by field and data.
*
* @see https://developer.wordpress.org/reference/functions/get_user_by/
*
* @param int $identifier User account identifier.
* @return array WordPress user object with data.
*/
public static function getUserByID($identifier = 0)
{
return get_user_by('id', $identifier);
}
/**
* Retrieve a list of all admin user accounts.
*
* @see https://developer.wordpress.org/reference/functions/get_users/
*
* @return array|bool List of admin users, false otherwise.
*/
public static function getAdminUsers()
{
return get_users(array('role' => 'administrator'));
}
/**
* Get a list of user emails that can be used to generate an API key for this
* website. Only accounts with the status in zero will be returned, the status
* field in the users table is officially deprecated but some 3rd-party plugins
* still use it to check if the account was activated by the owner of the email,
* a value different than zero generally means that the email was not verified
* successfully.
*
* @return array List of user identifiers and email addresses.
*/
public static function getUsersForAPIKey()
{
$valid_users = array();
$users = self::getAdminUsers();
if ($users !== false) {
foreach ($users as $user) {
if ($user->user_status === '0') {
$valid_users[$user->ID] = sprintf(
'%s - %s',
$user->user_login,
$user->user_email
);
}
}
}
return $valid_users;
}
/**
* Retrieve the date in localized format, based on timestamp.
*
* If the locale specifies the locale month and weekday, then the locale will
* take over the format for the date. If it isn't, then the date format string
* will be used instead.
*
* @param null|string|int $timestamp Unix timestamp.
* @param string $format Optional format for the date and time.
* @return string The date, translated if locale specifies it.
*/
public static function datetime($timestamp = null, $format = null)
{
$diff = 0; /* use server time */
$tz = SucuriScanOption::getOption(':timezone');
$length = strlen($tz); /* example: UTC+12.75 */
if ($timestamp === null) {
$timestamp = time();
}
if ($length === 9
&& substr($tz, 0, 3) === 'UTC'
&& ($tz[3] === '-' || $tz[3] === '+')
&& $tz[6] === '.'
) {
$hour = (float) substr($tz, 4);
$sign = ($tz[3] === '-') ? -1 : +1;
$diff = ($hour * $sign);
} else {
/* reset; value seems to be corrupt */
SucuriScanOption::deleteOption(':timezone');
}
if ($format === null) {
$format = sprintf(
"%s\x20%s",
SucuriScanOption::getOption('date_format'),
SucuriScanOption::getOption('time_format')
);
}
return date($format, (intval($timestamp) + ($diff * 3600)));
}
/**
* Check whether an IP address has a valid format or not.
*
* @param string $remote_addr The host IP address.
* @return bool Whether the IP address specified is valid or not.
*/
public static function isValidIP($remote_addr = '')
{
return (bool) @filter_var($remote_addr, FILTER_VALIDATE_IP);
}
/**
* Check whether an IP address is formatted as CIDR or not.
*
* @param string $remote_addr The supposed ip address that will be checked.
* @return bool Either TRUE or FALSE if the ip address specified is valid or not.
*/
public static function isValidCIDR($remote_addr = '')
{
if (!is_string($remote_addr)) {
return false;
}
if (preg_match('/^([0-9\.]{7,15})\/(8|16|24)$/', $remote_addr, $match)) {
return self::isValidIP($match[1]);
}
return false;
}
/**
* Separate the parts of an IP address.
*
* @param string $remote_addr The supposed ip address that will be formatted.
* @return array|bool Clean address, CIDR range, and CIDR format; FALSE otherwise.
*/
public static function getIPInfo($remote_addr = '')
{
if ($remote_addr) {
$ip_parts = explode('/', $remote_addr);
if (isset($ip_parts[0]) && self::isValidIP($ip_parts[0])) {
$addr_info = array();
$addr_info['remote_addr'] = $ip_parts[0];
$addr_info['cidr_range'] = isset($ip_parts[1]) ? $ip_parts[1] : '32';
$addr_info['cidr_format'] = $addr_info['remote_addr'] . '/' . $addr_info['cidr_range'];
return $addr_info;
}
}
return false;
}
/**
* Validate email address.
*
* This use the native PHP method filter_var which is available in PHP >=
* 5.2.0 if it is not found in the interpreter this method will sue regular
* expressions to check whether the email address passed is valid or not.
*
* @see https://www.php.net/manual/en/function.filter-var.php
*
* @param string $email The string that will be validated as an email address.
* @return bool TRUE if the email address passed to the method is valid, FALSE if not.
*/
public static function isValidEmail($email = '')
{
return (bool) @filter_var($email, FILTER_VALIDATE_EMAIL);
}
/**
* Cut a long text to the length specified, and append suspensive points at the end.
*
* @param string $text String of characters that will be cut.
* @param int $length Maximum length of the returned string, default is 10.
* @return string Short version of the text specified.
*/
public static function excerpt($text = '', $length = 10)
{
$text_length = strlen($text);
if ($text_length > $length) {
return substr($text, 0, $length) . '...';
}
return $text;
}
/**
* Check whether an list is a multidimensional array or not.
*
* @param array $list An array or multidimensional array of different values.
* @return bool TRUE if the list is multidimensional, FALSE otherwise.
*/
public static function isMultiList($list = array())
{
if (empty($list)) {
return false;
}
$status = false;
foreach ((array) $list as $item) {
if (is_array($item)) {
$status = true;
break;
}
}
return $status;
}
/**
* Join array elements with a string no matter if it is multidimensional.
*
* @param string $separator Character that will act as a separator, default to an empty string.
* @param array $list The array of strings to implode.
* @return string String of all the items in the list, with the separator between them.
*/
public static function implode($separator = '', $list = array())
{
if (!is_array($list)) {
return 'INVALID_ARGS';
}
if (self::isMultiList($list)) {
$pieces = array();
foreach ($list as $items) {
$pieces[] = @implode($separator, $items);
}
return '(' . implode('), (', $pieces) . ')';
}
return implode($separator, $list);
}
/**
* Check whether the site is running over the Nginx web server.
*
* @return bool TRUE if the site is running over Nginx, FALSE otherwise.
*/
public static function isNginxServer()
{
return (bool) (stripos(@$_SERVER['SERVER_SOFTWARE'], 'nginx') !== false);
}
/**
* Check whether the site is running over the Nginx web server.
*
* @return bool TRUE if the site is running over Nginx, FALSE otherwise.
*/
public static function isIISServer()
{
return (bool) (stripos(@$_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false);
}
/**
* Returns the md5 hash representing the content of a file.
*
* @param string $file Relative path to the file.
* @return string Seven first characters in the hash of the file.
*/
public static function fileVersion($file = '')
{
return substr(md5_file(SUCURISCAN_PLUGIN_PATH . '/' . $file), 0, 7);
}
}