' . $filename . '
' . $src . '
" ); ini_set('error_append_string', ''); error_reporting(E_ALL); } else { error_reporting(E_ALL & ~E_NOTICE); } } } class Download { // Timeout delay in seconds public const CURL_TIMEOUT = 2; public const ERROR_CURL = 1001; private static $sAppName = ''; private static string $sFileName = ''; private static string $sSourceURL = ''; private static bool $bDebug = false; private static string $sDebugFileName = ''; public function __construct($ApplicationName) { static::$bDebug = false; static::$sAppName = $ApplicationName; } /** * Enable the debug mode for this class. * * @param mixed $bOnOff */ public function debugMode($bOnOff) { static::$bDebug = $bOnOff; if ($bOnOff) { // A debug.log file will be created in // the folder of the calling script static::$sDebugFileName = DIR . 'debug.log'; } } // URL where the script will find a file to download public function setURL($sURL) { static::$sSourceURL = trim((string) $sURL); } /** * Once download, a file will be created on the disk. * Use this property to specify the name of that file. * * @param mixed $sName */ public function setFileName($sName) { static::$sFileName = trim((string) $sName); } /** * Download the application package ZIP file. * * @param type $url * @param type $file * * @return string */ public function download() { $wError = 0; // Try to use CURL, if installed if (self::iscURLEnabled()) { // $sFileName is the fullname of the file to create f.i. // /home/www/username/rootweb/downloaded-file.zip $fp = @fopen(static::$sFileName, 'w'); if (!$fp) { throw new Exception(static::$sAppName . ' - Could not open the file!'); } if (!file_exists(static::$sFileName)) { $wError = self::ERROR_CURL; } else { @fclose($fp); @chmod(static::$sFileName, 0644); } if (0 === $wError) { // Ok, try to download the file $ch = curl_init(static::$sSourceURL); if ($ch) { // Start the download process @set_time_limit(0); $fp = @fopen(static::$sFileName, 'w'); if (!curl_setopt($ch, CURLOPT_URL, static::$sSourceURL)) { fclose($fp); curl_close($ch); $wError = self::ERROR_CURL; } else { // Download curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' . 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 ' . 'Safari/537.36 FirePHP/4Chrome'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::CURL_TIMEOUT); // Output curl debugging messages into a text file if (static::$bDebug) { // output debugging info in a txt file curl_setopt($ch, CURLOPT_VERBOSE, true); $fdebug = fopen(static::$sDebugFileName, 'w'); curl_setopt($ch, CURLOPT_STDERR, $fdebug); } // Add CURLOPT_SSL if the protocol is https if ('https' == substr((string) static::$sSourceURL, 0, 5)) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); } curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_MAXREDIRS, 3); $rc = curl_exec($ch); curl_close($ch); fclose($fp); if (!$rc) { $wError = self::ERROR_CURL; } @chmod(static::$sFileName, 0644); } } } } self::removeIfNull(); if (!file_exists(static::$sFileName)) { // Unsuccessful, try with fopen() // Use a context to be able to define a timeout $context = stream_context_create( ['http' => ['timeout' => self::CURL_TIMEOUT]] ); // Get the content if fopen() is enabled $content = @fopen(static::$sSourceURL, 'r', false, $context); if ('' !== $content) { @file_put_contents(static::$sFileName, $content); } self::removeIfNull(); if (file_exists(static::$sFileName)) { $wError = 0; } } return $wError; } /** * Return a text for the encountered error. * * @param mixed $code */ public function getErrorMessage($code) { $sReturn = '
Your system configuration doesn\'t allow to download the file.
' . 'Please click ' . 'here to ' . 'manually download the file, then open your ' . 'FTP client and send the downloaded file to your ' . 'website folder.
' . 'Once this is done, just refresh this page.
' . 'Note: the filename should be ' . static::$sFileName . '
'; return $sReturn; } /** * Detect if the CURL library is loaded. */ private function iscURLEnabled() { return (!function_exists('curl_init') && !function_exists('curl_setopt') && !function_exists('curl_exec') && !function_exists('curl_close')) ? false : true; } /** * If the file is there and has a size of 0 byte, * it's a failure, the file wasn't downloaded. */ private function removeIfNull() { if (file_exists(static::$sFileName)) { if (filesize(static::$sFileName) < 1000) { unlink(static::$sFileName); } } } } class aeSecureDownload { /** * Download a file from GitHub like "aesecure_quickscan_pattern.json", ... * See the DOWNLOAD_URL constant for the URL. * * @param [type] $file * @param mixed $uri * * @return void */ public static function get($file, $uri) { try { // Try to download $aeDownload = new Download('Quickscan'); $aeDownload->debugMode(DEBUG); // Be sure to have only one "/" and not two if (trim('' !== $uri)) { $uri = ltrim(rtrim((string) $uri, '/'), '/') . '/'; } $url = rtrim(DOWNLOAD_URL, '/') . '/' . $uri . basename((string) $file); $aeDownload->setURL($url); $aeDownload->setFileName($file); $wReturn = $aeDownload->download(); if (0 !== $wReturn) { $sErrorMsg = $aeDownload->getErrorMessage($wReturn); } } catch (Exception $e) { $wReturn = 1001; $sErrorMsg = $e->getMessage(); } unset($aeDownload); } } /** * Add localization; read an external json file with translations. */ class aeSecureLanguage { public const DEFAULT_LANGUAGE = 'en-GB'; // Filename pattern for languages files public const LANG_FILE = 'aesecure_quickscan_lang_%s.json'; // Hard-coded list of supported languages // @See https://github.com/cavo789/aesecure_quickscan for xxx_lang_xxxx.json files public const SUPPORTED_LANGUAGES = 'en;en-GB;fr;fr-FR;nl;nl-BE'; private string $_filename = ''; private $_lang = null; private $_arrLanguage = null; private bool $_bLoaded = false; private $supportedLanguages = null; private $browserLanguages = null; protected static $instance = null; public function __construct($lang = null) { $aeSession = aeSecureSession::getInstance(); if (null == $lang) { $lang = str_replace('_', '-', (string) aeSecureFct::getParam('lang', 'string', '', 5)); } // Initialize the list of supported languages $this->supportedLanguages = explode(';', self::SUPPORTED_LANGUAGES); // Get the list of languages supported by the Browser and by aeSecure // (presence of the language's file) self::getBrowserLanguage(); if (in_array($lang, $this->supportedLanguages)) { // Perfect match // The language (f.i. nl-BE) is supported; we've a nl-BE.json file; use it $result = $lang; } elseif (in_array(substr((string) $lang, 0, 2), $this->supportedLanguages)) { // If the user ask for f.i. en-US and we've a file for "en", use that file. // For instance en-GB $result = substr((string) $lang, 0, 2); } else { // No, not found. Use the languages supported by the browser and check if aeSecure // support that language $result = ''; // Search for a perfect match so if the language is en_US, try to find en_US.json // and not en_GB.json foreach ($this->browserLanguages as $lang => $value) { if (in_array($lang, $this->supportedLanguages)) { $result = $lang; break; } } // If $result is still empty, no perfect match so search on the language and not // language and country. So, if the language is en-US and if a file en-GB is found, get it. if ('' == $result) { $result = 'en-GB'; foreach ($this->browserLanguages as $lang => $value) { // Check if there is a language file (f.i. if $lang is "fr" // (and not "fr_FR"), the glob function will return // the list of files like fr*.json if (in_array(substr((string) $lang, 0, 2), $this->supportedLanguages)) { $result = substr((string) $lang, 0, 2); break; } } } // Still not? Use en-GB by default if ('' == $result) { $result = self::DEFAULT_LANGUAGE; } } $aeSession->set('Lang', $lang); // Max 5 characters $lang = substr((string) $lang, 0, 5); // Just be sure to have en-GB and not f.i. EN-GB or en_gb $lang = strtolower(substr($lang, 0, 2)) . '-' . strtoupper(substr($lang, -2)); if ('en-US' == $lang) { $lang = 'en-GB'; } $this->_lang = $lang; $this->_filename = DIR . DS . sprintf(self::LANG_FILE, $this->_lang); $this->_bLoaded = false; if (!file_exists($this->_filename)) { // Try to download if not present aeSecureDownload::get($this->_filename, 'settings/'); } if (file_exists($this->_filename)) { $string = file_get_contents($this->_filename); $string = str_replace('\\u', '\u', $string); if (null === json_decode($string, true, 512, JSON_THROW_ON_ERROR)) { die('There is a problem in ' . $this->_filename . '. Probably an invalid json file' . html_entity_decode($string) . ''); } $this->_arrLanguage = json_decode($string, true, 512, JSON_THROW_ON_ERROR); $this->_bLoaded = true; } // If the parametrized file isn't found (f.i. the user set fr-FR has // preferred language and the file is not // present), then use by default en-GB if (!$this->_bLoaded) { // Try to download if not present $this->_filename = DIR . DS . sprintf(self::LANG_FILE, self::DEFAULT_LANGUAGE); if (!file_exists($this->_filename)) { aeSecureDownload::get($this->_filename, 'settings/'); } if (file_exists($this->_filename)) { $string = file_get_contents($this->_filename); $string = str_replace('\\u', '\u', $string); if (null === json_decode($string, true, 512, JSON_THROW_ON_ERROR)) { die('There is a problem in ' . $this->_filename . '. ' . 'Probably an invalid json file
' . html_entity_decode($string) . ''); } $this->_arrLanguage = json_decode($string, true, 512, JSON_THROW_ON_ERROR); $this->_bLoaded = true; } } // Still not? Use the first language file that is present if ((!$this->_bLoaded) && (count($this->supportedLanguages) > 0)) { foreach ($this->supportedLanguages as $key => $value) { $this->_filename = DIR . DS . sprintf(self::LANG_FILE, $value); if (file_exists($this->_filename)) { $string = file_get_contents($this->_filename); $string = str_replace('\\u', '\u', $string); if (null === json_decode($string, true, 512, JSON_THROW_ON_ERROR)) { die('There is a problem in ' . $this->_filename . '. Probably an invalid json file
' . html_entity_decode($string) . ''); } $this->_arrLanguage = json_decode($string, true, 512, JSON_THROW_ON_ERROR); $this->_bLoaded = true; $this->_lang = $value; break; } } } return true; } public function ready(): bool { return $this->_bLoaded; } /** * Translation functionality, search the CODE in the json file and returns its * value (the translated text). */ public function get(string $code): string { $sText = ''; if (isset($this->_arrLanguage[$code])) { $sText = $this->_arrLanguage[$code]; } return $sText; } public function getlang(): string { return $this->_lang; } /** * $language can be initialized or not. If not, the script will detect supported * languages as defined in the user's browser. If initialized, should be something * like 'en-GB', 'fr-FR', ... */ public static function getInstance(?string $lang = null): self { if (null === self::$instance) { self::$instance = new aeSecureLanguage($lang); } return self::$instance; } /** * Read the HTTP_ACCEPT_LANGUAGE browser info to determine the best language * to use for aeSecure based on the browser's preferences. * * @return string Returns f.i. en-GB, fr-FR, nl-NL, ... */ private function getBrowserLanguage(): string { $default = null; $httplanguages = $_SERVER['HTTP_ACCEPT_LANGUAGE']; if (empty($httplanguages)) { return $default; } $this->browserLanguages = []; $result = ''; // $this->browserLanguages is an array, sorted by priority order, of the // supported languages; for instance: // array // 'fr' => float 1 // 'en_US' => float 0.8 // 'en' => float 0.6 foreach (preg_split('/,\s*/', (string) $httplanguages) as $accept) { $result = preg_match('/^([a-z]{1,8}(?:[-_][a-z]{1,8})*)(?:;\s*' . 'q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i', (string) $accept, $match); if (!$result) { continue; } $quality = (isset($match[2]) ? (float)$match[2] : 1.0); $countries = explode('-', $match[1]); $region = array_shift($countries); $country_sub = explode('_', $region); $region = array_shift($country_sub); foreach ($countries as $country) { $this->browserLanguages[$region . '-' . strtoupper($country)] = $quality; } foreach ($country_sub as $country) { $this->browserLanguages[$region . '-' . strtoupper($country)] = $quality; } $this->browserLanguages[$region] = $quality; } return true; } } /** * A few helping functions. */ class aeSecureFct { /** * Remove special characters, f.i clean('a|"bc!@£de^&$f g') will return 'abcdef-g'. */ public static function sanitize(string $string): string { // Replaces all spaces with hyphens. $string = str_replace(' ', '-', $string); // Removes special chars. return (string) preg_replace('/[^A-Za-z0-9\-]/', '', $string); } /** * Generic function for adding a js in the HTML response. * * @param type $localfile * @param type $weblocation * @param mixed $defer * * @return string */ public static function addJavascript($localfile, $weblocation = '', $defer = false) { $return = ''; // Perhaps the script (aesecure_quickscan.php) is a symbolic link so __DIR__ // is the folder where the real file can be found and SCRIPT_FILENAME his link, // the line below should therefore not be used anymore if (is_file(str_replace('/', DS, dirname((string) $_SERVER['SCRIPT_FILENAME'])) . DS . $localfile)) { $return = ''; } else { if ('' != $weblocation) { $return = ''; } } return $return; } /** * Generic function for adding a css in the HTML response. * * @param type $localfile * @param type $weblocation * * @return string */ public static function addStylesheet($localfile, $weblocation = '') { $return = ''; // Perhaps the script (aesecure_quickscan.php) is a symbolic link so __DIR__ is the // folder where the real file can be found and SCRIPT_FILENAME his link, the line // below should therefore not be used anymore if (is_file(str_replace('/', DS, dirname((string) $_SERVER['SCRIPT_FILENAME'])) . DS . $localfile)) { $return = ''; } else { if ('' != $weblocation) { $return = ''; } } return $return; } public static function human_filesize($bytes, $decimals = 2) { $sz = 'BKMGTP'; $factor = intval(floor((strlen((string) $bytes) - 1) / 3)); return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; } /** * Return a string like '1 an 10 mois 6 jours 3 heures'... ie the age of f.i. a file. * * echo aeSecureFct::time_elapsed_string(filemtime($filename)) */ public static function time_elapsed_string(int $ptime): string { $diff = time() - $ptime; $calc_times = []; $timeleft = []; // Prepare array, depending on the output we want to get. $calc_times[] = ['an', 'ans', 31557600]; $calc_times[] = ['mois', 'mois', 2592000]; $calc_times[] = ['jour', 'jour', 86400]; $calc_times[] = ['heure', 'heures', 3600]; $calc_times[] = ['minute', 'minutes', 60]; $calc_times[] = ['seconde', 'secondes', 1]; foreach ($calc_times as $timedata) { [$time_sing, $time_plur, $offset] = $timedata; if ($diff >= $offset) { $left = floor($diff / $offset); $diff -= ($left * $offset); $timeleft[] = "{$left} " . (1 == $left ? $time_sing : $time_plur); } } return $timeleft ? (time() > $ptime ? null : '-') . implode(' ', $timeleft) : 0; } /** * Return true when the call to the php script has been done through an ajax request. */ public static function isAjaxRequest(): bool { $bAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ('XMLHttpRequest' == $_SERVER['HTTP_X_REQUESTED_WITH'])); return $bAjax; } /** * Return the memory usage when this function is called. By calling this function * at different place in the code, it's then possible to determine which part is * eating a lot of memory. * * @return type */ public static function getMemoryUsed() { $mem_usage = memory_get_peak_usage(true); return ($mem_usage < 1048576) ? round($mem_usage / 1024, 2) . ' kb' : round($mem_usage / 1048576, 2) . ' mb'; } /** * Safely read values from posted forms ($_POST). * * @param mixed $type */ public static function getParam(string $name, $type = 'string', mixed $default = '', int $maxlen = 0): mixed { $tmp = ''; $return = $default; if (isset($_POST[$name])) { if (in_array($type, ['int', 'integer'])) { $return = htmlspecialchars((string) $_POST[$name], ENT_QUOTES); // filter_input(INPUT_POST, $name, FILTER_SANITIZE_NUMBER_INT); } elseif ('boolean' == $type) { // false = 5 characters $tmp = substr(htmlspecialchars((string) $_POST[$name], ENT_QUOTES), 0, 5); // substr(filter_input(INPUT_POST, $name, FILTER_SANITIZE_STRING), 0, 5); $return = (in_array(strtolower($tmp), ['on', 'true'])) ? true : false; } elseif ('string' == $type) { $return = htmlspecialchars((string) $_POST[$name], ENT_QUOTES); //filter_input(INPUT_POST, $name, FILTER_SANITIZE_STRING); if ($maxlen > 0) { $return = substr($return, 0, $maxlen); } } elseif ('unsafe' == $type) { $return = $_POST[$name]; } } else { $aeSession = aeSecureSession::getInstance(); // Get from the $_GET only in debug mode or for very few parameters like "lang" (to allow to switch between // languages) and "aes" (boolean set to 1 when QuickScan is started from within the aeSecure Firewall interface) if ((true === $aeSession->get('Debug', DEBUG)) || in_array($name, ['aes', 'lang'])) { if (isset($_GET[$name])) { if (in_array($type, ['int', 'integer'])) { $return = htmlspecialchars((string) $_GET[$name], ENT_QUOTES); //filter_input(INPUT_GET, $name, FILTER_SANITIZE_NUMBER_INT); } elseif ('boolean' == $type) { // false = 5 characters $tmp = substr(htmlspecialchars((string) $_GET[$name], ENT_QUOTES), 0, 5); $return = (in_array(strtolower($tmp), ['1', 'on', 'true'])) ? true : false; } elseif ('string' == $type) { $return = htmlspecialchars((string) $_GET[$name], ENT_QUOTES); } elseif ('unsafe' == $type) { $return = $_GET[$name]; } } } } if ('boolean' == $type) { $return = (in_array($return, ['on', '1']) ? true : false); } return $return; } } /** * Logging functionality. */ class aeSecureLog { private $_sLogFile = null; protected static $instance = null; public function __construct($sLogFile, $killFile = true) { $this->_sLogFile = $sLogFile; if ((true == $killFile) && (file_exists($this->_sLogFile)) && (is_writable($this->_sLogFile))) { unlink($this->_sLogFile); } return true; } public function kill() { if ((file_exists($this->_sLogFile)) && (is_writable($this->_sLogFile))) { unlink($this->_sLogFile); } } public function filename() { return $this->_sLogFile; } /** * Add a line in the $sLogFile log file. * * @param type $sLine * * @return type */ public function addLog($sLine) { if (!is_writable(dirname((string) $this->_sLogFile))) { return; } if ('' != $this->_sLogFile) { if ($handle = fopen($this->_sLogFile, 'a')) { fwrite($handle, (string) ($sLine . "\n")); fclose($handle); } } } /** * @param type $sLogFile Name of the logfile that will be used * @param type $killFile Default True : kill the logfile if present when starting the run * * @return type */ public static function getInstance($sLogFile = null, $killFile = false) { if (null != $sLogFile) { if (null === self::$instance) { self::$instance = new aeSecureLog($sLogFile, $killFile); } } return self::$instance; } } /** * Working with files and folders. */ class aeSecureFiles { protected $aeSession = null; protected $aeLog = null; protected static $instance = null; public function __construct() { $this->aeSession = aeSecureSession::getInstance(); } public function SeeFile(?string $filename = null): ?string { $return = null; if (null != $filename) { if ((is_file($filename)) && is_readable($filename)) { $return = file_get_contents($filename); } } return $return; } /** * Kill physically a file. * * @return type -1 if the file has been removed successfully */ public function KillFile(?string $filename = null): int { $return = 0; if (null == $filename) { return $return; } if ((is_file($filename)) && is_writable($filename)) { try { if (true === $this->aeSession->get('Debug', DEBUG)) { if (null == $this->aeLog) { $this->aeLog = aeSecureLog::getInstance(); } $this->aeLog->addLog('*** Kill ' . $filename . ' ***'); } unlink($filename); if (!is_file($filename)) { $return = -1; } } catch (Exception $e) { $return = -999; } } else { $return = -50; } echo $return; } /** * Remove recursively folders * (f.i. rrmdir(__DIR__/hashes/cms/joomla/2.5.27) will kill the full tree below * the specified folder). * * @param bool $killroot If true, the folder himself will be removed. * rrmdir(__DIR__/hashes/cms/joomla/2.5.27, true) ==> * remove folder 2.5.27 too and not only his children */ public function rrmdir(string $folder, bool $killroot = false, array $arrIgnoreFiles = ['.htaccess', 'index.html']): bool { try { if (!is_dir($folder) && file_exists($folder)) { try { if (!is_writable($folder)) { $bResult = @chmod($folder, octdec('755')); } return @unlink($folder); } catch (Exception $e) { return false; } } foreach (scandir($folder) as $file) { if ('.' == $file || '..' == $file || in_array($file, $arrIgnoreFiles)) { continue; } if (!self::rrmdir($folder . DS . $file)) { if (!is_writable($folder . DS . $file)) { $bResult = chmod($folder . DS . $file, octdec('755')); } if (!self::rrmdir($folder . DS . $file)) { return false; } } } // Remove the folder only if not read-only and empty if ((is_writable($folder)) && (0 === count(glob("$folder/*")))) { @rmdir($folder); } return true; } catch (Exception $ex) { if (true === $this->aeSession->get('Debug', DEBUG)) { echo '
' . print_r($ex, true) . ''; } return false; } } public static function getInstance(): self { if (null === self::$instance) { self::$instance = new aeSecureFiles(); } return self::$instance; } public static function getFileMimeType(string $filename): ?string { $mime_type = null; if (is_file($filename)) { $finfo = null; if (class_exists('info')) { // return mime type $finfo = new finfo(FILEINFO_MIME); } if ($finfo) { $file_info = $finfo->file($filename); $mime_type = substr($file_info, 0, strpos($file_info, ';')); } else { // Requires to enable "extension=php_fileinfo.dll" on a Windows machine if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); } if ($finfo) { $mime_type = finfo_file($finfo, $filename); finfo_close($finfo); } } } return $mime_type; } /** * Return true if the file contains text content. */ public static function isTextFileContent(string $filename): string { $mimeType = aeSecureFiles::getFileMimeType($filename); $isTextType = false; // Try to determine if it's a text file; in that case the MIME type is // something like text/plain or text/richtext i.e. starting with the "text/" prefix $isTextType = ('text/' == substr((string) $mimeType, 0, 5)); // A few mimetype are also text like application/javascript if (!$isTextType) { $isTextType = in_array($mimeType, ['application/xml']); // Still not? Try to use the file's extension to determine this if (!$isTextType) { $ext = pathinfo($filename, PATHINFO_EXTENSION); $isTextType = in_array($ext, ['css', 'csv', 'eot', 'html', 'htm', 'ini', 'js', 'json', 'php', 'sh', 'svg', 'txt', 'xml']); } } return $isTextType; } } /** * Session helper. */ class aeSecureSession { protected static $instance = null; protected static $prefix = 'aeS_'; public function __construct($bDestroy = false) { // server should keep session data for AT LEAST 1 hour try { ini_set('session.gc_maxlifetime', 3600); // each client should remember their session id for EXACTLY 1 hour session_set_cookie_params(3600); } catch (\Exception $exception) { } if (!isset($_SESSION)) { try { session_start(); } catch (Exception $e) { // 1.1.9 - On some hoster the path where to store session is incorrectly // set and this gives a fatal error // Handle this and use the /tmp folder in this case. try { session_destroy(); session_save_path(sys_get_temp_dir()); try { session_start(); } catch (Exception $e) { // 1.1.12 // Still not? Use the current dir session_destroy(); session_save_path(DIR); session_start(); } } catch (\Exception $exception) { } } } if ($bDestroy) { session_destroy(); } return true; } public static function getInstance($bDestroy = false): self { if (null === self::$instance) { self::$instance = new aeSecureSession($bDestroy); } return self::$instance; } public static function set(string $name, mixed $value): void { $_SESSION[static::$prefix . $name] = $value; } public static function get(string $name, mixed $defaultvalue): mixed { return $_SESSION[static::$prefix . $name] ?? $defaultvalue; } } /** * CMS functionnalities. */ class aeSecureCMS { public const SUPPORTED_CMS = 'aesecure_quickscan_supported_cms.json'; /** * Try to determine if the site is a CMS site and in that case, get the CMS version. */ public static function getInfo(string $directory): array { $CMS = ''; $MainVersion = ''; $Version = ''; $FullVersion = ''; // Try to derive the root folder $root = rtrim($directory, DS) . DS; // Get the list of CMS (it's a json string stored in a constant) // and try to find a CMS $file = DIR . DS . self::SUPPORTED_CMS; if (!is_file($file)) { aeSecureDownload::get($file, 'settings/'); } if (!is_file($file)) { die(sprintf('Sorry, the file [%s] is missing', basename($file))); } $arrCMS = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); foreach ($arrCMS as $key => $value) { if (method_exists('aeSecureCMS', 'is' . $key)) { $method = self::class . '::is' . $key; // PHP 8.2 [$return, $CMS, $Filename, $FullVersion, $MainVersion, $Version] = call_user_func($method, $root); if (true === $return) { break; } } } return [(string) $CMS, (string) $FullVersion, (string) $MainVersion, (string) $Version, (string) $root]; } /** * Detect if the CMS is Cake. */ private static function iscake(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'admin' . DS . 'cake' . DS . 'config' . DS . 'config.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*\\Cake\.version\'\] *= *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Cake', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is Concrete5. */ private static function isConcrete5(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'concrete' . DS . 'config' . DS . 'concrete.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*\'version\' *\=\> *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Concrete5', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is Contao. */ private static function isContao(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'system' . DS . 'config' . DS . 'constants.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $MainVersion = (count($arrMatches) > 0) ? $arrMatches[1][0] : ''; preg_match('/.*BUILD\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = $MainVersion . '.' . ((count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''); return [true, 'Contao', $filename, $FullVersion, $MainVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is Dolibarr. */ private static function isDolibarr(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'htdocs' . DS . 'filefunc.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*DOL_VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Dolibarr', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is Drupal. */ private static function isDrupal(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'modules' . DS . 'system' . DS . 'system.module'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*define\(\'VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? $arrMatches[1][0] : ''; return [true, 'Drupal', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is eFront. */ private static function iseFront(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'configuration.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*G_VERSION_NUM\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'eFront', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is EspoCRM. */ private static function isEspoCRM(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'data' . DS . 'config.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*version\' \=\> *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'EspoCRM', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is FormaLMS. */ private static function isFormaLMS(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'appCore' . DS . 'index.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*_file_version_\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'FormaLMS', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is Grav. */ private static function isGrav(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'system' . DS . 'defines.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*define\(\'GRAV_VERSION\', *\'(.*)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Grav', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is GRR. */ private static function isGrr(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'include' . DS . 'misc.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/\\$version_grr[[:blank:]]=[[:blank:]]"(.*)"/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'GRR', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return false; } } /** * Detect if the CMS is Joomla. */ private static function isJoomla(string $root): bool|array { if ( (($wpos = strpos($root, DS . 'administrator' . DS)) > 0) || (($wpos = strpos($root, DS . 'bin' . DS)) > 0) || (($wpos = strpos($root, DS . 'cache' . DS)) > 0) || (($wpos = strpos($root, DS . 'cli' . DS)) > 0) || (($wpos = strpos($root, DS . 'components' . DS)) > 0) || (($wpos = strpos($root, DS . 'images' . DS)) > 0) || (($wpos = strpos($root, DS . 'includes' . DS)) > 0) || (($wpos = strpos($root, DS . 'language' . DS)) > 0) || (($wpos = strpos($root, DS . 'layouts' . DS)) > 0) || (($wpos = strpos($root, DS . 'libraries' . DS)) > 0) || (($wpos = strpos($root, DS . 'logs' . DS)) > 0) || (($wpos = strpos($root, DS . 'media' . DS)) > 0) || (($wpos = strpos($root, DS . 'modules' . DS)) > 0) || (($wpos = strpos($root, DS . 'plugins' . DS)) > 0) || (($wpos = strpos($root, DS . 'templates' . DS)) > 0) || (($wpos = strpos($root, DS . 'tmp' . DS)) > 0) ) { $root = substr($root, 0, $wpos); } // Now, $root is probably the website root. Check if we can found a Joomla!® instance $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'src' . DS . 'Version.php'; // Now, $root is probably the website root. Check if we can found a Joomla!® instance if (!file_exists($filename)) { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'cms' . DS . 'version' . DS . 'version.php'; } if (!file_exists($filename)) { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'joomla' . DS . 'version.php'; } if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/MAJOR_VERSION = (\d+)/'; if (preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE) > 0) { // As from Joomla 4, RELEASE, DEV_LEVEL, ... are removed. $arr = ['MAJOR_VERSION' => 0, 'MINOR_VERSION' => 0, 'PATCH_VERSION' => 0, 'RELDATE' => 0, 'RELTIME' => 0, 'RELTZ' => 0]; } else { $arr = ['RELEASE' => 0, 'DEV_LEVEL' => 0, 'DEV_STATUS' => 0, 'RELDATE' => 0, 'RELTIME' => 0, 'RELTZ' => 0]; } foreach ($arr as $key => $value) { // Use [[:blank:]] and not just a space character because sometimes the // version.php file contains something other than a space // this is the case for J1.5.26 // Note : since J3.5, variables are now constants and without the preceding dollar sign so // before J3.5, it was $RELEASE f.i., since 3.5, it's just RELEASE $pattern = '/.*\$?' . $key . "[[:blank:]]*=[[:blank:]]*'?([0-9A-Za-z\-\.]*)'?/"; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); if (count($arrMatches) > 0) { $arr[$key] = $arrMatches[1][0]; } } if (isset($arr['MAJOR_VERSION'])) { // Joomla 3.8.2 or greater $MainVersion = $arr['MAJOR_VERSION'] . '.' . $arr['MINOR_VERSION']; $Version = $arr['MAJOR_VERSION'] . '.' . $arr['MINOR_VERSION'] . '.' . $arr['PATCH_VERSION']; $FullVersion = $arr['MAJOR_VERSION'] . '.' . $arr['MINOR_VERSION'] . '.' . $arr['PATCH_VERSION'] . ' ' . '(' . $arr['RELDATE'] . ' ' . $arr['RELTIME'] . ' ' . $arr['RELTZ'] . ')'; } else { $MainVersion = $arr['RELEASE']; $Version = $arr['RELEASE'] . '.' . $arr['DEV_LEVEL']; $FullVersion = $arr['RELEASE'] . '.' . $arr['DEV_LEVEL'] . ' (' . $arr['DEV_STATUS'] . ') ' . '(' . $arr['RELDATE'] . ' ' . $arr['RELTIME'] . ' ' . $arr['RELTZ'] . ')'; } return [true, 'Joomla', $filename, (string) $FullVersion, (string) $MainVersion, (string) $Version]; } else { return false; } } /** * Detect if the CMS is Magento. */ private static function isMagento(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'app' . DS . 'Mage.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $arr = ['major' => 0, 'minor' => 0, 'revision' => 0, 'patch' => 0]; foreach ($arr as $key => $value) { preg_match('/.*\'' . $key . '\' *=\> *\'(\\d+)\'/', $content, $arrMatches, PREG_OFFSET_CAPTURE); if (count($arrMatches) > 0) { (string) $arr[$key] = $arrMatches[1][0]; } } $FullVersion = $arr['major'] . '.' . $arr['minor'] . '.' . $arr['revision'] . '.' . $arr['patch']; return [true, 'Magento', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is MediaWiki. */ private static function isMediaWiki(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'includes' . DS . 'DefaultSettings.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*\$wgVersion \= \'(\\d+\\.\\d+\\.\\d+)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'MediaWiki', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return false; } } /** * Detect if the CMS is phpBB. */ private static function isphpBB(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'styles' . DS . 'prosilver' . DS . 'style.cfg'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*phpbb_version \= (\\d+\\.\\d+\\.\\d+)/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'phpBB', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is phpList. */ private static function isphpList(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'public_html' . DS . 'lists' . DS . 'admin' . DS . 'init.php'; if (file_exists($filename)) { $content = file_get_contents($filename); preg_match('/.*"VERSION", "(.*)"/', $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'phpList', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the framework is phpMyAdmin. */ private static function isphpmyadmin(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'config.class.php'; if (!file_exists($filename)) { $filename = rtrim($root, DS) . DS . 'libraries' . DS . 'config.php'; } if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*PMA_VERSION\', *\'(\\d+\\.\\d+\\.\\d+)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'phpmyadmin', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return false; } } /** * Detect if the framework is PMP. */ private static function isPMB(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'includes' . DS . 'config.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/\\$pmb_version_brut[[:blank:]]=[[:blank:]]"(\\d+\\.\\d+\\.\\d+(.*))"/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'PMB', $filename, $FullVersion, $FullVersion, $FullVersion, $root]; } else { return false; } } /** * Detect if the CMS is Prestashop. */ private static function isPrestashop(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'config' . DS . 'settings.inc.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*_PS_VERSION_\', *\'(.*)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'Prestashop', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is SilverStripe. */ private static function isSilverStripe(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'cms' . DS . 'silverstripe_version'; if (file_exists($filename)) { $FullVersion = (string) file_get_contents($filename); return [true, 'silverstripe', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is WordPress. */ private static function isWordPress(string $root): bool|array { $filename = ''; if (strpos($root, 'wp-admin') > 0) { $filename = rtrim(substr($filename, 0, strpos($root, 'wp-admin')), DS) . DS . 'wp-includes' . DS . 'version.php'; } elseif (strpos($root, 'wp-content') > 0) { $filename = rtrim(substr($filename, 0, strpos($root, 'wp-content')), DS) . DS . 'wp-includes' . DS . 'version.php'; } elseif (strpos($root, 'wp-includes') > 0) { $filename = rtrim(substr($filename, 0, strpos($root, 'wp-includes')), DS) . DS . 'wp-includes' . DS . 'version.php'; } else { $filename = rtrim($root, DS) . DS . 'wp-includes' . DS . 'version.php'; if (!file_exists($filename)) { $filename = rtrim(dirname($root), DS) . DS . 'wp-includes' . DS . 'version.php'; } } if (file_exists($filename)) { // --------------------------- // --- WordPress --- // --------------------------- $CMS = 'Wordpress'; $configuration = file_get_contents($filename); $pattern = '/.*\\$wp_version *= *\'(.*)\'/'; preg_match($pattern, $configuration, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; // Get the website root folder $root = dirname(dirname($filename)); return [true, $CMS, $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } /** * Detect if the CMS is x3cms. */ private static function isx3cms(string $root): bool|array { $filename = rtrim($root, DS) . DS . 'INSTALL' . DS . 'index.php'; if (file_exists($filename)) { $content = file_get_contents($filename); $pattern = '/.*\'X4VERSION\', *\'(\\d+\\.\\d+\\.\\d+)\'/'; preg_match($pattern, $content, $arrMatches, PREG_OFFSET_CAPTURE); $FullVersion = (count($arrMatches) > 0) ? (string) $arrMatches[1][0] : ''; return [true, 'x3cms', $filename, $FullVersion, $FullVersion, $FullVersion]; } else { return false; } } } /** * PHP, JS and HTML code related to the progress bar functionnality. */ class aeSecureProgressBar { protected $aeSession = null; private $_filename = null; private $_ID = null; private $_CSS = 'progress-bar'; private int $_frequency = 1000; // refresh the progress bar each xx seconds (1000=one second) private int $_start = 0; // f.i. 0 (current step in the progress bar) private int $_end = 0; // f.i. 100 (number of steps) private int $_pct = 0; // calculated progression in percentage protected static $instance = null; public function __construct($ID, $Class) { $this->_ID = $ID; $this->_CSS = $Class; $this->_filename = str_replace('.php', '.tmp', __FILE__); $this->_start = 0; $this->_end = 100; $this->_pct = 0; // Refresh frequency $this->_frequency = PROGRESSBARFREQUENCY * 1000; $this->aeSession = aeSecureSession::getInstance(); return true; } /** * Getter & Setter for the Start position of the progress bar (by default, 0). */ public function getStart(): int { return $this->_start; } public function setStart(int $value = 0): void { $this->_start = $value; } /** * Getter & Setter for the End position of the progress bar (by default, 100). */ public function getEnd(): int { return 0 == $this->_end ? 1 : $this->_end; } public function setEnd(int $value = 100): void { $this->_end = $value; } /** * Remove the temporary file with the progress indicator. */ public function clean() { if (file_exists($this->_filename)) { unlink($this->_filename); } } /** * Increment the position of the progress bar percentage (write the percentage * in the temporary file that will be used by the ajax Progress bar). * * @return bool */ public function incTaskCount() { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } ++$this->_start; if (($this->_start / $this->getEnd()) > $this->_pct) { $this->_pct = intval(intval($this->_start) / $this->getEnd()); // ******************************************************************* // ******************************************************************* // ******************************************************************* // // The session should be closed otherwise Ajax request won't be called // asynchronously and the progress bar won't be incremented // http://stackoverflow.com/questions/3506574 session_write_close(); // // ******************************************************************* if ($handle = fopen($this->_filename, 'w+')) { fwrite($handle, (int)($this->_pct * 100)); fclose($handle); } } return true; } /** * Return the current progress value; read it from a file. */ public function getProgress() { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } header('Content-Type: application/json'); header('Cache-Control: no-cache'); if (file_exists($this->_filename)) { echo json_encode(['pct' => file_get_contents($this->_filename)], JSON_THROW_ON_ERROR); } else { echo json_encode(['pct' => '100']); } try { ob_end_flush(); flush(); } catch (Exception $e) { } die(); } /** * Generate the HTML code for the progress bar. * * @return type */ public function getHTML() { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } echo ' '; } /** * Generate the JS code for the progress bar (initialization and show evolution * during the scanning). * * @param mixed $what */ public function getJSFunction($what = 'function') { if (true === $this->aeSession->get('Debug', DEBUG)) { return false; } switch ($what) { case 'initialize': // Variables needed for the progress bar JS code echo 'progressFct=null;previousPct=0;'; break; case 'ajax_before': // The long process will start; initialize the progress bar and show it echo 'previousPct=0;' . '$(".' . $this->_CSS . '").attr("aria-valuenow", 0);' . '$(".' . $this->_CSS . '").css("width","0%");' . '$(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%");' . '$("#' . $this->_ID . '").fadeIn(300); ' . 'progressFct=setInterval( function () {getProgress();},' . $this->_frequency . ');'; break; case 'ajax_success': // The long process is now finished, put the progress bar to 100% and then hide it echo '$(".' . $this->_CSS . '").attr("aria-valuenow", 100);' . '$(".' . $this->_CSS . '").css("width","100%");' . '$(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%");' . 'clearTimeout(progressFct);' . '$("#' . $this->_ID . '").fadeOut(300);'; break; case 'function': // The long process is running, update the progress bar echo 'function getProgress() { $.ajax({ url:"' . FILE . '", data:"task=progress", type:"' . ((true === $this->aeSession->get('Debug', DEBUG)) ? 'GET' : 'POST') . '", async:true, timeout: 600000, // Scanning a site can be very long cache:false, dataType:"json", success: function(json) { percentage=parseInt(json.pct); if (percentage>=100) { $(".' . $this->_CSS . '").attr("aria-valuenow", 100); $(".' . $this->_CSS . '").css("width","100%"); $(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%"); clearTimeout(progressFct); $("#' . $this->_ID . '").fadeOut(300); } else { if (percentage>previousPct) { if ($("#gettingFiles").length) $("#gettingFiles").hide(); if (percentage>100) percentage=100; $(".' . $this->_CSS . '").attr("aria-valuenow", Math.round(percentage)); $(".' . $this->_CSS . '").css("width", percentage + "%"); $(".' . $this->_CSS . '").html($(".' . $this->_CSS . '").attr("aria-valuenow") + "%"); $("#' . $this->_ID . '").show(); previousPct=percentage; } } } // success }); return; } // function getProgress()'; break; } } /** * @param type $ID ID to give to the HTML progress bar container * @param type $Class CSS class to give to the HTML container * * @return type */ public static function getInstance($ID = 'ajaxResultPct', $Class = 'progress-bar') { if (null === self::$instance) { self::$instance = new aeSecureProgressBar($ID, $Class); } return self::$instance; } } /** * The scanner himself. */ class aeSecureScan { // hash of files already scanned and are viruses (the file is a virus) public const BLACKLIST = 'aesecure_quickscan_blacklist.json'; // JSON for the detected CMS (f. i. aesecure_quickscan_J!3.9.0.json for a Joomla 3.9.0 version) public const CMS = 'aesecure_quickscan_%s.json'; // hash of files already scanned and where a virus was found (the file contains a virus) public const EDITED = 'aesecure_quickscan_edited.json'; // hash of files that can be considered as safe public const OTHER = 'aesecure_quickscan_other.json'; // JSON with patterns to scan for finding viruses public const PATTERN = 'aesecure_quickscan_pattern.json'; // hash of files that can be considered as safe public const WHITELIST = 'aesecure_quickscan_whitelist.json'; // List of supported CMS public const SUPPORTED_CMS = 'aesecure_quickscan_supported_cms.json'; public const FOLDERS = 'aesecure_quickscan_folders.json'; protected $aeFiles = null; protected $aeLanguage = null; protected $aeLog = null; protected $aeSession = null; protected $aeProgress = null; private $_directory = null; // Folder to scan private $_start = 0; // When processing files by block (files #1 till #2500, #2501 till #5000, ...) start is the "from" part f.i. 2501 private int $_end = 0; // and _end will be the end part f.i. 5000 private $_arrCMS = null; // Used by the hash functions private $_arrRegex = null; // Array with regex patterns to match private $_arrCMSHashes = null; // Array with the hash of the installed CMS private $_arrWhiteListHashes = null; // Array with whitelisted files (files from the CMS core) private $_arrOtherHashes = null; // Array with whitelisted files (files whitelisted during aeSecure DeepScan runs) private $_arrBlackListHashes = null; // Array with blacklisted files (the file is a virus) private $_arrEditedHashes = null; // Array with hash of edited files (a virus was appended in a file) public function __construct() { date_default_timezone_set('Europe/Brussels'); setlocale(LC_TIME, 'fr_FR.utf8', 'fra'); // Instanciate objects $this->aeLanguage = aeSecureLanguage::getInstance(); $this->aeFiles = aeSecureFiles::getInstance(); $this->aeProgress = aeSecureProgressBar::getInstance(); $this->aeSession = aeSecureSession::getInstance(); // Create the logfile if (true === $this->aeSession->get('Debug', DEBUG)) { $this->aeLog = aeSecureLog::getInstance(str_replace('.php', '.files.tmp', __FILE__)); } $rootFolder = DIR; $this->aeSession->set('folder', ''); // By default, scan the current directory. // In Expert mode, allow to use a session to store the name of the folder // Get the folder to process if (true === $this->aeSession->get('Expert', EXPERT)) { $folder = base64_decode((string) aeSecureFct::getParam('folder', 'string', '')); if ('' == $folder) { $folder = $this->aeSession->get('folder', $rootFolder); } // Check if the user has specified a folder in the user entry form if (is_dir($folder)) { $this->aeSession->set('folder', $folder); } } else { $tmp = $this->aeSession->get('folder', ''); if ('' == $tmp) { $this->aeSession->set('folder', $rootFolder); } } // When running the scan for f.i. only 1000 files and not all files present on the server, // the start parameter will f.i. be set to 0 while the end parameter will be set to 1000. // This is just like a pagination so processing the 1000 next files will be : // start=1000 and end=1000. $this->_start = aeSecureFct::getParam('start', 'integer', 0); $this->_end = $this->_start + aeSecureFct::getParam('end', 'integer', 0); $this->_directory = $rootFolder; if ($this->aeSession->get('Expert', EXPERT)) { $this->_directory = trim((string) $this->aeSession->get('folder', $rootFolder)); if ('' == $this->_directory) { $this->_directory = $rootFolder; } } $this->_arrCMS = [['joomla' => 'J!'], ['wordpress' => 'WP']]; if (!is_file($file = DIR . DS . self::PATTERN)) { aeSecureDownload::get($file, 'settings/'); } return true; } public function directory() { return $this->_directory; } /** * The aesecure_quickscan_pattern.json is using constant for the disclaimer info. * These constants should be replaced by their translated text. * * @param type $disclaimer * * @return string */ public function getDisclaimerText($disclaimer) { $return = match ($disclaimer) { 'HIGHPROBALITY' => $this->aeLanguage->get('HIGHPROBALITY'), 'HIGHPROBALITYFALSEGIF' => $this->aeLanguage->get('HIGHPROBALITYFALSEGIF'), 'WARNINGBASE64ENCODEDPATTERN' => $this->aeLanguage->get('WARNINGBASE64ENCODEDPATTERN'), 'HIGHPROBALITYBADSITE' => $this->aeLanguage->get('HIGHPROBALITYBADSITE'), 'HIGHPROBALITYBASE64KEYWORD' => $this->aeLanguage->get('HIGHPROBALITYBASE64KEYWORD'), 'WARNINGNOTMANDATORYAVIRUS' => $this->aeLanguage->get('WARNINGNOTMANDATORYAVIRUS'), default => $disclaimer . ((true === $this->aeSession->get('Debug', DEBUG)) ? ' *please add translation*' : ''), }; return $return; } /** * Add a file in the whitelist. */ public function WhiteList(?string $filename = null) { // Don't white list files in demo mode, simulate that everything was ok (return -1) if (DEMO) { return -1; } if (!is_file($file = DIR . DS . self::WHITELIST)) { aeSecureDownload::get($file, 'settings/'); } if (!is_file($file)) { die(sprintf('Sorry, the file [%s] is missing', basename($file))); } if (file_exists($filename)) { $json = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); $sha = md5_file($filename); if (!(isset($json[$sha]))) { $json[$sha] = 1; } asort($json); // Output the file with all hashes $fp = fopen($file, 'w'); fwrite($fp, json_encode($json, JSON_THROW_ON_ERROR)); fclose($fp); unset($fp); } return -1; } /** * Process the action (like running the scan or displaying the progress bar). */ public function Process() { if (!is_file($file = DIR . DS . self::PATTERN)) { aeSecureDownload::get($file, 'settings/'); } if (!is_file($file)) { die(sprintf('Sorry, the file [%s] is missing', basename($file))); } $this->_arrRegex = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); // Process the task if any, from POST or GET depending on the debug mode state // Get the folder to process $this->_directory = DIR; if ($this->aeSession->get('Expert', EXPERT)) { $this->_directory = trim((string) $this->aeSession->get('folder', DIR)); if ('' == $this->_directory) { $this->_directory = DIR; } } $task = aeSecureFct::getParam('task', 'string', ''); if ('' !== $task) { switch ($task) { case 'checkwhitelist': { // Detect the existence of the whitelist.json file $filename = DIR . DS . self::WHITELIST; echo file_exists($filename) ? 1 : 0; die(); break; } case 'byebye': { // Don't kill files in demo mode, simulate that everything was ok (return -1) if (DEMO) { die(-1); } // keepwhitelist is a variable posted by the Ajax request and will // inform if the script should or not remove the user's whitelist file. $bKeepWhiteList = aeSecureFct::getParam('keepwhitelist', 'boolean', true); if (true !== $this->aeSession->get('Debug', DEBUG)) { // Get the list of aeSecure QuickScan JSON // Need to use "DIR . DS" so filenames will be absolute which is needed $arrDeleteFiles = glob(DIR . DS . 'aesecure_quickscan_*.json'); // And don't scan this script also $arrDeleteFiles[] = DIR . DS . FILE; // Kill the debug log file if present if (null !== $this->aeLog) { $arrDeleteFiles[] = $this->aeLog->filename(); } foreach ($arrDeleteFiles as $filename) { $bDelete = true; if ($filename == DIR . DS . self::WHITELIST) { // If $bKeepWhiteList=true, we can't delete the file $bDelete = !$bKeepWhiteList; } if ($bDelete) { if (is_file($filename) && is_readable($filename)) { unlink($filename); } } } } die('
' . $src . ''); break; } case 'seefile': { if (DEMO) { // Don't return source code in DEMO mode echo '
' . $src . '
' . print_r($ex, true) . ''; } } } unset($arrSkipFiles); // Put the array in a session variable and remember the processed folder $this->aeSession->set('Folder', $this->_directory); $this->aeSession->set('arrFiles', json_encode($arrFiles, JSON_THROW_ON_ERROR)); } else { // The user is running the script once more for the same folder // ==> don't scan the disk again, just user the session variable to speed up the process $arrFiles = json_decode((string) $arrFiles, null, 512, JSON_THROW_ON_ERROR); } if (null == $arrFiles) { $arrFiles = json_decode((string) $this->aeSession->get('arrFiles', null), null, 512, JSON_THROW_ON_ERROR); } unset($this->arrOtherHashes, $this->arrWhiteListHashes, $this->arrBlackListHashes, $this->arrCMSHashes); if (true == $echo) { try { header('Content-Type: application/json'); header('Cache-Control: no-cache'); } catch (\Exception $exception) { } sort($arrFilesBlacklisted); echo sprintf( '{"count":%d,"whitelisted":%d,"blacklisted":%d,"blacklisting":%s,"edited":%d,"editlisting":%s,"skipped":%d}', count($arrFiles), $wNbrWhitelisted ?? 0, $wNbrBlacklisted ?? 0, json_encode($arrFilesBlacklisted), $wNbrEdited ?? 0, json_encode($arrFilesEditlisted), $wNbrSkipped ?? 0 ); // Prevent a warning; flush only if there is something to flush try { while (ob_get_level() > 0) { ob_end_flush(); } flush(); } catch (\Exception $e) { } die(); } else { return true; } } /** * Start the scan. * * @global type $sLogFile * @global type $sProgressFile */ private function doScan(): string|bool { $aeLanguage = aeSecureLanguage::getInstance(); try { if (!get_cfg_var('safe_mode')) { // set_time_limit isn't used when safe_mode is active // No max execution time @ini_set('max_execution_time', '0'); // Remove time limit; avoid 504 HTTP errors @ini_set('set_time_limit', '0'); } } catch (Exception $e) { } // Allocate the maximum allowed memory to the script (-1 = no limit) @ini_set('memory_limit', (true !== $this->aeSession->get('Debug', DEBUG)) ? -1 : MEMORY_LIMIT); $wFile = 0; $wCount = 0; $wFound = 0; $wProcessedFile = 0; $wSkipSize = 0; $wSkipChmod = 0; $wSkipHashes = 0; $wUnreadable = 0; if (!is_dir($this->_directory)) { echo '
' . trim(str_replace('<', '<', file_get_contents($filename))) . '' . ''; $output_line = str_replace('$FOUND$', $FOUND, $OutputTemplate); if (FULLDEBUG && !aeSecureFct::isAjaxRequest()) { echo sprintf( '%s IS BLACKLISTED, VIRUS FOUND
' . trim(str_replace('<', '<', file_get_contents($filename))) . '' . ''; $output_line = str_replace('$FOUND$', $FOUND, $OutputTemplate); if (FULLDEBUG && !aeSecureFct::isAjaxRequest()) { echo sprintf( '%s CONTAINS A VIRUS
' . str_replace( $keyword, '' . $keyword . '', trim(str_replace('<', '<', $sContext)) ) . '' . ''; } } } } unset($arrMatch); } if ($bInfected) { $output_line = str_replace('$FOUND$', $FOUND, $OutputTemplate); } unset($content); } } if ($bInfected) { ++$wFound; } unset($content); } else { ++$wSkipSize; if (true === $this->aeSession->get('Debug', DEBUG)) { $this->aeLog->addLog('Scanning #' . ($wFile + 1) . '. ' . $filename . ' SKIP; Too big.'); } // The filename wasn't whitelisted, show it. $bFound = true; $output_line = '