PageRenderTime 56ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 2ms

/wp-content/plugins/sucuri-scanner/sucuri.php

https://github.com/CaffeinatedJim/catsinmyyard
PHP | 11683 lines | 7249 code | 1669 blank | 2765 comment | 1250 complexity | 3f9a5bf111a2bd57ffe47be637e3db9a MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, GPL-3.0, BSD-3-Clause, Apache-2.0, MIT, AGPL-1.0, LGPL-2.1
  1. <?php
  2. /*
  3. Plugin Name: Sucuri Security - Auditing, Malware Scanner and Hardening
  4. Plugin URI: http://wordpress.sucuri.net/
  5. Description: The <a href="http://sucuri.net/" target="_blank">Sucuri</a> plugin provides the website owner the best Activity Auditing, SiteCheck Remote Malware Scanning, Effective Security Hardening and Post-Hack features. SiteCheck will check for malware, spam, blacklisting and other security issues like .htaccess redirects, hidden eval code, etc. The best thing about it is it's completely free.
  6. Author: Sucuri, INC
  7. Version: 1.7.7
  8. Author URI: http://sucuri.net
  9. */
  10. /**
  11. * Main file to control the plugin.
  12. *
  13. * @package Sucuri Security
  14. * @author Yorman Arias <yorman.arias@sucuri.net>
  15. * @author Daniel Cid <dcid@sucuri.net>
  16. * @copyright Since 2010-2015 Sucuri Inc.
  17. * @license Released under the GPL - see LICENSE file for details.
  18. * @link https://wordpress.sucuri.net/
  19. * @since File available since Release 0.1
  20. */
  21. /**
  22. * Plugin dependencies.
  23. *
  24. * List of required functions for the execution of this plugin, we are assuming
  25. * that this site was built on top of the WordPress project, and that it is
  26. * being loaded through a pluggable system, these functions most be defined
  27. * before to continue.
  28. *
  29. * @var array
  30. */
  31. $sucuriscan_dependencies = array(
  32. 'wp',
  33. 'wp_die',
  34. 'add_action',
  35. 'remove_action',
  36. 'wp_remote_get',
  37. 'wp_remote_post',
  38. );
  39. // Terminate execution if any of the functions mentioned above is not defined.
  40. foreach ( $sucuriscan_dependencies as $dependency ) {
  41. if ( ! function_exists( $dependency ) ) {
  42. exit(0);
  43. }
  44. }
  45. /**
  46. * Plugin's constants.
  47. *
  48. * These constants will hold the basic information of the plugin, file/folder
  49. * paths, version numbers, read-only variables that will affect the functioning
  50. * of the rest of the code. The conditional will act as a container helping in
  51. * the readability of the code considering the total number of lines that this
  52. * file will have.
  53. */
  54. /**
  55. * Unique name of the plugin through out all the code.
  56. */
  57. define( 'SUCURISCAN', 'sucuriscan' );
  58. /**
  59. * Current version of the plugin's code.
  60. */
  61. define( 'SUCURISCAN_VERSION', '1.7.7' );
  62. /**
  63. * The name of the Sucuri plugin main file.
  64. */
  65. define( 'SUCURISCAN_PLUGIN_FILE', 'sucuri.php' );
  66. /**
  67. * The name of the folder where the plugin's files will be located.
  68. */
  69. define( 'SUCURISCAN_PLUGIN_FOLDER', 'sucuri-scanner' );
  70. /**
  71. * The fullpath where the plugin's files will be located.
  72. */
  73. define( 'SUCURISCAN_PLUGIN_PATH', WP_PLUGIN_DIR.'/'.SUCURISCAN_PLUGIN_FOLDER );
  74. /**
  75. * The fullpath of the main plugin file.
  76. */
  77. define( 'SUCURISCAN_PLUGIN_FILEPATH', SUCURISCAN_PLUGIN_PATH.'/'.SUCURISCAN_PLUGIN_FILE );
  78. /**
  79. * The local URL where the plugin's files and assets are served.
  80. */
  81. define( 'SUCURISCAN_URL', rtrim( plugin_dir_url( SUCURISCAN_PLUGIN_FILEPATH ), '/' ) );
  82. /**
  83. * Checksum of this file to check the integrity of the plugin.
  84. */
  85. define( 'SUCURISCAN_PLUGIN_CHECKSUM', @md5_file( SUCURISCAN_PLUGIN_FILEPATH ) );
  86. /**
  87. * Remote URL where the public Sucuri API service is running.
  88. */
  89. define( 'SUCURISCAN_API', 'https://wordpress.sucuri.net/api/' );
  90. /**
  91. * Latest version of the public Sucuri API.
  92. */
  93. define( 'SUCURISCAN_API_VERSION', 'v1' );
  94. /**
  95. * Remote URL where the CloudProxy API service is running.
  96. */
  97. define( 'SUCURISCAN_CLOUDPROXY_API', 'https://waf.sucuri.net/api' );
  98. /**
  99. * Latest version of the CloudProxy API.
  100. */
  101. define( 'SUCURISCAN_CLOUDPROXY_API_VERSION', 'v2' );
  102. /**
  103. * The maximum quantity of entries that will be displayed in the last login page.
  104. */
  105. define( 'SUCURISCAN_LASTLOGINS_USERSLIMIT', 25 );
  106. /**
  107. * The maximum quantity of entries that will be displayed in the audit logs page.
  108. */
  109. define( 'SUCURISCAN_AUDITLOGS_PER_PAGE', 50 );
  110. /**
  111. * The maximum quantity of buttons in the paginations.
  112. */
  113. define( 'SUCURISCAN_MAX_PAGINATION_BUTTONS', 20 );
  114. /**
  115. * The minimum quantity of seconds to wait before each filesystem scan.
  116. */
  117. define( 'SUCURISCAN_MINIMUM_RUNTIME', 10800 );
  118. /**
  119. * The life time of the cache for the results of the SiteCheck scans.
  120. */
  121. define( 'SUCURISCAN_SITECHECK_LIFETIME', 1200 );
  122. /**
  123. * The life time of the cache for the results of the get_plugins function.
  124. */
  125. define( 'SUCURISCAN_GET_PLUGINS_LIFETIME', 1800 );
  126. /**
  127. * Plugin's global variables.
  128. *
  129. * These variables will be defined globally to allow the inclusion in multiple
  130. * functions and classes defined in the libraries loaded by this plugin. The
  131. * conditional will act as a container helping in the readability of the code
  132. * considering the total number of lines that this file will have.
  133. */
  134. if ( defined( 'SUCURISCAN' ) ){
  135. /**
  136. * List an associative array with the sub-pages of this plugin.
  137. *
  138. * @return array
  139. */
  140. $sucuriscan_pages = array(
  141. 'sucuriscan' => 'Dashboard',
  142. 'sucuriscan_scanner' => 'Malware Scan',
  143. 'sucuriscan_monitoring' => 'Firewall (WAF)',
  144. 'sucuriscan_hardening' => 'Hardening',
  145. 'sucuriscan_posthack' => 'Post-Hack',
  146. 'sucuriscan_lastlogins' => 'Last Logins',
  147. 'sucuriscan_settings' => 'Settings',
  148. 'sucuriscan_infosys' => 'Site Info',
  149. );
  150. /**
  151. * Settings options.
  152. *
  153. * The following global variables are mostly associative arrays where the key is
  154. * linked to an option that will be stored in the database, and their
  155. * correspondent values are the description of the option. These variables will
  156. * be used in the settings page to offer the user a way to configure the
  157. * behaviour of the plugin.
  158. *
  159. * @var array
  160. */
  161. $sucuriscan_notify_options = array(
  162. 'sucuriscan_notify_plugin_change' => 'Receive email alerts for <strong>Sucuri</strong> plugin changes',
  163. 'sucuriscan_prettify_mails' => 'Receive email alerts in HTML <em>(there may be issues with some mail services)</em>',
  164. 'sucuriscan_lastlogin_redirection' => 'Allow redirection after login to report the last-login information',
  165. 'sucuriscan_notify_user_registration' => 'user:Receive email alerts for new user registration',
  166. 'sucuriscan_notify_success_login' => 'user:Receive email alerts for successful login attempts',
  167. 'sucuriscan_notify_failed_login' => 'user:Receive email alerts for failed login attempts',
  168. 'sucuriscan_notify_bruteforce_attack' => 'user:Receive email alerts for password guessing brute force attacks',
  169. 'sucuriscan_notify_post_publication' => 'Receive email alerts for new content <em>(posts, attachments, forms, etc)</em>',
  170. 'sucuriscan_notify_website_updated' => 'Receive email alerts when the WordPress version is updated',
  171. 'sucuriscan_notify_settings_updated' => 'Receive email alerts when your website settings are updated',
  172. 'sucuriscan_notify_theme_editor' => 'Receive email alerts when a file is modified with theme/plugin editor',
  173. 'sucuriscan_notify_plugin_installed' => 'plugin:Receive email alerts when a plugin is installed',
  174. 'sucuriscan_notify_plugin_activated' => 'plugin:Receive email alerts when a plugin is activated',
  175. 'sucuriscan_notify_plugin_deactivated' => 'plugin:Receive email alerts when a plugin is deactivated',
  176. 'sucuriscan_notify_plugin_updated' => 'plugin:Receive email alerts when a plugin is updated',
  177. 'sucuriscan_notify_plugin_deleted' => 'plugin:Receive email alerts when a plugin is deleted',
  178. 'sucuriscan_notify_widget_added' => 'widget:Receive email alerts when a widget is added to a sidebar',
  179. 'sucuriscan_notify_widget_deleted' => 'widget:Receive email alerts when a widget is deleted from a sidebar',
  180. 'sucuriscan_notify_theme_installed' => 'theme:Receive email alerts when a theme is installed',
  181. 'sucuriscan_notify_theme_activated' => 'theme:Receive email alerts when a theme is activated',
  182. 'sucuriscan_notify_theme_updated' => 'theme:Receive email alerts when a theme is updated',
  183. 'sucuriscan_notify_theme_deleted' => 'theme:Receive email alerts when a theme is deleted',
  184. );
  185. $sucuriscan_schedule_allowed = array(
  186. 'hourly' => 'Every three hours (3 hours)',
  187. 'twicedaily' => 'Twice daily (12 hours)',
  188. 'daily' => 'Once daily (24 hours)',
  189. '_oneoff' => 'Never',
  190. );
  191. $sucuriscan_interface_allowed = array(
  192. 'spl' => 'SPL (high performance)',
  193. 'opendir' => 'OpenDir (medium)',
  194. 'glob' => 'Glob (low)',
  195. );
  196. $sucuriscan_emails_per_hour = array(
  197. '5' => 'Maximum 5 per hour',
  198. '10' => 'Maximum 10 per hour',
  199. '20' => 'Maximum 20 per hour',
  200. '40' => 'Maximum 40 per hour',
  201. '80' => 'Maximum 80 per hour',
  202. '160' => 'Maximum 160 per hour',
  203. 'unlimited' => 'Unlimited',
  204. );
  205. $sucuriscan_maximum_failed_logins = array(
  206. '30' => '30 failed logins per hour',
  207. '60' => '60 failed logins per hour',
  208. '120' => '120 failed logins per hour',
  209. '240' => '240 failed logins per hour',
  210. '480' => '480 failed logins per hour',
  211. );
  212. $sucuriscan_verify_ssl_cert = array(
  213. 'true' => 'Verify peer\'s cert',
  214. 'false' => 'Stop peer\'s cert verification',
  215. );
  216. $sucuriscan_no_notices_in = array(
  217. /* Value of the page parameter to ignore. */
  218. );
  219. $sucuriscan_email_subjects = array(
  220. 'Sucuri Alert, :domain, :event',
  221. 'Sucuri Alert, :domain, :event, :remoteaddr',
  222. 'Sucuri Alert, :event, :remoteaddr',
  223. 'Sucuri Alert, :event',
  224. );
  225. /**
  226. * Remove the WordPress generator meta-tag from the source code.
  227. */
  228. remove_action( 'wp_head', 'wp_generator' );
  229. /**
  230. * Run a specific function defined in the plugin's code to locate every
  231. * directory and file, collect their checksum and file size, and send this
  232. * information to the Sucuri API service where a security and integrity scan
  233. * will be performed against the hashes provided and the official versions.
  234. */
  235. add_action( 'sucuriscan_scheduled_scan', 'SucuriScanEvent::filesystem_scan' );
  236. /**
  237. * Initialize the execute of the main plugin's functions.
  238. *
  239. * This will load the menu options in the WordPress administrator panel, and
  240. * execute the bootstrap function of the plugin.
  241. */
  242. add_action( 'init', 'SucuriScanInterface::initialize', 1 );
  243. add_action( 'admin_init', 'SucuriScanInterface::create_datastore_folder' );
  244. add_action( 'admin_init', 'SucuriScanInterface::handle_old_plugins' );
  245. add_action( 'admin_enqueue_scripts', 'SucuriScanInterface::enqueue_scripts', 1 );
  246. add_action( 'admin_menu', 'SucuriScanInterface::add_interface_menu' );
  247. /**
  248. * Function call interceptors.
  249. *
  250. * Define the names for the hooks that will intercept specific function calls in
  251. * the admin interface and parts of the external site, an event report will be
  252. * sent to the API service and an email notification to the administrator of the
  253. * site.
  254. *
  255. * @see Class SucuriScanHook
  256. */
  257. if ( class_exists( 'SucuriScanHook' ) ){
  258. $sucuriscan_hooks = array(
  259. // Passes.
  260. 'add_attachment',
  261. 'add_link',
  262. 'create_category',
  263. 'delete_post',
  264. 'delete_user',
  265. 'login_form_resetpass',
  266. 'private_to_published',
  267. 'publish_page',
  268. 'publish_post',
  269. 'publish_phone',
  270. 'xmlrpc_publish_post',
  271. 'retrieve_password',
  272. 'switch_theme',
  273. 'user_register',
  274. 'wp_login',
  275. 'wp_login_failed',
  276. 'wp_trash_post',
  277. );
  278. foreach ( $sucuriscan_hooks as $hook_name ){
  279. $hook_func = 'SucuriScanHook::hook_' . $hook_name;
  280. add_action( $hook_name, $hook_func, 50 );
  281. }
  282. add_action( 'admin_init', 'SucuriScanHook::hook_undefined_actions' );
  283. add_action( 'login_form', 'SucuriScanHook::hook_undefined_actions' );
  284. } else {
  285. SucuriScanInterface::error( 'Function call interceptors are not working properly.' );
  286. }
  287. /**
  288. * Display a message if the plugin is not activated.
  289. *
  290. * Display a message at the top of the administration panel with a button that
  291. * once clicked will send the site's email and domain name to the Sucuri API
  292. * service where an API key will be generated for the site, this key will allow
  293. * the plugin to execute the filesystem scans, the project integrity, and the
  294. * email notifications.
  295. */
  296. add_action( 'admin_notices', 'SucuriScanInterface::setup_notice' );
  297. /**
  298. * Heartbeat API
  299. *
  300. * Update the settings of the Heartbeat API according to the values set by an
  301. * administrator. This tool may cause an increase in the CPU usage, a bad
  302. * configuration may cause low account to run out of resources, but in better
  303. * cases it may improve the performance of the site by reducing the quantity of
  304. * requests sent to the server per session.
  305. */
  306. add_filter( 'init', 'SucuriScanHeartbeat::register_script', 1 );
  307. add_filter( 'heartbeat_settings', 'SucuriScanHeartbeat::update_settings' );
  308. add_filter( 'heartbeat_send', 'SucuriScanHeartbeat::respond_to_send', 10, 3 );
  309. add_filter( 'heartbeat_received', 'SucuriScanHeartbeat::respond_to_received', 10, 3 );
  310. add_filter( 'heartbeat_nopriv_send', 'SucuriScanHeartbeat::respond_to_send', 10, 3 );
  311. add_filter( 'heartbeat_nopriv_received', 'SucuriScanHeartbeat::respond_to_received', 10, 3 );
  312. }
  313. /**
  314. * Miscellaneous library.
  315. *
  316. * Multiple and generic functions that will be used through out the code of
  317. * other libraries extending from this and functions defined in other files, be
  318. * aware of the hierarchy and check the other libraries for duplicated methods.
  319. */
  320. class SucuriScan {
  321. /**
  322. * Class constructor.
  323. */
  324. public function __construct(){
  325. }
  326. /**
  327. * Return name of a variable with the plugin's prefix (if needed).
  328. *
  329. * To facilitate the development, you can prefix the name of the key in the
  330. * request (when accessing it) with a single colon, this function will
  331. * automatically replace that character with the unique identifier of the
  332. * plugin.
  333. *
  334. * @param string $var_name Name of a variable with an optional colon at the beginning.
  335. * @return string Full name of the variable with the extra characters (if needed).
  336. */
  337. public static function variable_prefix( $var_name = '' ){
  338. if ( preg_match( '/^:(.*)/', $var_name, $match ) ){
  339. $var_name = sprintf( '%s_%s', SUCURISCAN, $match[1] );
  340. }
  341. return $var_name;
  342. }
  343. /**
  344. * Gets the value of a configuration option.
  345. *
  346. * @param string $property The configuration option name.
  347. * @return string Value of the configuration option as a string on success.
  348. */
  349. public static function ini_get( $property = '' ){
  350. $ini_value = ini_get( $property );
  351. if ( empty($ini_value) || is_null( $ini_value ) ){
  352. switch ( $property ){
  353. case 'error_log': $ini_value = 'error_log'; break;
  354. case 'safe_mode': $ini_value = 'Off'; break;
  355. case 'allow_url_fopen': $ini_value = '1'; break;
  356. case 'memory_limit': $ini_value = '128M'; break;
  357. case 'upload_max_filesize': $ini_value = '2M'; break;
  358. case 'post_max_size': $ini_value = '8M'; break;
  359. case 'max_execution_time': $ini_value = '30'; break;
  360. case 'max_input_time': $ini_value = '-1'; break;
  361. }
  362. }
  363. if ( $property == 'error_log' ) {
  364. $ini_value = basename( $ini_value );
  365. }
  366. return $ini_value;
  367. }
  368. /**
  369. * Encodes the less-than, greater-than, ampersand, double quote and single quote
  370. * characters, will never double encode entities.
  371. *
  372. * @param string $text The text which is to be encoded.
  373. * @return string The encoded text with HTML entities.
  374. */
  375. public static function escape( $text = '' ){
  376. // Escape the value of the variable using a built-in function if possible.
  377. if ( function_exists( 'esc_attr' ) ){
  378. $text = esc_attr( $text );
  379. } else {
  380. $text = htmlspecialchars( $text );
  381. }
  382. return $text;
  383. }
  384. /**
  385. * Generates a lowercase random string with an specific length.
  386. *
  387. * @param integer $length Length of the string that will be generated.
  388. * @return string The random string generated.
  389. */
  390. public static function random_char( $length = 4 ){
  391. $string = '';
  392. $chars = range( 'a','z' );
  393. for ( $i = 0; $i < $length; $i++ ){
  394. $string .= $chars[ rand( 0, count( $chars ) -1 ) ];
  395. }
  396. return $string;
  397. }
  398. /**
  399. * Translate a given number in bytes to a human readable file size using the
  400. * a approximate value in Kylo, Mega, Giga, etc.
  401. *
  402. * @link http://www.php.net/manual/en/function.filesize.php#106569
  403. * @param integer $bytes An integer representing a file size in bytes.
  404. * @param integer $decimals How many decimals should be returned after the translation.
  405. * @return string Human readable representation of the given number in Kylo, Mega, Giga, etc.
  406. */
  407. public static function human_filesize( $bytes = 0, $decimals = 2 ){
  408. $sz = 'BKMGTP';
  409. $factor = floor( (strlen( $bytes ) - 1) / 3 );
  410. return sprintf( "%.{$decimals}f", $bytes / pow( 1024, $factor ) ) . @$sz[ $factor ];
  411. }
  412. /**
  413. * Returns the system filepath to the relevant user uploads directory for this
  414. * site. This is a multisite capable function.
  415. *
  416. * @param string $path The relative path that needs to be completed to get the absolute path.
  417. * @return string The full filesystem path including the directory specified.
  418. */
  419. public static function datastore_folder_path( $path = '' ){
  420. $datastore_path = SucuriScanOption::get_option( ':datastore_path' );
  421. $datastore_dirname = 'sucuri';
  422. // Use the uploads folder by default.
  423. if ( empty($datastore_path) ) {
  424. $uploads_path = false;
  425. // Multisite installations may have different paths.
  426. if ( function_exists( 'wp_upload_dir' ) ) {
  427. $upload_dir = wp_upload_dir();
  428. if ( isset($upload_dir['basedir']) ) {
  429. $uploads_path = rtrim( $upload_dir['basedir'], '/' );
  430. }
  431. }
  432. if ( $uploads_path === false ) {
  433. if ( defined( 'WP_CONTENT_DIR' ) ) {
  434. $uploads_path = rtrim( WP_CONTENT_DIR, '/' ) . '/uploads';
  435. } else {
  436. $uploads_path = rtrim( ABSPATH, '/' ) . '/wp-content/uploads';
  437. }
  438. }
  439. $datastore_path = $uploads_path . '/' . $datastore_dirname;
  440. SucuriScanOption::update_option( ':datastore_path', $datastore_path );
  441. }
  442. $wp_filepath = rtrim( $datastore_path, '/' ) . '/' . $path;
  443. return $wp_filepath;
  444. }
  445. /**
  446. * Check whether the current site is working as a multi-site instance.
  447. *
  448. * @return boolean Either TRUE or FALSE in case WordPress is being used as a multi-site instance.
  449. */
  450. public static function is_multisite(){
  451. if (
  452. function_exists( 'is_multisite' )
  453. && is_multisite()
  454. ){
  455. return true;
  456. }
  457. return false;
  458. }
  459. /**
  460. * Find and retrieve the current version of Wordpress installed.
  461. *
  462. * @return string The version number of Wordpress installed.
  463. */
  464. public static function site_version(){
  465. global $wp_version;
  466. if ( $wp_version === null ) {
  467. $wp_version_path = ABSPATH . WPINC . '/version.php';
  468. if ( file_exists( $wp_version_path ) ) {
  469. include($wp_version_path);
  470. $wp_version = isset($wp_version) ? $wp_version : '0.0';
  471. }
  472. else {
  473. $option_version = get_option( 'version' );
  474. $wp_version = $option_version ? $option_version : '0.0';
  475. }
  476. }
  477. $wp_version = self::escape( $wp_version );
  478. return $wp_version;
  479. }
  480. /**
  481. * Find and retrieve the absolute path of the WordPress configuration file.
  482. *
  483. * @return string Absolute path of the WordPress configuration file.
  484. */
  485. public static function get_wpconfig_path(){
  486. if ( defined( 'ABSPATH' ) ){
  487. $file_path = ABSPATH . '/wp-config.php';
  488. // if wp-config.php doesn't exist, or is not readable check one directory up.
  489. if ( ! file_exists( $file_path ) ){
  490. $file_path = ABSPATH . '/../wp-config.php';
  491. }
  492. // Remove duplicated double slashes.
  493. $file_path = @realpath( $file_path );
  494. if ( $file_path ){
  495. return $file_path;
  496. }
  497. }
  498. return false;
  499. }
  500. /**
  501. * Find and retrieve the absolute path of the main WordPress htaccess file.
  502. *
  503. * @return string Absolute path of the main WordPress htaccess file.
  504. */
  505. public static function get_htaccess_path(){
  506. if ( defined( 'ABSPATH' ) ){
  507. $base_dirs = array(
  508. rtrim( ABSPATH, '/' ),
  509. dirname( ABSPATH ),
  510. dirname( dirname( ABSPATH ) ),
  511. );
  512. foreach ( $base_dirs as $base_dir ){
  513. $htaccess_path = sprintf( '%s/.htaccess', $base_dir );
  514. if ( file_exists( $htaccess_path ) ){
  515. return $htaccess_path;
  516. }
  517. }
  518. }
  519. return false;
  520. }
  521. /**
  522. * Get the pattern of the definition related with a WordPress secret key.
  523. *
  524. * @return string Secret key definition pattern.
  525. */
  526. public static function secret_key_pattern(){
  527. return '/define\((\s+)?\'([A-Z_]+)\',(\s+)?\'(.*)\'(\s+)?\);/';
  528. }
  529. /**
  530. * Retrieve the real ip address of the user in the current request.
  531. *
  532. * @param boolean $return_header Whether the header name where the address was found must be returned.
  533. * @return string The real ip address of the user in the current request.
  534. */
  535. public static function get_remote_addr( $return_header = false ){
  536. $remote_addr = '';
  537. $header_used = 'unknown';
  538. if (
  539. self::support_reverse_proxy()
  540. || self::is_behind_cloudproxy()
  541. ) {
  542. $alternatives = array(
  543. 'HTTP_X_SUCURI_CLIENTIP',
  544. 'HTTP_X_REAL_IP',
  545. 'HTTP_CLIENT_IP',
  546. 'HTTP_X_FORWARDED_FOR',
  547. 'HTTP_X_FORWARDED',
  548. 'HTTP_FORWARDED_FOR',
  549. 'HTTP_FORWARDED',
  550. 'SUCURI_RIP',
  551. 'REMOTE_ADDR',
  552. );
  553. foreach ( $alternatives as $alternative ){
  554. if (
  555. isset($_SERVER[ $alternative ])
  556. && self::is_valid_ip( $_SERVER[ $alternative ] )
  557. ){
  558. $remote_addr = $_SERVER[ $alternative ];
  559. $header_used = $alternative;
  560. break;
  561. }
  562. }
  563. }
  564. elseif ( isset($_SERVER['REMOTE_ADDR']) ) {
  565. $remote_addr = $_SERVER['REMOTE_ADDR'];
  566. $header_used = 'REMOTE_ADDR';
  567. }
  568. if ( $remote_addr == '::1' ){
  569. $remote_addr = '127.0.0.1';
  570. }
  571. if ( $return_header ){
  572. return $header_used;
  573. }
  574. return $remote_addr;
  575. }
  576. /**
  577. * Return the HTTP header used to retrieve the remote address.
  578. *
  579. * @return string The HTTP header used to retrieve the remote address.
  580. */
  581. public static function get_remote_addr_header(){
  582. return self::get_remote_addr( true );
  583. }
  584. /**
  585. * Retrieve the user-agent from the current request.
  586. *
  587. * @return string The user-agent from the current request.
  588. */
  589. public static function get_user_agent(){
  590. if ( isset($_SERVER['HTTP_USER_AGENT']) ){
  591. return self::escape( $_SERVER['HTTP_USER_AGENT'] );
  592. }
  593. return false;
  594. }
  595. /**
  596. * Get the clean version of the current domain.
  597. *
  598. * @return string The domain of the current site.
  599. */
  600. public static function get_domain( $return_tld = false ){
  601. if ( function_exists( 'get_site_url' ) ) {
  602. $site_url = get_site_url();
  603. $pattern = '/([fhtps]+:\/\/)?([^:\/]+)(:[0-9:]+)?(\/.*)?/';
  604. $replacement = ( $return_tld === true ) ? '$2' : '$2$3$4';
  605. $domain_name = @preg_replace( $pattern, $replacement, $site_url );
  606. return $domain_name;
  607. }
  608. return false;
  609. }
  610. /**
  611. * Get top-level domain (TLD) of the website.
  612. *
  613. * @return string Top-level domain (TLD) of the website.
  614. */
  615. public static function get_top_level_domain(){
  616. return self::get_domain( true );
  617. }
  618. /**
  619. * Check whether reverse proxy servers must be supported.
  620. *
  621. * @return boolean TRUE if reverse proxies must be supported, FALSE otherwise.
  622. */
  623. public static function support_reverse_proxy(){
  624. return (bool) ( SucuriScanOption::get_option( ':revproxy' ) === 'enabled' );
  625. }
  626. /**
  627. * Check whether the site is behing the Sucuri CloudProxy network.
  628. *
  629. * @param boolean $verbose Return an array with the hostname, address, and status, or not.
  630. * @return boolean Either TRUE or FALSE if the site is behind CloudProxy.
  631. */
  632. public static function is_behind_cloudproxy( $verbose = false ){
  633. $http_host = self::get_top_level_domain();
  634. $host_by_addr = @gethostbyname( $http_host );
  635. $host_by_name = @gethostbyaddr( $host_by_addr );
  636. $status = (bool) preg_match( '/^cloudproxy[0-9]+\.sucuri\.net$/', $host_by_name );
  637. /*
  638. * If the DNS reversion failed but the CloudProxy API key is set, then consider
  639. * the site as protected by a firewall. A fake key can be used to bypass the DNS
  640. * checking, but that is not something that will affect us, only the client.
  641. */
  642. if (
  643. $status === false
  644. && SucuriScanAPI::get_cloudproxy_key()
  645. ) {
  646. $status = true;
  647. }
  648. if ( $verbose ){
  649. return array(
  650. 'http_host' => $http_host,
  651. 'host_name' => $host_by_name,
  652. 'host_addr' => $host_by_addr,
  653. 'status' => $status,
  654. );
  655. }
  656. return $status;
  657. }
  658. /**
  659. * Get the email address set by the administrator to receive the notifications
  660. * sent by the plugin, if the email is missing the WordPress email address is
  661. * chosen by default.
  662. *
  663. * @return string The administrator email address.
  664. */
  665. public static function get_site_email(){
  666. $email = get_option( 'admin_email' );
  667. if ( self::is_valid_email( $email ) ){
  668. return $email;
  669. }
  670. return false;
  671. }
  672. /**
  673. * Returns the current time measured in the number of seconds since the Unix Epoch.
  674. *
  675. * @return integer Return current Unix timestamp.
  676. */
  677. public static function local_time(){
  678. if ( function_exists( 'current_time' ) ){
  679. return current_time( 'timestamp' );
  680. } else {
  681. return time();
  682. }
  683. }
  684. /**
  685. * Retrieve the date in localized format, based on timestamp.
  686. *
  687. * If the locale specifies the locale month and weekday, then the locale will
  688. * take over the format for the date. If it isn't, then the date format string
  689. * will be used instead.
  690. *
  691. * @param integer $timestamp Unix timestamp.
  692. * @return string The date, translated if locale specifies it.
  693. */
  694. public static function datetime( $timestamp = 0 ){
  695. if ( is_numeric( $timestamp ) && $timestamp > 0 ){
  696. $date_format = get_option( 'date_format' );
  697. $time_format = get_option( 'time_format' );
  698. $timezone_format = sprintf( '%s %s', $date_format, $time_format );
  699. return date_i18n( $timezone_format, $timestamp );
  700. }
  701. return null;
  702. }
  703. /**
  704. * Retrieve the date in localized format based on the current time.
  705. *
  706. * @return string The date, translated if locale specifies it.
  707. */
  708. public static function current_datetime(){
  709. $local_time = self::local_time();
  710. return self::datetime( $local_time );
  711. }
  712. /**
  713. * Return the time passed since the specified timestamp until now.
  714. *
  715. * @param integer $timestamp The Unix time number of the date/time before now.
  716. * @return string The time passed since the timestamp specified.
  717. */
  718. public static function time_ago( $timestamp = 0 ){
  719. if ( ! is_numeric( $timestamp ) ){
  720. $timestamp = strtotime( $timestamp );
  721. }
  722. $local_time = self::local_time();
  723. $diff = abs( $local_time - intval( $timestamp ) );
  724. if ( $diff == 0 ){ return 'just now'; }
  725. $intervals = array(
  726. 1 => array( 'year', 31556926, ),
  727. $diff < 31556926 => array( 'month', 2592000, ),
  728. $diff < 2592000 => array( 'week', 604800, ),
  729. $diff < 604800 => array( 'day', 86400, ),
  730. $diff < 86400 => array( 'hour', 3600, ),
  731. $diff < 3600 => array( 'minute', 60, ),
  732. $diff < 60 => array( 'second', 1, ),
  733. );
  734. $value = floor( $diff / $intervals[1][1] );
  735. $time_ago = sprintf(
  736. '%s %s%s ago',
  737. $value,
  738. $intervals[1][0],
  739. ( $value > 1 ? 's' : '' )
  740. );
  741. return $time_ago;
  742. }
  743. /**
  744. * Convert an string of characters into a valid variable name.
  745. *
  746. * @see http://www.php.net/manual/en/language.variables.basics.php
  747. *
  748. * @param string $text A text containing alpha-numeric and special characters.
  749. * @return string A valid variable name.
  750. */
  751. public static function human2var( $text = '' ){
  752. $text = strtolower( $text );
  753. $pattern = '/[^a-z0-9_]/';
  754. $var_name = preg_replace( $pattern, '_', $text );
  755. return $var_name;
  756. }
  757. /**
  758. * Check whether a variable contains a serialized data or not.
  759. *
  760. * @param string $data The data that will be checked.
  761. * @return boolean TRUE if the data was serialized, FALSE otherwise.
  762. */
  763. public static function is_serialized( $data = '' ){
  764. return ( is_string( $data ) && preg_match( '/^(a|O):[0-9]+:.+/', $data ) );
  765. }
  766. /**
  767. * Check whether an IP address has a valid format or not.
  768. *
  769. * @param string $remote_addr The host IP address.
  770. * @return boolean Whether the IP address specified is valid or not.
  771. */
  772. public static function is_valid_ip( $remote_addr = '' ){
  773. // Check for IPv4 and IPv6.
  774. if ( function_exists( 'filter_var' ) ){
  775. return (bool) filter_var( $remote_addr, FILTER_VALIDATE_IP );
  776. }
  777. // Assuming older version of PHP and server, so only will check for IPv4.
  778. elseif ( strlen( $remote_addr ) >= 7 ) {
  779. $pattern = '/^([0-9]{1,3}\.){3}[0-9]{1,3}$/';
  780. if ( preg_match( $pattern, $remote_addr, $match ) ){
  781. for ( $i = 0; $i < 4; $i++ ){
  782. if ( $match[ $i ] > 255 ){ return false; }
  783. }
  784. return true;
  785. }
  786. }
  787. return false;
  788. }
  789. /**
  790. * Check whether an IP address is formatted as CIDR or not.
  791. *
  792. * @param string $remote_addr The supposed ip address that will be checked.
  793. * @return boolean Either TRUE or FALSE if the ip address specified is valid or not.
  794. */
  795. public static function is_valid_cidr( $remote_addr = '' ){
  796. if ( preg_match( '/^([0-9\.]{7,15})\/(8|16|24)$/', $remote_addr, $match ) ) {
  797. if ( self::is_valid_ip( $match[1] ) ) {
  798. return true;
  799. }
  800. }
  801. return false;
  802. }
  803. /**
  804. * Separate the parts of an IP address.
  805. *
  806. * @param string $remote_addr The supposed ip address that will be formatted.
  807. * @return array Clean address, CIDR range, and CIDR format; FALSE otherwise.
  808. */
  809. public static function get_ip_info( $remote_addr = '' ){
  810. if ( $remote_addr ) {
  811. $ip_parts = explode( '/', $remote_addr );
  812. if (
  813. array_key_exists( 0, $ip_parts )
  814. && self::is_valid_ip( $ip_parts[0] )
  815. ) {
  816. $addr_info = array();
  817. $addr_info['remote_addr'] = $ip_parts[0];
  818. $addr_info['cidr_range'] = isset($ip_parts[1]) ? $ip_parts[1] : '32';
  819. $addr_info['cidr_format'] = $addr_info['remote_addr'] . '/' . $addr_info['cidr_range'];
  820. return $addr_info;
  821. }
  822. }
  823. return false;
  824. }
  825. /**
  826. * Validate email address.
  827. *
  828. * This use the native PHP function filter_var which is available in PHP >=
  829. * 5.2.0 if it is not found in the interpreter this function will sue regular
  830. * expressions to check whether the email address passed is valid or not.
  831. *
  832. * @see http://www.php.net/manual/en/function.filter-var.php
  833. *
  834. * @param string $email The string that will be validated as an email address.
  835. * @return boolean TRUE if the email address passed to the function is valid, FALSE if not.
  836. */
  837. public static function is_valid_email( $email = '' ){
  838. if ( function_exists( 'filter_var' ) ){
  839. return (bool) filter_var( $email, FILTER_VALIDATE_EMAIL );
  840. } else {
  841. $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/ix';
  842. return (bool) preg_match( $pattern, $email );
  843. }
  844. }
  845. /**
  846. * Return a string with all the valid email addresses.
  847. *
  848. * @param string $email The string that will be validated as an email address.
  849. * @param boolean $as_array TRUE to return the list of valid email addresses as an array.
  850. * @return string All the valid email addresses separated by a comma.
  851. */
  852. public static function get_valid_email( $email = '', $as_array = false ){
  853. $valid_emails = array();
  854. $is_valid_string = (bool) ( is_string( $email ) && ! empty($email) );
  855. if (
  856. $is_valid_string === true
  857. && strpos( $email, ',' ) !== false
  858. ) {
  859. $addresses = explode( ',', $email );
  860. foreach ( $addresses as $address ){
  861. $address = trim( $address );
  862. if ( self::is_valid_email( $address ) ){
  863. $valid_emails[] = $address;
  864. }
  865. }
  866. }
  867. elseif (
  868. $is_valid_string === true
  869. && self::is_valid_email( $email )
  870. ) {
  871. $valid_emails[] = $email;
  872. }
  873. if ( ! empty($valid_emails) ) {
  874. $valid_emails = array_unique( $valid_emails );
  875. if ( $as_array === true ) {
  876. return $valid_emails;
  877. }
  878. return self::implode( ', ', $valid_emails );
  879. }
  880. return false;
  881. }
  882. /**
  883. * Cut a long text to the length specified, and append suspensive points at the end.
  884. *
  885. * @param string $text String of characters that will be cut.
  886. * @param integer $length Maximum length of the returned string, default is 10.
  887. * @return string Short version of the text specified.
  888. */
  889. public static function excerpt( $text = '', $length = 10 ){
  890. $text_length = strlen( $text );
  891. if ( $text_length > $length ){
  892. return substr( $text, 0, $length ) . '...';
  893. }
  894. return $text;
  895. }
  896. /**
  897. * Same as the excerpt method but with the string reversed.
  898. *
  899. * @param string $text String of characters that will be cut.
  900. * @param integer $length Maximum length of the returned string, default is 10.
  901. * @return string Short version of the text specified.
  902. */
  903. public static function excerpt_rev( $text = '', $length = 10 ){
  904. $str_reversed = strrev( $text );
  905. $str_excerpt = self::excerpt( $str_reversed, $length );
  906. $text_transformed = strrev( $str_excerpt );
  907. return $text_transformed;
  908. }
  909. /**
  910. * Check whether an list is a multidimensional array or not.
  911. *
  912. * @param array $list An array or multidimensional array of different values.
  913. * @return boolean TRUE if the list is multidimensional, FALSE otherwise.
  914. */
  915. public static function is_multi_list( $list = array() ){
  916. if ( ! empty($list) ){
  917. foreach ( (array) $list as $item ) {
  918. if ( is_array( $item ) ) {
  919. return true;
  920. }
  921. }
  922. }
  923. return false;
  924. }
  925. /**
  926. * Join array elements with a string no matter if it is multidimensional.
  927. *
  928. * @param string $separator Character that will act as a separator, default to an empty string.
  929. * @param array $list The array of strings to implode.
  930. * @return string String of all the items in the list, with the separator between them.
  931. */
  932. public static function implode( $separator = '', $list = array() ){
  933. if ( self::is_multi_list( $list ) ){
  934. $pieces = array();
  935. foreach ( $list as $items ){
  936. $pieces[] = @implode( $separator, $items );
  937. }
  938. $joined_pieces = '(' . implode( '), (', $pieces ) . ')';
  939. return $joined_pieces;
  940. } else {
  941. return implode( $separator, $list );
  942. }
  943. }
  944. /**
  945. * Determine if the plugin notices can be displayed in the current page.
  946. *
  947. * @param string $current_page Identifier of the current page.
  948. * @return boolean TRUE if the current page must not have noticies.
  949. */
  950. public static function no_notices_here( $current_page = false ){
  951. global $sucuriscan_no_notices_in;
  952. if ( $current_page === false ) {
  953. $current_page = SucuriScanRequest::get( 'page' );
  954. }
  955. if (
  956. isset($sucuriscan_no_notices_in)
  957. && is_array( $sucuriscan_no_notices_in )
  958. && ! empty($sucuriscan_no_notices_in)
  959. ) {
  960. return (bool) in_array( $current_page, $sucuriscan_no_notices_in );
  961. }
  962. return false;
  963. }
  964. /**
  965. * Check whether the site is running over the Nginx web server.
  966. *
  967. * @return boolean TRUE if the site is running over Nginx, FALSE otherwise.
  968. */
  969. public static function is_nginx_server(){
  970. return (bool) preg_match( '/^nginx(\/[0-9\.]+)?$/', @$_SERVER['SERVER_SOFTWARE'] );
  971. }
  972. /**
  973. * Check whether the site is running over the Nginx web server.
  974. *
  975. * @return boolean TRUE if the site is running over Nginx, FALSE otherwise.
  976. */
  977. public static function is_iis_server(){
  978. return (bool) preg_match( '/Microsoft-IIS/i', @$_SERVER['SERVER_SOFTWARE'] );
  979. }
  980. }
  981. /**
  982. * HTTP request handler.
  983. *
  984. * Function definitions to retrieve, validate, and clean the parameters during a
  985. * HTTP request, generally after a form submission or while loading a URL. Use
  986. * these methods at most instead of accessing an index in the global PHP
  987. * variables _POST, _GET, _REQUEST since they may come with insecure data.
  988. */
  989. class SucuriScanRequest extends SucuriScan {
  990. /**
  991. * Returns the value stored in a specific index in the global _GET, _POST or
  992. * _REQUEST variables, you can specify a pattern as the second argument to
  993. * match allowed values.
  994. *
  995. * @param array $list The array where the specified key will be searched.
  996. * @param string $key Name of the index where the requested variable is supposed to be.
  997. * @param string $pattern Optional pattern to match allowed values in the requested key.
  998. * @return string The value stored in the specified key inside the global _GET variable.
  999. */
  1000. public static function request( $list = array(), $key = '', $pattern = '' ){
  1001. $key = self::variable_prefix( $key );
  1002. if (
  1003. is_array( $list )
  1004. && is_string( $key )
  1005. && isset($list[ $key ])
  1006. ){
  1007. // Select the key from the list and escape its content.
  1008. $key_value = $list[ $key ];
  1009. // Define regular expressions for specific value types.
  1010. if ( $pattern === '' ){
  1011. $pattern = '/.*/';
  1012. } else {
  1013. switch ( $pattern ){
  1014. case '_nonce': $pattern = '/^[a-z0-9]{10}$/'; break;
  1015. case '_page': $pattern = '/^[a-z_]+$/'; break;
  1016. case '_array': $pattern = '_array'; break;
  1017. case '_yyyymmdd': $pattern = '/^[0-9]{4}(\-[0-9]{2}){2}$/'; break;
  1018. default: $pattern = '/^'.$pattern.'$/'; break;
  1019. }
  1020. }
  1021. // If the request data is an array, then only cast the value.
  1022. if ( $pattern == '_array' && is_array( $key_value ) ){
  1023. return (array) $key_value;
  1024. }
  1025. // Check the format of the request data with a regex defined above.
  1026. if ( @preg_match( $pattern, $key_value ) ){
  1027. return self::escape( $key_value );
  1028. }
  1029. }
  1030. return false;
  1031. }
  1032. /**
  1033. * Returns the value stored in a specific index in the global _GET variable,
  1034. * you can specify a pattern as the second argument to match allowed values.
  1035. *
  1036. * @param string $key Name of the index where the requested variable is supposed to be.
  1037. * @param string $pattern Optional pattern to match allowed values in the requested key.
  1038. * @return string The value stored in the specified key inside the global _GET variable.
  1039. */
  1040. public static function get( $key = '', $pattern = '' ){
  1041. return self::request( $_GET, $key, $pattern );
  1042. }
  1043. /**
  1044. * Returns the value stored in a specific index in the global _POST variable,
  1045. * you can specify a pattern as the second argument to match allowed values.
  1046. *
  1047. * @param string $key Name of the index where the requested variable is supposed to be.
  1048. * @param string $pattern Optional pattern to match allowed values in the requested key.
  1049. * @return string The value stored in the specified key inside the global _POST variable.
  1050. */
  1051. public static function post( $key = '', $pattern = '' ){
  1052. return self::request( $_POST, $key, $pattern );
  1053. }
  1054. /**
  1055. * Returns the value stored in a specific index in the global _REQUEST variable,
  1056. * you can specify a pattern as the second argument to match allowed values.
  1057. *
  1058. * @param string $key Name of the index where the requested variable is supposed to be.
  1059. * @param string $pattern Optional pattern to match allowed values in the requested key.
  1060. * @return string The value stored in the specified key inside the global _POST variable.
  1061. */
  1062. public static function get_or_post( $key = '', $pattern = '' ){
  1063. return self::request( $_REQUEST, $key, $pattern );
  1064. }
  1065. }
  1066. /**
  1067. * Class to process files and folders.
  1068. *
  1069. * Here are implemented the functions needed to open, scan, read, create files
  1070. * and folders using the built-in PHP class SplFileInfo. The SplFileInfo class
  1071. * offers a high-level object oriented interface to information for an individual
  1072. * file.
  1073. */
  1074. class SucuriScanFileInfo extends SucuriScan {
  1075. /**
  1076. * Define the interface that will be used to execute the file system scans, the
  1077. * available options are SPL, OpenDir, and Glob (all in lowercase). This can be
  1078. * configured from the settings page.
  1079. *
  1080. * @var string
  1081. */
  1082. public $scan_interface = 'spl';
  1083. /**
  1084. * Whether the list of files that can be ignored from the filesystem scan will
  1085. * be used to return the directory tree, this should be disabled when scanning a
  1086. * directory without the need to filter the items in the list.
  1087. *
  1088. * @var boolean
  1089. */
  1090. public $ignore_files = true;
  1091. /**
  1092. * Whether the list of folders that can be ignored from the filesystem scan will
  1093. * be used to return the directory tree, this should be disabled when scanning a
  1094. * path without the need to filter the items in the list.
  1095. *
  1096. * @var boolean
  1097. */
  1098. public $ignore_directories = true;
  1099. /**
  1100. * A list of ignored directory paths, these folders will be skipped during the
  1101. * execution of the file system scans, and any sub-directory or files inside
  1102. * these paths will be ignored too.
  1103. *
  1104. * @see SucuriScanFSScanner.get_ignored_directories()
  1105. * @var array
  1106. */
  1107. private $ignored_directories = array();
  1108. /**
  1109. * Whether the filesystem scanner should run recursively or not.
  1110. *
  1111. * @var boolean
  1112. */
  1113. public $run_recursively = true;
  1114. /**
  1115. * Class constructor.
  1116. */
  1117. public function __construct(){
  1118. }
  1119. /**
  1120. * Retrieve a long text string with signatures of all the files contained
  1121. * in the main and subdirectories of the folder specified, also the filesize
  1122. * and md5sum of that file. Some folders and files will be ignored depending
  1123. * on some rules defined by the developer.
  1124. *
  1125. * @param string $directory Parent directory where the filesystem scan will start.
  1126. * @param boolean $as_array Whether the result of the operation will be returned as an array or string.
  1127. * @return array List of files in the main and subdirectories of the folder specified.
  1128. */
  1129. public function get_directory_tree_md5( $directory = '', $as_array = false ){
  1130. $project_signatures = '';
  1131. $abs_path = rtrim( ABSPATH, DIRECTORY_SEPARATOR );
  1132. $files = $this->get_directory_tree( $directory );
  1133. if ( $as_array ){
  1134. $project_signatures = array();
  1135. }
  1136. if ( $files ){
  1137. sort( $files );
  1138. foreach ( $files as $filepath ){
  1139. $file_checksum = @md5_file( $filepath );
  1140. $filesize = @filesize( $filepath );
  1141. if ( $as_array ){
  1142. $basename = str_replace( $abs_path . DIRECTORY_SEPARATOR, '', $filepath );
  1143. $project_signatures[ $basename ] = array(
  1144. 'filepath' => $filepath,
  1145. 'checksum' => $file_checksum,
  1146. 'filesize' => $filesize,
  1147. 'created_at' => @filectime( $filepath ),
  1148. 'modified_at' => @filemtime( $filepath ),
  1149. );
  1150. } else {
  1151. $filepath = str_replace( $abs_path, $abs_path . DIRECTORY_SEPARATOR, $filepath );
  1152. $project_signatures .= sprintf(
  1153. "%s%s%s%s\n",
  1154. $file_checksum,
  1155. $filesize,
  1156. chr( 32 ),
  1157. $filepath
  1158. );
  1159. }
  1160. }
  1161. }
  1162. return $project_signatures;
  1163. }
  1164. /**
  1165. * Retrieve a list with all the files contained in the main and subdirectories
  1166. * of the folder specified. Some folders and files will be ignored depending
  1167. * on some rules defined by the developer.
  1168. *
  1169. * @param string $directory Parent directory where the filesystem scan will start.
  1170. * @return array List of files in the main and subdirectories of the folder specified.
  1171. */
  1172. public function get_directory_tree( $directory = '' ){
  1173. if ( file_exists( $directory ) && is_dir( $directory ) ){
  1174. $tree = array();
  1175. // Check whether the ignore scanning feature is enabled or not.
  1176. if ( SucuriScanFSScanner::will_ignore_scanning() ){
  1177. $this->ignored_directories = SucuriScanFSScanner::get_ignored_directories();
  1178. }
  1179. switch ( $this->scan_interface ){
  1180. case 'spl':
  1181. if ( $this->is_spl_available() ){
  1182. $tree = $this->get_directory_tree_with_spl( $directory );
  1183. } else {
  1184. $this->scan_interface = 'opendir';
  1185. $tree = $this->get_directory_tree( $directory );
  1186. }
  1187. break;
  1188. case 'glob':
  1189. $tree = $this->get_directory_tree_with_glob( $directory );
  1190. break;
  1191. case 'opendir':
  1192. $tree = $this->get_directory_tree_with_opendir( $directory );
  1193. break;
  1194. default:
  1195. $this->scan_interface = 'spl';
  1196. $tree = $this->get_directory_tree( $directory );
  1197. break;
  1198. }
  1199. return $tree;
  1200. }
  1201. return false;
  1202. }
  1203. /**
  1204. * Find a file under the directory tree specified.
  1205. *
  1206. * @param string $filename Name of the folder or file being scanned at the moment.
  1207. * @param string $directory Directory where the scanner is located at the moment.
  1208. * @return array List of file paths where the file was found.
  1209. */
  1210. public function find_file( $filename = '', $directory = null ){
  1211. $file_paths = array();
  1212. if (
  1213. is_null( $directory )
  1214. && defined( 'ABSPATH' )
  1215. ){
  1216. $directory = ABSPATH;
  1217. }
  1218. if ( is_dir( $directory ) ){
  1219. $dir_tree = $this->get_directory_tree( $directory );
  1220. foreach ( $dir_tree as $filepath ){
  1221. if ( stripos( $filepath, $filename ) !== false ){
  1222. $file_paths[] = $filepath;
  1223. }
  1224. }
  1225. }
  1226. return $file_paths;
  1227. }
  1228. /**
  1229. * Check whether the built-in class SplFileObject is available in the system
  1230. * or not, it is required to have PHP >= 5.1.0. The SplFileObject class offers
  1231. * an object oriented interface for a file.
  1232. *
  1233. * @link http://www.php.net/manual/en/class.splfileobject.php
  1234. *
  1235. * @return boolean Whether the PHP class "SplFileObject" is available or not.
  1236. */
  1237. public static function is_spl_available(){
  1238. return (bool) class_exists( 'SplFileObject' );
  1239. }
  1240. /**
  1241. * Retrieve a list with all the files contained in the main and subdirectories
  1242. * of the folder specified. Some folders and files will be ignored depending
  1243. * on some rules defined by the developer.
  1244. *
  1245. * @link http://www.php.net/manual/en/class.recursivedirectoryiterator.php
  1246. * @see RecursiveDirectoryIterator extends FilesystemIterator
  1247. * @see FilesystemIterator extends DirectoryIterator
  1248. * @see DirectoryIterator extends SplFileInfo
  1249. * @see SplFileInfo
  1250. *
  1251. * @param string $directory Parent directory where the filesystem scan will start.
  1252. * @return array List of files in the main and subdirectories of the folder specified.
  1253. */
  1254. private function get_directory_tree_with_spl( $directory = '' ){
  1255. $files = array();
  1256. $filepath = @realpath( $directory );
  1257. if ( ! class_exists( 'FilesystemIterator' ) ){
  1258. return $this->get_directory_tree( $directory, 'opendir' );
  1259. }
  1260. if ( $this->run_recursively ){
  1261. $flags = FilesystemIterator::KEY_AS_PATHNAME
  1262. | FilesystemIterator::CURRENT_AS_FILEINFO
  1263. | FilesystemIterator::SKIP_DOTS
  1264. | FilesystemIterator::UNIX_PATHS;
  1265. $objects = new RecursiveIteratorIterator(
  1266. new RecursiveDirectoryIterator( $filepath, $flags ),
  1267. RecursiveIteratorIterator::SELF_FIRST,
  1268. RecursiveIteratorIterator::CATCH_GET_CHILD
  1269. );
  1270. } else {
  1271. $objects = new DirectoryIterator( $filepath );
  1272. }
  1273. foreach ( $objects as $filepath => $fileinfo ){
  1274. if ( $fileinfo->isDir() ) { continue; }
  1275. if ( $this->run_recursively ){
  1276. $directory = dirname( $filepath );
  1277. $filename = $fileinfo->getFilename();
  1278. } else {
  1279. $directory = $fileinfo->getPath();
  1280. $filename = $fileinfo->getFilename();
  1281. $filepath = $directory . '/' . $filename;
  1282. }
  1283. if ( $this->ignore_folderpath( $directory, $filename ) ){ continue; }
  1284. if ( $this->ignore_filepath( $filename ) ){ continue; }
  1285. $files[] = $filepath;
  1286. }
  1287. return $files;
  1288. }
  1289. /**
  1290. * Retrieve a list with all the files contained in the main and subdirectories
  1291. * of the folder specified. Some folders and files will be ignored depending
  1292. * on some rules defined by the developer.
  1293. *
  1294. * @param string $directory Parent directory where the filesystem scan will start.
  1295. * @return array List of files in the main and subdirectories of the folder specified.
  1296. */
  1297. private function get_directory_tree_with_glob( $directory = '' ){
  1298. $files = array();
  1299. $directory_pattern = sprintf( '%s/*', rtrim( $directory,'/' ) );
  1300. $files_found = glob( $directory_pattern );
  1301. if ( is_array( $files_found ) ){
  1302. foreach ( $files_found as $filepath ){
  1303. $filepath = @realpath( $filepath );
  1304. $directory = dirname( $filepath );
  1305. $filepath_parts = explode( '/', $filepath );
  1306. $filename = array_pop( $filepath_parts );
  1307. if ( is_dir( $filepath ) ){
  1308. if ( $this->ignore_folderpath( $directory, $filename ) ){ continue; }
  1309. if ( $this->run_recursively ){
  1310. $sub_files = $this->get_directory_tree_with_glob( $filepath );
  1311. if ( $sub_files ){
  1312. $files = array_merge( $files, $sub_files );
  1313. }
  1314. }
  1315. } else {
  1316. if ( $this->ignore_filepath( $filename ) ){ continue; }
  1317. $files[] = $filepath;
  1318. }
  1319. }
  1320. }
  1321. return $files;
  1322. }
  1323. /**
  1324. * Retrieve a list with all the files contained in the main and subdirectories
  1325. * of the folder specified. Some folders and files will be ignored depending
  1326. * on some rules defined by the developer.
  1327. *
  1328. * @param string $directory Parent directory where the filesystem scan will start.
  1329. * @return array List of files in the main and subdirectories of the folder specified.
  1330. */
  1331. private function get_directory_tree_with_opendir( $directory = '' ){
  1332. $files = array();
  1333. $dh = @opendir( $directory );
  1334. if ( ! $dh ) { return false; }
  1335. while ( ($filename = readdir( $dh )) !== false ){
  1336. $filepath = @realpath( $directory . '/' . $filename );
  1337. if ( $filepath === false ) {
  1338. continue;
  1339. } elseif ( is_dir( $filepath ) ){
  1340. if ( $this->ignore_folderpath( $directory, $filename ) ){ continue; }
  1341. if ( $this->run_recursively ){
  1342. $sub_files = $this->get_directory_tree_with_opendir( $filepath );
  1343. if ( $sub_files ){
  1344. $files = array_merge( $files, $sub_files );
  1345. }
  1346. }
  1347. } else {
  1348. if ( $this->ignore_filepath( $filename ) ){ continue; }
  1349. $files[] = $filepath;
  1350. }
  1351. }
  1352. closedir( $dh );
  1353. return $files;
  1354. }
  1355. /**
  1356. * Skip some specific directories and file paths from the filesystem scan.
  1357. *
  1358. * @param string $directory Directory where the scanner is located at the moment.
  1359. * @param string $filename Name of the folder or file being scanned at the moment.
  1360. * @return boolean Either TRUE or FALSE representing that the scan should ignore this folder or not.
  1361. */
  1362. private function ignore_folderpath( $directory = '', $filename = '' ){
  1363. // Ignoring current and parent folders.
  1364. if ( $filename == '.' || $filename == '..' ){ return true; }
  1365. if ( $this->ignore_directories ){
  1366. // Ignore directories based on a common regular expression.
  1367. $filepath = @realpath( $directory . '/' . $filename );
  1368. $pattern = '/\/wp-content\/(uploads|cache|backup|w3tc)/';
  1369. if ( preg_match( $pattern, $filepath ) ){
  1370. return true;
  1371. }
  1372. // Ignore directories specified by the administrator.
  1373. if ( ! empty($this->ignored_directories) ){
  1374. foreach ( $this->ignored_directories['directories'] as $ignored_dir ){
  1375. if ( strpos( $directory, $ignored_dir ) !== false ){
  1376. return true;
  1377. }
  1378. }
  1379. }
  1380. }
  1381. return false;
  1382. }
  1383. /**
  1384. * Skip some specific files from the filesystem scan.
  1385. *
  1386. * @param string $filename Name of the folder or file being scanned at the moment.
  1387. * @return boolean Either TRUE or FALSE representing that the scan should ignore this filename or not.
  1388. */
  1389. private function ignore_filepath( $filename = '' ){
  1390. if ( ! $this->ignore_files ){ return false; }
  1391. // Ignoring backup files from our clean ups.
  1392. if ( strpos( $filename, '_sucuribackup.' ) !== false ){ return true; }
  1393. // Any file maching one of these rules WILL NOT be ignored.
  1394. if (
  1395. ( strpos( $filename, '.php' ) !== false) ||
  1396. ( strpos( $filename, '.htm' ) !== false) ||
  1397. ( strpos( $filename, '.js' ) !== false) ||
  1398. ( strcmp( $filename, '.htaccess' ) == 0 ) ||
  1399. ( strcmp( $filename, 'php.ini' ) == 0 )
  1400. ){ return false; }
  1401. return true;
  1402. }
  1403. /**
  1404. * Retrieve a list of unique directory paths.
  1405. *
  1406. * @param array $dir_tree A list of files under a directory.
  1407. * @return array A list of unique directory paths.
  1408. */
  1409. public function get_diretories_only( $dir_tree = array() ){
  1410. $dirs = array();
  1411. if ( is_string( $dir_tree ) ){
  1412. $dir_tree = $this->get_directory_tree( $dir_tree );
  1413. }
  1414. if ( is_array( $dir_tree ) && ! empty($dir_tree) ){
  1415. foreach ( $dir_tree as $filepath ){
  1416. $dir_path = dirname( $filepath );
  1417. if (
  1418. ! in_array( $dir_path, $dirs )
  1419. && ! in_array( $dir_path, $this->ignored_directories['directories'] )
  1420. ){
  1421. $dirs[] = $dir_path;
  1422. }
  1423. }
  1424. }
  1425. return $dirs;
  1426. }
  1427. /**
  1428. * Remove a directory recursively.
  1429. *
  1430. * @param string $directory Path of the existing directory that will be removed.
  1431. * @return boolean TRUE if all the files and folder inside the directory were removed.
  1432. */
  1433. public function remove_directory_tree( $directory = '' ){
  1434. $all_removed = true;
  1435. $dir_tree = $this->get_directory_tree( $directory );
  1436. if ( $dir_tree ){
  1437. $dirs_only = array();
  1438. foreach ( $dir_tree as $filepath ){
  1439. if ( is_file( $filepath ) ){
  1440. $removed = @unlink( $filepath );
  1441. if ( ! $removed ){
  1442. $all_removed = false;
  1443. }
  1444. }
  1445. elseif ( is_dir( $filepath ) ){
  1446. $dirs_only[] = $filepath;
  1447. }
  1448. }
  1449. if ( ! function_exists( 'sucuriscan_strlen_diff' ) ){
  1450. /**
  1451. * Evaluates the difference between the length of two strings.
  1452. *
  1453. * @param string $a First string of characters that will be measured.
  1454. * @param string $b Second string of characters that will be measured.
  1455. * @return integer The difference in length between the two strings.
  1456. */
  1457. function sucuriscan_strlen_diff( $a = '', $b = '' ){
  1458. return strlen( $b ) - strlen( $a );
  1459. }
  1460. }
  1461. usort( $dirs_only, 'sucuriscan_strlen_diff' );
  1462. foreach ( $dirs_only as $dir_path ){
  1463. @rmdir( $dir_path );
  1464. }
  1465. }
  1466. return $all_removed;
  1467. }
  1468. /**
  1469. * Return the lines of a file as an array, it will automatically remove the new
  1470. * line characters from the end of each line, and skip empty lines from the
  1471. * list.
  1472. *
  1473. * @param string $filepath Path to the file.
  1474. * @return array An array where each element is a line in the file.
  1475. */
  1476. public static function file_lines( $filepath = '' ){
  1477. return @file( $filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
  1478. }
  1479. /**
  1480. * Function to emulate the UNIX tail function by displaying the last X number of
  1481. * lines in a file. Useful for large files, such as logs, when you want to
  1482. * process lines in PHP or write lines to a database.
  1483. *
  1484. * @param string $file_path Path to the file.
  1485. * @param integer $lines Number of lines to retrieve from the end of the file.
  1486. * @param boolean $adaptive Whether the buffer will adapt to a specific number of bytes or not.
  1487. * @return string Text contained at the end of the file.
  1488. */
  1489. public static function tail_file( $file_path = '', $lines = 1, $adaptive = true ) {
  1490. $file = @fopen( $file_path, 'rb' );
  1491. $limit = $lines;
  1492. if ( $file ) {
  1493. fseek( $file, -1, SEEK_END );
  1494. if ( $adaptive && $lines < 2 ) { $buffer = 64; }
  1495. elseif ( $adaptive && $lines < 10 ) { $buffer = 512; }
  1496. else { $buffer = 4096; }
  1497. if ( fread( $file, 1 ) != "\n" ) { $lines -= 1; }
  1498. $output = '';
  1499. $chunk = '';
  1500. while ( ftell( $file ) > 0 && $lines >= 0 ) {
  1501. $seek = min( ftell( $file ), $buffer );
  1502. fseek( $file, -$seek, SEEK_CUR );
  1503. $chunk = fread( $file, $seek );
  1504. $output = $chunk . $output;
  1505. fseek( $file, -mb_strlen( $chunk, '8bit' ), SEEK_CUR );
  1506. $lines -= substr_count( $chunk, "\n" );
  1507. }
  1508. fclose( $file );
  1509. $lines_arr = explode( "\n", $output );
  1510. $lines_count = count( $lines_arr );
  1511. $result = array_slice( $lines_arr, ($lines_count - $limit) );
  1512. return $result;
  1513. }
  1514. return false;
  1515. }
  1516. /**
  1517. * Gets inode change time of file.
  1518. *
  1519. * @param string $file_path Path to the file.
  1520. * @return integer Time the file was last changed.
  1521. */
  1522. public static function creation_time( $file_path = '' ){
  1523. if ( file_exists( $file_path ) ) {
  1524. clearstatcache( $file_path );
  1525. return filectime( $file_path );
  1526. }
  1527. return 0;
  1528. }
  1529. /**
  1530. * Gets file modification time.
  1531. *
  1532. * @param string $file_path Path to the file.
  1533. * @return integer Time the file was last modified.
  1534. */
  1535. public static function modification_time( $file_path = '' ){
  1536. if ( file_exists( $file_path ) ) {
  1537. clearstatcache( $file_path );
  1538. return filemtime( $file_path );
  1539. }
  1540. return 0;
  1541. }
  1542. }
  1543. /**
  1544. * File-based cache library.
  1545. *
  1546. * WP_Object_Cache [1] is WordPress' class for caching data which may be
  1547. * computationally expensive to regenerate, such as the result of complex
  1548. * database queries. However the object cache is non-persistent. This means that
  1549. * data stored in the cache resides in memory only and only for the duration of
  1550. * the request. Cached data will not be stored persistently across page loads
  1551. * unless of the installation of a 3party persistent caching plugin [2].
  1552. *
  1553. * [1] http://codex.wordpress.org/Class_Reference/WP_Object_Cache
  1554. * [2] http://codex.wordpress.org/Class_Reference/WP_Object_Cache#Persistent_Caching
  1555. */
  1556. class SucuriScanCache extends SucuriScan {
  1557. /**
  1558. * The unique name (or identifier) of the file with the data.
  1559. *
  1560. * The file should be located in the same folder where the dynamic data
  1561. * generated by the plugin is stored, and using the following format [1], it
  1562. * most be a PHP file because it is expected to have an exit point in the first
  1563. * line of the file causing it to stop the execution if a unauthorized user
  1564. * tries to access it directly.
  1565. *
  1566. * [1] /public/data/sucuri-DATASTORE.php
  1567. *
  1568. * @var null|string
  1569. */
  1570. private $datastore = null;
  1571. /**
  1572. * The full path of the datastore file.
  1573. *
  1574. * @var string
  1575. */
  1576. private $datastore_path = '';
  1577. /**
  1578. * Whether the datastore file is usable or not.
  1579. *
  1580. * This variable will only be TRUE if the datastore file specified exists, is
  1581. * writable and readable, in any other case it will always be FALSE.
  1582. *
  1583. * @var boolean
  1584. */
  1585. private $usable_datastore = false;
  1586. /**
  1587. * Class constructor.
  1588. *
  1589. * @param string $datastore Unique name (or identifier) of the file with the data.
  1590. * @return void
  1591. */
  1592. public function __construct( $datastore = '' ){
  1593. $this->datastore = $datastore;
  1594. $this->datastore_path = $this->datastore_file_path();
  1595. $this->usable_datastore = (bool) $this->datastore_path;
  1596. }
  1597. /**
  1598. * Default attributes for every datastore file.
  1599. *
  1600. * @return string Default attributes for every datastore file.
  1601. */
  1602. private function datastore_default_info(){
  1603. $attrs = array(
  1604. 'datastore' => $this->datastore,
  1605. 'created_on' => time(),
  1606. 'updated_on' => time(),
  1607. );
  1608. return $attrs;
  1609. }
  1610. /**
  1611. * Default content of every datastore file.
  1612. *
  1613. * @param array $finfo Rainbow table with the key names and decoded values.
  1614. * @return string Default content of every datastore file.
  1615. */
  1616. private function datastore_info( $finfo = array() ){
  1617. $attrs = $this->datastore_default_info();
  1618. $info_is_available = (bool) isset($finfo['info']);
  1619. $info = "<?php\n";
  1620. foreach ( $attrs as $attr_name => $attr_value ){
  1621. if (
  1622. $info_is_available
  1623. && $attr_name != 'updated_on'
  1624. && isset($finfo['info'][ $attr_name ])
  1625. ){
  1626. $attr_value = $finfo['info'][ $attr_name ];
  1627. }
  1628. $info .= sprintf( "// %s=%s;\n", $attr_name, $attr_value );
  1629. }
  1630. $info .= "exit(0);\n";
  1631. $info .= "?>\n";
  1632. return $info;
  1633. }
  1634. /**
  1635. * Check if the datastore file exists, if it's writable and readable by the same
  1636. * user running the server, in case that it does not exists the function will
  1637. * tries to create it by itself with the right permissions to use it.
  1638. *
  1639. * @return string The full path where the datastore file is located, FALSE otherwise.
  1640. */
  1641. private function datastore_file_path(){
  1642. if ( ! is_null( $this->datastore ) ){
  1643. $folder_path = $this->datastore_folder_path();
  1644. $file_path = $folder_path . 'sucuri-' . $this->datastore . '.php';
  1645. // Create the datastore file is it does not exists and the folder is writable.
  1646. if (
  1647. ! file_exists( $file_path )
  1648. && is_writable( $folder_path )
  1649. ){
  1650. @file_put_contents( $file_path, $this->datastore_info(), LOCK_EX );
  1651. }
  1652. // Continue the operation after an attemp to create the datastore file.
  1653. if (
  1654. file_exists( $file_path )
  1655. && is_writable( $file_path )
  1656. && is_readable( $file_path )
  1657. ){
  1658. return $file_path;
  1659. }
  1660. }
  1661. return false;
  1662. }
  1663. /**
  1664. * Define the pattern for the regular expression that will check if a cache key
  1665. * is valid or not, and also will help the function that parses the file to see
  1666. * which characters of each line are the keys are which are the values.
  1667. *
  1668. * @param string $action Either "valid", "content", or "header".
  1669. * @return string Cache key pattern.
  1670. */
  1671. private function key_pattern( $action = 'valid' ){
  1672. if ( $action == 'valid' ){
  1673. return '/^([0-9a-zA-Z_]+)$/';
  1674. }
  1675. if ( $action == 'content' ){
  1676. return '/^([0-9a-zA-Z_]+):(.+)/';
  1677. }
  1678. if ( $action == 'header' ){
  1679. return '/^\/\/ ([a-z_]+)=(.*);$/';
  1680. }
  1681. return false;
  1682. }
  1683. /**
  1684. * Check whether a key has a valid name or not.
  1685. *
  1686. * @param string $key Unique name to identify the data in the datastore file.
  1687. * @return boolean TRUE if the format of the key name is valid, FALSE otherwise.
  1688. */
  1689. private function valid_key_name( $key = '' ){
  1690. return (bool) preg_match( $this->key_pattern( 'valid' ), $key );
  1691. }
  1692. /**
  1693. * Update the content of the datastore file with the new entries.
  1694. *
  1695. * @param array $finfo Rainbow table with the key names and decoded values.
  1696. * @return boolean TRUE if the operation finished successfully, FALSE otherwise.
  1697. */
  1698. private function save_new_entries( $finfo = array() ){
  1699. $data_string = $this->datastore_info( $finfo );
  1700. if ( ! empty($finfo) ){
  1701. foreach ( $finfo['entries'] as $key => $data ){
  1702. if ( $this->valid_key_name( $key ) ){
  1703. $data = json_encode( $data );
  1704. $data_string .= sprintf( "%s:%s\n", $key, $data );
  1705. }
  1706. }
  1707. }
  1708. $saved = @file_put_contents( $this->datastore_path, $data_string, LOCK_EX );
  1709. return (bool) $saved;
  1710. }
  1711. /**
  1712. * Retrieve and parse the datastore file, and generate a rainbow table with the
  1713. * key names and decoded data as the values of each entry. Duplicated key names
  1714. * will be removed automatically while adding the keys to the array and their
  1715. * values will correspond to the first occurrence found in the file.
  1716. *
  1717. * @param boolean $assoc When TRUE returned objects will be converted into associative arrays.
  1718. * @return array Rainbow table with the key names and decoded values.
  1719. */
  1720. private function get_datastore_content( $assoc = false ){
  1721. $data_object = array(
  1722. 'info' => array(),
  1723. 'entries' => array(),
  1724. );
  1725. if ( $this->usable_datastore ){
  1726. $data_lines = SucuriScanFileInfo::file_lines( $this->datastore_path );
  1727. if ( ! empty($data_lines) ){
  1728. foreach ( $data_lines as $line ){
  1729. if ( preg_match( $this->key_pattern( 'header' ), $line, $match ) ){
  1730. $data_object['info'][ $match[1] ] = $match[2];
  1731. }
  1732. elseif ( preg_match( $this->key_pattern( 'content' ), $line, $match ) ){
  1733. if (
  1734. $this->valid_key_name( $match[1] )
  1735. && ! array_key_exists( $match[1], $data_object )
  1736. ){
  1737. $data_object['entries'][ $match[1] ] = @json_decode( $match[2], $assoc );
  1738. }
  1739. }
  1740. }
  1741. }
  1742. }
  1743. return $data_object;
  1744. }
  1745. /**
  1746. * Retrieve the headers of the datastore file.
  1747. *
  1748. * Each datastore file has a list of attributes at the beginning of the it with
  1749. * information like the creation and last update time. If you are extending the
  1750. * functionality of these headers please refer to the function that contains the
  1751. * default attributes and their values [1].
  1752. *
  1753. * [1] SucuriScanCache::datastore_default_info()
  1754. *
  1755. * @return array Default content of every datastore file.
  1756. */
  1757. public function get_datastore_info(){
  1758. $finfo = $this->get_datastore_content();
  1759. if ( ! empty($finfo['info']) ){
  1760. return $finfo['info'];
  1761. }
  1762. return false;
  1763. }
  1764. /**
  1765. * Get the total number of unique entries in the datastore file.
  1766. *
  1767. * @param array $finfo Rainbow table with the key names and decoded values.
  1768. * @return integer Total number of unique entries found in the datastore file.
  1769. */
  1770. public function get_count( $finfo = null ){
  1771. if ( ! is_array( $finfo ) ){
  1772. $finfo = $this->get_datastore_content();
  1773. }
  1774. return count( $finfo['entries'] );
  1775. }
  1776. /**
  1777. * Check whether the last update time of the datastore file has surpassed the
  1778. * lifetime specified for a key name. This function is the only one related with
  1779. * the caching process, any others besides this are just methods used to handle
  1780. * the data inside those files.
  1781. *
  1782. * @param integer $lifetime Life time of the key in the datastore file.
  1783. * @param array $finfo Rainbow table with the key names and decoded values.
  1784. * @return boolean TRUE if the life time of the data has expired, FALSE otherwise.
  1785. */
  1786. public function data_has_expired( $lifetime = 0, $finfo = null ){
  1787. if ( is_null( $finfo ) ){
  1788. $finfo = $this->get_datastore_content();
  1789. }
  1790. if ( $lifetime > 0 && ! empty($finfo['info']) ){
  1791. $diff_time = time() - intval( $finfo['info']['updated_on'] );
  1792. if ( $diff_time >= $lifetime ){
  1793. return true;
  1794. }
  1795. }
  1796. return false;
  1797. }
  1798. /**
  1799. * Execute the action using the key name and data specified.
  1800. *
  1801. * @param string $key Unique name to identify the data in the datastore file.
  1802. * @param string $data Mixed data stored in the datastore file following the unique key name.
  1803. * @param string $action Either add, set, get, or delete.
  1804. * @param integer $lifetime Life time of the key in the datastore file.
  1805. * @param boolean $assoc When TRUE returned objects will be converted into associative arrays.
  1806. * @return boolean TRUE if the operation finished successfully, FALSE otherwise.
  1807. */
  1808. private function handle_key_data( $key = '', $data = null, $action = '', $lifetime = 0, $assoc = false ){
  1809. if ( preg_match( '/^(add|set|get|get_all|exists|delete)$/', $action ) ){
  1810. if (
  1811. $this->valid_key_name( $key )
  1812. && $this->usable_datastore
  1813. ){
  1814. $finfo = $this->get_datastore_content( $assoc );
  1815. switch ( $action ){
  1816. case 'add': /* no_break */
  1817. case 'set':
  1818. $finfo['entries'][ $key ] = $data;
  1819. return $this->save_new_entries( $finfo );
  1820. break;
  1821. case 'get':
  1822. if (
  1823. ! $this->data_has_expired( $lifetime, $finfo )
  1824. && array_key_exists( $key, $finfo['entries'] )
  1825. ){
  1826. return $finfo['entries'][ $key ];
  1827. }
  1828. break;
  1829. case 'get_all': /* no_break */
  1830. if ( ! $this->data_has_expired( $lifetime, $finfo ) ) {
  1831. return $finfo['entries'];
  1832. }
  1833. case 'exists':
  1834. if (
  1835. ! $this->data_has_expired( $lifetime, $finfo )
  1836. && array_key_exists( $key, $finfo['entries'] )
  1837. ){
  1838. return true;
  1839. }
  1840. break;
  1841. case 'delete':
  1842. unset($finfo['entries'][ $key ]);
  1843. return $this->save_new_entries( $finfo );
  1844. break;
  1845. }
  1846. }
  1847. }
  1848. return false;
  1849. }
  1850. /**
  1851. * JSON-encode the data and store it in the datastore file identifying it with
  1852. * the key name, the data will be added to the file even if the key is
  1853. * duplicated, but when getting the value of the same key later again it will
  1854. * return only the value of the first occurrence found in the file.
  1855. *
  1856. * @param string $key Unique name to identify the data in the datastore file.
  1857. * @param string $data Mixed data stored in the datastore file following the unique key name.
  1858. * @return boolean TRUE if the data was stored successfully, FALSE otherwise.
  1859. */
  1860. public function add( $key = '', $data = '' ){
  1861. return $this->handle_key_data( $key, $data, 'add' );
  1862. }
  1863. /**
  1864. * Update the data of all the key names matching the one specified.
  1865. *
  1866. * @param string $key Unique name to identify the data in the datastore file.
  1867. * @param string $data Mixed data stored in the datastore file following the unique key name.
  1868. * @return boolean TRUE if the data was stored successfully, FALSE otherwise.
  1869. */
  1870. public function set( $key = '', $data = '' ){
  1871. return $this->handle_key_data( $key, $data, 'set' );
  1872. }
  1873. /**
  1874. * Retrieve the first occurrence of the key found in the datastore file.
  1875. *
  1876. * @param string $key Unique name to identify the data in the datastore file.
  1877. * @param integer $lifetime Life time of the key in the datastore file.
  1878. * @param boolean $assoc When TRUE returned objects will be converted into associative arrays.
  1879. * @return string Mixed data stored in the datastore file following the unique key name.
  1880. */
  1881. public function get( $key = '', $lifetime = 0, $assoc = false ){
  1882. $assoc = ( $assoc == 'array' ? true : $assoc );
  1883. return $this->handle_key_data( $key, null, 'get', $lifetime, $assoc );
  1884. }
  1885. /**
  1886. * Retrieve all the entries found in the datastore file.
  1887. *
  1888. * @param integer $lifetime Life time of the key in the datastore file.
  1889. * @param boolean $assoc When TRUE returned objects will be converted into associative arrays.
  1890. * @return string Mixed data stored in the datastore file following the unique key name.
  1891. */
  1892. public function get_all( $lifetime = 0, $assoc = false ){
  1893. $assoc = ( $assoc == 'array' ? true : $assoc );
  1894. return $this->handle_key_data( 'temp', null, 'get_all', $lifetime, $assoc );
  1895. }
  1896. /**
  1897. * Check whether a specific key exists in the datastore file.
  1898. *
  1899. * @param string $key Unique name to identify the data in the datastore file.
  1900. * @return boolean TRUE if the key exists in the datastore file, FALSE otherwise.
  1901. */
  1902. public function exists( $key = '' ){
  1903. return $this->handle_key_data( $key, null, 'exists' );
  1904. }
  1905. /**
  1906. * Delete any entry from the datastore file matching the key name specified.
  1907. *
  1908. * @param string $key Unique name to identify the data in the datastore file.
  1909. * @return boolean TRUE if the entries were removed, FALSE otherwise.
  1910. */
  1911. public function delete( $key = '' ){
  1912. return $this->handle_key_data( $key, null, 'delete' );
  1913. }
  1914. /**
  1915. * Remove all the entries from the datastore file.
  1916. *
  1917. * @return boolean Always TRUE unless the datastore file is not writable.
  1918. */
  1919. public function flush(){
  1920. $finfo = $this->get_datastore_content();
  1921. return $this->save_new_entries( $finfo );
  1922. }
  1923. }
  1924. /**
  1925. * Plugin options handler.
  1926. *
  1927. * Options are pieces of data that WordPress uses to store various preferences
  1928. * and configuration settings. Listed below are the options, along with some of
  1929. * the default values from the current WordPress install. By using the
  1930. * appropriate function, options can be added, changed, removed, and retrieved,
  1931. * from the wp_options table.
  1932. *
  1933. * The Options API is a simple and standardized way of storing data in the
  1934. * database. The API makes it easy to create, access, update, and delete
  1935. * options. All the data is stored in the wp_options table under a given custom
  1936. * name. This page contains the technical documentation needed to use the
  1937. * Options API. A list of default options can be found in the Option Reference.
  1938. *
  1939. * Note that the _site_ functions are essentially the same as their
  1940. * counterparts. The only differences occur for WP Multisite, when the options
  1941. * apply network-wide and the data is stored in the wp_sitemeta table under the
  1942. * given custom name.
  1943. *
  1944. * @see http://codex.wordpress.org/Option_Reference
  1945. * @see http://codex.wordpress.org/Options_API
  1946. */
  1947. class SucuriScanOption extends SucuriScanRequest {
  1948. /**
  1949. * Default values for all the plugin's options.
  1950. *
  1951. * @return array Default values for all the plugin's options.
  1952. */
  1953. public static function get_default_option_values(){
  1954. $defaults = array(
  1955. 'sucuriscan_account' => '',
  1956. 'sucuriscan_ads_visibility' => 'enabled',
  1957. 'sucuriscan_api_key' => false,
  1958. 'sucuriscan_audit_report' => 'disabled',
  1959. 'sucuriscan_cloudproxy_apikey' => '',
  1960. 'sucuriscan_collect_wrong_passwords' => 'disabled',
  1961. 'sucuriscan_datastore_path' => '',
  1962. 'sucuriscan_email_subject' => 'Sucuri Alert, :domain, :event',
  1963. 'sucuriscan_emails_per_hour' => 5,
  1964. 'sucuriscan_emails_sent' => 0,
  1965. 'sucuriscan_errorlogs_limit' => 30,
  1966. 'sucuriscan_fs_scanner' => 'enabled',
  1967. 'sucuriscan_heartbeat' => 'enabled',
  1968. 'sucuriscan_heartbeat_autostart' => 'enabled',
  1969. 'sucuriscan_heartbeat_interval' => 'standard',
  1970. 'sucuriscan_heartbeat_pulse' => 15,
  1971. 'sucuriscan_ignore_scanning' => 'disabled',
  1972. 'sucuriscan_ignored_events' => '',
  1973. 'sucuriscan_last_email_at' => time(),
  1974. 'sucuriscan_lastlogin_redirection' => 'enabled',
  1975. 'sucuriscan_logs4report' => 500,
  1976. 'sucuriscan_maximum_failed_logins' => 30,
  1977. 'sucuriscan_notify_bruteforce_attack' => 'disabled',
  1978. 'sucuriscan_notify_failed_login' => 'enabled',
  1979. 'sucuriscan_notify_plugin_activated' => 'disabled',
  1980. 'sucuriscan_notify_plugin_change' => 'disabled',
  1981. 'sucuriscan_notify_plugin_deactivated' => 'disabled',
  1982. 'sucuriscan_notify_plugin_deleted' => 'disabled',
  1983. 'sucuriscan_notify_plugin_installed' => 'disabled',
  1984. 'sucuriscan_notify_plugin_updated' => 'disabled',
  1985. 'sucuriscan_notify_post_publication' => 'enabled',
  1986. 'sucuriscan_notify_settings_updated' => 'disabled',
  1987. 'sucuriscan_notify_success_login' => 'enabled',
  1988. 'sucuriscan_notify_theme_activated' => 'disabled',
  1989. 'sucuriscan_notify_theme_deleted' => 'disabled',
  1990. 'sucuriscan_notify_theme_editor' => 'enabled',
  1991. 'sucuriscan_notify_theme_installed' => 'disabled',
  1992. 'sucuriscan_notify_theme_updated' => 'disabled',
  1993. 'sucuriscan_notify_to' => '',
  1994. 'sucuriscan_notify_user_registration' => 'disabled',
  1995. 'sucuriscan_notify_website_updated' => 'disabled',
  1996. 'sucuriscan_notify_widget_added' => 'disabled',
  1997. 'sucuriscan_notify_widget_deleted' => 'disabled',
  1998. 'sucuriscan_parse_errorlogs' => 'enabled',
  1999. 'sucuriscan_prettify_mails' => 'disabled',
  2000. 'sucuriscan_request_timeout' => 90,
  2001. 'sucuriscan_revproxy' => 'disabled',
  2002. 'sucuriscan_runtime' => 0,
  2003. 'sucuriscan_scan_checksums' => 'enabled',
  2004. 'sucuriscan_scan_errorlogs' => 'disabled',
  2005. 'sucuriscan_scan_frequency' => 'twicedaily',
  2006. 'sucuriscan_scan_interface' => 'spl',
  2007. 'sucuriscan_scan_modfiles' => 'disabled',
  2008. 'sucuriscan_site_version' => '0.0',
  2009. 'sucuriscan_sitecheck_counter' => 0,
  2010. 'sucuriscan_sitecheck_scanner' => 'enabled',
  2011. 'sucuriscan_verify_ssl_cert' => 'true',
  2012. );
  2013. return $defaults;
  2014. }
  2015. /**
  2016. * Name of all valid plugin's options.
  2017. *
  2018. * @return array Name of all valid plugin's options.
  2019. */
  2020. public static function get_default_option_names() {
  2021. $options = self::get_default_option_values();
  2022. $names = array_keys( $options );
  2023. return $names;
  2024. }
  2025. /**
  2026. * Check whether an option is used in the plugin or not.
  2027. *
  2028. * @param string $option_name Name of the option that will be checked.
  2029. * @return boolean True if the option is part of the plugin, False otherwise.
  2030. */
  2031. public static function is_valid_plugin_option( $option_name = '' ) {
  2032. $valid_options = self::get_default_option_names();
  2033. $is_valid_option = (bool) array_key_exists( $option_name, $valid_options );
  2034. return $is_valid_option;
  2035. }
  2036. /**
  2037. * Retrieve the default values for some specific options.
  2038. *
  2039. * @param string|array $settings Either an array that will be complemented or a string with the name of the option.
  2040. * @return string|array The default values for the specified options.
  2041. */
  2042. public static function get_default_options( $settings = '' ){
  2043. $default_options = self::get_default_option_values();
  2044. // Use framework built-in function.
  2045. if ( function_exists( 'get_option' ) ) {
  2046. $admin_email = get_option( 'admin_email' );
  2047. $default_options['sucuriscan_account'] = $admin_email;
  2048. $default_options['sucuriscan_notify_to'] = $admin_email;
  2049. }
  2050. if ( is_array( $settings ) ) {
  2051. foreach ( $default_options as $option_name => $option_value ) {
  2052. if ( ! isset($settings[ $option_name ]) ){
  2053. $settings[ $option_name ] = $option_value;
  2054. }
  2055. }
  2056. return $settings;
  2057. }
  2058. if (
  2059. is_string( $settings )
  2060. && ! empty($settings)
  2061. && array_key_exists( $settings, $default_options )
  2062. ) {
  2063. return $default_options[ $settings ];
  2064. }
  2065. return false;
  2066. }
  2067. /**
  2068. * Alias function for the method Common::SucuriScan_Get_Options()
  2069. *
  2070. * This function search the specified option in the database, not only the options
  2071. * set by the plugin but all the options set for the site. If the value retrieved
  2072. * is FALSE the method tries to search for a default value.
  2073. *
  2074. * To facilitate the development, you can prefix the name of the key in the
  2075. * request (when accessing it) with a single colon, this function will
  2076. * automatically replace that character with the unique identifier of the
  2077. * plugin.
  2078. *
  2079. * @see http://codex.wordpress.org/Function_Reference/get_option
  2080. *
  2081. * @param string $option_name Optional parameter that you can use to filter the results to one option.
  2082. * @return string The value (or default value) of the option specified.
  2083. */
  2084. public static function get_option( $option_name = '' ){
  2085. if ( function_exists( 'update_option' ) ) {
  2086. $option_name = self::variable_prefix( $option_name );
  2087. $option_value = get_option( $option_name );
  2088. if ( $option_value === false && preg_match( '/^sucuriscan_/', $option_name ) ){
  2089. $option_value = self::get_default_options( $option_name );
  2090. }
  2091. return $option_value;
  2092. }
  2093. return false;
  2094. }
  2095. /**
  2096. * Update the value of an database' option.
  2097. *
  2098. * Use the function to update a named option/value pair to the options database
  2099. * table. The option name value is escaped with a special database method before
  2100. * the insert SQL statement but not the option value, this value should always
  2101. * be properly sanitized.
  2102. *
  2103. * @see http://codex.wordpress.org/Function_Reference/update_option
  2104. *
  2105. * @param string $option_name Name of the option to update which must not exceed 64 characters.
  2106. * @param string $option_value The new value for the option, can be an integer, string, array, or object.
  2107. * @return boolean True if option value has changed, false if not or if update failed.
  2108. */
  2109. public static function update_option( $option_name = '', $option_value = '' ){
  2110. if ( function_exists( 'update_option' ) ) {
  2111. $option_name = self::variable_prefix( $option_name );
  2112. return update_option( $option_name, $option_value );
  2113. }
  2114. return false;
  2115. }
  2116. /**
  2117. * Remove an option from the database.
  2118. *
  2119. * A safe way of removing a named option/value pair from the options database table.
  2120. *
  2121. * @see http://codex.wordpress.org/Function_Reference/delete_option
  2122. *
  2123. * @param string $option_name Name of the option to be deleted.
  2124. * @return boolean True, if option is successfully deleted. False on failure, or option does not exist.
  2125. */
  2126. public static function delete_option( $option_name = '' ){
  2127. if ( function_exists( 'delete_option' ) ) {
  2128. $option_name = self::variable_prefix( $option_name );
  2129. return delete_option( $option_name );
  2130. }
  2131. return false;
  2132. }
  2133. /**
  2134. * Delete all the plugin options from the database.
  2135. *
  2136. * @return void
  2137. */
  2138. public static function delete_plugin_options(){
  2139. global $wpdb;
  2140. $options = $wpdb->get_results(
  2141. "SELECT * FROM {$wpdb->options}
  2142. WHERE option_name LIKE 'sucuriscan%'
  2143. ORDER BY option_id ASC"
  2144. );
  2145. foreach ( $options as $option ) {
  2146. self::delete_option( $option->option_name );
  2147. }
  2148. }
  2149. /**
  2150. * Retrieve all the options stored by Wordpress in the database. The options
  2151. * containing the word "transient" are excluded from the results, this function
  2152. * is compatible with multisite instances.
  2153. *
  2154. * @return array All the options stored by Wordpress in the database, except the transient options.
  2155. */
  2156. public static function get_site_options(){
  2157. global $wpdb;
  2158. $settings = array();
  2159. $results = $wpdb->get_results(
  2160. "SELECT * FROM {$wpdb->options}
  2161. WHERE option_name NOT LIKE '%_transient_%'
  2162. ORDER BY option_id ASC"
  2163. );
  2164. foreach ( $results as $row ) {
  2165. $settings[ $row->option_name ] = $row->option_value;
  2166. }
  2167. return $settings;
  2168. }
  2169. /**
  2170. * Check what Wordpress options were changed comparing the values in the database
  2171. * with the values sent through a simple request using a GET or POST method.
  2172. *
  2173. * @param array $request The content of the global variable GET or POST considering SERVER[REQUEST_METHOD].
  2174. * @return array A list of all the options that were changes through this request.
  2175. */
  2176. public static function what_options_were_changed( $request = array() ){
  2177. $options_changed = array(
  2178. 'original' => array(),
  2179. 'changed' => array()
  2180. );
  2181. $site_options = self::get_site_options();
  2182. foreach ( $request as $req_name => $req_value ){
  2183. if (
  2184. array_key_exists( $req_name, $site_options )
  2185. && $site_options[ $req_name ] != $req_value
  2186. ){
  2187. $options_changed['original'][ $req_name ] = $site_options[ $req_name ];
  2188. $options_changed['changed'][ $req_name ] = $req_value;
  2189. }
  2190. }
  2191. return $options_changed;
  2192. }
  2193. /**
  2194. * Check the nonce comming from any of the settings pages.
  2195. *
  2196. * @return boolean TRUE if the nonce is valid, FALSE otherwise.
  2197. */
  2198. public static function check_options_nonce(){
  2199. // Create the option_page value if permalink submission.
  2200. if (
  2201. ! isset($_POST['option_page'])
  2202. && isset($_POST['permalink_structure'])
  2203. ){
  2204. $_POST['option_page'] = 'permalink';
  2205. }
  2206. // Check if the option_page has an allowed value.
  2207. if ( $option_page = SucuriScanRequest::post( 'option_page' ) ){
  2208. $nonce = '_wpnonce';
  2209. $action = '';
  2210. switch ( $option_page ){
  2211. case 'general': /* no_break */
  2212. case 'writing': /* no_break */
  2213. case 'reading': /* no_break */
  2214. case 'discussion': /* no_break */
  2215. case 'media': /* no_break */
  2216. case 'options': /* no_break */
  2217. $action = $option_page . '-options';
  2218. break;
  2219. case 'permalink':
  2220. $action = 'update-permalink';
  2221. break;
  2222. }
  2223. // Check the nonce validity.
  2224. if (
  2225. ! empty($action)
  2226. && isset($_REQUEST[ $nonce ])
  2227. && wp_verify_nonce( $_REQUEST[ $nonce ], $action )
  2228. ){
  2229. return true;
  2230. }
  2231. }
  2232. return false;
  2233. }
  2234. /**
  2235. * Get a list of the post types ignored to receive email notifications when the
  2236. * "new site content" hook is triggered.
  2237. *
  2238. * @return array List of ignored posts-types to send notifications.
  2239. */
  2240. public static function get_ignored_events(){
  2241. $post_types = self::get_option( ':ignored_events' );
  2242. $post_types_arr = false;
  2243. // Encode (old) serialized data into JSON.
  2244. if ( self::is_serialized( $post_types ) ){
  2245. $post_types_arr = @unserialize( $post_types );
  2246. $post_types_fix = json_encode( $post_types_arr );
  2247. self::update_option( ':ignored_events', $post_types_fix );
  2248. return $post_types_arr;
  2249. }
  2250. // Decode JSON-encoded data as an array.
  2251. elseif ( preg_match( '/^\{.+\}$/', $post_types ) ){
  2252. $post_types_arr = @json_decode( $post_types, true );
  2253. }
  2254. if ( ! is_array( $post_types_arr ) ){
  2255. $post_types_arr = array();
  2256. }
  2257. return $post_types_arr;
  2258. }
  2259. /**
  2260. * Add a new post type to the list of ignored events to send notifications.
  2261. *
  2262. * @param string $event_name Unique post-type name.
  2263. * @return boolean Whether the event was ignored or not.
  2264. */
  2265. public static function add_ignored_event( $event_name = '' ){
  2266. if ( function_exists( 'get_post_types' ) ){
  2267. $post_types = get_post_types();
  2268. // Check if the event is a registered post-type.
  2269. if ( array_key_exists( $event_name, $post_types ) ){
  2270. $ignored_events = self::get_ignored_events();
  2271. // Check if the event is not ignored already.
  2272. if ( ! array_key_exists( $event_name, $ignored_events ) ){
  2273. $ignored_events[ $event_name ] = time();
  2274. $saved = self::update_option( ':ignored_events', json_encode( $ignored_events ) );
  2275. return $saved;
  2276. }
  2277. }
  2278. }
  2279. return false;
  2280. }
  2281. /**
  2282. * Remove a post type from the list of ignored events to send notifications.
  2283. *
  2284. * @param string $event_name Unique post-type name.
  2285. * @return boolean Whether the event was removed from the list or not.
  2286. */
  2287. public static function remove_ignored_event( $event_name = '' ){
  2288. $ignored_events = self::get_ignored_events();
  2289. if ( array_key_exists( $event_name, $ignored_events ) ){
  2290. unset( $ignored_events[ $event_name ] );
  2291. $saved = self::update_option( ':ignored_events', json_encode( $ignored_events ) );
  2292. return $saved;
  2293. }
  2294. return false;
  2295. }
  2296. /**
  2297. * Check whether an event is being ignored to send notifications or not.
  2298. *
  2299. * @param string $event_name Unique post-type name.
  2300. * @return boolean Whether an event is being ignored or not.
  2301. */
  2302. public static function is_ignored_event( $event_name = '' ){
  2303. $event_name = strtolower( $event_name );
  2304. $ignored_events = self::get_ignored_events();
  2305. if ( array_key_exists( $event_name, $ignored_events ) ){
  2306. return true;
  2307. }
  2308. return false;
  2309. }
  2310. /**
  2311. * Retrieve a list of basic security keys and check whether their values were
  2312. * randomized correctly.
  2313. *
  2314. * @return array Array with three keys: good, missing, bad.
  2315. */
  2316. public static function get_security_keys(){
  2317. $response = array(
  2318. 'good' => array(),
  2319. 'missing' => array(),
  2320. 'bad' => array(),
  2321. );
  2322. $key_names = array(
  2323. 'AUTH_KEY',
  2324. 'AUTH_SALT',
  2325. 'LOGGED_IN_KEY',
  2326. 'LOGGED_IN_SALT',
  2327. 'NONCE_KEY',
  2328. 'NONCE_SALT',
  2329. 'SECURE_AUTH_KEY',
  2330. 'SECURE_AUTH_SALT',
  2331. );
  2332. foreach ( $key_names as $key_name ){
  2333. if ( defined( $key_name ) ){
  2334. $key_value = constant( $key_name );
  2335. if ( stripos( $key_value, 'unique phrase' ) !== false ){
  2336. $response['bad'][ $key_name ] = $key_value;
  2337. } else {
  2338. $response['good'][ $key_name ] = $key_value;
  2339. }
  2340. } else {
  2341. $response['missing'][ $key_name ] = false;
  2342. }
  2343. }
  2344. return $response;
  2345. }
  2346. }
  2347. /**
  2348. * System events, reports and actions.
  2349. *
  2350. * An event is an action or occurrence detected by the program that may be
  2351. * handled by the program. Typically events are handled synchronously with the
  2352. * program flow, that is, the program has one or more dedicated places where
  2353. * events are handled, frequently an event loop. Typical sources of events
  2354. * include the user; another source is a hardware device such as a timer. Any
  2355. * program can trigger its own custom set of events as well, e.g. to communicate
  2356. * the completion of a task. A computer program that changes its behavior in
  2357. * response to events is said to be event-driven, often with the goal of being
  2358. * interactive.
  2359. *
  2360. * @see http://en.wikipedia.org/wiki/Event_(computing)
  2361. */
  2362. class SucuriScanEvent extends SucuriScan {
  2363. /**
  2364. * Schedule the task to run the first filesystem scan.
  2365. *
  2366. * @return void
  2367. */
  2368. public static function schedule_task(){
  2369. $task_name = 'sucuriscan_scheduled_scan';
  2370. if ( ! wp_next_scheduled( $task_name ) ){
  2371. wp_schedule_event( time() + 10, 'twicedaily', $task_name );
  2372. }
  2373. wp_schedule_single_event( time() + 300, $task_name );
  2374. }
  2375. /**
  2376. * Checks last time we ran to avoid running twice (or too often).
  2377. *
  2378. * @param integer $runtime When the filesystem scan must be scheduled to run.
  2379. * @param boolean $force_scan Whether the filesystem scan was forced by an administrator user or not.
  2380. * @return boolean Either TRUE or FALSE representing the success or fail of the operation respectively.
  2381. */
  2382. private static function verify_run( $runtime = 0, $force_scan = false ){
  2383. $option_name = ':runtime';
  2384. $last_run = SucuriScanOption::get_option( $option_name );
  2385. $current_time = time();
  2386. // The filesystem scanner can be disabled from the settings page.
  2387. if (
  2388. SucuriScanOption::get_option( ':fs_scanner' ) == 'disabled'
  2389. && $force_scan === false
  2390. ){
  2391. return false;
  2392. }
  2393. // Check if the last runtime is too near the current time.
  2394. if ( $last_run && ! $force_scan ){
  2395. $runtime_diff = $current_time - $runtime;
  2396. if ( $last_run >= $runtime_diff ){
  2397. return false;
  2398. }
  2399. }
  2400. SucuriScanOption::update_option( $option_name, $current_time );
  2401. return true;
  2402. }
  2403. /**
  2404. * Check whether the current WordPress version must be reported to the API
  2405. * service or not, this is to avoid duplicated information in the audit logs.
  2406. *
  2407. * @return boolean TRUE if the current WordPress version must be reported, FALSE otherwise.
  2408. */
  2409. private static function report_site_version(){
  2410. $option_name = ':site_version';
  2411. $reported_version = SucuriScanOption::get_option( $option_name );
  2412. $wp_version = self::site_version();
  2413. if ( $reported_version != $wp_version ){
  2414. SucuriScanEvent::report_info_event( 'WordPress version detected ' . $wp_version );
  2415. SucuriScanOption::update_option( $option_name, $wp_version );
  2416. return true;
  2417. }
  2418. return false;
  2419. }
  2420. /**
  2421. * Gather all the checksums (aka. file hashes) of this site, send them, and
  2422. * analyze them using the Sucuri Monitoring service, this will generate the
  2423. * audit logs for this site and be part of the integrity checks.
  2424. *
  2425. * @param boolean $force_scan Whether the filesystem scan was forced by an administrator user or not.
  2426. * @return boolean TRUE if the filesystem scan was successful, FALSE otherwise.
  2427. */
  2428. public static function filesystem_scan( $force_scan = false ){
  2429. $minimum_runtime = SUCURISCAN_MINIMUM_RUNTIME;
  2430. if (
  2431. self::verify_run( $minimum_runtime, $force_scan )
  2432. && class_exists( 'SucuriScanFileInfo' )
  2433. && SucuriScanAPI::get_plugin_key()
  2434. ){
  2435. self::report_site_version();
  2436. $sucuri_fileinfo = new SucuriScanFileInfo();
  2437. $sucuri_fileinfo->scan_interface = SucuriScanOption::get_option( ':scan_interface' );
  2438. $signatures = $sucuri_fileinfo->get_directory_tree_md5( ABSPATH );
  2439. if ( $signatures ){
  2440. $hashes_sent = SucuriScanAPI::send_hashes( $signatures );
  2441. if ( $hashes_sent ){
  2442. SucuriScanOption::update_option( ':runtime', time() );
  2443. return true;
  2444. } else {
  2445. SucuriScanInterface::error( 'The file hashes could not be stored.' );
  2446. }
  2447. } else {
  2448. SucuriScanInterface::error( 'The file hashes could not be retrieved, the filesystem scan failed.' );
  2449. }
  2450. }
  2451. return false;
  2452. }
  2453. /**
  2454. * Generates an audit event log (to be sent later).
  2455. *
  2456. * @param integer $severity Importance of the event that will be reported, values from one to five.
  2457. * @param string $location In which part of the system was the event triggered.
  2458. * @param string $message The explanation of the event.
  2459. * @return boolean TRUE if the event was logged in the monitoring service, FALSE otherwise.
  2460. */
  2461. private static function report_event( $severity = 0, $location = '', $message = '' ){
  2462. $user = wp_get_current_user();
  2463. $username = false;
  2464. $current_time = date( 'Y-m-d H:i:s' );
  2465. $remote_ip = self::get_remote_addr();
  2466. // Identify current user in session.
  2467. if (
  2468. $user instanceof WP_User
  2469. && isset($user->user_login)
  2470. && ! empty($user->user_login)
  2471. ){
  2472. if ( $user->user_login != $user->display_name ){
  2473. $username = sprintf( "\x20%s (%s),", $user->display_name, $user->user_login );
  2474. } else {
  2475. $username = sprintf( "\x20%s,", $user->user_login );
  2476. }
  2477. }
  2478. // Fixing severity value.
  2479. $severity = (int) $severity;
  2480. // Convert the severity number into a readable string.
  2481. switch ( $severity ){
  2482. case 0: $severity_name = 'Debug'; break;
  2483. case 1: $severity_name = 'Notice'; break;
  2484. case 2: $severity_name = 'Info'; break;
  2485. case 3: $severity_name = 'Warning'; break;
  2486. case 4: $severity_name = 'Error'; break;
  2487. case 5: $severity_name = 'Critical'; break;
  2488. default: $severity_name = 'Info'; break;
  2489. }
  2490. // Clear event message.
  2491. $message = strip_tags( $message );
  2492. $message = str_replace( "\r", '', $message );
  2493. $message = str_replace( "\n", '', $message );
  2494. $message = str_replace( "\t", '', $message );
  2495. $event_message = sprintf(
  2496. '%s:%s %s; %s',
  2497. $severity_name,
  2498. $username,
  2499. $remote_ip,
  2500. $message
  2501. );
  2502. return SucuriScanAPI::send_log( $event_message );
  2503. }
  2504. /**
  2505. * Reports a debug event on the website.
  2506. *
  2507. * @param string $message Text witht the explanation of the event or action performed.
  2508. * @return boolean Either true or false depending on the success of the operation.
  2509. */
  2510. public static function report_debug_event( $message = '' ){
  2511. return self::report_event( 0, 'core', $message );
  2512. }
  2513. /**
  2514. * Reports a notice event on the website.
  2515. *
  2516. * @param string $message Text witht the explanation of the event or action performed.
  2517. * @return boolean Either true or false depending on the success of the operation.
  2518. */
  2519. public static function report_notice_event( $message = '' ){
  2520. return self::report_event( 1, 'core', $message );
  2521. }
  2522. /**
  2523. * Reports a info event on the website.
  2524. *
  2525. * @param string $message Text witht the explanation of the event or action performed.
  2526. * @return boolean Either true or false depending on the success of the operation.
  2527. */
  2528. public static function report_info_event( $message = '' ){
  2529. return self::report_event( 2, 'core', $message );
  2530. }
  2531. /**
  2532. * Reports a warning event on the website.
  2533. *
  2534. * @param string $message Text witht the explanation of the event or action performed.
  2535. * @return boolean Either true or false depending on the success of the operation.
  2536. */
  2537. public static function report_warning_event( $message = '' ){
  2538. return self::report_event( 3, 'core', $message );
  2539. }
  2540. /**
  2541. * Reports a error event on the website.
  2542. *
  2543. * @param string $message Text witht the explanation of the event or action performed.
  2544. * @return boolean Either true or false depending on the success of the operation.
  2545. */
  2546. public static function report_error_event( $message = '' ){
  2547. return self::report_event( 4, 'core', $message );
  2548. }
  2549. /**
  2550. * Reports a critical event on the website.
  2551. *
  2552. * @param string $message Text witht the explanation of the event or action performed.
  2553. * @return boolean Either true or false depending on the success of the operation.
  2554. */
  2555. public static function report_critical_event( $message = '' ){
  2556. return self::report_event( 5, 'core', $message );
  2557. }
  2558. /**
  2559. * Reports a notice or error event for enable and disable actions.
  2560. *
  2561. * @param string $message Text witht the explanation of the event or action performed.
  2562. * @param string $action An optional text, hopefully either enabled or disabled.
  2563. * @return boolean Either true or false depending on the success of the operation.
  2564. */
  2565. public static function report_auto_event( $message = '', $action = '' ){
  2566. $message = strip_tags( $message );
  2567. // Auto-detect the action performed, either enabled or disabled.
  2568. if ( preg_match( '/( was )?(enabled|disabled)$/', $message, $match ) ) {
  2569. $action = $match[2];
  2570. }
  2571. // Report the correct event for the action performed.
  2572. if ( $action == 'enabled' ) {
  2573. return self::report_notice_event( $message );
  2574. } elseif ( $action == 'disabled' ) {
  2575. return self::report_error_event( $message );
  2576. } else {
  2577. return self::report_info_event( $message );
  2578. }
  2579. }
  2580. /**
  2581. * Send a notification to the administrator of the specified events, only if
  2582. * the administrator accepted to receive alerts for this type of events.
  2583. *
  2584. * @param string $event The name of the event that was triggered.
  2585. * @param string $content Body of the email that will be sent to the administrator.
  2586. * @return void
  2587. */
  2588. public static function notify_event( $event = '', $content = '' ){
  2589. $notify = SucuriScanOption::get_option( ':notify_' . $event );
  2590. $email = SucuriScanOption::get_option( ':notify_to' );
  2591. $email_params = array();
  2592. if ( self::is_trusted_ip() ) {
  2593. $notify = 'disabled';
  2594. }
  2595. if ( $notify == 'enabled' ){
  2596. if ( $event == 'post_publication' ){
  2597. $event = 'post_update';
  2598. }
  2599. elseif ( $event == 'failed_login' ){
  2600. $content .= "<br>\n<br>\n<em>Explanation: Someone failed to login to your site. If you";
  2601. $content .= ' are getting too many of these messages, it is likely your site is under a brute';
  2602. $content .= ' force attack. You can disable the notifications for failed logins from here [1].';
  2603. $content .= " More details at Password Guessing Brute Force Attacks [2].</em><br>\n<br>\n";
  2604. $content .= '[1] ' . SucuriScanTemplate::get_url( 'settings' ) . " <br>\n";
  2605. $content .= "[2] http://kb.sucuri.net/definitions/attacks/brute-force/password-guessing <br>\n";
  2606. }
  2607. // Send a notification even if the limit of emails per hour was reached.
  2608. elseif ( $event == 'bruteforce_attack' ){
  2609. $email_params['Force'] = true;
  2610. }
  2611. $title = str_replace( '_', chr( 32 ), $event );
  2612. $mail_sent = SucuriScanMail::send_mail( $email, $title, $content, $email_params );
  2613. return $mail_sent;
  2614. }
  2615. return false;
  2616. }
  2617. /**
  2618. * Check whether an IP address is being trusted or not.
  2619. *
  2620. * @param string $remote_addr The supposed ip address that will be checked.
  2621. * @return boolean TRUE if the IP address of the user is trusted, FALSE otherwise.
  2622. */
  2623. private static function is_trusted_ip( $remote_addr = '' ){
  2624. $cache = new SucuriScanCache( 'trustip' );
  2625. $trusted_ips = $cache->get_all();
  2626. if ( ! $remote_addr ) {
  2627. $remote_addr = SucuriScan::get_remote_addr();
  2628. }
  2629. $addr_md5 = md5( $remote_addr );
  2630. // Check if the CIDR in range 32 of this IP is trusted.
  2631. if (
  2632. is_array( $trusted_ips )
  2633. && ! empty($trusted_ips)
  2634. && array_key_exists( $addr_md5, $trusted_ips )
  2635. ) {
  2636. return true;
  2637. }
  2638. if ( $trusted_ips ) {
  2639. foreach ( $trusted_ips as $cache_key => $ip_info ) {
  2640. $ip_parts = explode( '.', $ip_info->remote_addr );
  2641. $ip_pattern = false;
  2642. // Generate the regular expression for CIDR range 24.
  2643. if ( $ip_info->cidr_range == 24 ) {
  2644. $ip_pattern = sprintf( '/^%d\.%d\.%d\.[0-9]{1,3}$/', $ip_parts[0], $ip_parts[1], $ip_parts[2] );
  2645. }
  2646. // Generate the regular expression for CIDR range 16.
  2647. elseif ( $ip_info->cidr_range == 16 ) {
  2648. $ip_pattern = sprintf( '/^%d\.%d(\.[0-9]{1,3}){2}$/', $ip_parts[0], $ip_parts[1] );
  2649. }
  2650. // Generate the regular expression for CIDR range 8.
  2651. elseif ( $ip_info->cidr_range == 8 ) {
  2652. $ip_pattern = sprintf( '/^%d(\.[0-9]{1,3}){3}$/', $ip_parts[0] );
  2653. }
  2654. if ( $ip_pattern && preg_match( $ip_pattern, $remote_addr ) ) {
  2655. return true;
  2656. }
  2657. }
  2658. }
  2659. return false;
  2660. }
  2661. /**
  2662. * Generate and set a new password for a specific user not in session.
  2663. *
  2664. * @param integer $user_id The user identifier that will be changed, this must be different than the user in session.
  2665. * @return boolean Either TRUE or FALSE in case of success or error respectively.
  2666. */
  2667. public static function set_new_password( $user_id = 0 ){
  2668. $user_id = intval( $user_id );
  2669. if ( $user_id > 0 && function_exists( 'wp_generate_password' ) ){
  2670. $user = get_userdata( $user_id );
  2671. if ( $user instanceof WP_User ){
  2672. $new_password = wp_generate_password( 15, true, false );
  2673. $message = 'The password for your user account <strong>"'. $user->display_name .'"</strong> '
  2674. . 'in the website specified above was changed, this is the new password generated automatically '
  2675. . 'by the system, please update as soon as possible.<br><div style="display:inline-block;'
  2676. . 'background:#ddd;font-family:monaco,monospace,courier;font-size:30px;margin:0;padding:15px;'
  2677. . 'border:1px solid #999">'. $new_password .'</div>';
  2678. $data_set = array( 'Force' => true ); // Skip limit for emails per hour.
  2679. SucuriScanMail::send_mail( $user->user_email, 'Password changed', $message, $data_set );
  2680. wp_set_password( $new_password, $user_id );
  2681. return true;
  2682. }
  2683. }
  2684. return false;
  2685. }
  2686. /**
  2687. * Modify the WordPress configuration file and change the keys that were defined
  2688. * by a new random-generated list of keys retrieved from the official WordPress
  2689. * API. The result of the operation will be either FALSE in case of error, or an
  2690. * array containing multiple indexes explaining the modification, among them you
  2691. * will find the old and new keys.
  2692. *
  2693. * @return false|array Either FALSE in case of error, or an array with the old and new keys.
  2694. */
  2695. public static function set_new_config_keys(){
  2696. $new_wpconfig = '';
  2697. $config_path = self::get_wpconfig_path();
  2698. if ( $config_path ){
  2699. $pattern = self::secret_key_pattern();
  2700. $define_tpl = "define('%s',%s'%s');";
  2701. $config_lines = SucuriScanFileInfo::file_lines( $config_path );
  2702. $new_keys = SucuriScanAPI::get_new_secret_keys();
  2703. $old_keys = array();
  2704. $old_keys_string = '';
  2705. $new_keys_string = '';
  2706. foreach ( (array) $config_lines as $config_line ){
  2707. $config_line = str_replace( "\n", '', $config_line );
  2708. if ( preg_match( $pattern, $config_line, $match ) ){
  2709. $key_name = $match[1];
  2710. if ( array_key_exists( $key_name, $new_keys ) ){
  2711. $white_spaces = $match[2];
  2712. $old_keys[ $key_name ] = $match[3];
  2713. $config_line = sprintf( $define_tpl, $key_name, $white_spaces, $new_keys[ $key_name ] );
  2714. $old_keys_string .= sprintf( $define_tpl, $key_name, $white_spaces, $old_keys[ $key_name ] ) . "\n";
  2715. $new_keys_string .= $config_line . "\n";
  2716. }
  2717. }
  2718. $new_wpconfig .= $config_line . "\n";
  2719. }
  2720. $response = array(
  2721. 'updated' => is_writable( $config_path ),
  2722. 'old_keys' => $old_keys,
  2723. 'old_keys_string' => $old_keys_string,
  2724. 'new_keys' => $new_keys,
  2725. 'new_keys_string' => $new_keys_string,
  2726. 'new_wpconfig' => $new_wpconfig,
  2727. );
  2728. if ( $response['updated'] ){
  2729. file_put_contents( $config_path, $new_wpconfig, LOCK_EX );
  2730. }
  2731. return $response;
  2732. }
  2733. return false;
  2734. }
  2735. }
  2736. /**
  2737. * Function call interceptors.
  2738. *
  2739. * The term hooking covers a range of techniques used to alter or augment the
  2740. * behavior of an operating system, of applications, or of other software
  2741. * components by intercepting function calls or messages or events passed
  2742. * between software components. Code that handles such intercepted function
  2743. * calls, events or messages is called a "hook".
  2744. *
  2745. * Hooking is used for many purposes, including debugging and extending
  2746. * functionality. Examples might include intercepting keyboard or mouse event
  2747. * messages before they reach an application, or intercepting operating system
  2748. * calls in order to monitor behavior or modify the function of an application
  2749. * or other component; it is also widely used in benchmarking programs.
  2750. */
  2751. class SucuriScanHook extends SucuriScanEvent {
  2752. /**
  2753. * Send to Sucuri servers an alert notifying that an attachment was added to a post.
  2754. *
  2755. * @param integer $id The post identifier.
  2756. * @return void
  2757. */
  2758. public static function hook_add_attachment( $id = 0 ){
  2759. if ( $data = get_post( $id ) ) {
  2760. $id = $data->ID;
  2761. $title = $data->post_title;
  2762. $mime_type = $data->post_mime_type;
  2763. } else {
  2764. $title = 'unknown';
  2765. $mime_type = 'unknown';
  2766. }
  2767. $message = sprintf( 'Media file added; identifier: %s; name: %s; type: %s', $id, $title, $mime_type );
  2768. self::report_notice_event( $message );
  2769. self::notify_event( 'post_publication', $message );
  2770. }
  2771. /**
  2772. * Send an alert notifying that a new link was added to the bookmarks.
  2773. *
  2774. * @param integer $id Identifier of the new link created;
  2775. * @return void
  2776. */
  2777. public static function hook_add_link( $id = 0 ){
  2778. if ( $data = get_bookmark( $id ) ) {
  2779. $id = $data->link_id;
  2780. $title = $data->link_name;
  2781. $url = $data->link_url;
  2782. $target = $data->link_target;
  2783. } else {
  2784. $title = 'unknown';
  2785. $url = 'undefined/url';
  2786. $target = '_none';
  2787. }
  2788. $message = sprintf(
  2789. 'Bookmark link added; identifier: %s; name: %s; url: %s; target: %s',
  2790. $id, $title, $url, $target
  2791. );
  2792. self::report_warning_event( $message );
  2793. self::notify_event( 'post_publication', $message );
  2794. }
  2795. /**
  2796. * Send an alert notifying that a category was created.
  2797. *
  2798. * @param integer $id The identifier of the category created.
  2799. * @return void
  2800. */
  2801. public static function hook_create_category( $id = 0 ){
  2802. $title = ( is_int( $id ) ? get_cat_name( $id ) : 'Unknown' );
  2803. $message = sprintf( 'Category created; identifier: %s; name: %s', $id, $title );
  2804. self::report_notice_event( $message );
  2805. self::notify_event( 'post_publication', $message );
  2806. }
  2807. /**
  2808. * Send an alert notifying that a post was deleted.
  2809. *
  2810. * @param integer $id The identifier of the post deleted.
  2811. * @return void
  2812. */
  2813. public static function hook_delete_post( $id = 0 ){
  2814. self::report_warning_event( 'Post deleted; identifier: ' . $id );
  2815. }
  2816. /**
  2817. * Send an alert notifying that a post was moved to the trash.
  2818. *
  2819. * @param integer $id The identifier of the trashed post.
  2820. * @return void
  2821. */
  2822. public static function hook_wp_trash_post( $id = 0 ){
  2823. if ( $data = get_post( $id ) ) {
  2824. $title = $data->post_title;
  2825. $status = $data->post_status;
  2826. } else {
  2827. $title = 'Unknown';
  2828. $status = 'none';
  2829. }
  2830. $message = sprintf(
  2831. 'Post moved to trash; identifier: %s; name: %s; status: %s',
  2832. $id, $title, $status
  2833. );
  2834. self::report_warning_event( $message );
  2835. }
  2836. /**
  2837. * Send an alert notifying that a user account was deleted.
  2838. *
  2839. * @param integer $id The identifier of the user account deleted.
  2840. * @return void
  2841. */
  2842. public static function hook_delete_user( $id = 0 ){
  2843. self::report_warning_event( 'User account deleted; identifier: ' . $id );
  2844. }
  2845. /**
  2846. * Send an alert notifying that an attempt to reset the password
  2847. * of an user account was executed.
  2848. *
  2849. * @return void
  2850. */
  2851. public static function hook_login_form_resetpass(){
  2852. // Detecting WordPress 2.8.3 vulnerability - $key is array.
  2853. if ( isset($_GET['key']) && is_array( $_GET['key'] ) ) {
  2854. self::report_critical_event( 'Attempt to reset password by attacking WP/2.8.3 bug' );
  2855. }
  2856. }
  2857. /**
  2858. * Send an alert notifying that the state of a post was changed
  2859. * from private to published. This will only applies for posts not pages.
  2860. *
  2861. * @param integer $id The identifier of the post changed.
  2862. * @return void
  2863. */
  2864. public static function hook_private_to_published( $id = 0 ){
  2865. if ( $data = get_post( $id ) ) {
  2866. $title = $data->post_title;
  2867. $p_type = ucwords( $data->post_type );
  2868. } else {
  2869. $title = 'Unknown';
  2870. $p_type = 'Publication';
  2871. }
  2872. // Check whether the post-type is being ignored to send notifications.
  2873. if ( ! SucuriScanOption::is_ignored_event( $p_type ) ) {
  2874. $message = sprintf(
  2875. '%s (private to published); identifier: %s; name: %s',
  2876. $p_type, $id, $title
  2877. );
  2878. self::report_notice_event( $message );
  2879. self::notify_event( 'post_publication', $message );
  2880. }
  2881. }
  2882. /**
  2883. * Send an alert notifying that a post was published.
  2884. *
  2885. * @param integer $id The identifier of the post or page published.
  2886. * @return void
  2887. */
  2888. public static function hook_publish( $id = 0 ){
  2889. if ( $data = get_post( $id ) ) {
  2890. $title = $data->post_title;
  2891. $p_type = ucwords( $data->post_type );
  2892. $action = ( $data->post_date == $data->post_modified ? 'created' : 'updated' );
  2893. } else {
  2894. $title = 'Unknown';
  2895. $p_type = 'Publication';
  2896. $action = 'published';
  2897. }
  2898. $message = sprintf(
  2899. '%s was %s; identifier: %s; name: %s',
  2900. $p_type, $action, $id, $title
  2901. );
  2902. self::report_notice_event( $message );
  2903. self::notify_event( 'post_publication', $message );
  2904. }
  2905. /**
  2906. * Alias function for hook_publish()
  2907. *
  2908. * @param integer $id The identifier of the post or page published.
  2909. * @return void
  2910. */
  2911. public static function hook_publish_page( $id = 0 ){
  2912. self::hook_publish( $id );
  2913. }
  2914. /**
  2915. * Alias function for hook_publish()
  2916. *
  2917. * @param integer $id The identifier of the post or page published.
  2918. * @return void
  2919. */
  2920. public static function hook_publish_post( $id = 0 ){
  2921. self::hook_publish( $id );
  2922. }
  2923. /**
  2924. * Alias function for hook_publish()
  2925. *
  2926. * @param integer $id The identifier of the post or page published.
  2927. * @return void
  2928. */
  2929. public static function hook_publish_phone( $id = 0 ){
  2930. self::hook_publish( $id );
  2931. }
  2932. /**
  2933. * Alias function for hook_publish()
  2934. *
  2935. * @param integer $id The identifier of the post or page published.
  2936. * @return void
  2937. */
  2938. public static function hook_xmlrpc_publish_post( $id = 0 ){
  2939. self::hook_publish( $id );
  2940. }
  2941. /**
  2942. * Send an alert notifying that an attempt to retrieve the password
  2943. * of an user account was tried.
  2944. *
  2945. * @param string $title The name of the user account involved in the trasaction.
  2946. * @return void
  2947. */
  2948. public static function hook_retrieve_password( $title = '' ){
  2949. if ( empty($title) ) { $title = 'unknown'; }
  2950. self::report_error_event( 'Password retrieval attempt: ' . $title );
  2951. }
  2952. /**
  2953. * Send an alert notifying that the theme of the site was changed.
  2954. *
  2955. * @param string $title The name of the new theme selected to used through out the site.
  2956. * @return void
  2957. */
  2958. public static function hook_switch_theme( $title = '' ){
  2959. if ( empty($title) ) { $title = 'unknown'; }
  2960. $message = 'Theme activated: ' . $title;
  2961. self::report_warning_event( $message );
  2962. self::notify_event( 'theme_activated', $message );
  2963. }
  2964. /**
  2965. * Send an alert notifying that a new user account was created.
  2966. *
  2967. * @param integer $id The identifier of the new user account created.
  2968. * @return void
  2969. */
  2970. public static function hook_user_register( $id = 0 ){
  2971. if ( $data = get_userdata( $id ) ) {
  2972. $title = $data->user_login;
  2973. $email = $data->user_email;
  2974. $roles = @implode( ', ', $data->roles );
  2975. } else {
  2976. $title = 'unknown';
  2977. $email = 'user@domain.com';
  2978. $roles = 'none';
  2979. }
  2980. $message = sprintf(
  2981. 'User account created; identifier: %s; name: %s; email: %s; roles: %s',
  2982. $id, $title, $email, $roles
  2983. );
  2984. self::report_warning_event( $message );
  2985. self::notify_event( 'user_registration', $message );
  2986. }
  2987. /**
  2988. * Send an alert notifying that an attempt to login into the
  2989. * administration panel was successful.
  2990. *
  2991. * @param string $title The name of the user account involved in the transaction.
  2992. * @return void
  2993. */
  2994. public static function hook_wp_login( $title = '' ){
  2995. if ( empty($title) ) { $title = 'Unknown'; }
  2996. $message = 'User authentication succeeded: ' . $title;
  2997. self::report_notice_event( $message );
  2998. self::notify_event( 'success_login', $message );
  2999. }
  3000. /**
  3001. * Send an alert notifying that an attempt to login into the
  3002. * administration panel failed.
  3003. *
  3004. * @param string $title The name of the user account involved in the transaction.
  3005. * @return void
  3006. */
  3007. public static function hook_wp_login_failed( $title = '' ){
  3008. if ( empty($title) ){ $title = 'Unknown'; }
  3009. $title = sanitize_user( $title, true );
  3010. $password = SucuriScanRequest::post( 'pwd' );
  3011. $message = 'User authentication failed: ' . $title;
  3012. self::report_error_event( $message );
  3013. if ( sucuriscan_collect_wrong_passwords() === true ) {
  3014. $message .= "<br>\nUser wrong password: " . $password;
  3015. }
  3016. self::notify_event( 'failed_login', $message );
  3017. // Log the failed login in the internal datastore for future reports.
  3018. $logged = sucuriscan_log_failed_login( $title, $password );
  3019. // Check if the quantity of failed logins will be considered as a brute-force attack.
  3020. if ( $logged ){
  3021. $failed_logins = sucuriscan_get_failed_logins();
  3022. if ( $failed_logins ){
  3023. $max_time = 3600;
  3024. $maximum_failed_logins = SucuriScanOption::get_option( 'sucuriscan_maximum_failed_logins' );
  3025. /**
  3026. * If the time passed is within the hour, and the quantity of failed logins
  3027. * registered in the datastore file is bigger than the maximum quantity of
  3028. * failed logins allowed per hour (value configured by the administrator in the
  3029. * settings page), then send an email notification reporting the event and
  3030. * specifying that it may be a brute-force attack against the login page.
  3031. */
  3032. if (
  3033. $failed_logins['diff_time'] <= $max_time
  3034. && $failed_logins['count'] >= $maximum_failed_logins
  3035. ){
  3036. sucuriscan_report_failed_logins( $failed_logins );
  3037. }
  3038. /**
  3039. * If there time passed is superior to the hour, then reset the content of the
  3040. * datastore file containing the failed logins so far, any entry in that file
  3041. * will not be considered as part of a brute-force attack (if it exists) because
  3042. * the time passed between the first and last login attempt is big enough to
  3043. * mitigate the attack. We will consider the current failed login event as the
  3044. * first entry of that file in case of future attempts during the next sixty
  3045. * minutes.
  3046. */
  3047. elseif ( $failed_logins['diff_time'] > $max_time ){
  3048. sucuriscan_reset_failed_logins();
  3049. sucuriscan_log_failed_login( $title );
  3050. }
  3051. }
  3052. }
  3053. }
  3054. // TODO: Detect auto updates in core, themes, and plugin files.
  3055. /**
  3056. * Send a notifications to the administrator of some specific events that are
  3057. * not triggered through an hooked action, but through a simple request in the
  3058. * admin interface.
  3059. *
  3060. * @return integer Either one or zero representing the success or fail of the operation.
  3061. */
  3062. public static function hook_undefined_actions(){
  3063. $plugin_activate_actions = '(activate|deactivate)(\-selected)?';
  3064. $plugin_update_actions = '(upgrade-plugin|do-plugin-upgrade|update-selected)';
  3065. // Plugin activation and/or deactivation.
  3066. if (
  3067. current_user_can( 'activate_plugins' )
  3068. && (
  3069. SucuriScanRequest::get_or_post( 'action', $plugin_activate_actions )
  3070. || SucuriScanRequest::get_or_post( 'action2', $plugin_activate_actions )
  3071. )
  3072. ){
  3073. $plugin_list = array();
  3074. $items_affected = array();
  3075. // Get the action performed through action or action2 params.
  3076. $action_d = SucuriScanRequest::get_or_post( 'action' );
  3077. if ( $action_d == '-1' ) { $action_d = SucuriScanRequest::get_or_post( 'action2' ); }
  3078. $action_d .= 'd';
  3079. if (
  3080. SucuriScanRequest::get( 'plugin', '.+' )
  3081. && strpos( $_SERVER['REQUEST_URI'], 'plugins.php' ) !== false
  3082. ){
  3083. $plugin_list[] = SucuriScanRequest::get( 'plugin' );
  3084. }
  3085. elseif (
  3086. isset($_POST['checked'])
  3087. && is_array( $_POST['checked'] )
  3088. && ! empty($_POST['checked'])
  3089. ){
  3090. $plugin_list = SucuriScanRequest::post( 'checked', '_array' );
  3091. $action_d = str_replace( '-selected', '', $action_d );
  3092. }
  3093. foreach ( $plugin_list as $plugin ){
  3094. $plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
  3095. if (
  3096. ! empty($plugin_info['Name'])
  3097. && ! empty($plugin_info['Version'])
  3098. ){
  3099. $items_affected[] = sprintf(
  3100. '%s (v%s; %s)',
  3101. self::escape( $plugin_info['Name'] ),
  3102. self::escape( $plugin_info['Version'] ),
  3103. self::escape( $plugin )
  3104. );
  3105. }
  3106. }
  3107. // Report activated/deactivated plugins at once.
  3108. if ( ! empty($items_affected) ) {
  3109. $message_tpl = ( count( $items_affected ) > 1 )
  3110. ? 'Plugins %s: (multiple entries): %s'
  3111. : 'Plugin %s: %s';
  3112. $message = sprintf(
  3113. $message_tpl,
  3114. $action_d,
  3115. @implode( ',', $items_affected )
  3116. );
  3117. self::report_warning_event( $message );
  3118. self::notify_event( 'plugin_' . $action_d, $message );
  3119. }
  3120. }
  3121. // Plugin update request.
  3122. elseif (
  3123. current_user_can( 'update_plugins' )
  3124. && (
  3125. SucuriScanRequest::get_or_post( 'action', $plugin_update_actions )
  3126. || SucuriScanRequest::get_or_post( 'action2', $plugin_update_actions )
  3127. )
  3128. ){
  3129. $plugin_list = array();
  3130. $items_affected = array();
  3131. if (
  3132. SucuriScanRequest::get( 'plugin', '.+' )
  3133. && strpos( $_SERVER['REQUEST_URI'], 'wp-admin/update.php' ) !== false
  3134. ){
  3135. $plugin_list[] = SucuriScanRequest::get( 'plugin', '.+' );
  3136. }
  3137. elseif (
  3138. isset($_POST['checked'])
  3139. && is_array( $_POST['checked'] )
  3140. && ! empty($_POST['checked'])
  3141. ){
  3142. $plugin_list = SucuriScanRequest::post( 'checked', '_array' );
  3143. }
  3144. foreach ( $plugin_list as $plugin ){
  3145. $plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
  3146. if (
  3147. ! empty($plugin_info['Name'])
  3148. && ! empty($plugin_info['Version'])
  3149. ){
  3150. $items_affected[] = sprintf(
  3151. '%s (v%s; %s)',
  3152. self::escape( $plugin_info['Name'] ),
  3153. self::escape( $plugin_info['Version'] ),
  3154. self::escape( $plugin )
  3155. );
  3156. }
  3157. }
  3158. // Report updated plugins at once.
  3159. if ( ! empty($items_affected) ) {
  3160. $message_tpl = ( count( $items_affected ) > 1 )
  3161. ? 'Plugins updated: (multiple entries): %s'
  3162. : 'Plugin updated: %s';
  3163. $message = sprintf(
  3164. $message_tpl,
  3165. @implode( ',', $items_affected )
  3166. );
  3167. self::report_warning_event( $message );
  3168. self::notify_event( 'plugin_updated', $message );
  3169. }
  3170. }
  3171. // Plugin installation request.
  3172. elseif (
  3173. current_user_can( 'install_plugins' )
  3174. && SucuriScanRequest::get( 'action', '(install|upload)-plugin' )
  3175. ){
  3176. if ( isset($_FILES['pluginzip']) ){
  3177. $plugin = self::escape( $_FILES['pluginzip']['name'] );
  3178. } else {
  3179. $plugin = SucuriScanRequest::get( 'plugin', '.+' );
  3180. if ( ! $plugin ){ $plugin = 'Unknown'; }
  3181. }
  3182. $message = 'Plugin installed: ' . self::escape( $plugin );
  3183. SucuriScanEvent::report_warning_event( $message );
  3184. self::notify_event( 'plugin_installed', $message );
  3185. }
  3186. // Plugin deletion request.
  3187. elseif (
  3188. current_user_can( 'delete_plugins' )
  3189. && SucuriScanRequest::post( 'action', 'delete-selected' )
  3190. && SucuriScanRequest::post( 'verify-delete', '1' )
  3191. ){
  3192. $plugin_list = SucuriScanRequest::post( 'checked', '_array' );
  3193. $items_affected = array();
  3194. foreach ( (array) $plugin_list as $plugin ){
  3195. $plugin_info = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
  3196. if (
  3197. ! empty($plugin_info['Name'])
  3198. && ! empty($plugin_info['Version'])
  3199. ){
  3200. $items_affected[] = sprintf(
  3201. '%s (v%s; %s)',
  3202. self::escape( $plugin_info['Name'] ),
  3203. self::escape( $plugin_info['Version'] ),
  3204. self::escape( $plugin )
  3205. );
  3206. }
  3207. }
  3208. // Report deleted plugins at once.
  3209. if ( ! empty($items_affected) ) {
  3210. $message_tpl = ( count( $items_affected ) > 1 )
  3211. ? 'Plugins deleted: (multiple entries): %s'
  3212. : 'Plugin deleted: %s';
  3213. $message = sprintf(
  3214. $message_tpl,
  3215. @implode( ',', $items_affected )
  3216. );
  3217. self::report_warning_event( $message );
  3218. self::notify_event( 'plugin_deleted', $message );
  3219. }
  3220. }
  3221. // Plugin editor request.
  3222. elseif (
  3223. current_user_can( 'edit_plugins' )
  3224. && SucuriScanRequest::post( 'action', 'update' )
  3225. && SucuriScanRequest::post( 'plugin', '.+' )
  3226. && SucuriScanRequest::post( 'file', '.+' )
  3227. && strpos( $_SERVER['REQUEST_URI'], 'plugin-editor.php' ) !== false
  3228. ){
  3229. $filename = SucuriScanRequest::post( 'file' );
  3230. $message = 'Plugin editor used in: ' . SucuriScan::escape( $filename );
  3231. self::report_error_event( $message );
  3232. self::notify_event( 'theme_editor', $message );
  3233. }
  3234. // Theme editor request.
  3235. elseif (
  3236. current_user_can( 'edit_themes' )
  3237. && SucuriScanRequest::post( 'action', 'update' )
  3238. && SucuriScanRequest::post( 'theme', '.+' )
  3239. && SucuriScanRequest::post( 'file', '.+' )
  3240. && strpos( $_SERVER['REQUEST_URI'], 'theme-editor.php' ) !== false
  3241. ){
  3242. $theme_name = SucuriScanRequest::post( 'theme' );
  3243. $filename = SucuriScanRequest::post( 'file' );
  3244. $message = 'Theme editor used in: ' . SucuriScan::escape( $theme_name ) . '/' . SucuriScan::escape( $filename );
  3245. self::report_error_event( $message );
  3246. self::notify_event( 'theme_editor', $message );
  3247. }
  3248. // Theme installation request.
  3249. elseif (
  3250. current_user_can( 'install_themes' )
  3251. && SucuriScanRequest::get( 'action', 'install-theme' )
  3252. ){
  3253. $theme = SucuriScanRequest::get( 'theme', '.+' );
  3254. if ( ! $theme ){ $theme = 'Unknown'; }
  3255. $message = 'Theme installed: ' . self::escape( $theme );
  3256. SucuriScanEvent::report_warning_event( $message );
  3257. self::notify_event( 'theme_installed', $message );
  3258. }
  3259. // Theme deletion request.
  3260. elseif (
  3261. current_user_can( 'delete_themes' )
  3262. && SucuriScanRequest::get_or_post( 'action', 'delete' )
  3263. && SucuriScanRequest::get_or_post( 'stylesheet', '.+' )
  3264. ){
  3265. $theme = SucuriScanRequest::get( 'stylesheet', '.+' );
  3266. if ( ! $theme ){ $theme = 'Unknown'; }
  3267. $message = 'Theme deleted: ' . self::escape( $theme );
  3268. SucuriScanEvent::report_warning_event( $message );
  3269. self::notify_event( 'theme_deleted', $message );
  3270. }
  3271. // Theme update request.
  3272. elseif (
  3273. current_user_can( 'update_themes' )
  3274. && SucuriScanRequest::get( 'action', '(upgrade-theme|do-theme-upgrade)' )
  3275. && SucuriScanRequest::post( 'checked', '_array' )
  3276. ){
  3277. $themes = SucuriScanRequest::post( 'checked', '_array' );
  3278. $items_affected = array();
  3279. foreach ( (array) $themes as $theme ){
  3280. $theme_info = wp_get_theme( $theme );
  3281. $theme_name = ucwords( $theme );
  3282. $theme_version = '0.0';
  3283. if ( $theme_info->exists() ){
  3284. $theme_name = $theme_info->get( 'Name' );
  3285. $theme_version = $theme_info->get( 'Version' );
  3286. }
  3287. $items_affected[] = sprintf(
  3288. '%s (v%s; %s)',
  3289. self::escape( $theme_name ),
  3290. self::escape( $theme_version ),
  3291. self::escape( $theme )
  3292. );
  3293. }
  3294. // Report updated themes at once.
  3295. if ( ! empty($items_affected) ) {
  3296. $message_tpl = ( count( $items_affected ) > 1 )
  3297. ? 'Themes updated: (multiple entries): %s'
  3298. : 'Theme updated: %s';
  3299. $message = sprintf(
  3300. $message_tpl,
  3301. @implode( ',', $items_affected )
  3302. );
  3303. self::report_warning_event( $message );
  3304. self::notify_event( 'theme_updated', $message );
  3305. }
  3306. }
  3307. // WordPress update request.
  3308. elseif (
  3309. current_user_can( 'update_core' )
  3310. && SucuriScanRequest::get( 'action', '(do-core-upgrade|do-core-reinstall)' )
  3311. && SucuriScanRequest::post( 'upgrade' )
  3312. ){
  3313. $message = 'WordPress updated to version: ' . SucuriScanRequest::post( 'version' );
  3314. self::report_critical_event( $message );
  3315. self::notify_event( 'website_updated', $message );
  3316. }
  3317. // Widget addition or deletion.
  3318. elseif (
  3319. current_user_can( 'edit_theme_options' )
  3320. && SucuriScanRequest::post( 'action', 'save-widget' )
  3321. && SucuriScanRequest::post( 'id_base' ) !== false
  3322. && SucuriScanRequest::post( 'sidebar' ) !== false
  3323. ){
  3324. if ( SucuriScanRequest::post( 'delete_widget', '1' ) ){
  3325. $action_d = 'deleted';
  3326. $action_text = 'deleted from';
  3327. } else {
  3328. $action_d = 'added';
  3329. $action_text = 'added to';
  3330. }
  3331. $message = sprintf(
  3332. 'Widget %s (%s) %s %s (#%d; size %dx%d)',
  3333. SucuriScanRequest::post( 'id_base' ),
  3334. SucuriScanRequest::post( 'widget-id' ),
  3335. $action_text,
  3336. SucuriScanRequest::post( 'sidebar' ),
  3337. SucuriScanRequest::post( 'widget_number' ),
  3338. SucuriScanRequest::post( 'widget-width' ),
  3339. SucuriScanRequest::post( 'widget-height' )
  3340. );
  3341. self::report_warning_event( $message );
  3342. self::notify_event( 'widget_' . $action_d, $message );
  3343. }
  3344. // Detect any Wordpress settings modification.
  3345. elseif (
  3346. current_user_can( 'manage_options' )
  3347. && SucuriScanOption::check_options_nonce()
  3348. ){
  3349. // Get the settings available in the database and compare them with the submission.
  3350. $options_changed = SucuriScanOption::what_options_were_changed( $_POST );
  3351. $options_changed_str = '';
  3352. $options_changed_simple = '';
  3353. $options_changed_count = 0;
  3354. // Generate the list of options changed.
  3355. foreach ( $options_changed['original'] as $option_name => $option_value ){
  3356. $options_changed_count += 1;
  3357. $options_changed_str .= sprintf(
  3358. "The value of the option <b>%s</b> was changed from <b>'%s'</b> to <b>'%s'</b>.<br>\n",
  3359. self::escape( $option_name ),
  3360. self::escape( $option_value ),
  3361. self::escape( $options_changed['changed'][ $option_name ] )
  3362. );
  3363. $options_changed_simple .= sprintf(
  3364. "%s: from '%s' to '%s',",
  3365. self::escape( $option_name ),
  3366. self::escape( $option_value ),
  3367. self::escape( $options_changed['changed'][ $option_name ] )
  3368. );
  3369. }
  3370. // Get the option group (name of the page where the request was originated).
  3371. $option_page = isset($_POST['option_page']) ? $_POST['option_page'] : 'options';
  3372. $page_referer = false;
  3373. // Check which of these option groups where modified.
  3374. switch ( $option_page ){
  3375. case 'options':
  3376. $page_referer = 'Global';
  3377. break;
  3378. case 'general': /* no_break */
  3379. case 'writing': /* no_break */
  3380. case 'reading': /* no_break */
  3381. case 'discussion': /* no_break */
  3382. case 'media': /* no_break */
  3383. case 'permalink':
  3384. $page_referer = ucwords( $option_page );
  3385. break;
  3386. default:
  3387. $page_referer = 'Common';
  3388. break;
  3389. }
  3390. if ( $page_referer && $options_changed_count > 0 ){
  3391. $message = $page_referer . ' settings changed';
  3392. SucuriScanEvent::report_error_event( sprintf(
  3393. '%s: (multiple entries): %s',
  3394. $message,
  3395. rtrim( $options_changed_simple, ',' )
  3396. ) );
  3397. self::notify_event( 'settings_updated', $message . "<br>\n" . $options_changed_str );
  3398. }
  3399. }
  3400. }
  3401. }
  3402. /**
  3403. * Plugin API library.
  3404. *
  3405. * When used in the context of web development, an API is typically defined as a
  3406. * set of Hypertext Transfer Protocol (HTTP) request messages, along with a
  3407. * definition of the structure of response messages, which is usually in an
  3408. * Extensible Markup Language (XML) or JavaScript Object Notation (JSON) format.
  3409. * While "web API" historically has been virtually synonymous for web service,
  3410. * the recent trend (so-called Web 2.0) has been moving away from Simple Object
  3411. * Access Protocol (SOAP) based web services and service-oriented architecture
  3412. * (SOA) towards more direct representational state transfer (REST) style web
  3413. * resources and resource-oriented architecture (ROA). Part of this trend is
  3414. * related to the Semantic Web movement toward Resource Description Framework
  3415. * (RDF), a concept to promote web-based ontology engineering technologies. Web
  3416. * APIs allow the combination of multiple APIs into new applications known as
  3417. * mashups.
  3418. *
  3419. * @see http://en.wikipedia.org/wiki/Application_programming_interface#Web_APIs
  3420. */
  3421. class SucuriScanAPI extends SucuriScanOption {
  3422. /**
  3423. * Check whether the SSL certificates will be verified while executing a HTTP
  3424. * request or not. This is only for customization of the administrator, in fact
  3425. * not verifying the SSL certificates can lead to a "Man in the Middle" attack.
  3426. *
  3427. * @return boolean Whether the SSL certs will be verified while sending a request.
  3428. */
  3429. public static function verify_ssl_cert(){
  3430. return ( self::get_option( ':verify_ssl_cert' ) === 'true' );
  3431. }
  3432. /**
  3433. * Seconds before consider a HTTP request as timeout.
  3434. *
  3435. * @return integer Seconds to consider a HTTP request timeout.
  3436. */
  3437. public static function request_timeout(){
  3438. return intval( self::get_option( ':request_timeout' ) );
  3439. }
  3440. /**
  3441. * Generate an user-agent for the HTTP requests.
  3442. *
  3443. * @return string An user-agent for the HTTP requests.
  3444. */
  3445. private static function user_agent(){
  3446. $user_agent = sprintf(
  3447. 'WordPress/%s; %s',
  3448. self::site_version(),
  3449. self::get_domain()
  3450. );
  3451. return $user_agent;
  3452. }
  3453. /**
  3454. * Retrieves a URL using a changeable HTTP method, returning results in an
  3455. * array. Results include HTTP headers and content.
  3456. *
  3457. * @see http://codex.wordpress.org/Function_Reference/wp_remote_post
  3458. * @see http://codex.wordpress.org/Function_Reference/wp_remote_get
  3459. *
  3460. * @param string $url The target URL where the request will be sent.
  3461. * @param string $method HTTP method that will be used to send the request.
  3462. * @param array $params Parameters for the request defined in an associative array of key-value.
  3463. * @param array $args Request arguments like the timeout, redirections, headers, cookies, etc.
  3464. * @return array Array of results including HTTP headers or WP_Error if the request failed.
  3465. */
  3466. private static function api_call( $url = '', $method = 'GET', $params = array(), $args = array() ) {
  3467. if ( ! $url ){ return false; }
  3468. $req_args = array(
  3469. 'method' => $method,
  3470. 'timeout' => self::request_timeout(),
  3471. 'redirection' => 2,
  3472. 'httpversion' => '1.0',
  3473. 'user-agent' => self::user_agent(),
  3474. 'blocking' => true,
  3475. 'headers' => array(),
  3476. 'cookies' => array(),
  3477. 'compress' => false,
  3478. 'decompress' => false,
  3479. 'sslverify' => self::verify_ssl_cert(),
  3480. );
  3481. // Update the request arguments with the values passed tot he function.
  3482. foreach ( $args as $arg_name => $arg_value ) {
  3483. if ( array_key_exists( $arg_name, $req_args ) ) {
  3484. $req_args[ $arg_name ] = $arg_value;
  3485. }
  3486. }
  3487. if ( $method == 'GET' ) {
  3488. if ( ! empty($params) ) {
  3489. $url = sprintf( '%s?%s', $url, http_build_query( $params ) );
  3490. }
  3491. $response = wp_remote_get( $url, $req_args );
  3492. }
  3493. elseif ( $method == 'POST' ) {
  3494. $req_args['body'] = $params;
  3495. $response = wp_remote_post( $url, $req_args );
  3496. }
  3497. if ( isset($response) ) {
  3498. if ( is_wp_error( $response ) ) {
  3499. SucuriScanInterface::error(sprintf(
  3500. 'Something went wrong with an API call (%s action): %s',
  3501. ( isset($params['a']) ? $params['a'] : 'unknown' ),
  3502. $response->get_error_message()
  3503. ));
  3504. } else {
  3505. $response['body_raw'] = $response['body'];
  3506. // Check if the response data is JSON-encoded, then decode it.
  3507. if (
  3508. isset($response['headers']['content-type'])
  3509. && $response['headers']['content-type'] == 'application/json'
  3510. ) {
  3511. $assoc = ( isset($args['assoc']) && $args['assoc'] === true ) ? true : false;
  3512. $response['body'] = @json_decode( $response['body_raw'], $assoc );
  3513. }
  3514. // Check if the response data is serialized (which we will consider as insecure).
  3515. elseif ( self::is_serialized( $response['body'] ) ) {
  3516. $response['body_raw'] = null;
  3517. $response['body'] = 'ERROR:Serialized data is not supported.';
  3518. }
  3519. return $response;
  3520. }
  3521. } else {
  3522. SucuriScanInterface::error( 'HTTP method not allowed: ' . $method );
  3523. }
  3524. return false;
  3525. }
  3526. /**
  3527. * Store the API key locally.
  3528. *
  3529. * @param string $api_key An unique string of characters to identify this installation.
  3530. * @param boolean $validate Whether the format of the key should be validated before store it.
  3531. * @return boolean Either true or false if the key was saved successfully or not respectively.
  3532. */
  3533. public static function set_plugin_key( $api_key = '', $validate = false ) {
  3534. if ( $validate ) {
  3535. if ( ! preg_match( '/^[a-z0-9]{32}$/', $api_key ) ) {
  3536. SucuriScanInterface::error( 'Invalid API key format' );
  3537. return false;
  3538. }
  3539. }
  3540. if ( ! empty($api_key) ) {
  3541. SucuriScanEvent::notify_event( 'plugin_change', 'API key updated successfully: ' . $api_key );
  3542. }
  3543. return self::update_option( ':api_key', $api_key );
  3544. }
  3545. /**
  3546. * Retrieve the API key from the local storage.
  3547. *
  3548. * @return string|boolean The API key or false if it does not exists.
  3549. */
  3550. public static function get_plugin_key(){
  3551. $api_key = self::get_option( ':api_key' );
  3552. if ( $api_key && strlen( $api_key ) > 10 ) {
  3553. return $api_key;
  3554. }
  3555. return false;
  3556. }
  3557. /**
  3558. * Check and return the API key for the plugin.
  3559. *
  3560. * In this plugin the key is a pair of two strings concatenated by a single
  3561. * slash, the first part of it is in fact the key and the second part is the
  3562. * unique identifier of the site in the remote server.
  3563. *
  3564. * @return array|boolean false if the key is invalid or not present, an array otherwise.
  3565. */
  3566. public static function get_cloudproxy_key(){
  3567. $option_name = ':cloudproxy_apikey';
  3568. $api_key = self::get_option( $option_name );
  3569. // Check if the cloudproxy-waf plugin was previously installed.
  3570. if ( ! $api_key ) {
  3571. $api_key = self::get_option( 'sucuriwaf_apikey' );
  3572. if ( $api_key ) {
  3573. self::update_option( $option_name, $api_key );
  3574. self::delete_option( 'sucuriwaf_apikey' );
  3575. }
  3576. }
  3577. // Check the validity of the API key.
  3578. $match = self::is_valid_cloudproxy_key( $api_key, true );
  3579. if ( $match ) {
  3580. return array(
  3581. 'string' => $match[1].'/'.$match[2],
  3582. 'k' => $match[1],
  3583. 's' => $match[2],
  3584. );
  3585. }
  3586. return false;
  3587. }
  3588. /**
  3589. * Check whether the CloudProxy API key is valid or not.
  3590. *
  3591. * @param string $api_key The CloudProxy API key.
  3592. * @param boolean $return_match Whether the parts of the API key must be returned or not.
  3593. * @return boolean true if the API key specified is valid, false otherwise.
  3594. */
  3595. public static function is_valid_cloudproxy_key( $api_key = '', $return_match = false ) {
  3596. $pattern = '/^([a-z0-9]{32})\/([a-z0-9]{32})$/';
  3597. if ( $api_key && preg_match( $pattern, $api_key, $match ) ) {
  3598. if ( $return_match ){ return $match; }
  3599. return true;
  3600. }
  3601. return false;
  3602. }
  3603. /**
  3604. * Call an action from the remote API interface of our WordPress service.
  3605. *
  3606. * @param string $method HTTP method that will be used to send the request.
  3607. * @param array $params Parameters for the request defined in an associative array of key-value.
  3608. * @param boolean $send_api_key Whether the API key should be added to the request parameters or not.
  3609. * @param array $args Request arguments like the timeout, redirections, headers, cookies, etc.
  3610. * @return array Array of results including HTTP headers or WP_Error if the request failed.
  3611. */
  3612. public static function api_call_wordpress( $method = 'GET', $params = array(), $send_api_key = true, $args = array() ) {
  3613. $url = SUCURISCAN_API;
  3614. $params[ SUCURISCAN_API_VERSION ] = 1;
  3615. $params['p'] = 'wordpress';
  3616. if ( $send_api_key ) {
  3617. $api_key = self::get_plugin_key();
  3618. if ( ! $api_key ){ return false; }
  3619. $params['k'] = $api_key;
  3620. }
  3621. $response = self::api_call( $url, $method, $params, $args );
  3622. return $response;
  3623. }
  3624. /**
  3625. * Call an action from the remote API interface of our CloudProxy service.
  3626. *
  3627. * @param string $method HTTP method that will be used to send the request.
  3628. * @param array $params Parameters for the request defined in an associative array of key-value.
  3629. * @return array Array of results including HTTP headers or WP_Error if the request failed.
  3630. */
  3631. public static function api_call_cloudproxy( $method = 'GET', $params = array() ) {
  3632. $send_request = false;
  3633. if ( isset($params['k']) && isset($params['s']) ) {
  3634. $send_request = true;
  3635. } else {
  3636. $api_key = self::get_cloudproxy_key();
  3637. if ( $api_key ) {
  3638. $send_request = true;
  3639. $params['k'] = $api_key['k'];
  3640. $params['s'] = $api_key['s'];
  3641. }
  3642. }
  3643. if ( $send_request ) {
  3644. $url = SUCURISCAN_CLOUDPROXY_API;
  3645. $params[ SUCURISCAN_CLOUDPROXY_API_VERSION ] = 1;
  3646. $response = self::api_call( $url, $method, $params );
  3647. return $response;
  3648. }
  3649. return false;
  3650. }
  3651. /**
  3652. * Determine whether an API response was successful or not checking the expected
  3653. * generic variables and types, in case of an error a notification will appears
  3654. * in the administrator panel explaining the result of the operation.
  3655. *
  3656. * @param array $response Array of results including HTTP headers or WP_Error if the request failed.
  3657. * @return boolean Either true or false in case of success or failure of the API response (respectively).
  3658. */
  3659. private static function handle_response( $response = array() ) {
  3660. if ( $response ) {
  3661. if ( $response['body'] instanceof stdClass ) {
  3662. if ( isset($response['body']->status) ) {
  3663. if ( $response['body']->status == 1 ) {
  3664. return true;
  3665. } else {
  3666. $action_message = 'Unknown error, there is no more information.';
  3667. if ( isset($response['body']->messages[0]) ) {
  3668. $action_message = $response['body']->messages[0];
  3669. }
  3670. SucuriScanInterface::error( ucwords( $response['body']->action ) . ': ' . $action_message );
  3671. }
  3672. } else {
  3673. SucuriScanInterface::error( 'Could not determine the status of an API call.' );
  3674. }
  3675. } else {
  3676. $error_message = 'non JSON-encoded response.';
  3677. if (
  3678. isset($response['response'])
  3679. && isset($response['response']['message'])
  3680. && isset($response['response']['code'])
  3681. && $response['response']['code'] !== 200
  3682. ) {
  3683. $error_message = sprintf(
  3684. '(%s) %s',
  3685. $response['response']['code'],
  3686. $response['response']['message']
  3687. );
  3688. }
  3689. SucuriScanInterface::error( 'Malformed API response: ' . $error_message );
  3690. }
  3691. }
  3692. return false;
  3693. }
  3694. /**
  3695. * Send a request to the API to register this site.
  3696. *
  3697. * @return boolean true if the API key was generated, false otherwise.
  3698. */
  3699. public static function register_site(){
  3700. $response = self::api_call_wordpress( 'POST', array(
  3701. 'e' => self::get_site_email(),
  3702. 's' => self::get_domain(),
  3703. 'a' => 'register_site',
  3704. ), false );
  3705. if ( self::handle_response( $response ) ) {
  3706. self::set_plugin_key( $response['body']->output->api_key );
  3707. SucuriScanEvent::schedule_task();
  3708. SucuriScanEvent::notify_event( 'plugin_change', 'Site registered and API key generated' );
  3709. SucuriScanInterface::info( 'The API key for your site was successfully generated and saved.' );
  3710. return true;
  3711. }
  3712. return false;
  3713. }
  3714. /**
  3715. * Send a request to recover a previously registered API key.
  3716. *
  3717. * @return boolean true if the API key was sent to the administrator email, false otherwise.
  3718. */
  3719. public static function recover_key(){
  3720. $clean_domain = self::get_domain();
  3721. $response = self::api_call_wordpress( 'GET', array(
  3722. 'e' => self::get_site_email(),
  3723. 's' => $clean_domain,
  3724. 'a' => 'recover_key',
  3725. ), false );
  3726. if ( self::handle_response( $response ) ) {
  3727. SucuriScanEvent::notify_event( 'plugin_change', 'API key recovered for domain: ' . $clean_domain );
  3728. SucuriScanInterface::info( $response['body']->output->message );
  3729. return true;
  3730. }
  3731. return false;
  3732. }
  3733. /**
  3734. * Send a request to the API to store and analyze the events of the site. An
  3735. * event can be anything from a simple request, an internal modification of the
  3736. * settings or files in the administrator panel, or a notification generated by
  3737. * this plugin.
  3738. *
  3739. * @param string $event The information gathered through out the normal functioning of the site.
  3740. * @return boolean true if the event was logged in the monitoring service, false otherwise.
  3741. */
  3742. public static function send_log( $event = '' ) {
  3743. if ( ! empty($event) ) {
  3744. $response = self::api_call_wordpress( 'POST', array(
  3745. 'a' => 'send_log',
  3746. 'm' => $event,
  3747. ), true, array( 'timeout' => 20 ) );
  3748. if ( self::handle_response( $response ) ) {
  3749. return true;
  3750. }
  3751. }
  3752. return false;
  3753. }
  3754. /**
  3755. * Retrieve the event logs registered by the API service.
  3756. *
  3757. * @param integer $lines How many lines from the log file will be retrieved.
  3758. * @return string The response of the API service.
  3759. */
  3760. public static function get_logs( $lines = 50 ) {
  3761. $response = self::api_call_wordpress( 'GET', array(
  3762. 'a' => 'get_logs',
  3763. 'l' => $lines,
  3764. ) );
  3765. if ( self::handle_response( $response ) ) {
  3766. $response['body']->output_data = array();
  3767. $log_pattern = '/^([0-9-: ]+) (.*) : (.*)/';
  3768. $extra_pattern = '/(.+ \(multiple entries\):) (.+)/';
  3769. $generic_pattern = '/^([A-Z][a-z]{3,7}): ([0-9a-zA-Z@_\s\.\-\(\)]+, )?(\S+; )?(.+)/';
  3770. $auth_pattern = '/^User authentication (succeeded|failed): ([^<;]+)/';
  3771. foreach ( $response['body']->output as $log ) {
  3772. if ( preg_match( $log_pattern, $log, $log_match ) ) {
  3773. $log_data = array(
  3774. 'event' => 'notice',
  3775. 'datetime' => $log_match[1],
  3776. 'timestamp' => strtotime( $log_match[1] ),
  3777. 'account' => $log_match[2],
  3778. 'username' => 'system',
  3779. 'remote_addr' => '::1',
  3780. 'message' => $log_match[3],
  3781. 'file_list' => false,
  3782. 'file_list_count' => 0,
  3783. );
  3784. $log_data['message'] = str_replace( ', new size', '; new size', $log_data['message'] );
  3785. $log_data['message'] = str_replace( '<br>', '; ', $log_data['message'] );
  3786. // Extract more information from the generic audit logs.
  3787. if ( preg_match( $generic_pattern, $log_data['message'], $log_extra ) ) {
  3788. $log_data['event'] = strtolower( $log_extra[1] );
  3789. $log_data['message'] = trim( $log_extra[4] );
  3790. // Extract the remote address from the generic logs.
  3791. if ( ! empty($log_extra[3]) ) {
  3792. $log_data['remote_addr'] = str_replace( ";\x20", '', $log_extra[3] );
  3793. }
  3794. // Extract the username from the authentication logs.
  3795. if ( ! empty($log_extra[2]) ) {
  3796. $log_data['username'] = preg_replace( '/.*\((\S+)\),\s$/', '$1', $log_extra[2] );
  3797. $log_data['username'] = str_replace( ",\x20", '', $log_data['username'] );
  3798. }
  3799. // Match old user authentication logs.
  3800. $log_data['message'] = str_replace( 'logged in', 'authentication succeeded', $log_data['message'] );
  3801. if ( preg_match( $auth_pattern, $log_data['message'], $user_match ) ) {
  3802. $log_data['username'] = $user_match[2];
  3803. }
  3804. }
  3805. // Extract more information from the special formatted logs.
  3806. if ( preg_match( $extra_pattern, $log_data['message'], $log_extra ) ) {
  3807. $log_data['message'] = $log_extra[1];
  3808. $log_data['file_list'] = explode( ',', $log_extra[2] );
  3809. $log_data['file_list_count'] = count( $log_data['file_list'] );
  3810. }
  3811. $response['body']->output_data[] = $log_data;
  3812. }
  3813. }
  3814. return $response['body'];
  3815. }
  3816. return false;
  3817. }
  3818. /**
  3819. * Get a list of valid audit event types with their respective colors.
  3820. *
  3821. * @return array Valid audit event types with their colors.
  3822. */
  3823. public static function get_audit_event_types(){
  3824. $event_types = array(
  3825. 'critical' => '#000000',
  3826. 'debug' => '#c690ec',
  3827. 'error' => '#f27d7d',
  3828. 'info' => '#5bc0de',
  3829. 'notice' => '#428bca',
  3830. 'warning' => '#f0ad4e',
  3831. );
  3832. return $event_types;
  3833. }
  3834. /**
  3835. * Parse the event logs with multiple entries.
  3836. *
  3837. * @param string $event_log Event log that will be processed.
  3838. * @return array List of parts of the event log.
  3839. */
  3840. public static function parse_multiple_entries( $event_log = '' ) {
  3841. if ( preg_match( '/^(.*:\s)\(multiple entries\):\s(.+)/', $event_log, $match ) ) {
  3842. $event_log = array();
  3843. $event_log[] = trim( $match[1] );
  3844. $grouped_items = @explode( ',', $match[2] );
  3845. $event_log = array_merge( $event_log, $grouped_items );
  3846. }
  3847. return $event_log;
  3848. }
  3849. /**
  3850. * Collect the information for the audit log report.
  3851. *
  3852. * @param integer $lines How many lines from the log file will be retrieved.
  3853. * @return array All the information necessary to display the audit logs report.
  3854. */
  3855. public static function get_audit_report( $lines = 50 ) {
  3856. $audit_logs = self::get_logs( $lines );
  3857. if (
  3858. $audit_logs instanceof stdClass
  3859. && property_exists( $audit_logs, 'total_entries' )
  3860. && property_exists( $audit_logs, 'output_data' )
  3861. && ! empty($audit_logs->output_data)
  3862. ) {
  3863. // Data structure that will be returned.
  3864. $report = array(
  3865. 'total_events' => 0,
  3866. 'start_timestamp' => 0,
  3867. 'end_timestamp' => 0,
  3868. 'event_colors' => array(),
  3869. 'events_per_type' => array(),
  3870. 'events_per_user' => array(),
  3871. 'events_per_ipaddress' => array(),
  3872. 'events_per_login' => array(
  3873. 'successful' => 0,
  3874. 'failed' => 0,
  3875. ),
  3876. );
  3877. // Get a list of valid audit event types.
  3878. $event_types = self::get_audit_event_types();
  3879. foreach ( $event_types as $event => $event_color ) {
  3880. $report['events_per_type'][ $event ] = 0;
  3881. $report['event_colors'][] = sprintf( "'%s'", $event_color );
  3882. }
  3883. // Collect information for each report chart.
  3884. foreach ( $audit_logs->output_data as $event ) {
  3885. $report['total_events'] += 1;
  3886. // Increment the number of events for this event type.
  3887. if ( array_key_exists( $event['event'], $report['events_per_type'] ) ) {
  3888. $report['events_per_type'][ $event['event'] ] += 1;
  3889. } else {
  3890. $report['events_per_type'][ $event['event'] ] = 1;
  3891. }
  3892. // Find the lowest datetime among the filtered events.
  3893. if (
  3894. $event['timestamp'] <= $report['start_timestamp']
  3895. || $report['start_timestamp'] === 0
  3896. ) {
  3897. $report['start_timestamp'] = $event['timestamp'];
  3898. }
  3899. // Find the highest datetime among the filtered events.
  3900. if ( $event['timestamp'] >= $report['end_timestamp'] ) {
  3901. $report['end_timestamp'] = $event['timestamp'];
  3902. }
  3903. // Increment the number of events generated by this user account.
  3904. if ( array_key_exists( $event['username'], $report['events_per_user'] ) ) {
  3905. $report['events_per_user'][ $event['username'] ] += 1;
  3906. } else {
  3907. $report['events_per_user'][ $event['username'] ] = 1;
  3908. }
  3909. // Increment the number of events generated from this remote address.
  3910. if ( array_key_exists( $event['remote_addr'], $report['events_per_ipaddress'] ) ) {
  3911. $report['events_per_ipaddress'][ $event['remote_addr'] ] += 1;
  3912. } else {
  3913. $report['events_per_ipaddress'][ $event['remote_addr'] ] = 1;
  3914. }
  3915. // Detect successful and failed user authentications.
  3916. $auth_pattern = '/^User authentication (succeeded|failed):/';
  3917. if ( preg_match( $auth_pattern, $event['message'], $match ) ) {
  3918. if ( $match[1] == 'succeeded' ) {
  3919. $report['events_per_login']['successful'] += 1;
  3920. } else {
  3921. $report['events_per_login']['failed'] += 1;
  3922. }
  3923. }
  3924. // Backward compatibility for previous user login messages.
  3925. elseif ( preg_match( '/^User logged in:/', $event['message'] ) ) {
  3926. $report['events_per_login']['successful'] += 1;
  3927. }
  3928. }
  3929. if ( $report['total_events'] > 0 ) {
  3930. return $report;
  3931. }
  3932. }
  3933. return false;
  3934. }
  3935. /**
  3936. * Send a request to the API to store and analyze the file's hashes of the site.
  3937. * This will be the core of the monitoring tools and will enhance the
  3938. * information of the audit logs alerting the administrator of suspicious
  3939. * changes in the system.
  3940. *
  3941. * @param string $hashes The information gathered after the scanning of the site's files.
  3942. * @return boolean true if the hashes were stored, false otherwise.
  3943. */
  3944. public static function send_hashes( $hashes = '' ) {
  3945. if ( ! empty($hashes) ) {
  3946. $response = self::api_call_wordpress( 'POST', array(
  3947. 'a' => 'send_hashes',
  3948. 'h' => $hashes,
  3949. ) );
  3950. if ( self::handle_response( $response ) ) {
  3951. return true;
  3952. }
  3953. }
  3954. return false;
  3955. }
  3956. /**
  3957. * Retrieve the public settings of the account associated with the API keys
  3958. * registered by the administrator of the site. This function will send a HTTP
  3959. * request to the remote API service and process its response, when successful
  3960. * it will return an array/object containing the public attributes of the site.
  3961. *
  3962. * @param boolean $api_key The CloudProxy API key.
  3963. * @return array A hash with the settings of a CloudProxy account.
  3964. */
  3965. public static function get_cloudproxy_settings( $api_key = false ) {
  3966. $params = array( 'a' => 'show_settings' );
  3967. if ( $api_key ) {
  3968. $params = array_merge( $params, $api_key );
  3969. }
  3970. $response = self::api_call_cloudproxy( 'GET', $params );
  3971. if ( self::handle_response( $response ) ) {
  3972. return $response['body']->output;
  3973. }
  3974. return false;
  3975. }
  3976. /**
  3977. * Flush the cache of the site(s) associated with the API key.
  3978. *
  3979. * @param boolean $api_key The CloudProxy API key.
  3980. * @return string Message explaining the result of the operation.
  3981. */
  3982. public static function clear_cloudproxy_cache( $api_key = false ) {
  3983. $params = array( 'a' => 'clear_cache' );
  3984. if ( $api_key ) {
  3985. $params = array_merge( $params, $api_key );
  3986. }
  3987. $response = self::api_call_cloudproxy( 'GET', $params );
  3988. if ( self::handle_response( $response ) ) {
  3989. return $response['body'];
  3990. }
  3991. return false;
  3992. }
  3993. /**
  3994. * Retrieve the audit logs of the account associated with the API keys
  3995. * registered b the administrator of the site. This function will send a HTTP
  3996. * request to the remote API service and process its response, when successful
  3997. * it will return an array/object containing a list of requests blocked by our
  3998. * CloudProxy.
  3999. *
  4000. * By default the logs that will be retrieved are from today, if you need to see
  4001. * the logs of previous days you will need to add a new parameter to the request
  4002. * URL named "date" with format yyyy-mm-dd.
  4003. *
  4004. * @param boolean $api_key The CloudProxy API key.
  4005. * @param string $date An optional date to filter the result to a specific timespan: yyyy-mm-dd.
  4006. * @return array A list of objects with the detailed version of each request blocked by our service.
  4007. */
  4008. public static function get_cloudproxy_logs( $api_key = false, $date = '' ) {
  4009. $params = array(
  4010. 'a' => 'audit_trails',
  4011. 'date' => date( 'Y-m-d' ),
  4012. );
  4013. if ( preg_match( '/^[0-9]{4}(\-[0-9]{2}){2}$/', $date ) ) {
  4014. $params['date'] = $date;
  4015. }
  4016. if ( $api_key ) {
  4017. $params = array_merge( $params, $api_key );
  4018. }
  4019. $response = self::api_call_cloudproxy( 'GET', $params );
  4020. if ( self::handle_response( $response ) ) {
  4021. return $response['body']->output;
  4022. }
  4023. return false;
  4024. }
  4025. /**
  4026. * Scan a website through the public SiteCheck API [1] for known malware,
  4027. * blacklisting status, website errors, and out-of-date software.
  4028. *
  4029. * [1] http://sitecheck.sucuri.net/
  4030. *
  4031. * @param string $domain The clean version of the website's domain.
  4032. * @return object Serialized data of the scanning results for the site specified.
  4033. */
  4034. public static function get_sitecheck_results( $domain = '' ) {
  4035. if ( ! empty($domain) ) {
  4036. $url = 'http://sitecheck.sucuri.net/';
  4037. $response = self::api_call( $url, 'GET', array(
  4038. 'scan' => $domain,
  4039. 'fromwp' => 2,
  4040. 'clear' => 1,
  4041. 'json' => 1,
  4042. ), array(
  4043. 'assoc' => true,
  4044. ) );
  4045. if ( $response ) {
  4046. return $response['body'];
  4047. }
  4048. }
  4049. return false;
  4050. }
  4051. /**
  4052. * Extract detailed information from a SiteCheck malware payload.
  4053. *
  4054. * @param array $malware Array with two entries with basic malware information.
  4055. * @return array Detailed information of the malware found by SiteCheck.
  4056. */
  4057. public static function get_sitecheck_malware( $malware = array() ) {
  4058. if ( count( $malware ) >= 2 ) {
  4059. $data_set = array(
  4060. 'alert_message' => '',
  4061. 'infected_url' => '',
  4062. 'malware_type' => '',
  4063. 'malware_docs' => '',
  4064. 'malware_payload' => '',
  4065. );
  4066. // Extract the information from the alert message.
  4067. $alert_parts = explode( ':', $malware[0], 2 );
  4068. if ( isset($alert_parts[1]) ) {
  4069. $data_set['alert_message'] = $alert_parts[0];
  4070. $data_set['infected_url'] = $alert_parts[1];
  4071. }
  4072. // Extract the information from the malware message.
  4073. $malware_parts = explode( "\n", $malware[1] );
  4074. if ( isset($malware_parts[1]) ) {
  4075. if ( preg_match( '/(.+)\. Details: (.+)/', $malware_parts[0], $match ) ) {
  4076. $data_set['malware_type'] = $match[1];
  4077. $data_set['malware_docs'] = $match[2];
  4078. }
  4079. $payload = trim( $malware_parts[1] );
  4080. $payload = html_entity_decode( $payload );
  4081. if ( preg_match( '/<div id=\'HiddenDiv\'>(.+)<\/div>/', $payload, $match ) ) {
  4082. $data_set['malware_payload'] = trim( $match[1] );
  4083. }
  4084. }
  4085. return $data_set;
  4086. }
  4087. return false;
  4088. }
  4089. /**
  4090. * Retrieve a new set of keys for the WordPress configuration file using the
  4091. * official API provided by WordPress itself.
  4092. *
  4093. * @return array A list of the new set of keys generated by WordPress API.
  4094. */
  4095. public static function get_new_secret_keys(){
  4096. $pattern = self::secret_key_pattern();
  4097. $response = self::api_call( 'https://api.wordpress.org/secret-key/1.1/salt/', 'GET' );
  4098. if ( $response && preg_match_all( $pattern, $response['body'], $match ) ) {
  4099. $new_keys = array();
  4100. foreach ( $match[1] as $i => $value ) {
  4101. $new_keys[ $value ] = $match[3][ $i ];
  4102. }
  4103. return $new_keys;
  4104. }
  4105. return false;
  4106. }
  4107. /**
  4108. * Retrieve a list with the checksums of the files in a specific version of WordPress.
  4109. *
  4110. * @see Release Archive http://wordpress.org/download/release-archive/
  4111. *
  4112. * @param integer $version Valid version number of the WordPress project.
  4113. * @return object Associative object with the relative filepath and the checksums of the project files.
  4114. */
  4115. public static function get_official_checksums( $version = 0 ) {
  4116. $url = 'http://api.wordpress.org/core/checksums/1.0/';
  4117. $language = 'en_US'; /* WPLANG does not works. */
  4118. $response = self::api_call( $url, 'GET', array(
  4119. 'version' => $version,
  4120. 'locale' => $language,
  4121. ));
  4122. if ( $response ) {
  4123. if ( $response['body'] instanceof stdClass ) {
  4124. $json_data = $response['body'];
  4125. } else {
  4126. $json_data = @json_decode( $response['body'] );
  4127. }
  4128. if (
  4129. isset($json_data->checksums)
  4130. && ! empty($json_data->checksums)
  4131. ) {
  4132. $checksums = $json_data->checksums;
  4133. // Convert the object list to an array for better handle of the data.
  4134. if ( $checksums instanceof stdClass ) {
  4135. $checksums = (array) $checksums;
  4136. }
  4137. return $checksums;
  4138. }
  4139. }
  4140. return false;
  4141. }
  4142. /**
  4143. * Check the plugins directory and retrieve all plugin files with plugin data.
  4144. * This function will also retrieve the URL and name of the repository/page
  4145. * where it is being published at the WordPress plugins market.
  4146. *
  4147. * @return array Key is the plugin file path and the value is an array of the plugin data.
  4148. */
  4149. public static function get_plugins(){
  4150. // Check if the cache library was loaded.
  4151. $can_cache = class_exists( 'SucuriScanCache' );
  4152. if ( $can_cache ) {
  4153. $cache = new SucuriScanCache( 'plugindata' );
  4154. $cached_data = $cache->get( 'plugins', SUCURISCAN_GET_PLUGINS_LIFETIME, 'array' );
  4155. // Return the previously cached results of this function.
  4156. if ( $cached_data !== false ) {
  4157. return $cached_data;
  4158. }
  4159. }
  4160. // Get the plugin's basic information from WordPress transient data.
  4161. $plugins = get_plugins();
  4162. $pattern = '/^http(s)?:\/\/wordpress\.org\/plugins\/(.*)\/$/';
  4163. $wp_market = 'https://wordpress.org/plugins/%s/';
  4164. // Loop through each plugin data and complement its information with more attributes.
  4165. foreach ( $plugins as $plugin_path => $plugin_data ) {
  4166. // Default values for the plugin extra attributes.
  4167. $repository = '';
  4168. $repository_name = '';
  4169. $is_free_plugin = false;
  4170. // If the plugin's info object has already a plugin_uri.
  4171. if (
  4172. isset($plugin_data['PluginURI'])
  4173. && preg_match( $pattern, $plugin_data['PluginURI'], $match )
  4174. ) {
  4175. $repository = $match[0];
  4176. $repository_name = $match[2];
  4177. $is_free_plugin = true;
  4178. }
  4179. // Retrieve the WordPress plugin page from the plugin's filename.
  4180. else {
  4181. if ( strpos( $plugin_path, '/' ) !== false ) {
  4182. $plugin_path_parts = explode( '/', $plugin_path, 2 );
  4183. } else {
  4184. $plugin_path_parts = explode( '.', $plugin_path, 2 );
  4185. }
  4186. if ( isset($plugin_path_parts[0]) ) {
  4187. $possible_repository = sprintf( $wp_market, $plugin_path_parts[0] );
  4188. $resp = wp_remote_head( $possible_repository );
  4189. if (
  4190. ! is_wp_error( $resp )
  4191. && $resp['response']['code'] == 200
  4192. ) {
  4193. $repository = $possible_repository;
  4194. $repository_name = $plugin_path_parts[0];
  4195. $is_free_plugin = true;
  4196. }
  4197. }
  4198. }
  4199. // Complement the plugin's information with these attributes.
  4200. $plugins[ $plugin_path ]['Repository'] = $repository;
  4201. $plugins[ $plugin_path ]['RepositoryName'] = $repository_name;
  4202. $plugins[ $plugin_path ]['IsFreePlugin'] = $is_free_plugin;
  4203. $plugins[ $plugin_path ]['PluginType'] = ( $is_free_plugin ? 'free' : 'premium' );
  4204. $plugins[ $plugin_path ]['IsPluginActive'] = false;
  4205. if ( is_plugin_active( $plugin_path ) ) {
  4206. $plugins[ $plugin_path ]['IsPluginActive'] = true;
  4207. }
  4208. }
  4209. if ( $can_cache ) {
  4210. // Add the information of the plugins to the file-based cache.
  4211. $cache->add( 'plugins', $plugins );
  4212. }
  4213. return $plugins;
  4214. }
  4215. /**
  4216. * Retrieve plugin installer pages from WordPress Plugins API.
  4217. *
  4218. * It is possible for a plugin to override the Plugin API result with three
  4219. * filters. Assume this is for plugins, which can extend on the Plugin Info to
  4220. * offer more choices. This is very powerful and must be used with care, when
  4221. * overriding the filters.
  4222. *
  4223. * The first filter, 'plugins_api_args', is for the args and gives the action as
  4224. * the second parameter. The hook for 'plugins_api_args' must ensure that an
  4225. * object is returned.
  4226. *
  4227. * The second filter, 'plugins_api', is the result that would be returned.
  4228. *
  4229. * @param string $plugin Frienly name of the plugin.
  4230. * @return object Object on success, WP_Error on failure.
  4231. */
  4232. public static function get_remote_plugin_data( $plugin = '' ) {
  4233. if ( ! empty($plugin) ) {
  4234. $url = sprintf( 'http://api.wordpress.org/plugins/info/1.0/%s.json', $plugin );
  4235. $response = self::api_call( $url, 'GET' );
  4236. if ( $response ) {
  4237. if ( $response['body'] instanceof stdClass ) {
  4238. return $response['body'];
  4239. }
  4240. }
  4241. }
  4242. return false;
  4243. }
  4244. /**
  4245. * Retrieve a specific file from the official WordPress subversion repository,
  4246. * the content of the file is determined by the tags defined using the site
  4247. * version specified. Only official core files are allowed to fetch.
  4248. *
  4249. * @see http://core.svn.wordpress.org/
  4250. * @see http://i18n.svn.wordpress.org/
  4251. * @see http://core.svn.wordpress.org/tags/VERSION_NUMBER/
  4252. *
  4253. * @param string $filepath Relative file path of a project core file.
  4254. * @param string $version Optional site version, default will be the global version number.
  4255. * @return string Full content of the official file retrieved, false if the file was not found.
  4256. */
  4257. public static function get_original_core_file( $filepath = '', $version = 0 ) {
  4258. if ( ! empty($filepath) ) {
  4259. if ( $version == 0 ) {
  4260. $version = self::site_version();
  4261. }
  4262. $url = sprintf( 'http://core.svn.wordpress.org/tags/%s/%s', $version, $filepath );
  4263. $response = self::api_call( $url, 'GET' );
  4264. if ( $response ) {
  4265. if (
  4266. isset($response['headers']['content-length'])
  4267. && $response['headers']['content-length'] > 0
  4268. && is_string( $response['body'] )
  4269. ) {
  4270. return $response['body'];
  4271. }
  4272. }
  4273. }
  4274. return false;
  4275. }
  4276. }
  4277. /**
  4278. * Process and send emails.
  4279. *
  4280. * One of the core features of the plugin is the event alerts, a list of rules
  4281. * will check if the site is being compromised, in which case a notification
  4282. * will be sent to the site email address (an address that can be configured in
  4283. * the settings page).
  4284. */
  4285. class SucuriScanMail extends SucuriScanOption {
  4286. /**
  4287. * Check whether the email notifications will be sent in HTML or Plain/Text.
  4288. *
  4289. * @return boolean Whether the emails will be in HTML or Plain/Text.
  4290. */
  4291. public static function prettify_mails(){
  4292. return ( self::get_option( ':prettify_mails' ) === 'enabled' );
  4293. }
  4294. /**
  4295. * Send a message to a specific email address.
  4296. *
  4297. * @param string $email The email address of the recipient that will receive the message.
  4298. * @param string $subject The reason of the message that will be sent.
  4299. * @param string $message Body of the message that will be sent.
  4300. * @param array $data_set Optional parameter to add more information to the notification.
  4301. * @return boolean Whether the email contents were sent successfully.
  4302. */
  4303. public static function send_mail( $email = '', $subject = '', $message = '', $data_set = array() ){
  4304. $headers = array();
  4305. $subject = ucwords( strtolower( $subject ) );
  4306. $force = false;
  4307. $debug = false;
  4308. // Check whether the mail will be printed in the site instead of sent.
  4309. if (
  4310. isset($data_set['Debug'])
  4311. && $data_set['Debug'] == true
  4312. ){
  4313. $debug = true;
  4314. unset($data_set['Debug']);
  4315. }
  4316. // Check whether the mail will be even if the limit per hour was reached or not.
  4317. if (
  4318. isset($data_set['Force'])
  4319. && $data_set['Force'] == true
  4320. ){
  4321. $force = true;
  4322. unset($data_set['Force']);
  4323. }
  4324. // Check whether the email notifications will be sent in HTML or Plain/Text.
  4325. if ( self::prettify_mails() ){
  4326. $headers = array( 'content-type: text/html' );
  4327. $data_set['PrettifyType'] = 'pretty';
  4328. } else {
  4329. $message = strip_tags( $message );
  4330. }
  4331. if ( ! self::emails_per_hour_reached() || $force || $debug ){
  4332. $message = self::prettify_mail( $subject, $message, $data_set );
  4333. if ( $debug ){ die($message); }
  4334. $subject = self::get_email_subject( $subject );
  4335. $mail_sent = wp_mail( $email, $subject, $message, $headers );
  4336. if ( $mail_sent ){
  4337. $emails_sent_num = (int) self::get_option( ':emails_sent' );
  4338. self::update_option( ':emails_sent', $emails_sent_num + 1 );
  4339. self::update_option( ':last_email_at', time() );
  4340. return true;
  4341. }
  4342. }
  4343. return false;
  4344. }
  4345. /**
  4346. * Generate a subject for the email alerts.
  4347. *
  4348. * @param string $event The reason of the message that will be sent.
  4349. * @return string A text with the subject for the email alert.
  4350. */
  4351. private static function get_email_subject( $event = '' ){
  4352. $domain_name = self::get_domain();
  4353. $remote_addr = self::get_remote_addr();
  4354. $email_subject = self::get_option( ':email_subject' );
  4355. if ( $email_subject ) {
  4356. $email_subject = str_replace(
  4357. array( ':domain', ':event', ':remoteaddr' ),
  4358. array( $domain_name, $event, $remote_addr ),
  4359. strip_tags( $email_subject )
  4360. );
  4361. return $email_subject;
  4362. }
  4363. /**
  4364. * Probably a bad value in the options table. Delete the entry from the database
  4365. * and call this function to try again, it will probably fall in an infinite
  4366. * loop, but this is the easiest way to control this procedure.
  4367. */
  4368. else {
  4369. self::delete_option( ':email_subject' );
  4370. return self::get_email_subject( $event );
  4371. }
  4372. }
  4373. /**
  4374. * Generate a HTML version of the message that will be sent through an email.
  4375. *
  4376. * @param string $subject The reason of the message that will be sent.
  4377. * @param string $message Body of the message that will be sent.
  4378. * @param array $data_set Optional parameter to add more information to the notification.
  4379. * @return string The message formatted in a HTML template.
  4380. */
  4381. private static function prettify_mail( $subject = '', $message = '', $data_set = array() ){
  4382. $prettify_type = isset($data_set['PrettifyType']) ? $data_set['PrettifyType'] : 'simple';
  4383. $template_name = 'notification-' . $prettify_type;
  4384. $user = wp_get_current_user();
  4385. $display_name = '';
  4386. if (
  4387. $user instanceof WP_User
  4388. && isset($user->user_login)
  4389. && ! empty($user->user_login)
  4390. ){
  4391. $display_name = sprintf( 'User: %s (%s)', $user->display_name, $user->user_login );
  4392. }
  4393. // Format list of items when the event has multiple entries.
  4394. if ( strpos( $message, 'multiple' ) !== false ) {
  4395. $message_parts = SucuriScanAPI::parse_multiple_entries( $message );
  4396. if ( is_array( $message_parts ) ) {
  4397. $message = ( $prettify_type == 'pretty' ) ? $message_parts[0] . '<ul>' : $message_parts[0];
  4398. unset($message_parts[0]);
  4399. foreach ( $message_parts as $msg_part ) {
  4400. if ( $prettify_type == 'pretty' ) {
  4401. $message .= sprintf( "<li>%s</li>\n", $msg_part );
  4402. } else {
  4403. $message .= sprintf( "- %s\n", $msg_part );
  4404. }
  4405. }
  4406. $message .= ( $prettify_type == 'pretty' ) ? '</ul>' : '';
  4407. }
  4408. }
  4409. $mail_variables = array(
  4410. 'TemplateTitle' => 'Sucuri Alert',
  4411. 'Subject' => $subject,
  4412. 'Website' => self::get_option( 'siteurl' ),
  4413. 'RemoteAddress' => self::get_remote_addr(),
  4414. 'Message' => $message,
  4415. 'User' => $display_name,
  4416. 'Time' => SucuriScan::current_datetime(),
  4417. );
  4418. foreach ( $data_set as $var_key => $var_value ){
  4419. $mail_variables[ $var_key ] = $var_value;
  4420. }
  4421. return SucuriScanTemplate::get_section( $template_name, $mail_variables );
  4422. }
  4423. /**
  4424. * Check whether the maximum quantity of emails per hour was reached.
  4425. *
  4426. * @return boolean Whether the quota emails per hour was reached.
  4427. */
  4428. private static function emails_per_hour_reached(){
  4429. $max_per_hour = self::get_option( ':emails_per_hour' );
  4430. if ( $max_per_hour != 'unlimited' ){
  4431. // Check if we are still in that sixty minutes.
  4432. $current_time = time();
  4433. $last_email_at = self::get_option( ':last_email_at' );
  4434. $diff_time = abs( $current_time - $last_email_at );
  4435. if ( $diff_time <= 3600 ){
  4436. // Check if the quantity of emails sent is bigger than the configured.
  4437. $emails_sent = (int) self::get_option( ':emails_sent' );
  4438. $max_per_hour = intval( $max_per_hour );
  4439. if ( $emails_sent >= $max_per_hour ){
  4440. return true;
  4441. }
  4442. } else {
  4443. // Reset the counter of emails sent.
  4444. self::update_option( ':emails_sent', 0 );
  4445. }
  4446. }
  4447. return false;
  4448. }
  4449. }
  4450. /**
  4451. * Read, parse and handle everything related with the templates.
  4452. *
  4453. * A web template system uses a template processor to combine web templates to
  4454. * form finished web pages, possibly using some data source to customize the
  4455. * pages or present a large amount of content on similar-looking pages. It is a
  4456. * web publishing tool present in content management systems, web application
  4457. * frameworks, and HTML editors.
  4458. *
  4459. * Web templates can be used like the template of a form letter to either
  4460. * generate a large number of "static" (unchanging) web pages in advance, or to
  4461. * produce "dynamic" web pages on demand.
  4462. */
  4463. class SucuriScanTemplate extends SucuriScanRequest {
  4464. /**
  4465. * Replace all pseudo-variables from a string of characters.
  4466. *
  4467. * @param string $content The content of a template file which contains pseudo-variables.
  4468. * @param array $params List of pseudo-variables that will be replaced in the template.
  4469. * @return string The content of the template with the pseudo-variables replated.
  4470. */
  4471. private static function replace_pseudovars( $content = '', $params = array() ){
  4472. if ( is_array( $params ) ){
  4473. foreach ( $params as $tpl_key => $tpl_value ){
  4474. $tpl_key = '%%SUCURI.' . $tpl_key . '%%';
  4475. $content = str_replace( $tpl_key, $tpl_value, $content );
  4476. }
  4477. return $content;
  4478. }
  4479. return false;
  4480. }
  4481. /**
  4482. * Gather and generate the information required globally by all the template files.
  4483. *
  4484. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4485. * @return array A complementary list of pseudo-variables for the template files.
  4486. */
  4487. private static function shared_params( $params = array() ){
  4488. $params = is_array( $params ) ? $params : array();
  4489. // Base parameters, required to render all the pages.
  4490. $params = self::links_and_navbar( $params );
  4491. // Global parameters, used through out all the pages.
  4492. $params['PageTitle'] = isset($params['PageTitle']) ? '('.$params['PageTitle'].')' : '';
  4493. $params['PageNonce'] = wp_create_nonce( 'sucuriscan_page_nonce' );
  4494. $params['PageStyleClass'] = isset($params['PageStyleClass']) ? $params['PageStyleClass'] : 'base';
  4495. $params['CleanDomain'] = self::get_domain();
  4496. $params['AdminEmail'] = self::get_site_email();
  4497. // Hide the advertisements from the layout.
  4498. $ads_visibility = SucuriScanOption::get_option( ':ads_visibility' );
  4499. if ( $ads_visibility == 'disabled' ) {
  4500. $params['LayoutType'] = 'onecolumn';
  4501. $params['AdsVisibility'] = 'hidden';
  4502. $params['ReviewNavbarButton'] = 'visible';
  4503. } else {
  4504. $params['LayoutType'] = 'twocolumns';
  4505. $params['AdsVisibility'] = 'visible';
  4506. $params['ReviewNavbarButton'] = 'hidden';
  4507. }
  4508. return $params;
  4509. }
  4510. /**
  4511. * Return a string indicating the visibility of a HTML component.
  4512. *
  4513. * @param boolean $visible Whether the condition executed returned a positive value or not.
  4514. * @return string A string indicating the visibility of a HTML component.
  4515. */
  4516. public static function visibility( $visible = false ){
  4517. return ( $visible === true ? 'visible' : 'hidden' );
  4518. }
  4519. /**
  4520. * Generate an URL pointing to the page indicated in the function and that must
  4521. * be loaded through the administrator panel.
  4522. *
  4523. * @param string $page Short name of the page that will be generated.
  4524. * @return string Full string containing the link of the page.
  4525. */
  4526. public static function get_url( $page = '' ){
  4527. $url_path = admin_url( 'admin.php?page=sucuriscan' );
  4528. if ( ! empty($page) ){
  4529. $url_path .= '_' . strtolower( $page );
  4530. }
  4531. return $url_path;
  4532. }
  4533. /**
  4534. * Complement the list of pseudo-variables that will be used in the base
  4535. * template files, this will also generate the navigation bar and detect which
  4536. * items in it are selected by the current page.
  4537. *
  4538. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4539. * @return array A complementary list of pseudo-variables for the template files.
  4540. */
  4541. private static function links_and_navbar( $params = array() ){
  4542. global $sucuriscan_pages;
  4543. $params = is_array( $params ) ? $params : array();
  4544. $sub_pages = is_array( $sucuriscan_pages ) ? $sucuriscan_pages : array();
  4545. $params['Navbar'] = '';
  4546. $params['CurrentPageFunc'] = '';
  4547. if ( $_page = self::get( 'page', '_page' ) ){
  4548. $params['CurrentPageFunc'] = $_page;
  4549. }
  4550. foreach ( $sub_pages as $sub_page_func => $sub_page_title ){
  4551. if (
  4552. $sub_page_func == 'sucuriscan_scanner'
  4553. && self::is_sitecheck_disabled()
  4554. ) {
  4555. continue;
  4556. }
  4557. $func_parts = explode( '_', $sub_page_func, 2 );
  4558. if ( isset($func_parts[1]) ){
  4559. $unique_name = $func_parts[1];
  4560. $pseudo_var = 'URL.' . ucwords( $unique_name );
  4561. } else {
  4562. $unique_name = '';
  4563. $pseudo_var = 'URL.Home';
  4564. }
  4565. $params[ $pseudo_var ] = self::get_url( $unique_name );
  4566. $navbar_item_css_class = 'nav-tab';
  4567. if ( $params['CurrentPageFunc'] == $sub_page_func ){
  4568. $navbar_item_css_class .= chr( 32 ) . 'nav-tab-active';
  4569. }
  4570. $params['Navbar'] .= sprintf(
  4571. '<a class="%s" href="%s">%s</a>' . "\n",
  4572. $navbar_item_css_class,
  4573. $params[ $pseudo_var ],
  4574. $sub_page_title
  4575. );
  4576. }
  4577. return $params;
  4578. }
  4579. /**
  4580. * Generate a HTML code using a template and replacing all the pseudo-variables
  4581. * by the dynamic variables provided by the developer through one of the parameters
  4582. * of the function.
  4583. *
  4584. * @param string $html The HTML content of a template file with its pseudo-variables parsed.
  4585. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4586. * @return string The formatted HTML content of the base template.
  4587. */
  4588. public static function get_base_template( $html = '', $params = array() ){
  4589. $params = is_array( $params ) ? $params : array();
  4590. $params = self::shared_params( $params );
  4591. $params['PageContent'] = $html;
  4592. return self::get_template( 'base', $params );
  4593. }
  4594. /**
  4595. * Generate a HTML code using a template and replacing all the pseudo-variables
  4596. * by the dynamic variables provided by the developer through one of the parameters
  4597. * of the function.
  4598. *
  4599. * @param string $template Filename of the template that will be used to generate the page.
  4600. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4601. * @param boolean $type Either page, section or snippet indicating the type of template that will be retrieved.
  4602. * @return string The formatted HTML page after replace all the pseudo-variables.
  4603. */
  4604. public static function get_template( $template = '', $params = array(), $type = 'page' ){
  4605. switch ( $type ){
  4606. case 'page': /* no_break */
  4607. case 'section':
  4608. $template_path_pattern = '%s/%s/inc/tpl/%s.html.tpl';
  4609. break;
  4610. case 'snippet':
  4611. $template_path_pattern = '%s/%s/inc/tpl/%s.snippet.tpl';
  4612. break;
  4613. }
  4614. $template_content = '';
  4615. $template_path = sprintf( $template_path_pattern, WP_PLUGIN_DIR, SUCURISCAN_PLUGIN_FOLDER, $template );
  4616. $params = is_array( $params ) ? $params : array();
  4617. if ( file_exists( $template_path ) && is_readable( $template_path ) ){
  4618. $template_content = @file_get_contents( $template_path );
  4619. $params['SucuriURL'] = SUCURISCAN_URL;
  4620. // Detect the current page URL.
  4621. if ( $_page = self::get( 'page', '_page' ) ){
  4622. $params['CurrentURL'] = admin_url( 'admin.php?page=' . $_page );
  4623. } else {
  4624. $params['CurrentURL'] = admin_url();
  4625. }
  4626. // Replace the global pseudo-variables in the section/snippets templates.
  4627. if (
  4628. $template == 'base'
  4629. && isset($params['PageContent'])
  4630. && preg_match( '/%%SUCURI\.(.+)%%/', $params['PageContent'] )
  4631. ){
  4632. $params['PageContent'] = self::replace_pseudovars( $params['PageContent'], $params );
  4633. }
  4634. $template_content = self::replace_pseudovars( $template_content, $params );
  4635. }
  4636. if ( $template == 'base' || $type != 'page' ){
  4637. return $template_content;
  4638. }
  4639. return self::get_base_template( $template_content, $params );
  4640. }
  4641. /**
  4642. * Generate a HTML code using a template and replacing all the pseudo-variables
  4643. * by the dynamic variables provided by the developer through one of the parameters
  4644. * of the function.
  4645. *
  4646. * @param string $template Filename of the template that will be used to generate the page.
  4647. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4648. * @return string The formatted HTML page after replace all the pseudo-variables.
  4649. */
  4650. public static function get_section( $template = '', $params = array() ){
  4651. $params = self::shared_params( $params );
  4652. return self::get_template( $template, $params, 'section' );
  4653. }
  4654. /**
  4655. * Generate a HTML code using a template and replacing all the pseudo-variables
  4656. * by the dynamic variables provided by the developer through one of the parameters
  4657. * of the function.
  4658. *
  4659. * @param string $template Filename of the template that will be used to generate the page.
  4660. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4661. * @return string The formatted HTML page after replace all the pseudo-variables.
  4662. */
  4663. public static function get_modal( $template = '', $params = array() ) {
  4664. $required = array(
  4665. 'Title' => 'Lorem ipsum dolor sit amet',
  4666. 'CssClass' => '',
  4667. 'Content' => '<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
  4668. eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
  4669. veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
  4670. consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
  4671. cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
  4672. proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>',
  4673. );
  4674. if ( ! empty($template) && $template != 'none' ){
  4675. $params['Content'] = self::get_section( $template );
  4676. }
  4677. foreach ( $required as $param_name => $param_value ){
  4678. if ( ! isset($params[ $param_name ]) ){
  4679. $params[ $param_name ] = $param_value;
  4680. }
  4681. }
  4682. $params = self::shared_params( $params );
  4683. return self::get_template( 'modalwindow', $params, 'section' );
  4684. }
  4685. /**
  4686. * Generate a HTML code using a template and replacing all the pseudo-variables
  4687. * by the dynamic variables provided by the developer through one of the parameters
  4688. * of the function.
  4689. *
  4690. * @param string $template Filename of the template that will be used to generate the page.
  4691. * @param array $params A hash containing the pseudo-variable name as the key and the value that will replace it.
  4692. * @return string The formatted HTML page after replace all the pseudo-variables.
  4693. */
  4694. public static function get_snippet( $template = '', $params = array() ) {
  4695. return self::get_template( $template, $params, 'snippet' );
  4696. }
  4697. /**
  4698. * Generate the HTML code necessary to render a list of options in a form.
  4699. *
  4700. * @param array $allowed_values List with keys and values allowed for the options.
  4701. * @param string $selected_val Value of the option that will be selected by default.
  4702. * @return string Option list for a select form field.
  4703. */
  4704. public static function get_select_options( $allowed_values = array(), $selected_val = '' ){
  4705. $options = '';
  4706. foreach ( $allowed_values as $option_name => $option_label ){
  4707. $selected_str = '';
  4708. if ( $option_name == $selected_val ){
  4709. $selected_str = 'selected="selected"';
  4710. }
  4711. $options .= sprintf(
  4712. '<option value="%s" %s>%s</option>',
  4713. $option_name, $selected_str, $option_label
  4714. );
  4715. }
  4716. return $options;
  4717. }
  4718. /**
  4719. * Detect which number in a pagination was clicked.
  4720. *
  4721. * @return integer Page number of the link clicked in a pagination.
  4722. */
  4723. public static function get_page_number(){
  4724. $paged = self::get( 'paged', '[0-9]{1,5}' );
  4725. return ( $paged ? intval( $paged ) : 1 );
  4726. }
  4727. /**
  4728. * Generate the HTML code to display a pagination.
  4729. *
  4730. * @param string $base_url Base URL for the links before the page number.
  4731. * @param integer $total_items Total quantity of items retrieved from a query.
  4732. * @param integer $max_per_page Maximum number of items that will be shown per page.
  4733. * @return string HTML code for a pagination generated using the provided data.
  4734. */
  4735. public static function get_pagination( $base_url = '', $total_items = 0, $max_per_page = 1 ){
  4736. // Calculate the number of links for the pagination.
  4737. $html_links = '';
  4738. $page_number = self::get_page_number();
  4739. $max_pages = ceil( $total_items / $max_per_page );
  4740. $extra_url = '';
  4741. // Fix for inline anchor URLs.
  4742. if ( preg_match( '/^(.+)(#.+)$/', $base_url, $match ) ) {
  4743. $base_url = $match[1];
  4744. $extra_url = $match[2];
  4745. }
  4746. // Generate the HTML links for the pagination.
  4747. for ( $j = 1; $j <= $max_pages; $j++ ){
  4748. $link_class = 'sucuriscan-pagination-link';
  4749. if ( $page_number == $j ){
  4750. $link_class .= chr( 32 ) . 'sucuriscan-pagination-active';
  4751. }
  4752. $html_links .= sprintf(
  4753. '<li><a href="%s&paged=%d%s" class="%s">%s</a></li>',
  4754. $base_url, $j, $extra_url, $link_class, $j
  4755. );
  4756. }
  4757. return $html_links;
  4758. }
  4759. /**
  4760. * Check whether the SiteCheck scanner and the malware scan page are disabled.
  4761. *
  4762. * @return boolean TRUE if the SiteCheck scanner and malware scan page are disabled.
  4763. */
  4764. public static function is_sitecheck_disabled(){
  4765. return (bool) ( SucuriScanOption::get_option( ':sitecheck_scanner' ) === 'disabled' );
  4766. }
  4767. /**
  4768. * Check whether the SiteCheck scanner and the malware scan page are enabled.
  4769. *
  4770. * @return boolean TRUE if the SiteCheck scanner and malware scan page are enabled.
  4771. */
  4772. public static function is_sitecheck_enabled(){
  4773. return (bool) ( SucuriScanOption::get_option( ':sitecheck_scanner' ) !== 'disabled' );
  4774. }
  4775. }
  4776. /**
  4777. * File System Scanner
  4778. *
  4779. * The File System Scanner component performs full and incremental scans over a
  4780. * file system folder, maintaining a snapshot of the filesystem and comparing it
  4781. * with the current content to establish what content has been updated. Updated
  4782. * content is then submitted to the remote server and it is stored for future
  4783. * analysis.
  4784. */
  4785. class SucuriScanFSScanner extends SucuriScan {
  4786. /**
  4787. * Retrieve the last time when the filesystem scan was ran.
  4788. *
  4789. * @param boolean $format Whether the timestamp must be formatted as date/time or not.
  4790. * @return string The timestamp of the runtime, or an string with the date/time.
  4791. */
  4792. public static function get_filesystem_runtime( $format = false ){
  4793. $runtime = SucuriScanOption::get_option( ':runtime' );
  4794. if ( $runtime > 0 ){
  4795. if ( $format ){
  4796. return SucuriScan::datetime( $runtime );
  4797. }
  4798. return $runtime;
  4799. }
  4800. if ( $format ){
  4801. return '<em>Unknown</em>';
  4802. }
  4803. return false;
  4804. }
  4805. /**
  4806. * Check whether the administrator enabled the feature to ignore some
  4807. * directories during the file system scans. This function is overwritten by a
  4808. * GET parameter in the settings page named no_scan which must be equal to the
  4809. * number one.
  4810. *
  4811. * @return boolean Whether the feature to ignore files is enabled or not.
  4812. */
  4813. public static function will_ignore_scanning(){
  4814. return ( SucuriScanOption::get_option( ':ignore_scanning' ) === 'enabled' );
  4815. }
  4816. /**
  4817. * Add a new directory path to the list of ignored paths.
  4818. *
  4819. * @param string $directory_path The (full) absolute path of a directory.
  4820. * @return boolean TRUE if the directory path was added to the list, FALSE otherwise.
  4821. */
  4822. public static function ignore_directory( $directory_path = '' ){
  4823. $cache = new SucuriScanCache( 'ignorescanning' );
  4824. // Use the checksum of the directory path as the cache key.
  4825. $cache_key = md5( $directory_path );
  4826. $cache_value = array(
  4827. 'directory_path' => $directory_path,
  4828. 'ignored_at' => self::local_time(),
  4829. );
  4830. $cached = $cache->add( $cache_key, $cache_value );
  4831. return $cached;
  4832. }
  4833. /**
  4834. * Remove a directory path from the list of ignored paths.
  4835. *
  4836. * @param string $directory_path The (full) absolute path of a directory.
  4837. * @return boolean TRUE if the directory path was removed to the list, FALSE otherwise.
  4838. */
  4839. public static function unignore_directory( $directory_path = '' ){
  4840. $cache = new SucuriScanCache( 'ignorescanning' );
  4841. // Use the checksum of the directory path as the cache key.
  4842. $cache_key = md5( $directory_path );
  4843. $removed = $cache->delete( $cache_key );
  4844. return $removed;
  4845. }
  4846. /**
  4847. * Retrieve a list of directories ignored.
  4848. *
  4849. * Retrieve a list of directory paths that will be ignored during the file
  4850. * system scans, any sub-directory and files inside these folders will be
  4851. * skipped automatically and will not be used to detect malware or modifications
  4852. * in the site.
  4853. *
  4854. * The structure of the array returned by the function will always be composed
  4855. * by four (4) indexes which will facilitate the execution of common conditions
  4856. * in the implementation code.
  4857. *
  4858. * <ul>
  4859. * <li>raw: Will contains the raw data retrieved from the built-in cache system.</li>
  4860. * <li>checksums: Will contains the md5 of all the directory paths.</li>
  4861. * <li>directories: Will contains a list of directory paths.</li>
  4862. * <li>ignored_at_list: Will contains a list of timestamps for when the directories were ignored.</li>
  4863. * </ul>
  4864. *
  4865. * @return array List of ignored directory paths.
  4866. */
  4867. public static function get_ignored_directories(){
  4868. $response = array(
  4869. 'raw' => array(),
  4870. 'checksums' => array(),
  4871. 'directories' => array(),
  4872. 'ignored_at_list' => array(),
  4873. );
  4874. $cache = new SucuriScanCache( 'ignorescanning' );
  4875. $cache_lifetime = 0; // It is not necessary to expire this cache.
  4876. $ignored_directories = $cache->get_all( $cache_lifetime, 'array' );
  4877. if ( $ignored_directories ){
  4878. $response['raw'] = $ignored_directories;
  4879. foreach ( $ignored_directories as $checksum => $data ){
  4880. $response['checksums'][] = $checksum;
  4881. $response['directories'][] = $data['directory_path'];
  4882. $response['ignored_at_list'][] = $data['ignored_at'];
  4883. }
  4884. }
  4885. return $response;
  4886. }
  4887. /**
  4888. * Run file system scan and retrieve ignored folders.
  4889. *
  4890. * Run a file system scan and retrieve an array with two indexes, the first
  4891. * containing a list of ignored directory paths and their respective timestamps
  4892. * of when they were added by an administrator user, and the second containing a
  4893. * list of directories that are not being ignored.
  4894. *
  4895. * @return array List of ignored and not ignored directories.
  4896. */
  4897. public static function get_ignored_directories_live(){
  4898. $response = array(
  4899. 'is_ignored' => array(),
  4900. 'is_not_ignored' => array(),
  4901. );
  4902. // Get the ignored directories from the cache.
  4903. $ignored_directories = self::get_ignored_directories();
  4904. if ( $ignored_directories ){
  4905. $response['is_ignored'] = $ignored_directories['raw'];
  4906. }
  4907. // Scan the project and file all directories.
  4908. $sucuri_fileinfo = new SucuriScanFileInfo();
  4909. $sucuri_fileinfo->ignore_files = true;
  4910. $sucuri_fileinfo->ignore_directories = true;
  4911. $sucuri_fileinfo->scan_interface = SucuriScanOption::get_option( ':scan_interface' );
  4912. $directory_list = $sucuri_fileinfo->get_diretories_only( ABSPATH );
  4913. if ( $directory_list ){
  4914. $response['is_not_ignored'] = $directory_list;
  4915. }
  4916. return $response;
  4917. }
  4918. /**
  4919. * Read and parse the lines inside a PHP error log file.
  4920. *
  4921. * @param array $error_logs The content of an error log file, or an array with the lines.
  4922. * @return array List of valid error logs with their attributes separated.
  4923. */
  4924. public static function parse_error_logs( $error_logs = array() ){
  4925. $logs_arr = array();
  4926. $pattern = '/^'
  4927. . '(\[(\S+) ([0-9:]{5,8})( \S+)?\] )?' // Detect date, time, and timezone.
  4928. . '(PHP )?([a-zA-Z ]+):\s' // Detect PHP error severity.
  4929. . '(.+) in (.+)' // Detect error message, and file path.
  4930. . '(:| on line )([0-9]+)' // Detect line number.
  4931. . '$/';
  4932. if ( is_string( $error_logs ) ) {
  4933. $error_logs = explode( "\n", $error_logs );
  4934. }
  4935. foreach ( (array) $error_logs as $line ) {
  4936. if ( ! is_string( $line ) || empty($line) ) { continue; }
  4937. if ( preg_match( $pattern, $line, $match ) ) {
  4938. $data_set = array(
  4939. 'date' => '',
  4940. 'time' => '',
  4941. 'timestamp' => 0,
  4942. 'date_time' => '',
  4943. 'time_zone' => '',
  4944. 'error_type' => '',
  4945. 'error_code' => 'unknown',
  4946. 'error_message' => '',
  4947. 'file_path' => '',
  4948. 'line_number' => 0,
  4949. );
  4950. // Basic attributes from the scrapping.
  4951. $data_set['date'] = $match[2];
  4952. $data_set['time'] = $match[3];
  4953. $data_set['time_zone'] = trim( $match[4] );
  4954. $data_set['error_type'] = trim( $match[6] );
  4955. $data_set['error_message'] = trim( $match[7] );
  4956. $data_set['file_path'] = trim( $match[8] );
  4957. $data_set['line_number'] = (int) $match[10];
  4958. // Additional data from the attributes.
  4959. if ( $data_set['date'] ) {
  4960. $data_set['date_time'] = $data_set['date']
  4961. . "\x20" . $data_set['time']
  4962. . "\x20" . $data_set['time_zone'];
  4963. $data_set['timestamp'] = strtotime( $data_set['date_time'] );
  4964. }
  4965. if ( $data_set['error_type'] ) {
  4966. $valid_types = array( 'warning', 'notice', 'error' );
  4967. foreach ( $valid_types as $valid_type ) {
  4968. if ( stripos( $data_set['error_type'], $valid_type ) !== false ) {
  4969. $data_set['error_code'] = $valid_type;
  4970. break;
  4971. }
  4972. }
  4973. }
  4974. $logs_arr[] = (object) $data_set;
  4975. }
  4976. }
  4977. return $logs_arr;
  4978. }
  4979. }
  4980. /**
  4981. * Heartbeat library.
  4982. *
  4983. * The purpose of the Heartbeat API is to simulate bidirectional connection
  4984. * between the browser and the server. Initially it was used for autosave, post
  4985. * locking and log-in expiration warning while a user is writing or editing. The
  4986. * idea was to have an API that sends XHR (XML HTTP Request) requests to the
  4987. * server every fifteen seconds and triggers events (or callbacks) on receiving
  4988. * data.
  4989. *
  4990. * @see https://core.trac.wordpress.org/ticket/23216
  4991. */
  4992. class SucuriScanHeartbeat extends SucuriScanOption {
  4993. /**
  4994. * Stop execution of the heartbeat API in certain parts of the site.
  4995. *
  4996. * @return void
  4997. */
  4998. public static function register_script(){
  4999. global $pagenow;
  5000. $status = SucuriScanOption::get_option( ':heartbeat' );
  5001. // Enable heartbeat everywhere.
  5002. if ( $status == 'enabled' ){ /* do_nothing */ }
  5003. // Disable heartbeat everywhere.
  5004. elseif ( $status == 'disabled' ){
  5005. wp_deregister_script( 'heartbeat' );
  5006. }
  5007. // Disable heartbeat only on the dashboard and home pages.
  5008. elseif (
  5009. $status == 'dashboard'
  5010. && $pagenow == 'index.php'
  5011. ){
  5012. wp_deregister_script( 'heartbeat' );
  5013. }
  5014. // Disable heartbeat everywhere except in post edition.
  5015. elseif (
  5016. $status == 'addpost'
  5017. && $pagenow != 'post.php'
  5018. && $pagenow != 'post-new.php'
  5019. ){
  5020. wp_deregister_script( 'heartbeat' );
  5021. }
  5022. }
  5023. /**
  5024. * Update the settings of the Heartbeat API according to the values set by an
  5025. * administrator. This tool may cause an increase in the CPU usage, a bad
  5026. * configuration may cause low account to run out of resources, but in better
  5027. * cases it may improve the performance of the site by reducing the quantity of
  5028. * requests sent to the server per session.
  5029. *
  5030. * @param array $settings Heartbeat settings.
  5031. * @return array Updated version of the heartbeat settings.
  5032. */
  5033. public static function update_settings( $settings = array() ){
  5034. $pulse = SucuriScanOption::get_option( ':heartbeat_pulse' );
  5035. $autostart = SucuriScanOption::get_option( ':heartbeat_autostart' );
  5036. if ( $pulse < 15 || $pulse > 60 ){
  5037. SucuriScanOption::delete_option( ':heartbeat_pulse' );
  5038. $pulse = 15;
  5039. }
  5040. $settings['interval'] = $pulse;
  5041. $settings['autostart'] = ( $autostart == 'disabled' ? false : true );
  5042. return $settings;
  5043. }
  5044. /**
  5045. * Respond to the browser according to the data received.
  5046. *
  5047. * @param array $response Response received.
  5048. * @param array $data Data received from the beat.
  5049. * @param string $screen_id Identifier of the screen the heartbeat occurred on.
  5050. * @return array Response with new data.
  5051. */
  5052. public static function respond_to_received( $response = array(), $data = array(), $screen_id = '' ) {
  5053. $interval = SucuriScanOption::get_option( ':heartbeat_interval' );
  5054. if (
  5055. $interval == 'slow'
  5056. || $interval == 'fast'
  5057. || $interval == 'standard'
  5058. ){
  5059. $response['heartbeat_interval'] = $interval;
  5060. } else {
  5061. SucuriScanOption::delete_option( ':heartbeat_interval' );
  5062. }
  5063. return $response;
  5064. }
  5065. /**
  5066. * Respond to the browser according to the data sent.
  5067. *
  5068. * @param array $response Response sent.
  5069. * @param string $screen_id Identifier of the screen the heartbeat occurred on.
  5070. * @return array Response with new data.
  5071. */
  5072. public static function respond_to_send( $response = array(), $screen_id = '' ) {
  5073. return $response;
  5074. }
  5075. /**
  5076. * Allowed values for the heartbeat status.
  5077. *
  5078. * @return array Allowed values for the heartbeat status.
  5079. */
  5080. public static function statuses_allowed(){
  5081. return array(
  5082. 'enabled' => 'Enable everywhere',
  5083. 'disabled' => 'Disable everywhere',
  5084. 'dashboard' => 'Disable on dashboard page',
  5085. 'addpost' => 'Everywhere except post addition',
  5086. );
  5087. }
  5088. /**
  5089. * Allowed values for the heartbeat intervals.
  5090. *
  5091. * @return array Allowed values for the heartbeat intervals.
  5092. */
  5093. public static function intervals_allowed(){
  5094. return array(
  5095. 'slow' => 'Slow interval',
  5096. 'fast' => 'Fast interval',
  5097. 'standard' => 'Standard interval',
  5098. );
  5099. }
  5100. /**
  5101. * Allowed values for the heartbeat pulses.
  5102. *
  5103. * @return array Allowed values for the heartbeat pulses.
  5104. */
  5105. public static function pulses_allowed(){
  5106. $pulses = array();
  5107. for ( $i = 15; $i <= 60; $i++ ){
  5108. $pulses[ $i ] = sprintf( 'Run every %d seconds', $i );
  5109. }
  5110. return $pulses;
  5111. }
  5112. }
  5113. /**
  5114. * Plugin initializer.
  5115. *
  5116. * Define all the required variables, script, styles, and basic functions needed
  5117. * when the site is loaded, not even the administrator panel but also the front
  5118. * page, some bug-fixes will/are applied here for sites behind a proxy, and
  5119. * sites with old versions of the premium plugin (that was deprecated at
  5120. * July/2014).
  5121. */
  5122. class SucuriScanInterface {
  5123. /**
  5124. * Initialization code for the plugin.
  5125. *
  5126. * The initial variables and information needed by the plugin during the
  5127. * execution of other functions will be generated. Things like the real IP
  5128. * address of the client when it has been forwarded or it's behind an external
  5129. * service like a Proxy.
  5130. *
  5131. * @return void
  5132. */
  5133. public static function initialize(){
  5134. if ( SucuriScan::is_behind_cloudproxy() ) {
  5135. $_SERVER['SUCURIREAL_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
  5136. $_SERVER['REMOTE_ADDR'] = SucuriScan::get_remote_addr();
  5137. }
  5138. }
  5139. /**
  5140. * Define which javascript and css files will be loaded in the header of the
  5141. * plugin pages, only when the administrator panel is accessed.
  5142. *
  5143. * @return void
  5144. */
  5145. public static function enqueue_scripts(){
  5146. $asset_version = '';
  5147. if ( strlen( SUCURISCAN_PLUGIN_CHECKSUM ) >= 7 ){
  5148. $asset_version = substr( SUCURISCAN_PLUGIN_CHECKSUM, 0, 7 );
  5149. }
  5150. wp_register_style( 'sucuriscan', SUCURISCAN_URL . '/inc/css/sucuriscan-default-css.css', array(), $asset_version );
  5151. wp_register_script( 'sucuriscan', SUCURISCAN_URL . '/inc/js/sucuriscan-scripts.js', array(), $asset_version );
  5152. wp_enqueue_style( 'sucuriscan' );
  5153. wp_enqueue_script( 'sucuriscan' );
  5154. if ( SucuriScanRequest::get( 'page', 'sucuriscan' ) !== false ) {
  5155. wp_register_script( 'sucuriscan2', SUCURISCAN_URL . '/inc/js/d3.v3.min.js', array(), $asset_version );
  5156. wp_register_script( 'sucuriscan3', SUCURISCAN_URL . '/inc/js/c3.min.js', array(), $asset_version );
  5157. wp_enqueue_script( 'sucuriscan2' );
  5158. wp_enqueue_script( 'sucuriscan3' );
  5159. }
  5160. }
  5161. /**
  5162. * Generate the menu and submenus for the plugin in the admin interface.
  5163. *
  5164. * @return void
  5165. */
  5166. public static function add_interface_menu(){
  5167. global $sucuriscan_pages;
  5168. if (
  5169. function_exists( 'add_menu_page' )
  5170. && $sucuriscan_pages
  5171. ){
  5172. // Add main menu link.
  5173. add_menu_page(
  5174. 'Sucuri Security',
  5175. 'Sucuri Security',
  5176. 'manage_options',
  5177. 'sucuriscan',
  5178. 'sucuriscan_page',
  5179. SUCURISCAN_URL . '/inc/images/menu-icon.png'
  5180. );
  5181. $sub_pages = is_array( $sucuriscan_pages ) ? $sucuriscan_pages : array();
  5182. foreach ( $sub_pages as $sub_page_func => $sub_page_title ){
  5183. if (
  5184. $sub_page_func == 'sucuriscan_scanner'
  5185. && SucuriScanTemplate::is_sitecheck_disabled()
  5186. ) {
  5187. continue;
  5188. }
  5189. $page_func = $sub_page_func . '_page';
  5190. add_submenu_page(
  5191. 'sucuriscan',
  5192. $sub_page_title,
  5193. $sub_page_title,
  5194. 'manage_options',
  5195. $sub_page_func,
  5196. $page_func
  5197. );
  5198. }
  5199. }
  5200. }
  5201. /**
  5202. * Remove the old Sucuri plugins considering that with the new version (after
  5203. * 1.6.0) all the functionality of the others will be merged here, this will
  5204. * remove duplicated functionality, duplicated bugs and/or duplicated
  5205. * maintenance reports allowing us to focus in one unique project.
  5206. *
  5207. * @return void
  5208. */
  5209. public static function handle_old_plugins(){
  5210. if ( class_exists( 'SucuriScanFileInfo' ) ){
  5211. $sucuri_fileinfo = new SucuriScanFileInfo();
  5212. $sucuri_fileinfo->ignore_files = false;
  5213. $sucuri_fileinfo->ignore_directories = false;
  5214. $plugins = array(
  5215. 'sucuri-wp-plugin/sucuri.php',
  5216. 'sucuri-cloudproxy-waf/cloudproxy.php',
  5217. );
  5218. foreach ( $plugins as $plugin ){
  5219. $plugin_directory = dirname( WP_PLUGIN_DIR . '/' . $plugin );
  5220. if ( file_exists( $plugin_directory ) ){
  5221. if ( is_plugin_active( $plugin ) ){
  5222. deactivate_plugins( $plugin );
  5223. }
  5224. $plugin_removed = $sucuri_fileinfo->remove_directory_tree( $plugin_directory );
  5225. }
  5226. }
  5227. }
  5228. }
  5229. /**
  5230. * Create a folder in the WordPress upload directory where the plugin will
  5231. * store all the temporal or dynamic information.
  5232. *
  5233. * @return void
  5234. */
  5235. public static function create_datastore_folder(){
  5236. $plugin_upload_folder = SucuriScan::datastore_folder_path();
  5237. if ( ! file_exists( $plugin_upload_folder ) ) {
  5238. $datastore_folder_created = @mkdir( $plugin_upload_folder, 0755, true );
  5239. if ( $datastore_folder_created ) {
  5240. // Create last-logins datastore file.
  5241. sucuriscan_lastlogins_datastore_exists();
  5242. // Create a htaccess file to deny access from all.
  5243. @file_put_contents(
  5244. $plugin_upload_folder . '/.htaccess',
  5245. "Order Deny,Allow\nDeny from all\n",
  5246. LOCK_EX
  5247. );
  5248. // Create an index.html to avoid directory listing.
  5249. @file_put_contents(
  5250. $plugin_upload_folder . '/index.html',
  5251. '<!-- Prevent the directory listing. -->',
  5252. LOCK_EX
  5253. );
  5254. } else {
  5255. SucuriScanOption::delete_option( ':datastore_path' );
  5256. SucuriScanInterface::error(
  5257. 'Data folder does not exists and could not be created. Try to <a href="' .
  5258. SucuriScanTemplate::get_url( 'settings' ) . '">click this link</a> to see
  5259. if the plugin is able to fix this error automatically, if this message
  5260. reappears you will need to either change the location of the directory from
  5261. the plugin general settings page or create this directory manually and give it
  5262. write permissions:<code>' . $plugin_upload_folder . '</code>.'
  5263. );
  5264. }
  5265. }
  5266. }
  5267. /**
  5268. * Check whether a user has the permissions to see a page from the plugin.
  5269. *
  5270. * @return void
  5271. */
  5272. public static function check_permissions(){
  5273. if (
  5274. ! function_exists( 'current_user_can' )
  5275. || ! current_user_can( 'manage_options' )
  5276. ){
  5277. $page = SucuriScanRequest::get( 'page', '_page' );
  5278. wp_die( __( 'Access denied by <b>Sucuri</b> to see <code>' . $page . '</code>' ) );
  5279. }
  5280. }
  5281. /**
  5282. * Verify the nonce of the previous page after a form submission. If the
  5283. * validation fails the execution of the script will be stopped and a dead page
  5284. * will be printed to the client using the official WordPress method.
  5285. *
  5286. * @return boolean Either TRUE or FALSE if the nonce is valid or not respectively.
  5287. */
  5288. public static function check_nonce(){
  5289. if ( ! empty($_POST) ){
  5290. $nonce_name = 'sucuriscan_page_nonce';
  5291. $nonce_value = SucuriScanRequest::post( $nonce_name, '_nonce' );
  5292. if ( ! $nonce_value || ! wp_verify_nonce( $nonce_value, $nonce_name ) ){
  5293. wp_die( __( 'WordPress Nonce verification failed, try again going back and checking the form.' ) );
  5294. return false;
  5295. }
  5296. }
  5297. return true;
  5298. }
  5299. /**
  5300. * Prints a HTML alert in the WordPress admin interface.
  5301. *
  5302. * @param string $type The type of alert, it can be either Updated or Error.
  5303. * @param string $message The message that will be printed in the alert.
  5304. * @return void
  5305. */
  5306. private static function admin_notice( $type = 'updated', $message = '' ){
  5307. $alert_id = rand( 100, 999 );
  5308. if ( ! empty($message) ): ?>
  5309. <div id="sucuriscan-alert-<?php echo $alert_id; ?>" class="<?php echo $type; ?> sucuriscan-alert sucuriscan-alert-<?php echo $type; ?>">
  5310. <a href="javascript:void(0)" class="close" onclick="sucuriscan_alert_close('<?php echo $alert_id; ?>')">&times;</a>
  5311. <p><?php _e( $message ); ?></p>
  5312. </div>
  5313. <?php endif;
  5314. }
  5315. /**
  5316. * Prints a HTML alert of type ERROR in the WordPress admin interface.
  5317. *
  5318. * @param string $error_msg The message that will be printed in the alert.
  5319. * @return void
  5320. */
  5321. public static function error( $error_msg = '' ){
  5322. self::admin_notice( 'error', '<b>Sucuri:</b> ' . $error_msg );
  5323. }
  5324. /**
  5325. * Prints a HTML alert of type INFO in the WordPress admin interface.
  5326. *
  5327. * @param string $info_msg The message that will be printed in the alert.
  5328. * @return void
  5329. */
  5330. public static function info( $info_msg = '' ){
  5331. self::admin_notice( 'updated', '<b>Sucuri:</b> ' . $info_msg );
  5332. }
  5333. /**
  5334. * Display a notice message with instructions to continue the setup of the
  5335. * plugin, this includes the generation of the API key and other steps that need
  5336. * to be done to fully activate this plugin.
  5337. *
  5338. * @return void
  5339. */
  5340. public static function setup_notice(){
  5341. if (
  5342. current_user_can( 'manage_options' )
  5343. && SucuriScan::no_notices_here() === false
  5344. && ! SucuriScanAPI::get_plugin_key()
  5345. && SucuriScanRequest::post( ':plugin_api_key' ) === false
  5346. && SucuriScanRequest::post( ':recover_key' ) === false
  5347. && ! SucuriScanRequest::post( ':manual_api_key' )
  5348. ) {
  5349. echo SucuriScanTemplate::get_section( 'setup-notice' );
  5350. }
  5351. }
  5352. }
  5353. /**
  5354. * Display the page with a temporary message explaining the action that will be
  5355. * performed once the hidden form is submitted to retrieve the scanning results
  5356. * from the public SiteCheck API.
  5357. *
  5358. * @return void
  5359. */
  5360. function sucuriscan_scanner_page(){
  5361. SucuriScanInterface::check_permissions();
  5362. // Check if the information is already cached.
  5363. $cache = new SucuriScanCache( 'sitecheck' );
  5364. $scan_results = $cache->get( 'scan_results', SUCURISCAN_SITECHECK_LIFETIME, 'array' );
  5365. if (
  5366. (
  5367. $scan_results
  5368. && ! empty( $scan_results )
  5369. ) || (
  5370. SucuriScanInterface::check_nonce()
  5371. && SucuriScanRequest::post( ':malware_scan', '1' )
  5372. )
  5373. ){
  5374. sucuriscan_sitecheck_info( $scan_results );
  5375. } else {
  5376. echo SucuriScanTemplate::get_template( 'malwarescan', array(
  5377. 'PageTitle' => 'Malware Scan',
  5378. 'PageStyleClass' => 'scanner-loading',
  5379. ) );
  5380. }
  5381. }
  5382. /**
  5383. * Display the result of site scan made through SiteCheck.
  5384. *
  5385. * @param array $res Array with information of the scanning.
  5386. * @return void
  5387. */
  5388. function sucuriscan_sitecheck_info( $res = array() ){
  5389. // Will be TRUE only if the scanning results were retrieved from the cache.
  5390. $display_results = (bool) $res;
  5391. $clean_domain = SucuriScan::get_domain();
  5392. // If the results are not cached, then request a new scanning.
  5393. if ( $res === false ){
  5394. $res = SucuriScanAPI::get_sitecheck_results( $clean_domain );
  5395. // Check for error messages in the request's response.
  5396. if ( is_string( $res ) ){
  5397. if ( preg_match( '/^ERROR:(.*)/', $res, $error_m ) ){
  5398. SucuriScanInterface::error( 'The site <code>' . $clean_domain . '</code> was not scanned: ' . $error_m[1] );
  5399. } else {
  5400. SucuriScanInterface::error( 'SiteCheck error: ' . $res );
  5401. }
  5402. }
  5403. else {
  5404. $cache = new SucuriScanCache( 'sitecheck' );
  5405. $display_results = true;
  5406. // Cache the scanning results to reduce memory lose.
  5407. if ( ! $cache->add( 'scan_results', $res ) ){
  5408. SucuriScanInterface::error( 'Could not cache the results of the SiteCheck scanning.' );
  5409. }
  5410. }
  5411. }
  5412. // Count the number of scans.
  5413. if ( $display_results === true ) {
  5414. $sitecheck_counter = (int) SucuriScanOption::get_option( ':sitecheck_counter' );
  5415. SucuriScanOption::update_option( ':sitecheck_counter', $sitecheck_counter + 1 );
  5416. }
  5417. ob_start();
  5418. ?>
  5419. <?php if ( $display_results ): ?>
  5420. <?php
  5421. // Check for general warnings, and return the information for Infected/Clean site.
  5422. $malware_warns_exist = isset($res['MALWARE']['WARN']) ? true : false;
  5423. $blacklist_warns_exist = isset($res['BLACKLIST']['WARN']) ? true : false;
  5424. $outdated_warns_exist = isset($res['OUTDATEDSCAN']) ? true : false;
  5425. $recommendations_exist = isset($res['RECOMMENDATIONS']) ? true : false;
  5426. // Check whether this WordPress installation needs an update.
  5427. global $wp_version;
  5428. $wordpress_updated = false;
  5429. $updates = function_exists( 'get_core_updates' ) ? get_core_updates() : array();
  5430. if (
  5431. ! is_array( $updates )
  5432. || empty($updates)
  5433. || $updates[0]->response == 'latest'
  5434. ) {
  5435. $wordpress_updated = true;
  5436. }
  5437. if ( is_array( $res ) ) {
  5438. // Include the Thickbox library.
  5439. add_thickbox();
  5440. // Initialize the CSS classes with default values.
  5441. $sucuriscan_css_malware = 'sucuriscan-border-good';
  5442. $sitecheck_results_tab = '';
  5443. $blacklist_status_tab = '';
  5444. $website_details_tab = '';
  5445. // Generate the CSS classes for the blacklist status.
  5446. if ( $blacklist_warns_exist ){
  5447. $blacklist_status_tab = 'sucuriscan-red-tab';
  5448. }
  5449. // Generate the CSS classes for the SiteCheck scanning results.
  5450. if ( $malware_warns_exist ){
  5451. $sucuriscan_css_malware = 'sucuriscan-border-bad';
  5452. $sitecheck_results_tab = 'sucuriscan-red-tab';
  5453. }
  5454. // Generate the CSS classes for the outdated/recommendations panel.
  5455. if ( $outdated_warns_exist || $recommendations_exist ){
  5456. $website_details_tab = 'sucuriscan-red-tab';
  5457. }
  5458. $sucuriscan_css_wpupdate = $wordpress_updated ? 'sucuriscan-border-good' : 'sucuriscan-border-bad';
  5459. }
  5460. ?>
  5461. <div class="sucuriscan-tabs">
  5462. <ul>
  5463. <li class="<?php _e( $sitecheck_results_tab ) ?>">
  5464. <a href="#" data-tabname="sitecheck-results">Remote Scanner Results</a>
  5465. </li>
  5466. <li class="<?php _e( $website_details_tab ) ?>">
  5467. <a href="#" data-tabname="website-details">Website Details</a>
  5468. </li>
  5469. <li>
  5470. <a href="#" data-tabname="website-links">IFrames / Links / Scripts</a>
  5471. </li>
  5472. <li class="<?php _e( $blacklist_status_tab ) ?>">
  5473. <a href="#" data-tabname="blacklist-status">Blacklist Status</a>
  5474. </li>
  5475. <li>
  5476. <a href="#" data-tabname="modified-files">Modified Files</a>
  5477. </li>
  5478. </ul>
  5479. <div class="sucuriscan-tab-containers">
  5480. <div id="sucuriscan-sitecheck-results">
  5481. <table class="wp-list-table widefat sucuriscan-table sucuriscan-scanner-details">
  5482. <thead>
  5483. <tr>
  5484. <th colspan="3" class="thead-with-button">
  5485. <?php if ( $malware_warns_exist ): ?>
  5486. <span>Site compromised (malware was identified)</span>
  5487. <a href="http://sucuri.net/website-antivirus/" target="_blank"
  5488. class="thead-topright-action button-primary">Clean website</a>
  5489. <?php else: ?>
  5490. <span>Site clean (no malware was identified)</span>
  5491. <?php endif; ?>
  5492. </th>
  5493. </tr>
  5494. </thead>
  5495. <tbody>
  5496. <?php if ( $malware_warns_exist ): ?>
  5497. <?php foreach ( $res['MALWARE']['WARN'] as $key => $malres ): ?>
  5498. <?php $malres = SucuriScanAPI::get_sitecheck_malware( $malres ); ?>
  5499. <tr>
  5500. <?php if ( $malres !== false ): ?>
  5501. <td>
  5502. <a href="<?php _e( $malres['malware_docs'] ); ?>" target="_blank">
  5503. <?php _e( $malres['alert_message'] ); ?>
  5504. </a>
  5505. </td>
  5506. <td>
  5507. <span class="sucuriscan-monospace"><?php _e( $malres['malware_type'] ); ?></span>
  5508. </td>
  5509. <td>
  5510. <div class="sucuriscan-malware-link">
  5511. <a href="<?php _e( $malres['infected_url'] ); ?>" target="_blank"
  5512. class="sucuriscan-label sucuriscan-label-warning">View infected URL</a>
  5513. <a href="#TB_inline?width=600&height=300&inlineId=sucuriscan-malware-<?php _e( $key ); ?>"
  5514. title="SiteCheck: Malware Payload" class="thickbox sucuriscan-label sucuriscan-label-danger">
  5515. View malware</a>
  5516. </div>
  5517. <div id="sucuriscan-malware-<?php _e( $key ); ?>" style="display:none">
  5518. <div class="sucuriscan-malware-payload"><?php _e( $malres['malware_payload'] ); ?></div>
  5519. </div>
  5520. </td>
  5521. <?php endif; ?>
  5522. </tr>
  5523. <?php endforeach; ?>
  5524. <?php else: ?>
  5525. <tr>
  5526. <td><span class="sucuriscan-label sucuriscan-label-success">CLEAN</span></td>
  5527. <td colspan="3">Malware</td>
  5528. </tr>
  5529. <tr>
  5530. <td><span class="sucuriscan-label sucuriscan-label-success">CLEAN</span></td>
  5531. <td width="220">
  5532. <a href="http://kb.sucuri.net/malware/encoded-javascript" target="_blank">
  5533. Malicious javascript
  5534. </a>
  5535. </td>
  5536. <td>
  5537. <div>
  5538. JavaScript is a language (code) that can be executed directly by the browser and
  5539. many other applications that support it (PDF, email readers, etc). Because it is
  5540. a full programming language executed by the browser, attackers use it heavily to
  5541. run malicious code from the compromised sites.
  5542. </div>
  5543. </td>
  5544. </tr>
  5545. <tr>
  5546. <td><span class="sucuriscan-label sucuriscan-label-success">CLEAN</span></td>
  5547. <td width="220">
  5548. <a href="http://kb.sucuri.net/malware/malicious-iframes" target="_blank">
  5549. Malicious iframes
  5550. </a>
  5551. </td>
  5552. <td>
  5553. <div>
  5554. An inline frame (iframe) is used to embed another document within the current
  5555. HTML document. Because as the definition implies, it allows you to insert
  5556. another document inside the current HTML page. And the attackers use that
  5557. feature to insert malicious content into the compromised sites (to redirect to
  5558. spam, exploit kits, Fake AV, phishing, etc).
  5559. </div>
  5560. </td>
  5561. </tr>
  5562. <tr>
  5563. <td><span class="sucuriscan-label sucuriscan-label-success">CLEAN</span></td>
  5564. <td width="220">
  5565. <a href="http://kb.sucuri.net/malware/conditional-redirections" target="_blank">
  5566. Suspicious redirections (htaccess)
  5567. </a>
  5568. </td>
  5569. <td>
  5570. <div>
  5571. Conditional redirections are classified differently than the iframe/javascript
  5572. ones, because they are generally done though the HTTP headers (via .htaccess) to
  5573. redirect users from certain browsers or locations to malware/malicious
  5574. locations.
  5575. </div>
  5576. </td>
  5577. </tr>
  5578. <tr>
  5579. <td><span class="sucuriscan-label sucuriscan-label-success">CLEAN</span></td>
  5580. <td colspan="3">Blackhat SEO Spam</td>
  5581. </tr>
  5582. <tr>
  5583. <td><span class="sucuriscan-label sucuriscan-label-success">CLEAN</span></td>
  5584. <td colspan="3">Anomaly detection</td>
  5585. </tr>
  5586. <?php endif; ?>
  5587. <tr>
  5588. <td colspan="3">
  5589. <hr/>
  5590. <em>
  5591. More details at <a href="http://sitecheck.sucuri.net/results/<?php _e( $clean_domain ); ?>"
  5592. target="_blank">SiteCheck/<?php _e( $clean_domain ); ?></a>. If our free scanner
  5593. did not detect any issue, you may have a more complicated and hidden problem.
  5594. You can <a href="http://sucuri.net/signup" target="_blank">sign up</a> with
  5595. Sucuri for a complete and in depth scan+cleanup <strong>(not included in the
  5596. free checks)</strong>.
  5597. </em>
  5598. </td>
  5599. </tr>
  5600. </tbody>
  5601. </table>
  5602. </div>
  5603. <div id="sucuriscan-website-details">
  5604. <table class="wp-list-table widefat sucuriscan-table sucuriscan-scanner-details">
  5605. <thead>
  5606. <tr>
  5607. <th colspan="2" class="thead-with-button">
  5608. <span>System information</span>
  5609. <?php if ( ! $wordpress_updated ): ?>
  5610. <a href="<?php echo admin_url( 'update-core.php' ); ?>" class="button button-primary thead-topright-action">
  5611. Update to <?php _e( $updates[0]->version ) ?>
  5612. </a>
  5613. <?php endif; ?>
  5614. </th>
  5615. </tr>
  5616. </thead>
  5617. <tbody>
  5618. <!-- List of generic information from the site. -->
  5619. <?php
  5620. $possible_keys = array(
  5621. 'DOMAIN' => 'Domain Scanned',
  5622. 'IP' => 'Site IP Address',
  5623. 'HOSTING' => 'Hosting Company',
  5624. 'CMS' => 'CMS Found',
  5625. );
  5626. $possible_url_keys = array(
  5627. 'IFRAME' => 'List of iframes found',
  5628. 'JSEXTERNAL' => 'List of external scripts included',
  5629. 'JSLOCAL' => 'List of scripts included',
  5630. 'URL' => 'List of links found',
  5631. );
  5632. ?>
  5633. <?php foreach ( $possible_keys as $result_key => $result_title ): ?>
  5634. <?php if ( isset($res['SCAN'][ $result_key ]) ): ?>
  5635. <?php $result_value = implode( ', ', $res['SCAN'][ $result_key ] ); ?>
  5636. <tr>
  5637. <td><?php _e( $result_title ) ?></td>
  5638. <td><span class="sucuriscan-monospace"><?php _e( $result_value ) ?></span></td>
  5639. </tr>
  5640. <?php endif; ?>
  5641. <?php endforeach; ?>
  5642. <tr>
  5643. <td>WordPress Version</td>
  5644. <td><span class="sucuriscan-monospace"><?php _e( $wp_version ) ?></span></td>
  5645. </tr>
  5646. <tr>
  5647. <td>PHP Version</td>
  5648. <td><span class="sucuriscan-monospace"><?php _e( phpversion() ) ?></span></td>
  5649. </tr>
  5650. <!-- List of application details from the site. -->
  5651. <tr>
  5652. <th colspan="2">Web application details</th>
  5653. </tr>
  5654. <?php if ( isset($res['WEBAPP']) ): ?>
  5655. <?php foreach ( $res['WEBAPP'] as $webapp_key => $webapp_details ): ?>
  5656. <?php if ( is_array( $webapp_details ) ): ?>
  5657. <?php foreach ( $webapp_details as $i => $details ): ?>
  5658. <?php
  5659. if ( is_array( $details ) ) {
  5660. $details = isset($details[0]) ? $details[0] : '';
  5661. }
  5662. $details_parts = explode( ':', $details, 2 );
  5663. $details_abc = isset($details_parts[0]) ? trim( $details_parts[0] ) : '';
  5664. $details_xyz = isset($details_parts[1]) ? trim( $details_parts[1] ) : '';
  5665. ?>
  5666. <tr>
  5667. <td><?php _e( $details_abc ) ?></td>
  5668. <td><span class="sucuriscan-monospace"><?php _e( $details_xyz ) ?></span></td>
  5669. </tr>
  5670. <?php endforeach; ?>
  5671. <?php endif; ?>
  5672. <?php endforeach; ?>
  5673. <?php endif; ?>
  5674. <?php if ( isset($res['SYSTEM']['NOTICE']) ): ?>
  5675. <?php foreach ( $res['SYSTEM']['NOTICE'] as $j => $notice ): ?>
  5676. <?php if ( is_array( $notice ) ){ $notice = implode( ', ', $notice ); } ?>
  5677. <tr>
  5678. <td colspan="2">
  5679. <span class="sucuriscan-monospace"><?php _e( $notice ) ?></span>
  5680. </td>
  5681. </tr>
  5682. <?php endforeach; ?>
  5683. <?php endif; ?>
  5684. <?php if ( ! isset($res['WEBAPP']) && ! isset($res['SYSTEM']['NOTICE']) ): ?>
  5685. <tr>
  5686. <td colspan="2"><em>No more information was found.</em></td>
  5687. </tr>
  5688. <?php endif; ?>
  5689. <!-- Possible recommendations or outdated software on the site. -->
  5690. <?php if ( $outdated_warns_exist || $recommendations_exist ): ?>
  5691. <tr>
  5692. <th colspan="2">Recommendations for the site</th>
  5693. </tr>
  5694. <?php endif; ?>
  5695. <!-- Possible outdated software on the site. -->
  5696. <?php if ( $outdated_warns_exist ): ?>
  5697. <?php foreach ( $res['OUTDATEDSCAN'] as $outdated ): ?>
  5698. <?php if ( count( $outdated ) >= 3 ): ?>
  5699. <tr>
  5700. <td colspan="2" class="sucuriscan-border-bad">
  5701. <strong><?php _e( $outdated[0] ) ?></strong>
  5702. <em>(<?php _e( $outdated[2] ) ?>)</em>
  5703. <span><?php _e( $outdated[1] ) ?></span>
  5704. </td>
  5705. </tr>
  5706. <?php endif; ?>
  5707. <?php endforeach; ?>
  5708. <?php endif; ?>
  5709. <!-- Possible recommendations for the site. -->
  5710. <?php if ( $recommendations_exist ): ?>
  5711. <?php foreach ( $res['RECOMMENDATIONS'] as $recommendation ): ?>
  5712. <?php if ( count( $recommendation ) >= 3 ): ?>
  5713. <tr>
  5714. <td colspan="2" class="sucuriscan-border-bad">
  5715. <?php
  5716. printf(
  5717. '<strong>%s</strong><br><span>%s</span><br><a href="%s" target="_blank">%s</a>',
  5718. SucuriScan::escape( $recommendation[0] ),
  5719. SucuriScan::escape( $recommendation[1] ),
  5720. SucuriScan::escape( $recommendation[2] ),
  5721. SucuriScan::escape( $recommendation[2] )
  5722. );
  5723. ?>
  5724. </td>
  5725. </tr>
  5726. <?php endif; ?>
  5727. <?php endforeach; ?>
  5728. <?php endif; ?>
  5729. </tbody>
  5730. </table>
  5731. </div>
  5732. <div id="sucuriscan-website-links">
  5733. <table class="wp-list-table widefat sucuriscan-table sucuriscan-scanner-links">
  5734. <tbody>
  5735. <?php if ( isset($res['LINKS']) ): ?>
  5736. <?php foreach ( $possible_url_keys as $result_url_key => $result_url_title ): ?>
  5737. <?php if ( isset($res['LINKS'][ $result_url_key ]) ): ?>
  5738. <tr>
  5739. <th colspan="2">
  5740. <?php
  5741. printf(
  5742. '%s (%d found)',
  5743. __( $result_url_title ),
  5744. count( $res['LINKS'][ $result_url_key ] )
  5745. );
  5746. ?>
  5747. </th>
  5748. </tr>
  5749. <?php foreach ( $res['LINKS'][ $result_url_key ] as $url_path ): ?>
  5750. <tr>
  5751. <td colspan="2">
  5752. <span class="sucuriscan-monospace sucuriscan-wraptext"><?php _e( $url_path ) ?></span>
  5753. </td>
  5754. </tr>
  5755. <?php endforeach; ?>
  5756. <?php endif; ?>
  5757. <?php endforeach; ?>
  5758. <?php else: ?>
  5759. <tr>
  5760. <td><em>No iFrames, links, or script files were found.</em></td>
  5761. </tr>
  5762. <?php endif; ?>
  5763. </tbody>
  5764. </table>
  5765. </div>
  5766. <div id="sucuriscan-blacklist-status">
  5767. <table class="wp-list-table widefat sucuriscan-table sucuriscan-scanner-details">
  5768. <thead>
  5769. <tr>
  5770. <th colspan="3" class="thead-with-button">
  5771. <span>Site <?php echo $blacklist_warns_exist ? 'blacklisted' : 'blacklist-free'; ?></span>
  5772. </th>
  5773. </tr>
  5774. </thead>
  5775. <tbody>
  5776. <?php
  5777. $blacklist_types = array( 'INFO' => 'CLEAN', 'WARN' => 'WARNING' );
  5778. foreach ( $blacklist_types as $type => $group_title ):
  5779. ?>
  5780. <?php if ( isset($res['BLACKLIST'][ $type ]) ): ?>
  5781. <?php foreach ( $res['BLACKLIST'][ $type ] as $blres ): ?>
  5782. <?php
  5783. $report_site = SucuriScan::escape( $blres[0] );
  5784. $report_url = SucuriScan::escape( $blres[1] );
  5785. $css_blacklist = ( $type == 'INFO' ) ? 'success' : 'danger';
  5786. ?>
  5787. <tr>
  5788. <td>
  5789. <span class="sucuriscan-label sucuriscan-label-<?php _e( $css_blacklist ); ?>">
  5790. <?php _e( $group_title ); ?>
  5791. </span>
  5792. </td>
  5793. <td><?php _e( $report_site ); ?></td>
  5794. <td>
  5795. <a href="<?php _e( $report_url ); ?>" target="_blank">More details</a>
  5796. </td>
  5797. </tr>
  5798. <?php endforeach; ?>
  5799. <?php endif; ?>
  5800. <?php endforeach; ?>
  5801. </tbody>
  5802. </table>
  5803. </div>
  5804. <div id="sucuriscan-modified-files">
  5805. <?php echo sucuriscan_modified_files(); ?>
  5806. </div>
  5807. </div>
  5808. </div>
  5809. <?php if ( $malware_warns_exist || $blacklist_warns_exist ): ?>
  5810. <a href="http://sucuri.net/signup/" target="_blank" class="button button-primary button-hero sucuriscan-cleanup-btn">
  5811. Get your site protected with Sucuri
  5812. </a>
  5813. <?php endif; ?>
  5814. <?php endif; ?>
  5815. <?php
  5816. $_html = ob_get_contents();
  5817. ob_end_clean();
  5818. echo SucuriScanTemplate::get_base_template($_html, array(
  5819. 'PageTitle' => 'Malware Scan',
  5820. 'PageContent' => $_html,
  5821. 'PageStyleClass' => 'scanner-results',
  5822. ));
  5823. return;
  5824. }
  5825. /**
  5826. * CloudProxy monitoring page.
  5827. *
  5828. * It checks whether the WordPress core files are the original ones, and the state
  5829. * of the themes and plugins reporting the availability of updates. It also checks
  5830. * the user accounts under the administrator group.
  5831. *
  5832. * @return void
  5833. */
  5834. function sucuriscan_monitoring_page(){
  5835. SucuriScanInterface::check_permissions();
  5836. // Process all form submissions.
  5837. sucuriscan_monitoring_form_submissions();
  5838. // Get the dynamic values for the template variables.
  5839. $api_key = SucuriScanAPI::get_cloudproxy_key();
  5840. // Page pseudo-variables initialization.
  5841. $template_variables = array(
  5842. 'PageTitle' => 'Firewall WAF',
  5843. 'Monitoring.InstructionsVisibility' => 'visible',
  5844. 'Monitoring.Settings' => sucuriscan_monitoring_settings( $api_key ),
  5845. 'Monitoring.Logs' => sucuriscan_monitoring_logs( $api_key ),
  5846. /* Pseudo-variables for the monitoring logs. */
  5847. 'AuditLogs.List' => '',
  5848. 'AuditLogs.CountText' => '',
  5849. 'AuditLogs.DenialTypeOptions' => '',
  5850. 'AuditLogs.NoItemsVisibility' => '',
  5851. 'AuditLogs.PaginationVisibility' => '',
  5852. 'AuditLogs.AuditPagination' => '',
  5853. );
  5854. if ( $api_key ){
  5855. $template_variables['Monitoring.InstructionsVisibility'] = 'hidden';
  5856. }
  5857. echo SucuriScanTemplate::get_template( 'monitoring', $template_variables );
  5858. }
  5859. /**
  5860. * Process the requests sent by the form submissions originated in the monitoring
  5861. * page, all forms must have a nonce field that will be checked against the one
  5862. * generated in the template render function.
  5863. *
  5864. * @return void
  5865. */
  5866. function sucuriscan_monitoring_form_submissions(){
  5867. if ( SucuriScanInterface::check_nonce() ){
  5868. // Add and/or Update the Sucuri WAF API Key (do it before anything else).
  5869. $option_name = ':cloudproxy_apikey';
  5870. $api_key = SucuriScanRequest::post( $option_name );
  5871. if ( $api_key !== false ){
  5872. if ( SucuriScanAPI::is_valid_cloudproxy_key( $api_key ) ){
  5873. SucuriScanOption::update_option( $option_name, $api_key );
  5874. SucuriScanOption::update_option( ':revproxy', 'enabled' );
  5875. SucuriScanInterface::info( 'CloudProxy API key saved successfully' );
  5876. } elseif ( empty($api_key) ){
  5877. SucuriScanOption::delete_option( $option_name );
  5878. SucuriScanOption::update_option( ':revproxy', 'disabled' );
  5879. SucuriScanInterface::info( 'CloudProxy API key removed successfully' );
  5880. } else {
  5881. SucuriScanInterface::error( 'Invalid CloudProxy API key, check your settings and try again.' );
  5882. }
  5883. }
  5884. // Flush the cache of the site(s) associated with the API key.
  5885. if ( SucuriScanRequest::post( ':clear_cache', '1' ) ){
  5886. $clear_cache_resp = SucuriScanAPI::clear_cloudproxy_cache();
  5887. if ( $clear_cache_resp ){
  5888. if ( isset($clear_cache_resp->messages[0]) ){
  5889. // Clear W3 Total Cache if it is installed.
  5890. if ( function_exists( 'w3tc_flush_all' ) ){ w3tc_flush_all(); }
  5891. SucuriScanInterface::info( $clear_cache_resp->messages[0] );
  5892. } else {
  5893. SucuriScanInterface::error( 'Could not clear the cache of your site, try later again.' );
  5894. }
  5895. } else {
  5896. SucuriScanInterface::error( 'CloudProxy is not enabled on your site, or your API key is invalid.' );
  5897. }
  5898. }
  5899. }
  5900. }
  5901. /**
  5902. * Generate the HTML code for the monitoring settings panel.
  5903. *
  5904. * @param string $api_key The CloudProxy API key.
  5905. * @return string The parsed-content of the monitoring settings panel.
  5906. */
  5907. function sucuriscan_monitoring_settings( $api_key = '' ){
  5908. $template_variables = array(
  5909. 'Monitoring.APIKey' => '',
  5910. 'Monitoring.SettingsVisibility' => 'hidden',
  5911. 'Monitoring.SettingOptions' => '',
  5912. );
  5913. if ( $api_key ){
  5914. $settings = SucuriScanAPI::get_cloudproxy_settings( $api_key );
  5915. $template_variables['Monitoring.APIKey'] = $api_key['string'];
  5916. if ( $settings ){
  5917. $counter = 0;
  5918. $template_variables['Monitoring.SettingsVisibility'] = 'visible';
  5919. $settings = sucuriscan_explain_monitoring_settings( $settings );
  5920. foreach ( $settings as $option_name => $option_value ){
  5921. // Change the name of some options.
  5922. if ( $option_name == 'internal_ip' ){
  5923. $option_name = 'hosting_ip';
  5924. }
  5925. $css_class = ( $counter % 2 == 0 ) ? 'alternate' : '';
  5926. $option_title = ucwords( str_replace( '_', chr( 32 ), $option_name ) );
  5927. // Generate a HTML list when the option's value is an array.
  5928. if ( is_array( $option_value ) ){
  5929. $css_scrollable = count( $option_value ) > 10 ? 'sucuriscan-list-as-table-scrollable' : '';
  5930. $html_list = '<ul class="sucuriscan-list-as-table ' . $css_scrollable . '">';
  5931. foreach ( $option_value as $single_value ){
  5932. $html_list .= '<li>' . $single_value . '</li>';
  5933. }
  5934. $html_list .= '</ul>';
  5935. $option_value = $html_list;
  5936. }
  5937. // Parse the snippet template and replace the pseudo-variables.
  5938. $template_variables['Monitoring.SettingOptions'] .= SucuriScanTemplate::get_snippet('monitoring-settings', array(
  5939. 'Monitoring.OptionCssClass' => $css_class,
  5940. 'Monitoring.OptionName' => $option_title,
  5941. 'Monitoring.OptionValue' => $option_value,
  5942. ));
  5943. $counter += 1;
  5944. }
  5945. }
  5946. }
  5947. return SucuriScanTemplate::get_section( 'monitoring-settings', $template_variables );
  5948. }
  5949. /**
  5950. * Converts the value of some of the monitoring settings into a human-readable
  5951. * text, for example changing numbers or variable names into a more explicit
  5952. * text so the administrator can understand the meaning of these settings.
  5953. *
  5954. * @param array $settings A hash with the settings of a CloudProxy account.
  5955. * @return array The explained version of the CloudProxy settings.
  5956. */
  5957. function sucuriscan_explain_monitoring_settings( $settings = array() ){
  5958. if ( $settings ){
  5959. foreach ( $settings as $option_name => $option_value ){
  5960. switch ( $option_name ){
  5961. case 'security_level':
  5962. $new_value = ucwords( $option_value );
  5963. break;
  5964. case 'proxy_active':
  5965. $new_value = ( $option_value == 1 ) ? 'Active' : 'not active';
  5966. break;
  5967. case 'cache_mode':
  5968. $new_value = sucuriscan_cache_mode_title( $option_value );
  5969. break;
  5970. }
  5971. if ( isset($new_value) ){
  5972. $settings->{$option_name} = $new_value;
  5973. }
  5974. }
  5975. return $settings;
  5976. }
  5977. return false;
  5978. }
  5979. /**
  5980. * Get an explanation of the meaning of the value set for the account's attribute cache_mode.
  5981. *
  5982. * @param string $mode The value set for the cache settings of the site.
  5983. * @return string Explanation of the meaning of the cache_mode value.
  5984. */
  5985. function sucuriscan_cache_mode_title( $mode = '' ){
  5986. $title = '';
  5987. switch ( $mode ){
  5988. case 'docache': $title = 'Enabled (recommended)'; break;
  5989. case 'sitecache': $title = 'Site caching (using your site headers)'; break;
  5990. case 'nocache': $title = 'Minimal (only for a few minutes)'; break;
  5991. case 'nocacheatall': $title = 'Caching disabled (use with caution)'; break;
  5992. default: $title = 'Unknown'; break;
  5993. }
  5994. return $title;
  5995. }
  5996. /**
  5997. * Generate the HTML code for the monitoring logs panel.
  5998. *
  5999. * @param string $api_key The CloudProxy API key.
  6000. * @return string The parsed-content of the monitoring logs panel.
  6001. */
  6002. function sucuriscan_monitoring_logs( $api_key = '' ){
  6003. $template_variables = array(
  6004. 'AuditLogs.List' => '',
  6005. 'AuditLogs.CountText' => 0,
  6006. 'AuditLogs.DenialTypeOptions' => '',
  6007. 'AuditLogs.NoItemsVisibility' => 'visible',
  6008. 'AuditLogs.PaginationVisibility' => 'hidden',
  6009. 'AuditLogs.AuditPagination' => '',
  6010. 'AuditLogs.TargetDate' => '',
  6011. 'AuditLogs.DateYears' => '',
  6012. 'AuditLogs.DateMonths' => '',
  6013. 'AuditLogs.DateDays' => '',
  6014. );
  6015. $date = date( 'Y-m-d' );
  6016. if ( $api_key ){
  6017. // Retrieve the date filter from the GET request (if any).
  6018. if ( $date_by_get = SucuriScanRequest::get( 'date', '_yyyymmdd' ) ){
  6019. $date = $date_by_get;
  6020. }
  6021. // Retrieve the date filter from the POST request (if any).
  6022. $year = SucuriScanRequest::post( ':year' );
  6023. $month = SucuriScanRequest::post( ':month' );
  6024. $day = SucuriScanRequest::post( ':day' );
  6025. if ( $year && $month && $day ){
  6026. $date = sprintf( '%s-%s-%s', $year, $month, $day );
  6027. }
  6028. $logs_data = SucuriScanAPI::get_cloudproxy_logs( $api_key, $date );
  6029. if ( $logs_data ){
  6030. add_thickbox(); /* Include the Thickbox library. */
  6031. $template_variables['AuditLogs.NoItemsVisibility'] = 'hidden';
  6032. $template_variables['AuditLogs.CountText'] = $logs_data->limit . '/' . $logs_data->total_lines;
  6033. $template_variables['AuditLogs.List'] = sucuriscan_monitoring_access_logs( $logs_data->access_logs );
  6034. $template_variables['AuditLogs.DenialTypeOptions'] = sucuriscan_monitoring_denial_types( $logs_data->access_logs );
  6035. }
  6036. }
  6037. $template_variables['AuditLogs.TargetDate'] = SucuriScan::escape( $date );
  6038. $template_variables['AuditLogs.DateYears'] = sucuriscan_monitoring_dates( 'years', $date );
  6039. $template_variables['AuditLogs.DateMonths'] = sucuriscan_monitoring_dates( 'months', $date );
  6040. $template_variables['AuditLogs.DateDays'] = sucuriscan_monitoring_dates( 'days', $date );
  6041. return SucuriScanTemplate::get_section( 'monitoring-logs', $template_variables );
  6042. }
  6043. /**
  6044. * Generate the HTML code to show the table with the access-logs.
  6045. *
  6046. * @param array $access_logs The logs retrieved from the remote API service.
  6047. * @return string The HTML code to show the access-logs in the page as a table.
  6048. */
  6049. function sucuriscan_monitoring_access_logs( $access_logs = array() ){
  6050. $logs_html = '';
  6051. if ( $access_logs && ! empty($access_logs) ){
  6052. $counter = 0;
  6053. $needed_attrs = array(
  6054. 'request_date',
  6055. 'request_time',
  6056. 'request_timezone',
  6057. 'request_timestamp',
  6058. 'local_request_time',
  6059. 'remote_addr',
  6060. 'sucuri_block_reason',
  6061. 'resource_path',
  6062. 'request_method',
  6063. 'http_protocol',
  6064. 'http_status',
  6065. 'http_status_title',
  6066. 'http_bytes_sent',
  6067. 'http_referer',
  6068. 'http_user_agent',
  6069. );
  6070. $filter_by_denial_type = false;
  6071. $filter_by_keyword = false;
  6072. $filter_query = false;
  6073. if ( $q = SucuriScanRequest::post( ':monitoring_denial_type' ) ){
  6074. $filter_by_denial_type = true;
  6075. $filter_query = $q;
  6076. }
  6077. if ( $q = SucuriScanRequest::post( ':monitoring_log_filter' ) ){
  6078. $filter_by_keyword = true;
  6079. $filter_query = $q;
  6080. }
  6081. foreach ( $access_logs as $access_log ){
  6082. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  6083. $audit_log_snippet = array(
  6084. 'AuditLog.Id' => $counter,
  6085. 'AuditLog.CssClass' => $css_class,
  6086. );
  6087. // If there is a filter, check the access_log data and break the operation if needed.
  6088. if ( $filter_query ){
  6089. if ( $filter_by_denial_type ){
  6090. $denial_type_slug = SucuriScan::human2var( $access_log->sucuri_block_reason );
  6091. if ( $denial_type_slug != $filter_query ){ continue; }
  6092. }
  6093. if (
  6094. $filter_by_keyword
  6095. && strpos( $access_log->remote_addr, $filter_query ) === false
  6096. && strpos( $access_log->resource_path, $filter_query ) === false
  6097. ){
  6098. continue;
  6099. }
  6100. }
  6101. // Generate (dynamically) the pseudo-variables for the template.
  6102. foreach ( $needed_attrs as $attr_name ){
  6103. $attr_value = '';
  6104. $attr_title = str_replace( '_', chr( 32 ), $attr_name );
  6105. $attr_title = ucwords( $attr_title );
  6106. $attr_title = str_replace( chr( 32 ), '', $attr_title );
  6107. $attr_title = 'AuditLog.' . $attr_title;
  6108. if ( isset($access_log->{$attr_name}) ){
  6109. $attr_value = $access_log->{$attr_name};
  6110. if (
  6111. empty($attr_value)
  6112. && $attr_name == 'sucuri_block_reason'
  6113. ){
  6114. $attr_value = 'Unknown';
  6115. }
  6116. }
  6117. elseif ( $attr_name == 'local_request_time' ){
  6118. $attr_value = SucuriScan::datetime( $access_log->request_timestamp );
  6119. }
  6120. $audit_log_snippet[ $attr_title ] = SucuriScan::escape( $attr_value );
  6121. }
  6122. $logs_html .= SucuriScanTemplate::get_snippet( 'monitoring-logs', $audit_log_snippet );
  6123. $counter += 1;
  6124. }
  6125. }
  6126. return $logs_html;
  6127. }
  6128. /**
  6129. * Get a list of denial types using the reason of the blocking of a request from
  6130. * the from the audit logs. Examples of denial types can be: "Bad bot access
  6131. * denied", "Access to restricted folder", "Blocked by IDS", etc.
  6132. *
  6133. * @param array $access_logs A list of objects with the detailed version of each request blocked by our service.
  6134. * @param boolean $in_html Whether the list should be converted to a HTML select options or not.
  6135. * @return array Either a list of unique blocking types, or a HTML code.
  6136. */
  6137. function sucuriscan_monitoring_denial_types( $access_logs = array(), $in_html = true ){
  6138. $types = array();
  6139. if ( $access_logs && ! empty($access_logs) ){
  6140. foreach ( $access_logs as $access_log ){
  6141. if ( ! array_key_exists( $access_log->sucuri_block_reason, $types ) ){
  6142. $denial_type_k = SucuriScan::human2var( $access_log->sucuri_block_reason );
  6143. $denial_type_v = $access_log->sucuri_block_reason;
  6144. if ( empty($denial_type_v) ){ $denial_type_v = 'Unknown'; }
  6145. $types[ $denial_type_k ] = $denial_type_v;
  6146. }
  6147. }
  6148. }
  6149. if ( $in_html ){
  6150. $html_types = '<option value="">Filter</option>';
  6151. $selected = SucuriScanRequest::post( ':monitoring_denial_type', '.+' );
  6152. foreach ( $types as $type_key => $type_value ){
  6153. $selected_tag = ( $type_key === $selected ) ? 'selected="selected"' : '';
  6154. $html_types .= sprintf(
  6155. '<option value="%s" %s>%s</option>',
  6156. SucuriScan::escape( $type_key ),
  6157. $selected_tag,
  6158. SucuriScan::escape( $type_value )
  6159. );
  6160. }
  6161. return $html_types;
  6162. }
  6163. return $types;
  6164. }
  6165. /**
  6166. * Get a list of years, months or days depending of the type specified.
  6167. *
  6168. * @param string $type Either years, months or days.
  6169. * @param string $date Year, month and day selected from the request.
  6170. * @param boolean $in_html Whether the list should be converted to a HTML select options or not.
  6171. * @return array Either an array with the expected values, or a HTML code.
  6172. */
  6173. function sucuriscan_monitoring_dates( $type = '', $date = '', $in_html = true ){
  6174. $options = array();
  6175. $selected = '';
  6176. if ( preg_match( '/^([0-9]{4})\-([0-9]{2})\-([0-9]{2})$/', $date, $date_m ) ){
  6177. $s_year = $date_m[1];
  6178. $s_month = $date_m[2];
  6179. $s_day = $date_m[3];
  6180. } else {
  6181. $s_year = '';
  6182. $s_month = '';
  6183. $s_day = '';
  6184. }
  6185. switch ( $type ){
  6186. case 'years':
  6187. $selected = $s_year;
  6188. $current_year = (int) date( 'Y' );
  6189. $max_years = 5; /* Maximum number of years to keep the logs. */
  6190. $options = range( ($current_year - $max_years), $current_year );
  6191. break;
  6192. case 'months':
  6193. $selected = $s_month;
  6194. $options = array(
  6195. '01' => 'January',
  6196. '02' => 'February',
  6197. '03' => 'March',
  6198. '04' => 'April',
  6199. '05' => 'May',
  6200. '06' => 'June',
  6201. '07' => 'July',
  6202. '08' => 'August',
  6203. '09' => 'September',
  6204. '10' => 'October',
  6205. '11' => 'November',
  6206. '12' => 'December',
  6207. );
  6208. break;
  6209. case 'days':
  6210. $options = range( 1, 31 );
  6211. $selected = $s_day;
  6212. break;
  6213. }
  6214. if ( $in_html ){
  6215. $html_options = '';
  6216. foreach ( $options as $key => $value ){
  6217. if ( is_numeric( $value ) ){ $value = str_pad( $value, 2, 0, STR_PAD_LEFT ); }
  6218. if ( $type != 'months' ){ $key = $value; }
  6219. $selected_tag = ( $key == $selected ) ? 'selected="selected"' : '';
  6220. $html_options .= sprintf( '<option value="%s" %s>%s</option>', $key, $selected_tag, $value );
  6221. }
  6222. return $html_options;
  6223. }
  6224. return $options;
  6225. }
  6226. /**
  6227. * Sucuri one-click hardening page.
  6228. *
  6229. * It loads all the functions defined in /lib/hardening.php and shows the forms
  6230. * that the administrator can use to harden multiple parts of the site.
  6231. *
  6232. * @return void
  6233. */
  6234. function sucuriscan_hardening_page(){
  6235. SucuriScanInterface::check_permissions();
  6236. if (
  6237. SucuriScanRequest::post( ':run_hardening' )
  6238. && ! SucuriScanInterface::check_nonce()
  6239. ){
  6240. unset($_POST['sucuriscan_run_hardening']);
  6241. }
  6242. ob_start();
  6243. ?>
  6244. <div id="poststuff">
  6245. <form method="post">
  6246. <input type="hidden" name="sucuriscan_page_nonce" value="%%SUCURI.PageNonce%%" />
  6247. <input type="hidden" name="sucuriscan_run_hardening" value="1" />
  6248. <?php
  6249. sucuriscan_harden_version();
  6250. sucuriscan_cloudproxy_enabled();
  6251. sucuriscan_harden_removegenerator();
  6252. if ( SucuriScan::is_nginx_server() === true ) {
  6253. sucuriscan_harden_nginx_phpfpm();
  6254. } elseif ( SucuriScan::is_iis_server() === true ) {
  6255. /* TODO: Include IIS (Internet Information Services) hardening options. */
  6256. } else {
  6257. sucuriscan_harden_upload();
  6258. sucuriscan_harden_wpcontent();
  6259. sucuriscan_harden_wpincludes();
  6260. }
  6261. sucuriscan_harden_phpversion();
  6262. sucuriscan_harden_secretkeys();
  6263. sucuriscan_harden_readme();
  6264. sucuriscan_harden_adminuser();
  6265. sucuriscan_harden_fileeditor();
  6266. sucuriscan_harden_dbtables();
  6267. sucuriscan_harden_errorlog();
  6268. ?>
  6269. </form>
  6270. </div>
  6271. <?php
  6272. $_html = ob_get_contents();
  6273. ob_end_clean();
  6274. echo SucuriScanTemplate::get_base_template($_html, array(
  6275. 'PageTitle' => 'Hardening',
  6276. 'PageContent' => $_html,
  6277. 'PageStyleClass' => 'hardening',
  6278. ));
  6279. return;
  6280. }
  6281. /**
  6282. * Generate the HTML code necessary to show a form with the options to harden
  6283. * a specific part of the WordPress installation, if the Status variable is
  6284. * set as a positive integer the button is shown as "unharden".
  6285. *
  6286. * @param string $title Title of the panel.
  6287. * @param integer $status Either one or zero representing the state of the hardening, one for secure, zero for insecure.
  6288. * @param string $type Name of the hardening option, this will be used through out the form generation.
  6289. * @param string $messageok Message that will be shown if the hardening was executed.
  6290. * @param string $messagewarn Message that will be shown if the hardening is not executed.
  6291. * @param string $desc Optional description of the hardening.
  6292. * @param string $updatemsg Optional explanation of the hardening after the submission of the form.
  6293. * @return void
  6294. */
  6295. function sucuriscan_harden_status( $title = '', $status = 0, $type = '', $messageok = '', $messagewarn = '', $desc = null, $updatemsg = null ){ ?>
  6296. <div class="postbox">
  6297. <h3><?php _e( $title ) ?></h3>
  6298. <div class="inside">
  6299. <?php if ( $desc != null ): ?>
  6300. <p><?php _e( $desc ) ?></p>
  6301. <?php endif; ?>
  6302. <?php if ( $status <= 5 ): ?>
  6303. <div class="sucuriscan-hstatus sucuriscan-hstatus-<?php _e( $status ) ?>">
  6304. <?php if ( $type != null ): ?>
  6305. <?php if ( $status === 1 ): ?>
  6306. <input type="submit" name="<?php _e( $type ) ?>_unharden" value="Revert hardening" class="button-secondary" />
  6307. <?php elseif ( $status === 0 ): ?>
  6308. <input type="submit" name="<?php _e( $type ) ?>" value="Harden" class="button-primary" />
  6309. <?php endif; ?>
  6310. <?php endif; ?>
  6311. <span>
  6312. <?php if ( $status === 1 ): ?>
  6313. <?php _e( $messageok ) ?>
  6314. <?php elseif ( $status === 0 ): ?>
  6315. <?php _e( $messagewarn ) ?>
  6316. <?php elseif ( $status === 2 ): ?>
  6317. Can not be determined.
  6318. <?php endif; ?>
  6319. </span>
  6320. </div>
  6321. <?php endif; ?>
  6322. <?php if ( $updatemsg != null ): ?>
  6323. <p><?php _e( $updatemsg ) ?></p>
  6324. <?php endif; ?>
  6325. </div>
  6326. </div>
  6327. <?php }
  6328. /**
  6329. * Check whether the version number of the WordPress installed is the latest
  6330. * version available officially.
  6331. *
  6332. * @return void
  6333. */
  6334. function sucuriscan_harden_version(){
  6335. $site_version = SucuriScan::site_version();
  6336. $updates = get_core_updates();
  6337. $cp = ( ! is_array( $updates ) || empty($updates) ? 1 : 0 );
  6338. if ( isset($updates[0]) && $updates[0] instanceof stdClass ){
  6339. if (
  6340. $updates[0]->response == 'latest'
  6341. || $updates[0]->response == 'development'
  6342. ){
  6343. $cp = 1;
  6344. }
  6345. }
  6346. if ( strcmp( $site_version, '3.7' ) < 0 ){
  6347. $cp = 0;
  6348. }
  6349. $initial_msg = 'Why keep your site updated? WordPress is an open-source
  6350. project which means that with every update the details of the changes made
  6351. to the source code are made public, if there were security fixes then
  6352. someone with malicious intent can use this information to attack any site
  6353. that has not been upgraded.';
  6354. $messageok = sprintf( 'Your WordPress installation (%s) is current.', $site_version );
  6355. $messagewarn = sprintf(
  6356. 'Your current version (%s) is not current.<br>
  6357. <a href="update-core.php" class="button-primary">Update now!</a>',
  6358. $site_version
  6359. );
  6360. sucuriscan_harden_status( 'Verify WordPress version', $cp, null, $messageok, $messagewarn, $initial_msg );
  6361. }
  6362. /**
  6363. * Notify the state of the hardening for the removal of the Generator tag in
  6364. * HTML code printed by WordPress to show the current version number of the
  6365. * installation.
  6366. *
  6367. * @return void
  6368. */
  6369. function sucuriscan_harden_removegenerator(){
  6370. sucuriscan_harden_status(
  6371. 'Remove WordPress version',
  6372. 1,
  6373. null,
  6374. 'WordPress version properly hidden',
  6375. null,
  6376. 'It checks if your WordPress version is being hidden from being displayed '
  6377. .'in the generator tag (enabled by default with this plugin).'
  6378. );
  6379. }
  6380. function sucuriscan_harden_nginx_phpfpm(){
  6381. $description = 'It seems that you are using the Nginx web server, if that is
  6382. the case then you will need to add the following code into the global
  6383. <code>nginx.conf</code> file or the virtualhost associated with this
  6384. website. Choose the correct rules for the directories that you want to
  6385. protect. If you encounter errors after restart the web server then revert
  6386. the changes and contact the support team of your hosting company, or read
  6387. the official article about <a href="http://codex.wordpress.org/Nginx">
  6388. WordPress on Nginx</a>.</p>';
  6389. $description .= "<pre class='code'># Block PHP files in uploads directory.\nlocation ~* /(?:uploads|files)/.*\.php$ {\n\x20\x20deny all;\n}</pre>";
  6390. $description .= "<pre class='code'># Block PHP files in content directory.\nlocation ~* /wp-content/.*\.php$ {\n\x20\x20deny all;\n}</pre>";
  6391. $description .= "<pre class='code'># Block PHP files in includes directory.\nlocation ~* /wp-includes/.*\.php$ {\n\x20\x20deny all;\n}</pre>";
  6392. $description .= "<pre class='code'>";
  6393. $description .= "# Block PHP files in uploads, content, and includes directory.\n";
  6394. $description .= "location ~* /(?:uploads|files|wp-content|wp-includes)/.*\.php$ {\n";
  6395. $description .= "\x20\x20deny all;\n";
  6396. $description .= '}</pre>';
  6397. $description .= '<p class="sucuriscan-hidden">';
  6398. sucuriscan_harden_status(
  6399. 'Block PHP files',
  6400. 999,
  6401. null,
  6402. null,
  6403. null,
  6404. $description
  6405. );
  6406. }
  6407. /**
  6408. * Check whether the WordPress upload folder is protected or not.
  6409. *
  6410. * A htaccess file is placed in the upload folder denying the access to any php
  6411. * file that could be uploaded through a vulnerability in a Plugin, Theme or
  6412. * WordPress itself.
  6413. *
  6414. * @return void
  6415. */
  6416. function sucuriscan_harden_upload(){
  6417. $cp = 1;
  6418. $datastore_path = SucuriScan::datastore_folder_path();
  6419. $htaccess_upload = dirname( $datastore_path ) . '/.htaccess';
  6420. if ( ! is_readable( $htaccess_upload ) ){
  6421. $cp = 0;
  6422. } else {
  6423. $cp = 0;
  6424. $fcontent = SucuriScanFileInfo::file_lines( $htaccess_upload );
  6425. foreach ( $fcontent as $fline ){
  6426. if ( stripos( $fline, 'deny from all' ) !== false ){
  6427. $cp = 1;
  6428. break;
  6429. }
  6430. }
  6431. }
  6432. if ( SucuriScanRequest::post( ':run_hardening' ) ){
  6433. if ( SucuriScanRequest::post( ':harden_upload' ) && $cp == 0 ){
  6434. if ( @file_put_contents( $htaccess_upload, "\n<Files *.php>\ndeny from all\n</Files>" ) === false ){
  6435. SucuriScanInterface::error( 'Unable to create <code>.htaccess</code> file, folder destination is not writable.' );
  6436. } else {
  6437. $cp = 1;
  6438. $message = 'Hardening applied to the uploads directory';
  6439. SucuriScanEvent::report_notice_event( $message );
  6440. SucuriScanInterface::info( $message );
  6441. }
  6442. }
  6443. elseif ( SucuriScanRequest::post( ':harden_upload_unharden' ) ){
  6444. $htaccess_upload_writable = ( file_exists( $htaccess_upload ) && is_writable( $htaccess_upload ) ) ? true : false;
  6445. $htaccess_content = $htaccess_upload_writable ? file_get_contents( $htaccess_upload ) : '';
  6446. if ( $htaccess_upload_writable ){
  6447. $cp = 0;
  6448. if ( preg_match( '/<Files \*\.php>\ndeny from all\n<\/Files>/', $htaccess_content, $match ) ){
  6449. $htaccess_content = str_replace( "<Files *.php>\ndeny from all\n</Files>", '', $htaccess_content );
  6450. @file_put_contents( $htaccess_upload, $htaccess_content, LOCK_EX );
  6451. }
  6452. $message = 'Hardening reverted in the uploads directory';
  6453. SucuriScanEvent::report_error_event( $message );
  6454. SucuriScanInterface::info( $message );
  6455. } else {
  6456. SucuriScanInterface::error(
  6457. 'File <code>/wp-content/uploads/.htaccess</code> does not exists or
  6458. is not writable, you will need to remove the following code (manually):
  6459. <code>&lt;Files *.php&gt;deny from all&lt;/Files&gt;</code>'
  6460. );
  6461. }
  6462. }
  6463. }
  6464. sucuriscan_harden_status(
  6465. 'Protect uploads directory',
  6466. $cp,
  6467. 'sucuriscan_harden_upload',
  6468. 'Upload directory properly hardened',
  6469. 'Upload directory not hardened',
  6470. 'It checks if your upload directory allows PHP execution or if it is browsable.',
  6471. null
  6472. );
  6473. }
  6474. /**
  6475. * Check whether the WordPress content folder is protected or not.
  6476. *
  6477. * A htaccess file is placed in the content folder denying the access to any php
  6478. * file that could be uploaded through a vulnerability in a Plugin, Theme or
  6479. * WordPress itself.
  6480. *
  6481. * @return void
  6482. */
  6483. function sucuriscan_harden_wpcontent(){
  6484. $cp = 1;
  6485. $htaccess_upload = WP_CONTENT_DIR . '/.htaccess';
  6486. if ( ! is_readable( $htaccess_upload ) ){
  6487. $cp = 0;
  6488. } else {
  6489. $cp = 0;
  6490. $fcontent = SucuriScanFileInfo::file_lines( $htaccess_upload );
  6491. foreach ( $fcontent as $fline ){
  6492. if ( stripos( $fline, 'deny from all' ) !== false ){
  6493. $cp = 1;
  6494. break;
  6495. }
  6496. }
  6497. }
  6498. if ( SucuriScanRequest::post( ':run_hardening' ) ){
  6499. if ( SucuriScanRequest::post( ':harden_wpcontent' ) && $cp == 0 ){
  6500. if ( @file_put_contents( $htaccess_upload, "\n<Files *.php>\ndeny from all\n</Files>" ) === false ){
  6501. SucuriScanInterface::error( 'Unable to create <code>.htaccess</code> file, folder destination is not writable.' );
  6502. } else {
  6503. $cp = 1;
  6504. $message = 'Hardening applied to the content directory';
  6505. SucuriScanEvent::report_notice_event( $message );
  6506. SucuriScanInterface::info( $message );
  6507. }
  6508. }
  6509. elseif ( SucuriScanRequest::post( ':harden_wpcontent_unharden' ) ){
  6510. $htaccess_upload_writable = ( file_exists( $htaccess_upload ) && is_writable( $htaccess_upload ) ) ? true : false;
  6511. $htaccess_content = $htaccess_upload_writable ? file_get_contents( $htaccess_upload ) : '';
  6512. if ( $htaccess_upload_writable ){
  6513. $cp = 0;
  6514. if ( preg_match( '/<Files \*\.php>\ndeny from all\n<\/Files>/', $htaccess_content, $match ) ){
  6515. $htaccess_content = str_replace( "<Files *.php>\ndeny from all\n</Files>", '', $htaccess_content );
  6516. @file_put_contents( $htaccess_upload, $htaccess_content, LOCK_EX );
  6517. }
  6518. $message = 'Hardening reverted in the content directory';
  6519. SucuriScanEvent::report_error_event( $message );
  6520. SucuriScanInterface::info( $message );
  6521. } else {
  6522. SucuriScanInterface::info(
  6523. 'File <code>' . WP_CONTENT_DIR . '/.htaccess</code> does not exists or is not
  6524. writable, you will need to remove the following code manually from there:
  6525. <code>&lt;Files *.php&gt;deny from all&lt;/Files&gt;</code>'
  6526. );
  6527. }
  6528. }
  6529. }
  6530. $description = 'This option blocks direct PHP access to any file inside wp-content. If you experience '
  6531. . 'any issue after this with a theme or plugin in your site, like for example images not displaying, '
  6532. . 'remove the <code>.htaccess</code> file located in the content directory.'
  6533. . '</p><p><b>Note:</b> Many <em>(insecure)</em> themes and plugins use a PHP file in this directory '
  6534. . 'to generate images like thumbnails and captcha codes, this is intentional so it is recommended '
  6535. . 'to check your site once this option is enabled.';
  6536. sucuriscan_harden_status(
  6537. 'Restrict wp-content access',
  6538. $cp,
  6539. 'sucuriscan_harden_wpcontent',
  6540. 'WP-content directory properly hardened',
  6541. 'WP-content directory not hardened',
  6542. $description,
  6543. null
  6544. );
  6545. }
  6546. /**
  6547. * Check whether the WordPress includes folder is protected or not.
  6548. *
  6549. * A htaccess file is placed in the includes folder denying the access to any php
  6550. * file that could be uploaded through a vulnerability in a Plugin, Theme or
  6551. * WordPress itself, there are some exceptions for some specific files that must
  6552. * be available publicly.
  6553. *
  6554. * @return void
  6555. */
  6556. function sucuriscan_harden_wpincludes(){
  6557. $cp = 1;
  6558. $htaccess_upload = ABSPATH . '/wp-includes/.htaccess';
  6559. if ( ! is_readable( $htaccess_upload ) ){
  6560. $cp = 0;
  6561. } else {
  6562. $cp = 0;
  6563. $fcontent = SucuriScanFileInfo::file_lines( $htaccess_upload );
  6564. foreach ( $fcontent as $fline ){
  6565. if ( stripos( $fline, 'deny from all' ) !== false ){
  6566. $cp = 1;
  6567. break;
  6568. }
  6569. }
  6570. }
  6571. if ( SucuriScanRequest::post( ':run_hardening' ) ){
  6572. if ( SucuriScanRequest::post( ':harden_wpincludes' ) && $cp == 0 ){
  6573. $file_rules = "\n<Files *.php>"
  6574. . "\ndeny from all"
  6575. . "\n</Files>"
  6576. . "\n<Files wp-tinymce.php>"
  6577. . "\nallow from all"
  6578. . "\n</Files>"
  6579. . "\n<Files ms-files.php>"
  6580. . "\nallow from all"
  6581. . "\n</Files>"
  6582. . "\n";
  6583. if ( @file_put_contents( $htaccess_upload, $file_rules ) === false ){
  6584. SucuriScanInterface::error( 'Unable to create <code>.htaccess</code> file, folder destination is not writable.' );
  6585. } else {
  6586. $cp = 1;
  6587. $message = 'Hardening applied to the library directory';
  6588. SucuriScanEvent::report_notice_event( $message );
  6589. SucuriScanInterface::info( $message );
  6590. }
  6591. }
  6592. elseif ( SucuriScanRequest::post( ':harden_wpincludes_unharden' ) ){
  6593. $htaccess_upload_writable = ( file_exists( $htaccess_upload ) && is_writable( $htaccess_upload ) ) ? true : false;
  6594. $htaccess_content = $htaccess_upload_writable ? file_get_contents( $htaccess_upload ) : '';
  6595. if ( $htaccess_upload_writable ){
  6596. $cp = 0;
  6597. if ( preg_match_all( '/<Files (\*|wp-tinymce|ms-files)\.php>\n(deny|allow) from all\n<\/Files>/', $htaccess_content, $match ) ){
  6598. foreach ( $match[0] as $restriction ){
  6599. $htaccess_content = str_replace( $restriction, '', $htaccess_content );
  6600. }
  6601. @file_put_contents( $htaccess_upload, $htaccess_content, LOCK_EX );
  6602. }
  6603. $message = 'Hardening reverted in the library directory';
  6604. SucuriScanEvent::report_error_event( $message );
  6605. SucuriScanInterface::info( $message );
  6606. } else {
  6607. SucuriScanInterface::error(
  6608. 'File <code>wp-includes/.htaccess</code> does not exists or is not
  6609. writable, you will need to remove the following code manually from
  6610. there: <code>&lt;Files *.php&gt;deny from all&lt;/Files&gt;</code>'
  6611. );
  6612. }
  6613. }
  6614. }
  6615. sucuriscan_harden_status(
  6616. 'Restrict wp-includes access',
  6617. $cp,
  6618. 'sucuriscan_harden_wpincludes',
  6619. 'WP-Includes directory properly hardened',
  6620. 'WP-Includes directory not hardened',
  6621. 'This option blocks direct PHP access to any file inside <code>wp-includes</code>.',
  6622. null
  6623. );
  6624. }
  6625. /**
  6626. * Check the version number of the PHP interpreter set to work with the site,
  6627. * is considered that old versions of the PHP interpreter are insecure.
  6628. *
  6629. * @return void
  6630. */
  6631. function sucuriscan_harden_phpversion(){
  6632. $phpv = phpversion();
  6633. $cp = ( strncmp( $phpv, '5.', 2 ) < 0 ) ? 0 : 1;
  6634. sucuriscan_harden_status(
  6635. 'Verify PHP version',
  6636. $cp,
  6637. null,
  6638. 'Using an updated version of PHP (' . $phpv . ')',
  6639. 'The version of PHP you are using (' . $phpv . ') is not current, not recommended, and/or not supported',
  6640. 'This checks if you have the latest version of PHP installed.',
  6641. null
  6642. );
  6643. }
  6644. /**
  6645. * Check whether the site is behind a secure proxy server or not.
  6646. *
  6647. * @return void
  6648. */
  6649. function sucuriscan_cloudproxy_enabled(){
  6650. $btn_string = '';
  6651. $proxy_info = SucuriScan::is_behind_cloudproxy();
  6652. $status = 1;
  6653. $description = 'A WAF is a protection layer for your web site, blocking all sort of attacks (brute force attempts, '
  6654. . 'DDoS, SQL injections, etc) and helping it remain malware and blacklist free. This test checks if your site is '
  6655. . 'using <a href="http://cloudproxy.sucuri.net/" target="_blank">Sucuri\'s CloudProxy WAF</a> to protect your site.';
  6656. if ( $proxy_info === false ){
  6657. $status = 0;
  6658. $btn_string = '<a href="http://goo.gl/qfNkMq" target="_blank" class="button button-primary">Harden</a>';
  6659. }
  6660. sucuriscan_harden_status(
  6661. 'Website Firewall protection',
  6662. $status,
  6663. null,
  6664. 'Your website is protected by a Website Firewall (WAF)',
  6665. $btn_string . 'Your website is not protected by a Website Firewall (WAF)',
  6666. $description,
  6667. null
  6668. );
  6669. }
  6670. /**
  6671. * Check whether the Wordpress configuration file has the security keys recommended
  6672. * to avoid any unauthorized access to the interface.
  6673. *
  6674. * WordPress Security Keys is a set of random variables that improve encryption of
  6675. * information stored in the user’s cookies. There are a total of four security
  6676. * keys: AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, and NONCE_KEY.
  6677. *
  6678. * @return void
  6679. */
  6680. function sucuriscan_harden_secretkeys(){
  6681. $wp_config_path = SucuriScan::get_wpconfig_path();
  6682. $current_keys = SucuriScanOption::get_security_keys();
  6683. if ( $wp_config_path ){
  6684. $cp = 1;
  6685. $message = 'The main configuration file was found at: <code>'.$wp_config_path.'</code><br>';
  6686. if (
  6687. ! empty($current_keys['bad'])
  6688. || ! empty($current_keys['missing'])
  6689. ){
  6690. $cp = 0;
  6691. }
  6692. }else {
  6693. $cp = 0;
  6694. $message = 'The <code>wp-config.php</code> file was not found.<br>';
  6695. }
  6696. $message .= '<br>It checks whether you have proper random keys/salts created for WordPress. A
  6697. <a href="http://codex.wordpress.org/Editing_wp-config.php#Security_Keys" target="_blank">
  6698. secret key</a> makes your site harder to hack and access harder to crack by adding
  6699. random elements to the password. In simple terms, a secret key is a password with
  6700. elements that make it harder to generate enough options to break through your
  6701. security barriers.';
  6702. $messageok = 'Security keys and salts not set, we recommend to create them for security reasons'
  6703. . '<a href="' . SucuriScanTemplate::get_url( 'posthack' ) . '" class="button button-primary">'
  6704. . 'Harden</a>';
  6705. sucuriscan_harden_status(
  6706. 'Security keys',
  6707. $cp,
  6708. null,
  6709. 'Security keys and salts properly created',
  6710. $messageok,
  6711. $message,
  6712. null
  6713. );
  6714. }
  6715. /**
  6716. * Check whether the "readme.html" file is still available in the root of the
  6717. * site or not, which can lead to an attacker to know which version number of
  6718. * Wordpress is being used and search for possible vulnerabilities.
  6719. *
  6720. * @return void
  6721. */
  6722. function sucuriscan_harden_readme(){
  6723. $upmsg = null;
  6724. $cp = is_readable( ABSPATH.'/readme.html' ) ? 0 : 1;
  6725. // TODO: After hardening create an option to automatically remove this after WP upgrade.
  6726. if ( SucuriScanRequest::post( ':run_hardening' ) ){
  6727. if ( SucuriScanRequest::post( ':harden_readme' ) && $cp == 0 ){
  6728. if ( @unlink( ABSPATH.'/readme.html' ) === false ){
  6729. $upmsg = SucuriScanInterface::error( 'Unable to remove <code>readme.html</code> file.' );
  6730. } else {
  6731. $cp = 1;
  6732. $message = 'Hardening applied to the <code>readme.html</code> file';
  6733. SucuriScanEvent::report_notice_event( $message );
  6734. SucuriScanInterface::info( $message );
  6735. }
  6736. }
  6737. elseif ( SucuriScanRequest::post( ':harden_readme_unharden' ) ){
  6738. SucuriScanInterface::error( 'We can not revert this action, you must create the <code>readme.html</code> manually.' );
  6739. }
  6740. }
  6741. sucuriscan_harden_status(
  6742. 'Information leakage (readme.html)',
  6743. $cp,
  6744. ( $cp == 0 ? 'sucuriscan_harden_readme' : null ),
  6745. '<code>readme.html</code> file properly deleted',
  6746. '<code>readme.html</code> not deleted and leaking the WordPress version',
  6747. 'It checks whether you have the <code>readme.html</code> file available that leaks your WordPress version',
  6748. $upmsg
  6749. );
  6750. }
  6751. /**
  6752. * Check whether the main administrator user still has the default name "admin"
  6753. * or not, which can lead to an attacker to perform a brute force attack.
  6754. *
  6755. * @return void
  6756. */
  6757. function sucuriscan_harden_adminuser(){
  6758. global $wpdb;
  6759. $upmsg = null;
  6760. $user_query = new WP_User_Query(array(
  6761. 'search' => 'admin',
  6762. 'fields' => array( 'ID', 'user_login' ),
  6763. 'search_columns' => array( 'user_login' ),
  6764. ));
  6765. $results = $user_query->get_results();
  6766. $account_removed = ( count( $results ) === 0 ? 1 : 0 );
  6767. if ( $account_removed === 0 ){
  6768. $upmsg = '<i><strong>Notice.</strong> We do not offer an option to automatically change the user name.
  6769. Go to the <a href="'.admin_url( 'users.php' ).'" target="_blank">user list</a> and create a new
  6770. administrator user. Once created, log in as that user and remove the default <code>admin</code>
  6771. (make sure to assign all the admin posts to the new user too).</i>';
  6772. }
  6773. sucuriscan_harden_status(
  6774. 'Default admin account',
  6775. $account_removed,
  6776. null,
  6777. 'Default admin user account (admin) not being used',
  6778. 'Default admin user account (admin) being used. Not recommended',
  6779. 'It checks whether you have the default <code>admin</code> account enabled, security guidelines recommend creating a new admin user name.',
  6780. $upmsg
  6781. );
  6782. }
  6783. /**
  6784. * Enable or disable the user of the built-in Wordpress file editor.
  6785. *
  6786. * @return void
  6787. */
  6788. function sucuriscan_harden_fileeditor(){
  6789. $file_editor_disabled = defined( 'DISALLOW_FILE_EDIT' ) ? DISALLOW_FILE_EDIT : false;
  6790. if ( SucuriScanRequest::post( ':run_hardening' ) ){
  6791. $current_time = date( 'r' );
  6792. $wp_config_path = SucuriScan::get_wpconfig_path();
  6793. $wp_config_writable = ( file_exists( $wp_config_path ) && is_writable( $wp_config_path ) ) ? true : false;
  6794. $new_wpconfig = $wp_config_writable ? file_get_contents( $wp_config_path ) : '';
  6795. if ( SucuriScanRequest::post( ':harden_fileeditor' ) ){
  6796. if ( $wp_config_writable ){
  6797. if ( preg_match( '/(.*define\(.DB_COLLATE..*)/', $new_wpconfig, $match ) ){
  6798. $disallow_fileedit_definition = "\n\ndefine('DISALLOW_FILE_EDIT', TRUE); // Sucuri Security: {$current_time}\n";
  6799. $new_wpconfig = str_replace( $match[0], $match[0].$disallow_fileedit_definition, $new_wpconfig );
  6800. }
  6801. $file_editor_disabled = true;
  6802. @file_put_contents( $wp_config_path, $new_wpconfig, LOCK_EX );
  6803. $message = 'Hardening applied to the plugin and theme editor';
  6804. SucuriScanEvent::report_notice_event( $message );
  6805. SucuriScanInterface::info( $message );
  6806. } else {
  6807. SucuriScanInterface::error( 'The <code>wp-config.php</code> file is not in the default location
  6808. or is not writable, you will need to put the following code manually there:
  6809. <code>define("DISALLOW_FILE_EDIT", TRUE);</code>' );
  6810. }
  6811. }
  6812. elseif ( SucuriScanRequest::post( ':harden_fileeditor_unharden' ) ){
  6813. if ( preg_match( "/(.*define\('DISALLOW_FILE_EDIT', TRUE\);.*)/", $new_wpconfig, $match ) ){
  6814. if ( $wp_config_writable ){
  6815. $new_wpconfig = str_replace( "\n{$match[1]}", '', $new_wpconfig );
  6816. file_put_contents( $wp_config_path, $new_wpconfig, LOCK_EX );
  6817. $file_editor_disabled = false;
  6818. $message = 'Hardening reverted in the plugin and theme editor';
  6819. SucuriScanEvent::report_error_event( $message );
  6820. SucuriScanInterface::info( $message );
  6821. } else {
  6822. SucuriScanInterface::error( 'The <code>wp-config.php</code> file is not in the default location
  6823. or is not writable, you will need to remove the following code manually from there:
  6824. <code>define("DISALLOW_FILE_EDIT", TRUE);</code>' );
  6825. }
  6826. } else {
  6827. SucuriScanInterface::error( 'The theme and plugin editor are not disabled from the configuration file.' );
  6828. }
  6829. }
  6830. }
  6831. $message = 'Occasionally you may wish to disable the plugin or theme editor to prevent overzealous
  6832. users from being able to edit sensitive files and potentially crash the site. Disabling these
  6833. also provides an additional layer of security if a hacker gains access to a well-privileged
  6834. user account.';
  6835. sucuriscan_harden_status(
  6836. 'Plugin &amp; Theme editor',
  6837. ( $file_editor_disabled === false ? 0 : 1 ),
  6838. 'sucuriscan_harden_fileeditor',
  6839. 'File editor for Plugins and Themes is disabled',
  6840. 'File editor for Plugins and Themes is enabled',
  6841. $message,
  6842. null
  6843. );
  6844. }
  6845. /**
  6846. * Check whether the prefix of each table in the database designated for the site
  6847. * is the same as the default prefix defined by Wordpress "_wp", in that case the
  6848. * "harden" button will generate randomly a new prefix and rename all those tables.
  6849. *
  6850. * @return void
  6851. */
  6852. function sucuriscan_harden_dbtables(){
  6853. global $table_prefix;
  6854. $hardened = ( $table_prefix == 'wp_' ? 0 : 1 );
  6855. sucuriscan_harden_status(
  6856. 'Database table prefix',
  6857. $hardened,
  6858. null,
  6859. 'Database table prefix properly modified',
  6860. 'Database table set to the default value <code>wp_</code>.',
  6861. 'It checks whether your database table prefix has been changed from the default <code>wp_</code>',
  6862. '<strong>Be aware that this hardening procedure can cause your site to go down</strong>'
  6863. );
  6864. }
  6865. /**
  6866. * Check whether an error_log file exists in the project.
  6867. *
  6868. * @return void
  6869. */
  6870. function sucuriscan_harden_errorlog(){
  6871. $hardened = 1;
  6872. $log_filename = SucuriScan::ini_get( 'error_log' );
  6873. $scan_errorlogs = SucuriScanOption::get_option( ':scan_errorlogs' );
  6874. $description = 'PHP uses files named as <code>' . $log_filename . '</code> to log errors found in '
  6875. . 'the code, these files may leak sensitive information of your project allowing an attacker '
  6876. . 'to find vulnerabilities in the code. You must use these files to fix any bug while using '
  6877. . 'a development environment, and remove them in production mode.';
  6878. // Search error log files in the project.
  6879. if ( $scan_errorlogs != 'disabled' ){
  6880. $sucuri_fileinfo = new SucuriScanFileInfo();
  6881. $sucuri_fileinfo->ignore_files = false;
  6882. $sucuri_fileinfo->ignore_directories = false;
  6883. $error_logs = $sucuri_fileinfo->find_file( $log_filename );
  6884. $total_log_files = count( $error_logs );
  6885. } else {
  6886. $error_logs = array();
  6887. $total_log_files = 0;
  6888. $description .= '<div class="sucuriscan-inline-alert-error"><p>The filesystem scan for error '
  6889. . 'log files is disabled, so even if there are logs in your project they will be not '
  6890. . 'shown here. You can enable the scanner again from the plugin <em>Settings</em> '
  6891. . 'page.</p></div>';
  6892. }
  6893. // Remove every error log file found in the filesystem scan.
  6894. if ( SucuriScanRequest::post( ':run_hardening' ) ){
  6895. if ( SucuriScanRequest::post( ':harden_errorlog' ) ){
  6896. $removed_logs = 0;
  6897. SucuriScanEvent::report_notice_event( sprintf(
  6898. 'Error log files deleted: (multiple entries): %s',
  6899. @implode( ',', $error_logs )
  6900. ) );
  6901. foreach ( $error_logs as $i => $error_log_path ){
  6902. if ( unlink( $error_log_path ) ){
  6903. unset($error_logs[ $i ]);
  6904. $removed_logs += 1;
  6905. }
  6906. }
  6907. SucuriScanInterface::info( 'Error log files deleted <code>' . $removed_logs . ' out of ' . $total_log_files . '</code>' );
  6908. }
  6909. }
  6910. // List the error log files in a HTML table.
  6911. if ( ! empty($error_logs) ){
  6912. $hardened = 0;
  6913. $description .= '</p><ul class="sucuriscan-list-as-table">';
  6914. foreach ( $error_logs as $error_log_path ){
  6915. $error_log_path = str_replace( ABSPATH, '/', $error_log_path );
  6916. $description .= '<li>' . $error_log_path . '</li>';
  6917. }
  6918. $description .= '</ul><p>';
  6919. }
  6920. sucuriscan_harden_status(
  6921. 'Error logs',
  6922. $hardened,
  6923. ( $hardened == 0 ? 'sucuriscan_harden_errorlog' : null ),
  6924. 'There are no error log files in your project.',
  6925. 'There are ' . $total_log_files . ' error log files in your project.',
  6926. $description,
  6927. null
  6928. );
  6929. }
  6930. /**
  6931. * WordPress core integrity page.
  6932. *
  6933. * It checks whether the WordPress core files are the original ones, and the state
  6934. * of the themes and plugins reporting the availability of updates. It also checks
  6935. * the user accounts under the administrator group.
  6936. *
  6937. * @return void
  6938. */
  6939. function sucuriscan_page(){
  6940. SucuriScanInterface::check_permissions();
  6941. // Process all form submissions.
  6942. sucuriscan_integrity_form_submissions();
  6943. $template_variables = array(
  6944. 'WordpressVersion' => sucuriscan_wordpress_outdated(),
  6945. 'CoreFiles' => sucuriscan_core_files(),
  6946. 'AuditReports' => sucuriscan_auditreport(),
  6947. 'AuditLogs' => sucuriscan_auditlogs(),
  6948. );
  6949. echo SucuriScanTemplate::get_template( 'integrity', $template_variables );
  6950. }
  6951. /**
  6952. * Process the requests sent by the form submissions originated in the integrity
  6953. * page, all forms must have a nonce field that will be checked against the one
  6954. * generated in the template render function.
  6955. *
  6956. * @return void
  6957. */
  6958. function sucuriscan_integrity_form_submissions(){
  6959. if ( SucuriScanInterface::check_nonce() ){
  6960. // Force the execution of the filesystem scanner.
  6961. if ( SucuriScanRequest::post( ':force_scan' ) !== false ){
  6962. SucuriScanEvent::notify_event( 'plugin_change', 'Filesystem scan forced at: ' . date( 'r' ) );
  6963. SucuriScanEvent::filesystem_scan( true );
  6964. }
  6965. // Restore, Remove, Mark as fixed the core files.
  6966. $allowed_actions = '(restore|delete|fixed)';
  6967. $integrity_action = SucuriScanRequest::post( ':integrity_action', $allowed_actions );
  6968. if ( $integrity_action !== false ){
  6969. $cache = new SucuriScanCache( 'integrity' );
  6970. $integrity_files = SucuriScanRequest::post( ':integrity_files', '_array' );
  6971. $integrity_types = SucuriScanRequest::post( ':integrity_types', '_array' );
  6972. $files_selected = count( $integrity_files );
  6973. $files_affected = array();
  6974. $files_processed = 0;
  6975. $action_titles = array(
  6976. 'restore' => 'Core file restored',
  6977. 'delete' => 'Non-core file deleted',
  6978. 'fixed' => 'Core file marked as fixed',
  6979. );
  6980. if ( $integrity_files ) {
  6981. foreach ( (array) $integrity_files as $i => $file_path ){
  6982. $full_path = ABSPATH . $file_path;
  6983. $status_type = $integrity_types[ $i ];
  6984. switch ( $integrity_action ){
  6985. case 'restore':
  6986. $file_content = SucuriScanAPI::get_original_core_file( $file_path );
  6987. if ( $file_content ){
  6988. $restored = @file_put_contents( $full_path, $file_content, LOCK_EX );
  6989. $files_processed += ( $restored ? 1 : 0 );
  6990. $files_affected[] = $full_path;
  6991. }
  6992. break;
  6993. case 'delete':
  6994. if ( @unlink( $full_path ) ){
  6995. $files_processed += 1;
  6996. $files_affected[] = $full_path;
  6997. }
  6998. break;
  6999. case 'fixed':
  7000. $cache_key = md5( $file_path );
  7001. $cache_value = array(
  7002. 'file_path' => $file_path,
  7003. 'file_status' => $status_type,
  7004. 'ignored_at' => time(),
  7005. );
  7006. $cached = $cache->add( $cache_key, $cache_value );
  7007. $files_processed += ( $cached ? 1 : 0 );
  7008. $files_affected[] = $full_path;
  7009. break;
  7010. }
  7011. }
  7012. // Report files affected as a single event.
  7013. if ( ! empty($files_affected) ) {
  7014. $message_tpl = ( count( $files_affected ) > 1 )
  7015. ? '%s: (multiple entries): %s'
  7016. : '%s: %s';
  7017. $message = sprintf(
  7018. $message_tpl,
  7019. $action_titles[ $integrity_action ],
  7020. @implode( ',', $files_affected )
  7021. );
  7022. switch ( $integrity_action ){
  7023. case 'restore': SucuriScanEvent::report_info_event( $message ); break;
  7024. case 'delete': SucuriScanEvent::report_notice_event( $message ); break;
  7025. case 'fixed': SucuriScanEvent::report_warning_event( $message ); break;
  7026. }
  7027. }
  7028. SucuriScanInterface::info(sprintf(
  7029. '<code>%d</code> out of <code>%d</code> files were successfully processed.',
  7030. $files_selected,
  7031. $files_processed
  7032. ));
  7033. }
  7034. }
  7035. }
  7036. }
  7037. /**
  7038. * Retrieve a list of md5sum and last modification time of all the files in the
  7039. * folder specified. This is a recursive function.
  7040. *
  7041. * @param string $dir The base path where the scanning will start.
  7042. * @param boolean $recursive Either TRUE or FALSE if the scan should be performed recursively.
  7043. * @return array List of arrays containing the md5sum and last modification time of the files found.
  7044. */
  7045. function sucuriscan_get_integrity_tree( $dir = './', $recursive = false ){
  7046. $abs_path = rtrim( ABSPATH, '/' );
  7047. $sucuri_fileinfo = new SucuriScanFileInfo();
  7048. $sucuri_fileinfo->ignore_files = false;
  7049. $sucuri_fileinfo->ignore_directories = false;
  7050. $sucuri_fileinfo->run_recursively = $recursive;
  7051. $sucuri_fileinfo->scan_interface = SucuriScanOption::get_option( ':scan_interface' );
  7052. $integrity_tree = $sucuri_fileinfo->get_directory_tree_md5( $dir, true );
  7053. if ( ! $integrity_tree ){
  7054. $integrity_tree = array();
  7055. }
  7056. return $integrity_tree;
  7057. }
  7058. /**
  7059. * Print a HTML code with the content of the logs audited by the remote Sucuri
  7060. * API service, this page is part of the monitoring tool.
  7061. *
  7062. * @return void
  7063. */
  7064. function sucuriscan_auditlogs(){
  7065. // Initialize the values for the pagination.
  7066. $max_per_page = SUCURISCAN_AUDITLOGS_PER_PAGE;
  7067. $page_number = SucuriScanTemplate::get_page_number();
  7068. $logs_limit = $page_number * $max_per_page;
  7069. $audit_logs = SucuriScanAPI::get_logs( $logs_limit );
  7070. $template_variables = array(
  7071. 'PageTitle' => 'Audit Logs',
  7072. 'AuditLogs.List' => '',
  7073. 'AuditLogs.Count' => 0,
  7074. 'AuditLogs.MaxPerPage' => $max_per_page,
  7075. 'AuditLogs.NoItemsVisibility' => 'visible',
  7076. 'AuditLogs.PaginationVisibility' => 'hidden',
  7077. 'AuditLogs.PaginationLinks' => '',
  7078. 'AuditLogs.EnableAuditReportVisibility' => 'hidden',
  7079. );
  7080. if ( $audit_logs ){
  7081. $counter_i = 0;
  7082. $total_items = count( $audit_logs->output_data );
  7083. $iterator_start = ($page_number - 1) * $max_per_page;
  7084. $iterator_end = $total_items;
  7085. if (
  7086. $audit_logs->total_entries >= $max_per_page
  7087. && SucuriScanOption::get_option( ':audit_report' ) !== 'enabled'
  7088. ) {
  7089. $template_variables['AuditLogs.EnableAuditReportVisibility'] = 'visible';
  7090. }
  7091. for ( $i = $iterator_start; $i < $total_items; $i++ ){
  7092. if ( $counter_i > $max_per_page ){ break; }
  7093. if ( isset($audit_logs->output_data[ $i ]) ){
  7094. $audit_log = $audit_logs->output_data[ $i ];
  7095. $css_class = ( $counter_i % 2 == 0 ) ? '' : 'alternate';
  7096. $snippet_data = array(
  7097. 'AuditLog.CssClass' => $css_class,
  7098. 'AuditLog.Event' => SucuriScan::escape( $audit_log['event'] ),
  7099. 'AuditLog.EventTitle' => SucuriScan::escape( ucfirst( $audit_log['event'] ) ),
  7100. 'AuditLog.DateTime' => SucuriScan::datetime( $audit_log['timestamp'] ),
  7101. 'AuditLog.Account' => SucuriScan::escape( $audit_log['account'] ),
  7102. 'AuditLog.Username' => SucuriScan::escape( $audit_log['username'] ),
  7103. 'AuditLog.RemoteAddress' => SucuriScan::escape( $audit_log['remote_addr'] ),
  7104. 'AuditLog.Message' => SucuriScan::escape( $audit_log['message'] ),
  7105. 'AuditLog.Extra' => '',
  7106. );
  7107. // Print every file_list information item in a separate table.
  7108. if ( $audit_log['file_list'] ){
  7109. $css_scrollable = $audit_log['file_list_count'] > 10 ? 'sucuriscan-list-as-table-scrollable' : '';
  7110. $snippet_data['AuditLog.Extra'] .= '<ul class="sucuriscan-list-as-table ' . $css_scrollable . '">';
  7111. foreach ( $audit_log['file_list'] as $log_extra ){
  7112. $snippet_data['AuditLog.Extra'] .= '<li>' . SucuriScan::escape( $log_extra ) . '</li>';
  7113. }
  7114. $snippet_data['AuditLog.Extra'] .= '</ul>';
  7115. }
  7116. $template_variables['AuditLogs.List'] .= SucuriScanTemplate::get_snippet( 'integrity-auditlogs', $snippet_data );
  7117. $counter_i += 1;
  7118. }
  7119. }
  7120. $template_variables['AuditLogs.Count'] = $counter_i;
  7121. $template_variables['AuditLogs.NoItemsVisibility'] = 'hidden';
  7122. if ( $total_items > 1 ){
  7123. $max_pages = ceil( $audit_logs->total_entries / $max_per_page );
  7124. if ( $max_pages > SUCURISCAN_MAX_PAGINATION_BUTTONS ){
  7125. $max_pages = SUCURISCAN_MAX_PAGINATION_BUTTONS;
  7126. }
  7127. if ( $max_pages > 1 ) {
  7128. $template_variables['AuditLogs.PaginationVisibility'] = 'visible';
  7129. $template_variables['AuditLogs.PaginationLinks'] = SucuriScanTemplate::get_pagination(
  7130. '%%SUCURI.URL.Home%%',
  7131. $max_per_page * $max_pages,
  7132. $max_per_page
  7133. );
  7134. }
  7135. }
  7136. }
  7137. return SucuriScanTemplate::get_section( 'integrity-auditlogs', $template_variables );
  7138. }
  7139. /**
  7140. * Print a HTML code with the content of the logs audited by the remote Sucuri
  7141. * API service, this page is part of the monitoring tool.
  7142. *
  7143. * @return void
  7144. */
  7145. function sucuriscan_auditreport(){
  7146. $audit_report = false;
  7147. $logs4report = SucuriScanOption::get_option( ':logs4report' );
  7148. if ( SucuriScanOption::get_option( ':audit_report' ) !== 'disabled' ) {
  7149. $audit_report = SucuriScanAPI::get_audit_report( $logs4report );
  7150. }
  7151. $template_variables = array(
  7152. 'PageTitle' => 'Audit Reports',
  7153. 'AuditReport.EventColors' => '',
  7154. 'AuditReport.EventsPerType' => '',
  7155. 'AuditReport.EventsPerLogin' => '',
  7156. 'AuditReport.EventsPerUserCategories' => '',
  7157. 'AuditReport.EventsPerUserSeries' => '',
  7158. 'AuditReport.EventsPerIPAddressCategories' => '',
  7159. 'AuditReport.EventsPerIPAddressSeries' => '',
  7160. 'AuditReport.Logs4Report' => $logs4report,
  7161. );
  7162. if ( $audit_report ) {
  7163. $template_variables['AuditReport.EventColors'] = @implode( ',', $audit_report['event_colors'] );
  7164. // Generate report chart data for the events per type.
  7165. foreach ( $audit_report['events_per_type'] as $event => $times ) {
  7166. $template_variables['AuditReport.EventsPerType'] .= sprintf(
  7167. "[ '%s', %d ],\n",
  7168. ucwords( $event . "\x20events" ),
  7169. $times
  7170. );
  7171. }
  7172. // Generate report chart data for the events per login.
  7173. foreach ( $audit_report['events_per_login'] as $event => $times ) {
  7174. $template_variables['AuditReport.EventsPerLogin'] .= sprintf(
  7175. "[ '%s', %d ],\n",
  7176. ucwords( $event . "\x20logins" ),
  7177. $times
  7178. );
  7179. }
  7180. // Generate report chart data for the events per user.
  7181. foreach ( $audit_report['events_per_user'] as $event => $times ) {
  7182. $template_variables['AuditReport.EventsPerUserCategories'] .= sprintf( '"%s",', $event );
  7183. $template_variables['AuditReport.EventsPerUserSeries'] .= sprintf( '%d,', $times );
  7184. }
  7185. // Generate report chart data for the events per remote address.
  7186. foreach ( $audit_report['events_per_ipaddress'] as $event => $times ) {
  7187. $template_variables['AuditReport.EventsPerIPAddressCategories'] .= sprintf( '"%s",', $event );
  7188. $template_variables['AuditReport.EventsPerIPAddressSeries'] .= sprintf( '%d,', $times );
  7189. }
  7190. return SucuriScanTemplate::get_section( 'integrity-auditreport', $template_variables );
  7191. }
  7192. return '';
  7193. }
  7194. /**
  7195. * Check whether the WordPress version is outdated or not.
  7196. *
  7197. * @return string Panel with a warning advising that WordPress is outdated.
  7198. */
  7199. function sucuriscan_wordpress_outdated(){
  7200. $site_version = SucuriScan::site_version();
  7201. $updates = get_core_updates();
  7202. $cp = ( ! is_array( $updates ) || empty($updates) ? 1 : 0 );
  7203. $template_variables = array(
  7204. 'WordPress.Version' => $site_version,
  7205. 'WordPress.UpgradeURL' => admin_url( 'update-core.php' ),
  7206. 'WordPress.UpdateVisibility' => 'hidden',
  7207. 'WordPressBeta.Visibility' => 'hidden',
  7208. 'WordPressBeta.Version' => '0.0.0',
  7209. 'WordPressBeta.UpdateURL' => admin_url( 'update-core.php' ),
  7210. 'WordPressBeta.DownloadURL' => '#',
  7211. );
  7212. if ( isset($updates[0]) && $updates[0] instanceof stdClass ){
  7213. if ( $updates[0]->response == 'latest' ){
  7214. $cp = 1;
  7215. }
  7216. elseif ( $updates[0]->response == 'development' ){
  7217. $cp = 1;
  7218. $template_variables['WordPressBeta.Visibility'] = 'visible';
  7219. $template_variables['WordPressBeta.Version'] = $updates[0]->version;
  7220. $template_variables['WordPressBeta.DownloadURL'] = $updates[0]->download;
  7221. }
  7222. }
  7223. if ( strcmp( $site_version, '3.7' ) < 0 ){
  7224. $cp = 0;
  7225. }
  7226. if ( $cp == 0 ){
  7227. $template_variables['WordPress.UpdateVisibility'] = 'visible';
  7228. }
  7229. return SucuriScanTemplate::get_section( 'integrity-wpoutdate', $template_variables );
  7230. }
  7231. /**
  7232. * Compare the md5sum of the core files in the current site with the hashes hosted
  7233. * remotely in Sucuri servers. These hashes are updated every time a new version
  7234. * of WordPress is released.
  7235. *
  7236. * @return void
  7237. */
  7238. function sucuriscan_core_files(){
  7239. $site_version = SucuriScan::site_version();
  7240. $template_variables = array(
  7241. 'CoreFiles.List' => '',
  7242. 'CoreFiles.ListCount' => 0,
  7243. 'CoreFiles.GoodVisibility' => 'visible',
  7244. 'CoreFiles.BadVisibility' => 'hidden',
  7245. );
  7246. if ( $site_version && SucuriScanOption::get_option( ':scan_checksums' ) == 'enabled' ){
  7247. // Check if there are added, removed, or modified files.
  7248. $latest_hashes = sucuriscan_check_core_integrity( $site_version );
  7249. if ( $latest_hashes ){
  7250. $cache = new SucuriScanCache( 'integrity' );
  7251. $ignored_files = $cache->get_all();
  7252. $counter = 0;
  7253. foreach ( $latest_hashes as $list_type => $file_list ){
  7254. if (
  7255. $list_type == 'stable'
  7256. || empty($file_list)
  7257. ){
  7258. continue;
  7259. }
  7260. foreach ( $file_list as $file_path ){
  7261. $full_filepath = sprintf( '%s/%s', rtrim( ABSPATH, '/' ), $file_path );
  7262. // Skip files that were marked as fixed.
  7263. if ( $ignored_files ){
  7264. // Get the checksum of the base file name.
  7265. $file_path_checksum = md5( $file_path );
  7266. if ( array_key_exists( $file_path_checksum, $ignored_files ) ){
  7267. continue;
  7268. }
  7269. }
  7270. // Generate the HTML code from the snippet template for this file.
  7271. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  7272. $file_size = @filesize( $full_filepath );
  7273. $template_variables['CoreFiles.List'] .= SucuriScanTemplate::get_snippet('integrity-corefiles', array(
  7274. 'CoreFiles.CssClass' => $css_class,
  7275. 'CoreFiles.StatusType' => $list_type,
  7276. 'CoreFiles.FilePath' => $file_path,
  7277. 'CoreFiles.FileSize' => $file_size,
  7278. 'CoreFiles.FileSizeHuman' => SucuriScan::human_filesize( $file_size ),
  7279. 'CoreFiles.FileSizeNumber' => number_format( $file_size ),
  7280. ));
  7281. $counter += 1;
  7282. }
  7283. }
  7284. if ( $counter > 0 ){
  7285. $template_variables['CoreFiles.ListCount'] = $counter;
  7286. $template_variables['CoreFiles.GoodVisibility'] = 'hidden';
  7287. $template_variables['CoreFiles.BadVisibility'] = 'visible';
  7288. }
  7289. } else {
  7290. SucuriScanInterface::error( 'Error retrieving the WordPress core hashes, try again.' );
  7291. }
  7292. }
  7293. return SucuriScanTemplate::get_section( 'integrity-corefiles', $template_variables );
  7294. }
  7295. /**
  7296. * Check whether the core WordPress files where modified, removed or if any file
  7297. * was added to the core folders. This function returns an associative array with
  7298. * these keys:
  7299. *
  7300. * <ul>
  7301. * <li>modified: Files with a different checksum according to the official files of the WordPress version filtered,</li>
  7302. * <li>stable: Files with the same checksums than the official files,</li>
  7303. * <li>removed: Official files which are not present in the local project,</li>
  7304. * <li>added: Files present in the local project but not in the official WordPress packages.</li>
  7305. * </ul>
  7306. *
  7307. * @param integer $version Valid version number of the WordPress project.
  7308. * @return array Associative array with these keys: modified, stable, removed, added.
  7309. */
  7310. function sucuriscan_check_core_integrity( $version = 0 ){
  7311. $latest_hashes = SucuriScanAPI::get_official_checksums( $version );
  7312. if ( ! $latest_hashes ){ return false; }
  7313. $output = array(
  7314. 'added' => array(),
  7315. 'removed' => array(),
  7316. 'modified' => array(),
  7317. 'stable' => array(),
  7318. );
  7319. // Get current filesystem tree.
  7320. $wp_top_hashes = sucuriscan_get_integrity_tree( ABSPATH , false );
  7321. $wp_admin_hashes = sucuriscan_get_integrity_tree( ABSPATH . 'wp-admin', true );
  7322. $wp_includes_hashes = sucuriscan_get_integrity_tree( ABSPATH . 'wp-includes', true );
  7323. $wp_core_hashes = array_merge( $wp_top_hashes, $wp_admin_hashes, $wp_includes_hashes );
  7324. // Compare remote and local checksums and search removed files.
  7325. foreach ( $latest_hashes as $file_path => $remote_checksum ){
  7326. if ( sucuriscan_ignore_integrity_filepath( $file_path ) ){ continue; }
  7327. $full_filepath = sprintf( '%s/%s', ABSPATH, $file_path );
  7328. if ( file_exists( $full_filepath ) ){
  7329. $local_checksum = @md5_file( $full_filepath );
  7330. if ( $local_checksum && $local_checksum == $remote_checksum ){
  7331. $output['stable'][] = $file_path;
  7332. } else {
  7333. $output['modified'][] = $file_path;
  7334. }
  7335. } else {
  7336. $output['removed'][] = $file_path;
  7337. }
  7338. }
  7339. // Search added files (files not common in a normal wordpress installation).
  7340. foreach ( $wp_core_hashes as $file_path => $extra_info ){
  7341. $file_path = preg_replace( '/^\.\/(.*)/', '$1', $file_path );
  7342. if ( sucuriscan_ignore_integrity_filepath( $file_path ) ){ continue; }
  7343. if ( ! isset($latest_hashes[ $file_path ]) ){
  7344. $output['added'][] = $file_path;
  7345. }
  7346. }
  7347. return $output;
  7348. }
  7349. /**
  7350. * Ignore irrelevant files and directories from the integrity checking.
  7351. *
  7352. * @param string $file_path File path that will be compared.
  7353. * @return boolean TRUE if the file should be ignored, FALSE otherwise.
  7354. */
  7355. function sucuriscan_ignore_integrity_filepath( $file_path = '' ){
  7356. global $wp_local_package;
  7357. // List of files that will be ignored from the integrity checking.
  7358. $ignore_files = array(
  7359. '^sucuri-[0-9a-z]+\.php$',
  7360. '^favicon\.ico$',
  7361. '^php\.ini$',
  7362. '^\.htaccess$',
  7363. '^wp-includes\/\.htaccess$',
  7364. '^wp-admin\/setup-config\.php$',
  7365. '^wp-(config|pass|rss|feed|register|atom|commentsrss2|rss2|rdf)\.php$',
  7366. '^wp-content\/(themes|plugins)\/.+', // TODO: Add the popular themes/plugins integrity checks.
  7367. '^sitemap\.xml($|\.gz)$',
  7368. '^readme\.html$',
  7369. '^(503|404)\.php$',
  7370. '^500\.(shtml|php)$',
  7371. '^40[0-9]\.shtml$',
  7372. '^([^\/]*)\.(pdf|css|txt)$',
  7373. '^google[0-9a-z]{16}\.html$',
  7374. '^pinterest-[0-9a-z]{5}\.html$',
  7375. '(^|\/)error_log$',
  7376. );
  7377. /**
  7378. * Ignore i18n files.
  7379. *
  7380. * Sites with i18n have differences compared with the official English version
  7381. * of the project, basically they have files with new variables specifying the
  7382. * language that will be used in the admin panel, site options, and emails.
  7383. */
  7384. if (
  7385. isset($wp_local_package)
  7386. && $wp_local_package != 'en_US'
  7387. ){
  7388. $ignore_files[] = 'wp-includes\/version\.php';
  7389. $ignore_files[] = 'wp-config-sample\.php';
  7390. }
  7391. // Determine whether a file must be ignored from the integrity checks or not.
  7392. foreach ( $ignore_files as $ignore_pattern ){
  7393. if ( preg_match( '/'.$ignore_pattern.'/', $file_path ) ){
  7394. return true;
  7395. }
  7396. }
  7397. return false;
  7398. }
  7399. /**
  7400. * List all files inside wp-content that have been modified in the last days.
  7401. *
  7402. * @return void
  7403. */
  7404. function sucuriscan_modified_files(){
  7405. $valid_day_ranges = array( 1, 3, 7, 30, 60 );
  7406. $template_variables = array(
  7407. 'ModifiedFiles.List' => '',
  7408. 'ModifiedFiles.SelectOptions' => '',
  7409. 'ModifiedFiles.NoFilesVisibility' => 'visible',
  7410. 'ModifiedFiles.DisabledVisibility' => 'hidden',
  7411. 'ModifiedFiles.Days' => 0,
  7412. );
  7413. // Find files modified in the last days.
  7414. $back_days = SucuriScanRequest::post( ':last_days', '[0-9]+' );
  7415. if ( $back_days !== false ) {
  7416. if ( $back_days <= 0 ){ $back_days = 1; }
  7417. elseif ( $back_days >= 60 ){ $back_days = 60; }
  7418. } else {
  7419. $back_days = 7;
  7420. }
  7421. // Fix data type for the back days variable.
  7422. $back_days = intval( $back_days );
  7423. $template_variables['ModifiedFiles.Days'] = $back_days;
  7424. // Generate the options for the select field of the page form.
  7425. foreach ( $valid_day_ranges as $day ){
  7426. $selected_option = ($back_days == $day) ? 'selected="selected"' : '';
  7427. $template_variables['ModifiedFiles.SelectOptions'] .= sprintf(
  7428. '<option value="%d" %s>%d</option>',
  7429. $day, $selected_option, $day
  7430. );
  7431. }
  7432. // The scanner for modified files can be disabled from the settings page.
  7433. if ( SucuriScanOption::get_option( ':scan_modfiles' ) == 'enabled' ){
  7434. // Search modified files among the project's files.
  7435. $content_hashes = sucuriscan_get_integrity_tree( ABSPATH.'wp-content', true );
  7436. if ( ! empty($content_hashes) ){
  7437. $back_days = current_time( 'timestamp' ) - ( $back_days * 86400);
  7438. $counter = 0;
  7439. foreach ( $content_hashes as $file_path => $file_info ){
  7440. if (
  7441. isset($file_info['modified_at'])
  7442. && $file_info['modified_at'] >= $back_days
  7443. ){
  7444. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  7445. $mod_date = SucuriScan::datetime( $file_info['modified_at'] );
  7446. $template_variables['ModifiedFiles.List'] .= SucuriScanTemplate::get_snippet('integrity-modifiedfiles', array(
  7447. 'ModifiedFiles.CssClass' => $css_class,
  7448. 'ModifiedFiles.CheckSum' => $file_info['checksum'],
  7449. 'ModifiedFiles.FilePath' => $file_path,
  7450. 'ModifiedFiles.DateTime' => $mod_date,
  7451. 'ModifiedFiles.FileSize' => $file_info['filesize'],
  7452. 'ModifiedFiles.FileSizeHuman' => SucuriScan::human_filesize( $file_info['filesize'] ),
  7453. 'ModifiedFiles.FileSizeNumber' => number_format( $file_info['filesize'] ),
  7454. ));
  7455. $counter += 1;
  7456. }
  7457. }
  7458. if ( $counter > 0 ){
  7459. $template_variables['ModifiedFiles.NoFilesVisibility'] = 'hidden';
  7460. }
  7461. }
  7462. }
  7463. else {
  7464. $template_variables['ModifiedFiles.DisabledVisibility'] = 'visible';
  7465. }
  7466. return SucuriScanTemplate::get_section( 'integrity-modifiedfiles', $template_variables );
  7467. }
  7468. /**
  7469. * Generate and print the HTML code for the Post-Hack page.
  7470. *
  7471. * @return void
  7472. */
  7473. function sucuriscan_posthack_page(){
  7474. SucuriScanInterface::check_permissions();
  7475. $process_form = sucuriscan_posthack_process_form();
  7476. // Page pseudo-variables initialization.
  7477. $template_variables = array(
  7478. 'PageTitle' => 'Post-Hack',
  7479. 'UpdateSecretKeys' => sucuriscan_update_secret_keys( $process_form ),
  7480. 'ResetPassword' => sucuriscan_posthack_users( $process_form ),
  7481. 'ResetPlugins' => sucuriscan_posthack_plugins( $process_form ),
  7482. );
  7483. echo SucuriScanTemplate::get_template( 'posthack', $template_variables );
  7484. }
  7485. /**
  7486. * Check whether the "I understand this operation" checkbox was marked or not.
  7487. *
  7488. * @return boolean TRUE if a form submission should be processed, FALSE otherwise.
  7489. */
  7490. function sucuriscan_posthack_process_form(){
  7491. $process_form = SucuriScanRequest::post( ':process_form', '(0|1)' );
  7492. if (
  7493. SucuriScanInterface::check_nonce()
  7494. && $process_form !== false
  7495. ){
  7496. if ( $process_form === '1' ){
  7497. return true;
  7498. } else {
  7499. SucuriScanInterface::error( 'You need to confirm that you understand the risk of this operation.' );
  7500. }
  7501. }
  7502. return false;
  7503. }
  7504. /**
  7505. * Update the WordPress secret keys.
  7506. *
  7507. * @param $process_form Whether a form was submitted or not.
  7508. * @return string HTML code with the information of the process.
  7509. */
  7510. function sucuriscan_update_secret_keys( $process_form = false ){
  7511. $template_variables = array(
  7512. 'WPConfigUpdate.Visibility' => 'hidden',
  7513. 'WPConfigUpdate.NewConfig' => '',
  7514. 'SecurityKeys.List' => '',
  7515. );
  7516. // Update all WordPress secret keys.
  7517. if ( $process_form && SucuriScanRequest::post( ':update_wpconfig', '1' ) ){
  7518. $wpconfig_process = SucuriScanEvent::set_new_config_keys();
  7519. if ( $wpconfig_process ){
  7520. $template_variables['WPConfigUpdate.Visibility'] = 'visible';
  7521. SucuriScanEvent::report_notice_event( 'Generate new security keys' );
  7522. if ( $wpconfig_process['updated'] === true ){
  7523. SucuriScanInterface::info( 'Secret keys updated successfully (summary of the operation bellow).' );
  7524. $template_variables['WPConfigUpdate.NewConfig'] .= "// Old Keys\n";
  7525. $template_variables['WPConfigUpdate.NewConfig'] .= $wpconfig_process['old_keys_string'];
  7526. $template_variables['WPConfigUpdate.NewConfig'] .= "//\n";
  7527. $template_variables['WPConfigUpdate.NewConfig'] .= "// New Keys\n";
  7528. $template_variables['WPConfigUpdate.NewConfig'] .= $wpconfig_process['new_keys_string'];
  7529. } else {
  7530. SucuriScanInterface::error(
  7531. '<code>wp-config.php</code> file is not writable, replace the '
  7532. . 'old configuration file with the new values shown bellow.'
  7533. );
  7534. $template_variables['WPConfigUpdate.NewConfig'] = $wpconfig_process['new_wpconfig'];
  7535. }
  7536. } else {
  7537. SucuriScanInterface::error( '<code>wp-config.php</code> file was not found in the default location.' );
  7538. }
  7539. }
  7540. // Display the current status of the security keys.
  7541. $current_keys = SucuriScanOption::get_security_keys();
  7542. $counter = 0;
  7543. foreach ( $current_keys as $key_status => $key_list ){
  7544. foreach ( $key_list as $key_name => $key_value ){
  7545. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  7546. $key_value = SucuriScan::excerpt( $key_value, 50 );
  7547. switch ( $key_status ){
  7548. case 'good':
  7549. $key_status_text = 'good';
  7550. $key_status_css_class = 'success';
  7551. break;
  7552. case 'bad':
  7553. $key_status_text = 'not randomized';
  7554. $key_status_css_class = 'warning';
  7555. break;
  7556. case 'missing':
  7557. $key_value = '';
  7558. $key_status_text = 'not set';
  7559. $key_status_css_class = 'danger';
  7560. break;
  7561. }
  7562. if ( isset($key_status_text) ){
  7563. $template_variables['SecurityKeys.List'] .= SucuriScanTemplate::get_snippet('posthack-updatesecretkeys', array(
  7564. 'SecurityKey.CssClass' => $css_class,
  7565. 'SecurityKey.KeyName' => SucuriScan::escape( $key_name ),
  7566. 'SecurityKey.KeyValue' => SucuriScan::escape( $key_value ),
  7567. 'SecurityKey.KeyStatusText' => $key_status_text,
  7568. 'SecurityKey.KeyStatusCssClass' => $key_status_css_class,
  7569. ));
  7570. $counter += 1;
  7571. }
  7572. }
  7573. }
  7574. return SucuriScanTemplate::get_section( 'posthack-updatesecretkeys', $template_variables );
  7575. }
  7576. /**
  7577. * Display a list of users in a table that will be used to select the accounts
  7578. * where a password reset action will be executed.
  7579. *
  7580. * @param $process_form Whether a form was submitted or not.
  7581. * @return string HTML code for a table where a list of user accounts will be shown.
  7582. */
  7583. function sucuriscan_posthack_users( $process_form = false ){
  7584. $template_variables = array(
  7585. 'ResetPassword.UserList' => '',
  7586. 'ResetPassword.PaginationLinks' => '',
  7587. 'ResetPassword.PaginationVisibility' => 'hidden',
  7588. );
  7589. // Process the form submission (if any).
  7590. sucuriscan_reset_user_password( $process_form );
  7591. // Fill the user list for ResetPassword action.
  7592. $user_list = false;
  7593. $page_number = SucuriScanTemplate::get_page_number();
  7594. $max_per_page = SUCURISCAN_MAX_PAGINATION_BUTTONS;
  7595. $dbquery = new WP_User_Query( array(
  7596. 'number' => $max_per_page,
  7597. 'offset' => ( $page_number - 1 ) * $max_per_page,
  7598. 'fields' => 'all_with_meta',
  7599. 'orderby' => 'ID',
  7600. ) );
  7601. // Retrieve the results and build the pagination links.
  7602. if ( $dbquery ) {
  7603. $total_items = $dbquery->get_total();
  7604. $user_list = $dbquery->get_results();
  7605. $template_variables['ResetPassword.PaginationLinks'] = SucuriScanTemplate::get_pagination(
  7606. '%%SUCURI.URL.Posthack%%#reset-users-password',
  7607. $total_items,
  7608. $max_per_page
  7609. );
  7610. if ( $total_items > SUCURISCAN_MAX_PAGINATION_BUTTONS ) {
  7611. $template_variables['ResetPassword.PaginationVisibility'] = 'visible';
  7612. }
  7613. }
  7614. if ( $user_list !== false ) {
  7615. $counter = 0;
  7616. foreach ( $user_list as $user ){
  7617. $user->user_registered_timestamp = strtotime( $user->user_registered );
  7618. $user->user_registered_formatted = SucuriScan::datetime( $user->user_registered_timestamp );
  7619. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  7620. $display_username = ( $user->user_login != $user->display_name )
  7621. ? sprintf( '%s (%s)', $user->user_login, $user->display_name )
  7622. : $user->user_login;
  7623. $template_variables['ResetPassword.UserList'] .= SucuriScanTemplate::get_snippet('posthack-resetpassword', array(
  7624. 'ResetPassword.UserId' => $user->ID,
  7625. 'ResetPassword.Username' => SucuriScan::escape( $user->user_login ),
  7626. 'ResetPassword.Displayname' => SucuriScan::escape( $user->display_name ),
  7627. 'ResetPassword.DisplayUsername' => SucuriScan::escape( $display_username ),
  7628. 'ResetPassword.Email' => SucuriScan::escape( $user->user_email ),
  7629. 'ResetPassword.Registered' => $user->user_registered_formatted,
  7630. 'ResetPassword.Roles' => @implode( ', ', $user->roles ),
  7631. 'ResetPassword.CssClass' => $css_class,
  7632. ));
  7633. $counter += 1;
  7634. }
  7635. }
  7636. return SucuriScanTemplate::get_section( 'posthack-resetpassword', $template_variables );
  7637. }
  7638. /**
  7639. * Update the password of the user accounts specified.
  7640. *
  7641. * @param $process_form Whether a form was submitted or not.
  7642. * @return void
  7643. */
  7644. function sucuriscan_reset_user_password( $process_form = false ){
  7645. if ( $process_form && SucuriScanRequest::post( ':reset_password' ) ){
  7646. $user_identifiers = SucuriScanRequest::post( 'user_ids', '_array' );
  7647. $pwd_changed = array();
  7648. $pwd_not_changed = array();
  7649. if ( is_array( $user_identifiers ) && ! empty($user_identifiers) ){
  7650. arsort( $user_identifiers );
  7651. foreach ( $user_identifiers as $user_id ){
  7652. if ( SucuriScanEvent::set_new_password( $user_id ) ){
  7653. $pwd_changed[] = $user_id;
  7654. } else {
  7655. $pwd_not_changed[] = $user_id;
  7656. }
  7657. }
  7658. if ( ! empty($pwd_changed) ){
  7659. $message = 'Password changed for user identifiers <code>' . @implode( ', ',$pwd_changed ) . '</code>';
  7660. SucuriScanEvent::report_notice_event( $message );
  7661. SucuriScanInterface::info( $message );
  7662. }
  7663. if ( ! empty($pwd_not_changed) ){
  7664. SucuriScanInterface::error( 'Password change failed for users: ' . implode( ', ',$pwd_not_changed ) );
  7665. }
  7666. } else {
  7667. SucuriScanInterface::error( 'You did not select a user from the list.' );
  7668. }
  7669. }
  7670. }
  7671. /**
  7672. * Reset all the FREE plugins, even if they are not activated.
  7673. *
  7674. * @param boolean $process_form Whether a form was submitted or not.
  7675. * @return void
  7676. */
  7677. function sucuriscan_posthack_plugins( $process_form = false ){
  7678. $template_variables = array(
  7679. 'ResetPlugin.PluginList' => '',
  7680. );
  7681. sucuriscan_posthack_reinstall_plugins( $process_form );
  7682. $all_plugins = SucuriScanAPI::get_plugins();
  7683. $counter = 0;
  7684. foreach ( $all_plugins as $plugin_path => $plugin_data ){
  7685. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  7686. $plugin_type_class = ( $plugin_data['PluginType'] == 'free' ) ? 'primary' : 'warning';
  7687. $input_disabled = ( $plugin_data['PluginType'] == 'free' ) ? '' : 'disabled="disabled"';
  7688. $plugin_status = $plugin_data['IsPluginActive'] ? 'active' : 'not active';
  7689. $plugin_status_class = $plugin_data['IsPluginActive'] ? 'success' : 'default';
  7690. $template_variables['ResetPlugin.PluginList'] .= SucuriScanTemplate::get_snippet('posthack-resetplugins', array(
  7691. 'ResetPlugin.CssClass' => $css_class,
  7692. 'ResetPlugin.Disabled' => $input_disabled,
  7693. 'ResetPlugin.PluginPath' => SucuriScan::escape( $plugin_path ),
  7694. 'ResetPlugin.Plugin' => SucuriScan::excerpt( $plugin_data['Name'], 35 ),
  7695. 'ResetPlugin.Version' => $plugin_data['Version'],
  7696. 'ResetPlugin.Type' => $plugin_data['PluginType'],
  7697. 'ResetPlugin.TypeClass' => $plugin_type_class,
  7698. 'ResetPlugin.Status' => $plugin_status,
  7699. 'ResetPlugin.StatusClass' => $plugin_status_class,
  7700. ));
  7701. $counter += 1;
  7702. }
  7703. return SucuriScanTemplate::get_section( 'posthack-resetplugins', $template_variables );
  7704. }
  7705. /**
  7706. * Process the request that will start the execution of the plugin
  7707. * reinstallation, it will check if the plugins submitted are (in fact)
  7708. * installed in the system, then check if they are free download from the
  7709. * WordPress market place, and finally download and install them.
  7710. *
  7711. * @param boolean $process_form Whether a form was submitted or not.
  7712. * @return void
  7713. */
  7714. function sucuriscan_posthack_reinstall_plugins( $process_form = false ){
  7715. if ( $process_form && isset($_POST['sucuriscan_reset_plugins']) ){
  7716. include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
  7717. include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); // For plugins_api.
  7718. if ( $plugin_list = SucuriScanRequest::post( 'plugin_path', '_array' ) ){
  7719. // Create an instance of the FileInfo interface.
  7720. $sucuri_fileinfo = new SucuriScanFileInfo();
  7721. $sucuri_fileinfo->ignore_files = false;
  7722. $sucuri_fileinfo->ignore_directories = false;
  7723. // Get (possible) cached information from the installed plugins.
  7724. $all_plugins = SucuriScanAPI::get_plugins();
  7725. // Loop through all the installed plugins.
  7726. foreach ( $_POST['plugin_path'] as $plugin_path ){
  7727. if ( array_key_exists( $plugin_path, $all_plugins ) ){
  7728. $plugin_data = $all_plugins[ $plugin_path ];
  7729. // Check if the plugin can be downloaded from the free market.
  7730. if ( $plugin_data['IsFreePlugin'] === true ){
  7731. $plugin_info = SucuriScanAPI::get_remote_plugin_data( $plugin_data['RepositoryName'] );
  7732. if ( $plugin_info ){
  7733. // First, remove all files/sub-folders from the plugin's directory.
  7734. if ( substr_count( $plugin_path, '/' ) >= 1 ) {
  7735. $plugin_directory = dirname( WP_PLUGIN_DIR . '/' . $plugin_path );
  7736. $sucuri_fileinfo->remove_directory_tree( $plugin_directory );
  7737. }
  7738. // Install a fresh copy of the plugin's files.
  7739. $upgrader_skin = new Plugin_Installer_Skin();
  7740. $upgrader = new Plugin_Upgrader( $upgrader_skin );
  7741. $upgrader->install( $plugin_info->download_link );
  7742. SucuriScanEvent::report_notice_event( 'Plugin re-installed: ' . $plugin_path );
  7743. } else {
  7744. SucuriScanInterface::error( 'Could not establish a stable connection with the WordPress plugins market.' );
  7745. }
  7746. }
  7747. }
  7748. }
  7749. } else {
  7750. SucuriScanInterface::error( 'You did not select a free plugin to reinstall.' );
  7751. }
  7752. }
  7753. }
  7754. /**
  7755. * Generate and print the HTML code for the Last Logins page.
  7756. *
  7757. * This page will contains information of all the logins of the registered users.
  7758. *
  7759. * @return string Last-logings for the administrator accounts.
  7760. */
  7761. function sucuriscan_lastlogins_page(){
  7762. SucuriScanInterface::check_permissions();
  7763. // Reset the file with the last-logins logs.
  7764. if (
  7765. SucuriScanInterface::check_nonce()
  7766. && SucuriScanRequest::post( ':reset_lastlogins' ) !== false
  7767. ){
  7768. $file_path = sucuriscan_lastlogins_datastore_filepath();
  7769. if ( unlink( $file_path ) ){
  7770. sucuriscan_lastlogins_datastore_exists();
  7771. SucuriScanInterface::info( 'Last-Logins logs were reset successfully.' );
  7772. } else {
  7773. SucuriScanInterface::error( 'Could not reset the last-logins logs.' );
  7774. }
  7775. }
  7776. // Page pseudo-variables initialization.
  7777. $template_variables = array(
  7778. 'PageTitle' => 'Last Logins',
  7779. 'LastLogins.Admins' => sucuriscan_lastlogins_admins(),
  7780. 'LastLogins.AllUsers' => sucuriscan_lastlogins_all(),
  7781. 'LoggedInUsers' => sucuriscan_loggedin_users_panel(),
  7782. 'FailedLogins' => sucuriscan_failed_logins_panel(),
  7783. );
  7784. echo SucuriScanTemplate::get_template( 'lastlogins', $template_variables );
  7785. }
  7786. /**
  7787. * List all the user administrator accounts.
  7788. *
  7789. * @see http://codex.wordpress.org/Class_Reference/WP_User_Query
  7790. *
  7791. * @return void
  7792. */
  7793. function sucuriscan_lastlogins_admins(){
  7794. // Page pseudo-variables initialization.
  7795. $template_variables = array(
  7796. 'AdminUsers.List' => '',
  7797. );
  7798. $user_query = new WP_User_Query( array( 'role' => 'Administrator' ) );
  7799. $admins = $user_query->get_results();
  7800. foreach ( (array) $admins as $admin ){
  7801. $last_logins = sucuriscan_get_logins( 5, 0, $admin->ID );
  7802. $admin->lastlogins = $last_logins['entries'];
  7803. $user_snippet = array(
  7804. 'AdminUsers.Username' => SucuriScan::escape( $admin->user_login ),
  7805. 'AdminUsers.Email' => SucuriScan::escape( $admin->user_email ),
  7806. 'AdminUsers.LastLogins' => '',
  7807. 'AdminUsers.RegisteredAt' => 'Undefined',
  7808. 'AdminUsers.UserURL' => admin_url( 'user-edit.php?user_id='.$admin->ID ),
  7809. 'AdminUsers.NoLastLogins' => 'visible',
  7810. 'AdminUsers.NoLastLoginsTable' => 'hidden',
  7811. );
  7812. if ( ! empty($admin->lastlogins) ){
  7813. $user_snippet['AdminUsers.NoLastLogins'] = 'hidden';
  7814. $user_snippet['AdminUsers.NoLastLoginsTable'] = 'visible';
  7815. $user_snippet['AdminUsers.RegisteredAt'] = 'Unknown';
  7816. $counter = 0;
  7817. foreach ( $admin->lastlogins as $i => $lastlogin ){
  7818. if ( $i == 0 ){
  7819. $user_snippet['AdminUsers.RegisteredAt'] = SucuriScan::datetime( $lastlogin->user_registered_timestamp );
  7820. }
  7821. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  7822. $user_snippet['AdminUsers.LastLogins'] .= SucuriScanTemplate::get_snippet('lastlogins-admins-lastlogin', array(
  7823. 'AdminUsers.RemoteAddr' => SucuriScan::escape( $lastlogin->user_remoteaddr ),
  7824. 'AdminUsers.Datetime' => SucuriScan::datetime( $lastlogin->user_lastlogin_timestamp ),
  7825. 'AdminUsers.CssClass' => $css_class,
  7826. ));
  7827. $counter += 1;
  7828. }
  7829. }
  7830. $template_variables['AdminUsers.List'] .= SucuriScanTemplate::get_snippet( 'lastlogins-admins', $user_snippet );
  7831. }
  7832. return SucuriScanTemplate::get_section( 'lastlogins-admins', $template_variables );
  7833. }
  7834. /**
  7835. * List the last-logins for all user accounts in the site.
  7836. *
  7837. * This page will contains information of all the logins of the registered users.
  7838. *
  7839. * @return string Last-logings for all user accounts.
  7840. */
  7841. function sucuriscan_lastlogins_all(){
  7842. $max_per_page = SUCURISCAN_LASTLOGINS_USERSLIMIT;
  7843. $page_number = SucuriScanTemplate::get_page_number();
  7844. $offset = ($max_per_page * $page_number) - $max_per_page;
  7845. $template_variables = array(
  7846. 'UserList' => '',
  7847. 'UserList.Limit' => $max_per_page,
  7848. 'UserList.Total' => 0,
  7849. 'UserList.Pagination' => '',
  7850. 'UserList.PaginationVisibility' => 'hidden',
  7851. 'UserList.NoItemsVisibility' => 'visible',
  7852. );
  7853. if ( ! sucuriscan_lastlogins_datastore_is_writable() ){
  7854. SucuriScanInterface::error( 'Last-logins datastore file is not writable: <code>'.sucuriscan_lastlogins_datastore_filepath().'</code>' );
  7855. }
  7856. $counter = 0;
  7857. $last_logins = sucuriscan_get_logins( $max_per_page, $offset );
  7858. $template_variables['UserList.Total'] = $last_logins['total'];
  7859. if ( $last_logins['total'] > $max_per_page ){
  7860. $template_variables['UserList.PaginationVisibility'] = 'visible';
  7861. }
  7862. if ( $last_logins['total'] > 0 ){
  7863. $template_variables['UserList.NoItemsVisibility'] = 'hidden';
  7864. }
  7865. foreach ( $last_logins['entries'] as $user ){
  7866. $counter += 1;
  7867. $css_class = ( $counter % 2 == 0 ) ? 'alternate' : '';
  7868. $user_dataset = array(
  7869. 'UserList.Number' => $user->line_num,
  7870. 'UserList.UserId' => $user->user_id,
  7871. 'UserList.Username' => '<em>Unknown</em>',
  7872. 'UserList.Displayname' => '',
  7873. 'UserList.Email' => '',
  7874. 'UserList.Registered' => '',
  7875. 'UserList.RemoteAddr' => SucuriScan::escape( $user->user_remoteaddr ),
  7876. 'UserList.Hostname' => SucuriScan::escape( $user->user_hostname ),
  7877. 'UserList.Datetime' => SucuriScan::escape( $user->user_lastlogin ),
  7878. 'UserList.TimeAgo' => SucuriScan::time_ago( $user->user_lastlogin ),
  7879. 'UserList.UserURL' => admin_url( 'user-edit.php?user_id='.$user->user_id ),
  7880. 'UserList.CssClass' => $css_class,
  7881. );
  7882. if ( $user->user_exists ){
  7883. $user_dataset['UserList.Username'] = SucuriScan::escape( $user->user_login );
  7884. $user_dataset['UserList.Displayname'] = SucuriScan::escape( $user->display_name );
  7885. $user_dataset['UserList.Email'] = SucuriScan::escape( $user->user_email );
  7886. $user_dataset['UserList.Registered'] = SucuriScan::escape( $user->user_registered );
  7887. }
  7888. $template_variables['UserList'] .= SucuriScanTemplate::get_snippet( 'lastlogins-all', $user_dataset );
  7889. }
  7890. // Generate the pagination for the list.
  7891. $template_variables['UserList.Pagination'] = SucuriScanTemplate::get_pagination(
  7892. '%%SUCURI.URL.Lastlogins%%',
  7893. $last_logins['total'],
  7894. $max_per_page
  7895. );
  7896. return SucuriScanTemplate::get_section( 'lastlogins-all', $template_variables );
  7897. }
  7898. /**
  7899. * Get the filepath where the information of the last logins of all users is stored.
  7900. *
  7901. * @return string Absolute filepath where the user's last login information is stored.
  7902. */
  7903. function sucuriscan_lastlogins_datastore_filepath(){
  7904. return SucuriScan::datastore_folder_path( 'sucuri-lastlogins.php' );
  7905. }
  7906. /**
  7907. * Check whether the user's last login datastore file exists or not, if not then
  7908. * we try to create the file and check again the success of the operation.
  7909. *
  7910. * @return string Absolute filepath where the user's last login information is stored.
  7911. */
  7912. function sucuriscan_lastlogins_datastore_exists(){
  7913. $datastore_filepath = sucuriscan_lastlogins_datastore_filepath();
  7914. if ( ! file_exists( $datastore_filepath ) ){
  7915. if ( @file_put_contents( $datastore_filepath, "<?php exit(0); ?>\n", LOCK_EX ) ){
  7916. @chmod( $datastore_filepath, 0644 );
  7917. }
  7918. }
  7919. return file_exists( $datastore_filepath ) ? $datastore_filepath : false;
  7920. }
  7921. /**
  7922. * Check whether the user's last login datastore file is writable or not, if not
  7923. * we try to set the right permissions and check again the success of the operation.
  7924. *
  7925. * @return boolean Whether the user's last login datastore file is writable or not.
  7926. */
  7927. function sucuriscan_lastlogins_datastore_is_writable(){
  7928. $datastore_filepath = sucuriscan_lastlogins_datastore_exists();
  7929. if ( $datastore_filepath ){
  7930. if ( ! is_writable( $datastore_filepath ) ){
  7931. @chmod( $datastore_filepath, 0644 );
  7932. }
  7933. if ( is_writable( $datastore_filepath ) ){
  7934. return $datastore_filepath;
  7935. }
  7936. }
  7937. return false;
  7938. }
  7939. /**
  7940. * Check whether the user's last login datastore file is readable or not, if not
  7941. * we try to set the right permissions and check again the success of the operation.
  7942. *
  7943. * @return boolean Whether the user's last login datastore file is readable or not.
  7944. */
  7945. function sucuriscan_lastlogins_datastore_is_readable(){
  7946. $datastore_filepath = sucuriscan_lastlogins_datastore_exists();
  7947. if ( $datastore_filepath && is_readable( $datastore_filepath ) ){
  7948. return $datastore_filepath;
  7949. }
  7950. return false;
  7951. }
  7952. if ( ! function_exists( 'sucuri_set_lastlogin' ) ){
  7953. /**
  7954. * Add a new user session to the list of last user logins.
  7955. *
  7956. * @param string $user_login The name of the user account involved in the operation.
  7957. * @return void
  7958. */
  7959. function sucuriscan_set_lastlogin( $user_login = '' ){
  7960. $datastore_filepath = sucuriscan_lastlogins_datastore_is_writable();
  7961. if ( $datastore_filepath ){
  7962. $current_user = get_user_by( 'login', $user_login );
  7963. $remote_addr = SucuriScan::get_remote_addr();
  7964. $login_info = array(
  7965. 'user_id' => $current_user->ID,
  7966. 'user_login' => $current_user->user_login,
  7967. 'user_remoteaddr' => $remote_addr,
  7968. 'user_hostname' => @gethostbyaddr( $remote_addr ),
  7969. 'user_lastlogin' => current_time( 'mysql' )
  7970. );
  7971. @file_put_contents( $datastore_filepath, json_encode( $login_info )."\n", FILE_APPEND );
  7972. }
  7973. }
  7974. add_action( 'wp_login', 'sucuriscan_set_lastlogin', 50 );
  7975. }
  7976. /**
  7977. * Retrieve the list of all the user logins from the datastore file.
  7978. *
  7979. * The results of this operation can be filtered by specific user identifiers,
  7980. * or limiting the quantity of entries.
  7981. *
  7982. * @param integer $limit How many entries will be returned from the operation.
  7983. * @param integer $offset Initial point where the logs will be start counting.
  7984. * @param integer $user_id Optional user identifier to filter the results.
  7985. * @return array The list of all the user logins, and total of entries registered.
  7986. */
  7987. function sucuriscan_get_logins( $limit = 10, $offset = 0, $user_id = 0 ){
  7988. $datastore_filepath = sucuriscan_lastlogins_datastore_is_readable();
  7989. $last_logins = array(
  7990. 'total' => 0,
  7991. 'entries' => array(),
  7992. );
  7993. if ( $datastore_filepath ){
  7994. $parsed_lines = 0;
  7995. $data_lines = SucuriScanFileInfo::file_lines( $datastore_filepath );
  7996. if ( $data_lines ){
  7997. /**
  7998. * This count will not be 100% accurate considering that we are checking the
  7999. * syntax of each line in the loop bellow, there may be some lines without the
  8000. * right syntax which will differ from the total entries returned, but there's
  8001. * not other EASY way to do this without affect the performance of the code.
  8002. *
  8003. * @var integer
  8004. */
  8005. $total_lines = count( $data_lines );
  8006. $last_logins['total'] = $total_lines;
  8007. // Get a list with the latest entries in the first positions.
  8008. $reversed_lines = array_reverse( $data_lines );
  8009. /**
  8010. * Only the user accounts with administrative privileges can see the logs of all
  8011. * the users, for the rest of the accounts they will only see their own logins.
  8012. *
  8013. * @var object
  8014. */
  8015. $current_user = wp_get_current_user();
  8016. $is_admin_user = (bool) current_user_can( 'manage_options' );
  8017. for ( $i = $offset; $i < $total_lines; $i++ ){
  8018. $line = $reversed_lines[ $i ] ? trim( $reversed_lines[ $i ] ) : '';
  8019. // Check if the data is serialized (which we will consider as insecure).
  8020. if ( SucuriScan::is_serialized( $line ) ){
  8021. $last_login = @unserialize( $line ); // TODO: Remove after version 1.7.5
  8022. } else {
  8023. $last_login = @json_decode( $line, true );
  8024. }
  8025. if ( $last_login ){
  8026. $last_login['user_lastlogin_timestamp'] = strtotime( $last_login['user_lastlogin'] );
  8027. $last_login['user_registered_timestamp'] = 0;
  8028. // Only administrators can see all login stats.
  8029. if ( ! $is_admin_user && $current_user->user_login != $last_login['user_login'] ){
  8030. continue;
  8031. }
  8032. // Filter the user identifiers using the value passed tot his function.
  8033. if ( $user_id > 0 && $last_login['user_id'] != $user_id ){
  8034. continue;
  8035. }
  8036. // Get the WP_User object and add extra information from the last-login data.
  8037. $last_login['user_exists'] = false;
  8038. $user_account = get_userdata( $last_login['user_id'] );
  8039. if ( $user_account ){
  8040. $last_login['user_exists'] = true;
  8041. foreach ( $user_account->data as $var_name => $var_value ){
  8042. $last_login[ $var_name ] = $var_value;
  8043. if ( $var_name == 'user_registered' ){
  8044. $last_login['user_registered_timestamp'] = strtotime( $var_value );
  8045. }
  8046. }
  8047. }
  8048. $last_login['line_num'] = $i + 1;
  8049. $last_logins['entries'][] = (object) $last_login;
  8050. $parsed_lines += 1;
  8051. }
  8052. else {
  8053. $last_logins['total'] -= 1;
  8054. }
  8055. if ( preg_match( '/^[0-9]+$/', $limit ) && $limit > 0 ){
  8056. if ( $parsed_lines >= $limit ){ break; }
  8057. }
  8058. }
  8059. }
  8060. }
  8061. return $last_logins;
  8062. }
  8063. if ( ! function_exists( 'sucuri_login_redirect' ) ){
  8064. /**
  8065. * Hook for the wp-login action to redirect the user to a specific URL after
  8066. * his successfully login to the administrator interface.
  8067. *
  8068. * @param string $redirect_to URL where the browser must be originally redirected to, set by WordPress itself.
  8069. * @param object $request Optional parameter set by WordPress itself through the event triggered.
  8070. * @param boolean $user WordPress user object with the information of the account involved in the operation.
  8071. * @return string URL where the browser must be redirected to.
  8072. */
  8073. function sucuriscan_login_redirect( $redirect_to = '', $request = null, $user = false ){
  8074. $login_url = ! empty($redirect_to) ? $redirect_to : admin_url();
  8075. if ( $user instanceof WP_User && $user->ID ){
  8076. $login_url = add_query_arg( 'sucuriscan_lastlogin', 1, $login_url );
  8077. }
  8078. return $login_url;
  8079. }
  8080. if ( SucuriScanOption::get_option( ':lastlogin_redirection' ) == 'enabled' ){
  8081. add_filter( 'login_redirect', 'sucuriscan_login_redirect', 10, 3 );
  8082. }
  8083. }
  8084. if ( ! function_exists( 'sucuri_get_user_lastlogin' ) ){
  8085. /**
  8086. * Display the last user login at the top of the admin interface.
  8087. *
  8088. * @return void
  8089. */
  8090. function sucuriscan_get_user_lastlogin(){
  8091. if (
  8092. current_user_can( 'manage_options' )
  8093. && SucuriScanRequest::get( ':lastlogin', '1' )
  8094. ){
  8095. $current_user = wp_get_current_user();
  8096. // Select the penultimate entry, not the last one.
  8097. $last_logins = sucuriscan_get_logins( 2, 0, $current_user->ID );
  8098. if ( isset($last_logins['entries'][1]) ){
  8099. $row = $last_logins['entries'][1];
  8100. $lastlogin_message = sprintf(
  8101. 'Last time you logged in was at <code>%s</code> from <code>%s</code> - <code>%s</code>',
  8102. SucuriScan::datetime( $row->user_lastlogin_timestamp ),
  8103. $row->user_remoteaddr,
  8104. $row->user_hostname
  8105. );
  8106. $lastlogin_message .= chr( 32 ).'(<a href="'.SucuriScanTemplate::get_url( 'lastlogins' ).'">view all logs</a>)';
  8107. SucuriScanInterface::info( $lastlogin_message );
  8108. }
  8109. }
  8110. }
  8111. add_action( 'admin_notices', 'sucuriscan_get_user_lastlogin' );
  8112. }
  8113. /**
  8114. * Print a list of all the registered users that are currently in session.
  8115. *
  8116. * @return string The HTML code displaying a list of all the users logged in at the moment.
  8117. */
  8118. function sucuriscan_loggedin_users_panel(){
  8119. // Get user logged in list.
  8120. $template_variables = array(
  8121. 'LoggedInUsers.List' => '',
  8122. 'LoggedInUsers.Total' => 0,
  8123. );
  8124. $logged_in_users = sucuriscan_get_online_users( true );
  8125. if ( is_array( $logged_in_users ) && ! empty($logged_in_users) ){
  8126. $template_variables['LoggedInUsers.Total'] = count( $logged_in_users );
  8127. $counter = 0;
  8128. foreach ( (array) $logged_in_users as $logged_in_user ){
  8129. $counter += 1;
  8130. $logged_in_user['last_activity_datetime'] = SucuriScan::datetime( $logged_in_user['last_activity'] );
  8131. $logged_in_user['user_registered_datetime'] = SucuriScan::datetime( strtotime( $logged_in_user['user_registered'] ) );
  8132. $template_variables['LoggedInUsers.List'] .= SucuriScanTemplate::get_snippet('lastlogins-loggedin', array(
  8133. 'LoggedInUsers.Id' => SucuriScan::escape( $logged_in_user['user_id'] ),
  8134. 'LoggedInUsers.UserURL' => admin_url( 'user-edit.php?user_id='.$logged_in_user['user_id'] ),
  8135. 'LoggedInUsers.UserLogin' => SucuriScan::escape( $logged_in_user['user_login'] ),
  8136. 'LoggedInUsers.UserEmail' => SucuriScan::escape( $logged_in_user['user_email'] ),
  8137. 'LoggedInUsers.LastActivity' => SucuriScan::escape( $logged_in_user['last_activity_datetime'] ),
  8138. 'LoggedInUsers.Registered' => SucuriScan::escape( $logged_in_user['user_registered_datetime'] ),
  8139. 'LoggedInUsers.RemoveAddr' => SucuriScan::escape( $logged_in_user['remote_addr'] ),
  8140. 'LoggedInUsers.CssClass' => ( $counter % 2 == 0 ) ? '' : 'alternate',
  8141. ));
  8142. }
  8143. }
  8144. return SucuriScanTemplate::get_section( 'lastlogins-loggedin', $template_variables );
  8145. }
  8146. /**
  8147. * Get a list of all the registered users that are currently in session.
  8148. *
  8149. * @param boolean $add_current_user Whether the current user should be added to the list or not.
  8150. * @return array List of registered users currently in session.
  8151. */
  8152. function sucuriscan_get_online_users( $add_current_user = false ){
  8153. $users = array();
  8154. if ( SucuriScan::is_multisite() ){
  8155. $users = get_site_transient( 'online_users' );
  8156. } else {
  8157. $users = get_transient( 'online_users' );
  8158. }
  8159. // If not online users but current user is logged in, add it to the list.
  8160. if ( empty($users) && $add_current_user ){
  8161. $current_user = wp_get_current_user();
  8162. if ( $current_user->ID > 0 ){
  8163. sucuriscan_set_online_user( $current_user->user_login, $current_user );
  8164. return sucuriscan_get_online_users();
  8165. }
  8166. }
  8167. return $users;
  8168. }
  8169. /**
  8170. * Update the list of the registered users currently in session.
  8171. *
  8172. * Useful when you are removing users and need the list of the remaining users.
  8173. *
  8174. * @param array $logged_in_users List of registered users currently in session.
  8175. * @return boolean Either TRUE or FALSE representing the success or fail of the operation.
  8176. */
  8177. function sucuriscan_save_online_users( $logged_in_users = array() ){
  8178. $expiration = 30 * 60;
  8179. if ( SucuriScan::is_multisite() ){
  8180. return set_site_transient( 'online_users', $logged_in_users, $expiration );
  8181. } else {
  8182. return set_transient( 'online_users', $logged_in_users, $expiration );
  8183. }
  8184. }
  8185. if ( ! function_exists( 'sucuriscan_unset_online_user_on_logout' ) ){
  8186. /**
  8187. * Remove a logged in user from the list of registered users in session when
  8188. * the logout page is requested.
  8189. *
  8190. * @return void
  8191. */
  8192. function sucuriscan_unset_online_user_on_logout(){
  8193. $remote_addr = SucuriScan::get_remote_addr();
  8194. $current_user = wp_get_current_user();
  8195. $user_id = $current_user->ID;
  8196. sucuriscan_unset_online_user( $user_id, $remote_addr );
  8197. }
  8198. add_action( 'wp_logout', 'sucuriscan_unset_online_user_on_logout' );
  8199. }
  8200. /**
  8201. * Remove a logged in user from the list of registered users in session using
  8202. * the user identifier and the ip address of the last computer used to login.
  8203. *
  8204. * @param integer $user_id User identifier of the account that will be logged out.
  8205. * @param integer $remote_addr IP address of the computer where the user logged in.
  8206. * @return boolean Either TRUE or FALSE representing the success or fail of the operation.
  8207. */
  8208. function sucuriscan_unset_online_user( $user_id = 0, $remote_addr = 0 ){
  8209. $logged_in_users = sucuriscan_get_online_users();
  8210. // Remove the specified user identifier from the list.
  8211. if ( is_array( $logged_in_users ) && ! empty($logged_in_users) ){
  8212. foreach ( $logged_in_users as $i => $user ){
  8213. if (
  8214. $user['user_id'] == $user_id
  8215. && strcmp( $user['remote_addr'], $remote_addr ) == 0
  8216. ){
  8217. unset($logged_in_users[ $i ]);
  8218. break;
  8219. }
  8220. }
  8221. }
  8222. return sucuriscan_save_online_users( $logged_in_users );
  8223. }
  8224. if ( ! function_exists( 'sucuriscan_set_online_user' ) ){
  8225. /**
  8226. * Add an user account to the list of registered users in session.
  8227. *
  8228. * @param string $user_login The name of the user account that just logged in the site.
  8229. * @param boolean $user The WordPress object containing all the information associated to the user.
  8230. * @return void
  8231. */
  8232. function sucuriscan_set_online_user( $user_login = '', $user = false ){
  8233. if ( $user ){
  8234. // Get logged in user information.
  8235. $current_user = ($user instanceof WP_User) ? $user : wp_get_current_user();
  8236. $current_user_id = $current_user->ID;
  8237. $remote_addr = SucuriScan::get_remote_addr();
  8238. $current_time = current_time( 'timestamp' );
  8239. $logged_in_users = sucuriscan_get_online_users();
  8240. // Build the dataset array that will be stored in the transient variable.
  8241. $current_user_info = array(
  8242. 'user_id' => $current_user_id,
  8243. 'user_login' => $current_user->user_login,
  8244. 'user_email' => $current_user->user_email,
  8245. 'user_registered' => $current_user->user_registered,
  8246. 'last_activity' => $current_time,
  8247. 'remote_addr' => $remote_addr,
  8248. );
  8249. if ( ! is_array( $logged_in_users ) || empty($logged_in_users) ){
  8250. $logged_in_users = array( $current_user_info );
  8251. sucuriscan_save_online_users( $logged_in_users );
  8252. } else {
  8253. $do_nothing = false;
  8254. $update_existing = false;
  8255. $item_index = 0;
  8256. // Check if the user is already in the logged-in-user list and update it if is necessary.
  8257. foreach ( $logged_in_users as $i => $user ){
  8258. if (
  8259. $user['user_id'] == $current_user_id
  8260. && strcmp( $user['remote_addr'], $remote_addr ) == 0
  8261. ){
  8262. if ( $user['last_activity'] < ($current_time - (15 * 60)) ){
  8263. $update_existing = true;
  8264. $item_index = $i;
  8265. break;
  8266. } else {
  8267. $do_nothing = true;
  8268. break;
  8269. }
  8270. }
  8271. }
  8272. if ( $update_existing ){
  8273. $logged_in_users[ $item_index ] = $current_user_info;
  8274. sucuriscan_save_online_users( $logged_in_users );
  8275. } elseif ( $do_nothing ){
  8276. // Do nothing.
  8277. } else {
  8278. $logged_in_users[] = $current_user_info;
  8279. sucuriscan_save_online_users( $logged_in_users );
  8280. }
  8281. }
  8282. }
  8283. }
  8284. add_action( 'wp_login', 'sucuriscan_set_online_user', 10, 2 );
  8285. }
  8286. /**
  8287. * Print a list with the failed logins occurred during the last hour.
  8288. *
  8289. * @return string A list with the failed logins occurred during the last hour.
  8290. */
  8291. function sucuriscan_failed_logins_panel(){
  8292. $template_variables = array(
  8293. 'FailedLogins.List' => '',
  8294. 'FailedLogins.Total' => '',
  8295. 'FailedLogins.MaxFailedLogins' => 0,
  8296. 'FailedLogins.NoItemsVisibility' => 'visible',
  8297. 'FailedLogins.WarningVisibility' => 'visible',
  8298. 'FailedLogins.CollectPasswordsVisibility' => 'visible',
  8299. );
  8300. $max_failed_logins = SucuriScanOption::get_option( ':maximum_failed_logins' );
  8301. $notify_bruteforce_attack = SucuriScanOption::get_option( ':notify_bruteforce_attack' );
  8302. $failed_logins = sucuriscan_get_failed_logins();
  8303. $old_failed_logins = sucuriscan_get_failed_logins( true );
  8304. // Merge the new and old failed logins.
  8305. if (
  8306. is_array( $old_failed_logins )
  8307. && ! empty($old_failed_logins)
  8308. ) {
  8309. if (
  8310. is_array( $failed_logins )
  8311. && ! empty($failed_logins)
  8312. ) {
  8313. $failed_logins = array_merge( $failed_logins, $old_failed_logins );
  8314. } else {
  8315. $failed_logins = $old_failed_logins;
  8316. }
  8317. }
  8318. if ( $failed_logins ){
  8319. $counter = 0;
  8320. foreach ( $failed_logins['entries'] as $login_data ){
  8321. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  8322. $wrong_user_password = '<span class="sucuriscan-label-default">hidden</span>';
  8323. if ( sucuriscan_collect_wrong_passwords() === true ) {
  8324. if (
  8325. isset($login_data['user_password'])
  8326. && ! empty($login_data['user_password'])
  8327. ) {
  8328. $wrong_user_password = SucuriScan::escape( $login_data['user_password'] );
  8329. }
  8330. else {
  8331. $wrong_user_password = '<span class="sucuriscan-label-info">empty</span>';
  8332. }
  8333. }
  8334. $template_variables['FailedLogins.List'] .= SucuriScanTemplate::get_snippet('lastlogins-failedlogins', array(
  8335. 'FailedLogins.CssClass' => $css_class,
  8336. 'FailedLogins.Num' => ($counter + 1),
  8337. 'FailedLogins.Username' => SucuriScan::escape( $login_data['user_login'] ),
  8338. 'FailedLogins.Password' => $wrong_user_password,
  8339. 'FailedLogins.RemoteAddr' => SucuriScan::escape( $login_data['remote_addr'] ),
  8340. 'FailedLogins.Datetime' => SucuriScan::datetime( $login_data['attempt_time'] ),
  8341. 'FailedLogins.UserAgent' => SucuriScan::escape( $login_data['user_agent'] ),
  8342. ));
  8343. $counter += 1;
  8344. }
  8345. if ( $counter > 0 ){
  8346. $template_variables['FailedLogins.NoItemsVisibility'] = 'hidden';
  8347. }
  8348. }
  8349. $template_variables['FailedLogins.MaxFailedLogins'] = $max_failed_logins;
  8350. if ( $notify_bruteforce_attack == 'enabled' ){
  8351. $template_variables['FailedLogins.WarningVisibility'] = 'hidden';
  8352. }
  8353. if ( sucuriscan_collect_wrong_passwords() !== true ){
  8354. $template_variables['FailedLogins.CollectPasswordsVisibility'] = 'hidden';
  8355. }
  8356. return SucuriScanTemplate::get_section( 'lastlogins-failedlogins', $template_variables );
  8357. }
  8358. /**
  8359. * Whether or not to collect the password of failed logins.
  8360. *
  8361. * @return boolean TRUE if the password must be collected, FALSE otherwise.
  8362. */
  8363. function sucuriscan_collect_wrong_passwords(){
  8364. return (bool) ( SucuriScanOption::get_option( ':collect_wrong_passwords' ) === 'enabled' );
  8365. }
  8366. /**
  8367. * Find the full path of the file where the information of the failed logins
  8368. * will be stored, it will be created automatically if does not exists (and if
  8369. * the destination folder has permissions to write). This function can also be
  8370. * used to reset the content of the datastore file.
  8371. *
  8372. * @see sucuriscan_reset_failed_logins()
  8373. *
  8374. * @param boolean $get_old_logs Whether the old logs will be retrieved or not.
  8375. * @param boolean $reset Whether the file will be resetted or not.
  8376. * @return string The full (relative) path where the file is located.
  8377. */
  8378. function sucuriscan_failed_logins_datastore_path( $get_old_logs = false, $reset = false ){
  8379. $file_name = $get_old_logs ? 'sucuri-oldfailedlogins.php' : 'sucuri-failedlogins.php';
  8380. $datastore_path = SucuriScan::datastore_folder_path( $file_name );
  8381. $default_content = sucuriscan_failed_logins_default_content();
  8382. // Create the file if it does not exists.
  8383. if ( ! file_exists( $datastore_path ) || $reset ){
  8384. @file_put_contents( $datastore_path, $default_content, LOCK_EX );
  8385. }
  8386. // Return the datastore path if the file exists (or was created).
  8387. if (
  8388. file_exists( $datastore_path )
  8389. && is_readable( $datastore_path )
  8390. ){
  8391. return $datastore_path;
  8392. }
  8393. return false;
  8394. }
  8395. /**
  8396. * Default content of the datastore file where the failed logins are being kept.
  8397. *
  8398. * @return string Default content of the file.
  8399. */
  8400. function sucuriscan_failed_logins_default_content(){
  8401. $default_content = "<?php exit(0); ?>\n";
  8402. return $default_content;
  8403. }
  8404. /**
  8405. * Read and parse the content of the datastore file where the failed logins are
  8406. * being kept. This function will also calculate the difference in time between
  8407. * the first and last login attempt registered in the file to later decide if
  8408. * there is a brute-force attack in progress (and send an email notification
  8409. * with the report) or reset the file after considering it a normal behavior of
  8410. * the site.
  8411. *
  8412. * @param boolean $get_old_logs Whether the old logs will be retrieved or not.
  8413. * @return array Information and entries gathered from the failed logins datastore file.
  8414. */
  8415. function sucuriscan_get_failed_logins( $get_old_logs = false ){
  8416. $datastore_path = sucuriscan_failed_logins_datastore_path( $get_old_logs );
  8417. $default_content = sucuriscan_failed_logins_default_content();
  8418. $default_content_n = substr_count( $default_content, "\n" );
  8419. if ( $datastore_path ){
  8420. $lines = SucuriScanFileInfo::file_lines( $datastore_path );
  8421. if ( $lines ){
  8422. $failed_logins = array(
  8423. 'count' => 0,
  8424. 'first_attempt' => 0,
  8425. 'last_attempt' => 0,
  8426. 'diff_time' => 0,
  8427. 'entries' => array(),
  8428. );
  8429. // Read and parse all the entries found in the datastore file.
  8430. foreach ( $lines as $i => $line ){
  8431. if ( $i >= $default_content_n ){
  8432. $login_data = @json_decode( trim( $line ), true );
  8433. $login_data['attempt_date'] = date( 'r', $login_data['attempt_time'] );
  8434. if ( ! $login_data['user_agent'] ){
  8435. $login_data['user_agent'] = 'Unknown';
  8436. }
  8437. if ( ! isset($login_data['user_password']) ) {
  8438. $login_data['user_password'] = '';
  8439. }
  8440. $failed_logins['entries'][] = $login_data;
  8441. $failed_logins['count'] += 1;
  8442. }
  8443. }
  8444. // Calculate the different time between the first and last attempt.
  8445. if ( $failed_logins['count'] > 0 ){
  8446. $z = abs( $failed_logins['count'] - 1 );
  8447. $failed_logins['last_attempt'] = $failed_logins['entries'][ $z ]['attempt_time'];
  8448. $failed_logins['first_attempt'] = $failed_logins['entries'][0]['attempt_time'];
  8449. $failed_logins['diff_time'] = abs( $failed_logins['last_attempt'] - $failed_logins['first_attempt'] );
  8450. return $failed_logins;
  8451. }
  8452. }
  8453. }
  8454. return false;
  8455. }
  8456. /**
  8457. * Add a new entry in the datastore file where the failed logins are being kept,
  8458. * this entry will contain the username, timestamp of the login attempt, remote
  8459. * address of the computer sending the request, and the user-agent.
  8460. *
  8461. * @param string $user_login Information from the current failed login event.
  8462. * @param string $wrong_password Wrong password used during the supposed attack.
  8463. * @return boolean Whether the information of the current failed login event was stored or not.
  8464. */
  8465. function sucuriscan_log_failed_login( $user_login = '', $wrong_password = '' ){
  8466. $datastore_path = sucuriscan_failed_logins_datastore_path();
  8467. // Do not collect wrong passwords if it is not necessary.
  8468. if ( sucuriscan_collect_wrong_passwords() !== true ) {
  8469. $wrong_password = '';
  8470. }
  8471. if ( $datastore_path ){
  8472. $login_data = json_encode(array(
  8473. 'user_login' => $user_login,
  8474. 'user_password' => $wrong_password,
  8475. 'attempt_time' => time(),
  8476. 'remote_addr' => SucuriScan::get_remote_addr(),
  8477. 'user_agent' => SucuriScan::get_user_agent(),
  8478. ));
  8479. $logged = @file_put_contents( $datastore_path, $login_data . "\n", FILE_APPEND );
  8480. return $logged;
  8481. }
  8482. return false;
  8483. }
  8484. /**
  8485. * Read and parse all the entries in the datastore file where the failed logins
  8486. * are being kept, this will loop through all these items and generate a table
  8487. * in HTML code to send as a report via email according to the plugin settings
  8488. * for the email notifications.
  8489. *
  8490. * @param array $failed_logins Information and entries gathered from the failed logins datastore file.
  8491. * @return boolean Whether the report was sent via email or not.
  8492. */
  8493. function sucuriscan_report_failed_logins( $failed_logins = array() ){
  8494. if ( $failed_logins && $failed_logins['count'] > 0 ){
  8495. $prettify_mails = SucuriScanMail::prettify_mails();
  8496. $collect_wrong_passwords = sucuriscan_collect_wrong_passwords();
  8497. $mail_content = '';
  8498. if ( $prettify_mails ){
  8499. $table_html = '<table border="1" cellspacing="0" cellpadding="0">';
  8500. // Add the table headers.
  8501. $table_html .= '<thead>';
  8502. $table_html .= '<tr>';
  8503. $table_html .= '<th>Username</th>';
  8504. if ( $collect_wrong_passwords === true ) {
  8505. $table_html .= '<th>Password</th>';
  8506. }
  8507. $table_html .= '<th>IP Address</th>';
  8508. $table_html .= '<th>Attempt Timestamp</th>';
  8509. $table_html .= '<th>Attempt Date/Time</th>';
  8510. $table_html .= '</tr>';
  8511. $table_html .= '</thead>';
  8512. $table_html .= '<tbody>';
  8513. }
  8514. foreach ( $failed_logins['entries'] as $login_data ){
  8515. if ( $prettify_mails ){
  8516. $table_html .= '<tr>';
  8517. $table_html .= '<td>' . esc_attr( $login_data['user_login'] ) . '</td>';
  8518. if ( $collect_wrong_passwords === true ) {
  8519. $table_html .= '<td>' . esc_attr( $login_data['user_password'] ) . '</td>';
  8520. }
  8521. $table_html .= '<td>' . esc_attr( $login_data['remote_addr'] ) . '</td>';
  8522. $table_html .= '<td>' . $login_data['attempt_time'] . '</td>';
  8523. $table_html .= '<td>' . $login_data['attempt_date'] . '</td>';
  8524. $table_html .= '</tr>';
  8525. } else {
  8526. $mail_content .= "\n";
  8527. $mail_content .= 'Username: ' . $login_data['user_login'] . "\n";
  8528. if ( $collect_wrong_passwords === true ) {
  8529. $mail_content .= 'Password: ' . $login_data['user_password'] . "\n";
  8530. }
  8531. $mail_content .= 'IP Address: ' . $login_data['remote_addr'] . "\n";
  8532. $mail_content .= 'Attempt Timestamp: ' . $login_data['attempt_time'] . "\n";
  8533. $mail_content .= 'Attempt Date/Time: ' . $login_data['attempt_date'] . "\n";
  8534. }
  8535. }
  8536. if ( $prettify_mails ){
  8537. $table_html .= '</tbody>';
  8538. $table_html .= '</table>';
  8539. $mail_content = $table_html;
  8540. }
  8541. if ( SucuriScanEvent::notify_event( 'bruteforce_attack', $mail_content ) ){
  8542. sucuriscan_reset_failed_logins();
  8543. return true;
  8544. }
  8545. }
  8546. return false;
  8547. }
  8548. /**
  8549. * Remove all the entries in the datastore file where the failed logins are
  8550. * being kept. The execution of this function will not delete the file (which is
  8551. * likely the best move) but rather will clean its content and append the
  8552. * default code defined by another function above.
  8553. *
  8554. * @return boolean Whether the datastore file was resetted or not.
  8555. */
  8556. function sucuriscan_reset_failed_logins(){
  8557. $datastore_path = SucuriScan::datastore_folder_path( 'sucuri-failedlogins.php' );
  8558. $datastore_backup_path = sucuriscan_failed_logins_datastore_path( true, false );
  8559. $default_content = sucuriscan_failed_logins_default_content();
  8560. $current_content = @file_get_contents( $datastore_path );
  8561. $current_content = str_replace( $default_content, '', $current_content );
  8562. @file_put_contents(
  8563. $datastore_backup_path,
  8564. $current_content,
  8565. FILE_APPEND
  8566. );
  8567. return (bool) sucuriscan_failed_logins_datastore_path( false, true );
  8568. }
  8569. /**
  8570. * Process the requests sent by the form submissions originated in the settings
  8571. * page, all forms must have a nonce field that will be checked against the one
  8572. * generated in the template render function.
  8573. *
  8574. * @param boolean $page_nonce True if the nonce is valid, False otherwise.
  8575. * @return void
  8576. */
  8577. function sucuriscan_settings_form_submissions( $page_nonce = null ){
  8578. global $sucuriscan_schedule_allowed,
  8579. $sucuriscan_interface_allowed,
  8580. $sucuriscan_notify_options,
  8581. $sucuriscan_emails_per_hour,
  8582. $sucuriscan_maximum_failed_logins,
  8583. $sucuriscan_email_subjects,
  8584. $sucuriscan_verify_ssl_cert;
  8585. // Use this conditional to avoid double checking.
  8586. if ( is_null( $page_nonce ) ){
  8587. $page_nonce = SucuriScanInterface::check_nonce();
  8588. }
  8589. if ( $page_nonce ){
  8590. // Recover API key through the email registered previously.
  8591. if ( SucuriScanRequest::post( ':recover_key' ) !== false ){
  8592. SucuriScanAPI::recover_key();
  8593. SucuriScanEvent::report_info_event( 'Recovery of the Sucuri API key was requested.' );
  8594. }
  8595. // Save API key after it was recovered by the administrator.
  8596. if ( $api_key = SucuriScanRequest::post( ':manual_api_key' ) ){
  8597. SucuriScanAPI::set_plugin_key( $api_key, true );
  8598. SucuriScanEvent::schedule_task();
  8599. SucuriScanEvent::report_info_event( 'Sucuri API key was added manually.' );
  8600. }
  8601. // Remove API key from the local storage.
  8602. if ( SucuriScanRequest::post( ':remove_api_key' ) !== false ){
  8603. SucuriScanAPI::set_plugin_key( '' );
  8604. wp_clear_scheduled_hook( 'sucuriscan_scheduled_scan' );
  8605. SucuriScanEvent::report_critical_event( 'Sucuri API key was deleted.' );
  8606. SucuriScanEvent::notify_event( 'plugin_change', 'Sucuri API key removed' );
  8607. }
  8608. // Enable or disable the filesystem scanner.
  8609. if ( $fs_scanner = SucuriScanRequest::post( ':fs_scanner', '(en|dis)able' ) ){
  8610. $action_d = $fs_scanner . 'd';
  8611. $message = 'Main file system scanner was <code>' . $action_d . '</code>';
  8612. SucuriScanOption::update_option( ':fs_scanner', $action_d );
  8613. SucuriScanEvent::report_auto_event( $message );
  8614. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8615. SucuriScanInterface::info( $message );
  8616. }
  8617. // Enable or disable the filesystem scanner for modified files.
  8618. if ( $scan_modfiles = SucuriScanRequest::post( ':scan_modfiles', '(en|dis)able' ) ){
  8619. $action_d = $scan_modfiles . 'd';
  8620. $message = 'File system scanner for modified files was <code>' . $action_d . '</code>';
  8621. SucuriScanOption::update_option( ':scan_modfiles', $action_d );
  8622. SucuriScanEvent::report_auto_event( $message );
  8623. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8624. SucuriScanInterface::info( $message );
  8625. }
  8626. // Enable or disable the filesystem scanner for file integrity.
  8627. if ( $scan_checksums = SucuriScanRequest::post( ':scan_checksums', '(en|dis)able' ) ){
  8628. $action_d = $scan_checksums . 'd';
  8629. $message = 'File system scanner for file integrity was <code>' . $action_d . '</code>';
  8630. SucuriScanOption::update_option( ':scan_checksums', $action_d );
  8631. SucuriScanEvent::report_auto_event( $message );
  8632. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8633. SucuriScanInterface::info( $message );
  8634. }
  8635. // Enable or disable the filesystem scanner for error logs.
  8636. if ( $ignore_scanning = SucuriScanRequest::post( ':ignore_scanning', '(en|dis)able' ) ){
  8637. $action_d = $ignore_scanning . 'd';
  8638. $message = 'File system scanner rules to ignore directories was <code>' . $action_d . '</code>';
  8639. SucuriScanOption::update_option( ':ignore_scanning', $action_d );
  8640. SucuriScanEvent::report_auto_event( $message );
  8641. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8642. SucuriScanInterface::info( $message );
  8643. }
  8644. // Enable or disable the filesystem scanner for error logs.
  8645. if ( $scan_errorlogs = SucuriScanRequest::post( ':scan_errorlogs', '(en|dis)able' ) ){
  8646. $action_d = $scan_errorlogs . 'd';
  8647. $message = 'File system scanner for error logs was <code>' . $action_d . '</code>';
  8648. SucuriScanOption::update_option( ':scan_errorlogs', $action_d );
  8649. SucuriScanEvent::report_auto_event( $message );
  8650. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8651. SucuriScanInterface::info( $message );
  8652. }
  8653. // Enable or disable the error logs parsing.
  8654. if ( $parse_errorlogs = SucuriScanRequest::post( ':parse_errorlogs', '(en|dis)able' ) ){
  8655. $action_d = $parse_errorlogs . 'd';
  8656. $message = 'Analysis of main error log file was <code>' . $action_d . '</code>';
  8657. SucuriScanOption::update_option( ':parse_errorlogs', $action_d );
  8658. SucuriScanEvent::report_auto_event( $message );
  8659. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8660. SucuriScanInterface::info( $message );
  8661. }
  8662. // Enable or disable the SiteCheck scanner and the malware scan page.
  8663. if ( $sitecheck_scanner = SucuriScanRequest::post( ':sitecheck_scanner', '(en|dis)able' ) ){
  8664. $action_d = $sitecheck_scanner . 'd';
  8665. $message = 'SiteCheck malware and blacklist scanner was <code>' . $action_d . '</code>';
  8666. SucuriScanOption::update_option( ':sitecheck_scanner', $action_d );
  8667. SucuriScanEvent::report_auto_event( $message );
  8668. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8669. SucuriScanInterface::info( $message );
  8670. }
  8671. // Modify the schedule of the filesystem scanner.
  8672. if ( $frequency = SucuriScanRequest::post( ':scan_frequency' ) ){
  8673. if ( array_key_exists( $frequency, $sucuriscan_schedule_allowed ) ){
  8674. SucuriScanOption::update_option( ':scan_frequency', $frequency );
  8675. wp_clear_scheduled_hook( 'sucuriscan_scheduled_scan' );
  8676. if ( $frequency != '_oneoff' ){
  8677. wp_schedule_event( time() + 10, $frequency, 'sucuriscan_scheduled_scan' );
  8678. }
  8679. $frequency_title = strtolower( $sucuriscan_schedule_allowed[ $frequency ] );
  8680. $message = 'File system scanning frequency set to <code>' . $frequency_title . '</code>';
  8681. SucuriScanEvent::report_info_event( $message );
  8682. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8683. SucuriScanInterface::info( $message );
  8684. }
  8685. }
  8686. // Set the method (aka. interface) that will be used to scan the site.
  8687. if ( $interface = SucuriScanRequest::post( ':scan_interface' ) ){
  8688. $allowed_values = array_keys( $sucuriscan_interface_allowed );
  8689. if ( in_array( $interface, $allowed_values ) ){
  8690. $message = 'File system scanning interface set to <code>' . $interface . '</code>';
  8691. SucuriScanOption::update_option( ':scan_interface', $interface );
  8692. SucuriScanEvent::report_info_event( $message );
  8693. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8694. SucuriScanInterface::info( $message );
  8695. }
  8696. }
  8697. // Update the limit of error log lines to parse.
  8698. if ( $errorlogs_limit = SucuriScanRequest::post( ':errorlogs_limit', '[0-9]+' ) ){
  8699. if ( $errorlogs_limit > 1000 ) {
  8700. SucuriScanInterface::error( 'Analyze more than 1,000 lines will take too much time.' );
  8701. } else {
  8702. SucuriScanOption::update_option( ':errorlogs_limit', $errorlogs_limit );
  8703. SucuriScanInterface::info( 'Analyze last <code>' . $errorlogs_limit . '</code> entries encountered in the error logs.' );
  8704. if ( $errorlogs_limit == 0 ) {
  8705. SucuriScanOption::update_option( ':parse_errorlogs', 'disabled' );
  8706. }
  8707. }
  8708. }
  8709. // Reset the plugin security logs.
  8710. $allowed_log_files = '(integrity|lastlogins|failedlogins|sitecheck)';
  8711. if ( $reset_logfile = SucuriScanRequest::post( ':reset_logfile', $allowed_log_files ) ){
  8712. $files_to_delete = array(
  8713. 'sucuri-' . $reset_logfile . '.php',
  8714. 'sucuri-old' . $reset_logfile . '.php',
  8715. );
  8716. foreach ( $files_to_delete as $log_filename ) {
  8717. $log_filepath = SucuriScan::datastore_folder_path( $log_filename );
  8718. if ( @unlink( $log_filepath ) ) {
  8719. $log_filename_simple = str_replace( '.php', '', $log_filename );
  8720. $message = 'Deleted security log <code>' . $log_filename_simple . '</code>';
  8721. SucuriScanEvent::report_debug_event( $message );
  8722. SucuriScanInterface::info( $message );
  8723. }
  8724. }
  8725. }
  8726. // Update the value for the maximum emails per hour.
  8727. if ( $per_hour = SucuriScanRequest::post( ':emails_per_hour' ) ){
  8728. if ( array_key_exists( $per_hour, $sucuriscan_emails_per_hour ) ){
  8729. $per_hour_label = strtolower( $sucuriscan_emails_per_hour[ $per_hour ] );
  8730. $message = 'Maximum email alerts per hour set to <code>' . $per_hour_label . '</code>';
  8731. SucuriScanOption::update_option( ':emails_per_hour', $per_hour );
  8732. SucuriScanEvent::report_info_event( $message );
  8733. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8734. SucuriScanInterface::info( $message );
  8735. } else {
  8736. SucuriScanInterface::error( 'Invalid value for the maximum emails per hour.' );
  8737. }
  8738. }
  8739. // Update the email where the event notifications will be sent.
  8740. if ( $new_email = SucuriScanRequest::post( ':notify_to' ) ){
  8741. $valid_email = SucuriScan::get_valid_email( $new_email );
  8742. if ( $valid_email ){
  8743. $message = 'Sucuri alerts will be sent to this email: <code>' . $valid_email . '</code>';
  8744. SucuriScanOption::update_option( ':notify_to', $valid_email );
  8745. SucuriScanEvent::report_info_event( $message );
  8746. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8747. SucuriScanInterface::info( $message );
  8748. } else {
  8749. SucuriScanInterface::error( 'Email format not supported.' );
  8750. }
  8751. }
  8752. // Update the maximum failed logins per hour before consider it a brute-force attack.
  8753. if ( $failed_logins = SucuriScanRequest::post( ':maximum_failed_logins' ) ){
  8754. if ( array_key_exists( $failed_logins, $sucuriscan_maximum_failed_logins ) ){
  8755. $message = 'Consider brute-force attack after <code>' . $failed_logins . '</code> failed logins per hour';
  8756. SucuriScanOption::update_option( ':maximum_failed_logins', $failed_logins );
  8757. SucuriScanEvent::report_info_event( $message );
  8758. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8759. SucuriScanInterface::info( $message );
  8760. } else {
  8761. SucuriScanInterface::error( 'Invalid value for the maximum failed logins per hour before consider it a brute-force attack.' );
  8762. }
  8763. }
  8764. // Update the configuration for the SSL certificate verification.
  8765. if ( $verify_ssl_cert = SucuriScanRequest::post( ':verify_ssl_cert' ) ){
  8766. if ( array_key_exists( $verify_ssl_cert, $sucuriscan_verify_ssl_cert ) ){
  8767. $message = 'SSL certificate verification for API calls set to <code>' . $verify_ssl_cert . '</code>';
  8768. SucuriScanOption::update_option( ':verify_ssl_cert', $verify_ssl_cert );
  8769. SucuriScanEvent::report_warning_event( $message );
  8770. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8771. SucuriScanInterface::info( $message );
  8772. } else {
  8773. SucuriScanInterface::error( 'Invalid value for the SSL certificate verification.' );
  8774. }
  8775. }
  8776. // Enable or disable the audit logs report.
  8777. if ( $audit_report = SucuriScanRequest::post( ':audit_report', '(en|dis)able' ) ){
  8778. $action_d = $audit_report . 'd';
  8779. $message = 'Audit logs report was <code>' . $action_d . '</code>';
  8780. SucuriScanOption::update_option( ':audit_report', $action_d );
  8781. SucuriScanEvent::report_info_event( $message );
  8782. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8783. SucuriScanInterface::info( $message );
  8784. }
  8785. // Enable or disable the reverse proxy support.
  8786. if ( $revproxy = SucuriScanRequest::post( ':revproxy', '(en|dis)able' ) ){
  8787. $action_d = $revproxy . 'd';
  8788. $message = 'Reverse proxy support was <code>' . $action_d . '</code>';
  8789. SucuriScanOption::update_option( ':revproxy', $action_d );
  8790. SucuriScanEvent::report_info_event( $message );
  8791. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8792. SucuriScanInterface::info( $message );
  8793. }
  8794. // Update the limit for audit logs report.
  8795. if ( $logs4report = SucuriScanRequest::post( ':logs4report', '[0-9]{1,4}' ) ){
  8796. $message = 'Limit for audit logs report set to <code>' . $logs4report . '</code>';
  8797. SucuriScanOption::update_option( ':logs4report', $logs4report );
  8798. SucuriScanEvent::report_info_event( $message );
  8799. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8800. SucuriScanInterface::info( $message );
  8801. }
  8802. // Update the API request timeout.
  8803. if ( $request_timeout = SucuriScanRequest::post( ':request_timeout', '[0-9]+' ) ){
  8804. $message = 'API request timeout set to <code>' . $request_timeout . '</code> seconds.';
  8805. SucuriScanOption::update_option( ':request_timeout', $request_timeout );
  8806. SucuriScanEvent::report_info_event( $message );
  8807. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8808. SucuriScanInterface::info( $message );
  8809. }
  8810. // Update the collection of failed passwords settings.
  8811. if ( $collect_wrong_passwords = SucuriScanRequest::post( ':collect_wrong_passwords' ) ){
  8812. $collect_wrong_passwords = strtolower( $collect_wrong_passwords );
  8813. $message = 'Collect failed login passwords set to <code>%s</code>';
  8814. if ( $collect_wrong_passwords == 'yes' ) {
  8815. $collect_action = 'enabled';
  8816. $message = sprintf( $message, $collect_action );
  8817. SucuriScanEvent::report_critical_event( $message );
  8818. } else {
  8819. $collect_action = 'disabled';
  8820. $message = sprintf( $message, $collect_action );
  8821. SucuriScanEvent::report_info_event( $message );
  8822. }
  8823. SucuriScanOption::update_option( ':collect_wrong_passwords', $collect_action );
  8824. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8825. SucuriScanInterface::info( $message );
  8826. }
  8827. // Update the datastore path (if the new directory exists).
  8828. if ( $datastore_path = SucuriScanRequest::post( ':datastore_path' ) ){
  8829. $current_datastore_path = SucuriScanOption::datastore_folder_path();
  8830. if ( file_exists( $datastore_path ) ) {
  8831. if ( is_writable( $datastore_path ) ) {
  8832. $message = 'Datastore path set to <code>' . $datastore_path . '</code>';
  8833. SucuriScanOption::update_option( ':datastore_path', $datastore_path );
  8834. SucuriScanEvent::report_info_event( $message );
  8835. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8836. SucuriScanInterface::info( $message );
  8837. if ( file_exists( $current_datastore_path ) ) {
  8838. $new_datastore_path = SucuriScanOption::datastore_folder_path();
  8839. @rename( $current_datastore_path, $new_datastore_path );
  8840. }
  8841. }
  8842. else {
  8843. SucuriScanInterface::error( 'The new directory path is not writable.' );
  8844. }
  8845. }
  8846. else {
  8847. SucuriScanInterface::error( 'The directory path specified does not exists.' );
  8848. }
  8849. }
  8850. // Update the advertisement visibility settings.
  8851. if ( $ads_visibility = SucuriScanRequest::post( ':ads_visibility' ) ){
  8852. $ads_visibility = strtolower( $ads_visibility );
  8853. $option_value = ( $ads_visibility == 'hide' ) ? 'disabled' : 'enabled';
  8854. $message = sprintf( 'Plugin advertisement set to <code>%s</code>', $option_value );
  8855. SucuriScanOption::update_option( ':ads_visibility', $option_value );
  8856. SucuriScanEvent::report_info_event( $message );
  8857. SucuriScanInterface::info( $message );
  8858. }
  8859. // Update the notification settings.
  8860. if ( SucuriScanRequest::post( ':save_notification_settings' ) !== false ){
  8861. $options_updated_counter = 0;
  8862. foreach ( $sucuriscan_notify_options as $alert_type => $alert_label ){
  8863. $option_value = SucuriScanRequest::post( $alert_type, '(1|0)' );
  8864. if ( $option_value !== false ){
  8865. $current_value = SucuriScanOption::get_option( $alert_type );
  8866. $option_value = ( $option_value == '1' ) ? 'enabled' : 'disabled';
  8867. // Check that the option value was actually changed.
  8868. if ( $current_value !== $option_value ) {
  8869. SucuriScanOption::update_option( $alert_type, $option_value );
  8870. $options_updated_counter += 1;
  8871. }
  8872. }
  8873. }
  8874. if ( $options_updated_counter > 0 ){
  8875. $message = 'Alert settings were changed <code>' . $options_updated_counter . ' options</code>';
  8876. SucuriScanEvent::report_info_event( $message );
  8877. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8878. SucuriScanInterface::info( $message );
  8879. }
  8880. }
  8881. // Update the subject format for the email alerts.
  8882. if ( $email_subject = SucuriScanRequest::post( ':email_subject' ) ) {
  8883. $new_email_subject = false;
  8884. $current_value = SucuriScanOption::get_option( ':email_subject' );
  8885. // Validate the custom subject format.
  8886. if ( $email_subject == 'custom' ) {
  8887. $format_pattern = '/^[0-9a-zA-Z:,\s]+$/';
  8888. $custom_email_subject = SucuriScanRequest::post( ':custom_email_subject' );
  8889. if (
  8890. $custom_email_subject !== false
  8891. && ! empty($custom_email_subject)
  8892. && preg_match( $format_pattern, $custom_email_subject )
  8893. ) {
  8894. $new_email_subject = trim( $custom_email_subject );
  8895. } else {
  8896. SucuriScanInterface::error( 'Invalid characters found in the email alert subject format.' );
  8897. }
  8898. }
  8899. // Check if the email subject format is allowed.
  8900. elseif (
  8901. is_array( $sucuriscan_email_subjects )
  8902. && in_array( $email_subject, $sucuriscan_email_subjects )
  8903. ) {
  8904. $new_email_subject = trim( $email_subject );
  8905. }
  8906. // Proceed with the operation saving the new subject.
  8907. if (
  8908. $new_email_subject !== false
  8909. && $current_value !== $new_email_subject
  8910. ) {
  8911. $message = 'Alert subject format set to <code>' . $new_email_subject . '</code>';
  8912. SucuriScanOption::update_option( ':email_subject', $new_email_subject );
  8913. SucuriScanEvent::report_info_event( $message );
  8914. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8915. SucuriScanInterface::info( $message );
  8916. }
  8917. }
  8918. // Reset all the plugin's options.
  8919. if ( SucuriScanRequest::post( ':reset_options' ) !== false ){
  8920. // Notify the event before the API key is removed.
  8921. $message = 'Sucuri plugin options were reset';
  8922. SucuriScanEvent::report_critical_event( $message );
  8923. SucuriScanEvent::notify_event( 'plugin_change', $message );
  8924. // Remove all plugin options from the database.
  8925. SucuriScanOption::delete_plugin_options();
  8926. // Remove the scheduled tasks.
  8927. wp_clear_scheduled_hook( 'sucuriscan_scheduled_scan' );
  8928. SucuriScanInterface::info( 'All plugin options were reset successfully' );
  8929. }
  8930. // Ignore a new event for email notifications.
  8931. if ( $action = SucuriScanRequest::post( ':ignorerule_action', '(add|remove)' ) ){
  8932. $ignore_rule = SucuriScanRequest::post( ':ignorerule' );
  8933. if ( $action == 'add' ){
  8934. if ( SucuriScanOption::add_ignored_event( $ignore_rule ) ){
  8935. SucuriScanInterface::info( 'Post-type ignored successfully.' );
  8936. SucuriScanEvent::report_warning_event( 'Changes in <code>' . $ignore_rule . '</code> post-type will be ignored' );
  8937. } else {
  8938. SucuriScanInterface::error( 'The post-type is invalid or it may be already ignored.' );
  8939. }
  8940. }
  8941. elseif ( $action == 'remove' ) {
  8942. SucuriScanOption::remove_ignored_event( $ignore_rule );
  8943. SucuriScanInterface::info( 'Post-type removed from the list successfully.' );
  8944. SucuriScanEvent::report_notice_event( 'Changes in <code>' . $ignore_rule . '</code> post-type will not be ignored' );
  8945. }
  8946. }
  8947. // Ignore a new directory path for the file system scans.
  8948. if ( $action = SucuriScanRequest::post( ':ignorescanning_action', '(ignore|unignore)' ) ){
  8949. $ignore_directories = SucuriScanRequest::post( ':ignorescanning_dirs', '_array' );
  8950. if ( empty($ignore_directories) ){
  8951. SucuriScanInterface::error( 'You did not choose a directory from the list.' );
  8952. }
  8953. elseif ( $action == 'ignore' ){
  8954. foreach ( $ignore_directories as $directory_path ){
  8955. SucuriScanFSScanner::ignore_directory( $directory_path );
  8956. }
  8957. SucuriScanInterface::info( 'Directories selected will be ignored in future scans.' );
  8958. SucuriScanEvent::report_warning_event( sprintf(
  8959. 'Directories will not be scanned: (multiple entries): %s',
  8960. @implode( ',', $ignore_directories )
  8961. ) );
  8962. }
  8963. elseif ( $action == 'unignore' ) {
  8964. foreach ( $ignore_directories as $directory_path ){
  8965. SucuriScanFSScanner::unignore_directory( $directory_path );
  8966. }
  8967. SucuriScanInterface::info( 'Directories selected will not be ignored anymore.' );
  8968. SucuriScanEvent::report_notice_event( sprintf(
  8969. 'Directories will be scanned: (multiple entries): %s',
  8970. @implode( ',', $ignore_directories )
  8971. ) );
  8972. }
  8973. }
  8974. // Trust and IP address to ignore notifications for a subnet.
  8975. if ( $trust_ip = SucuriScanRequest::post( ':trust_ip' ) ){
  8976. if (
  8977. SucuriScan::is_valid_ip( $trust_ip )
  8978. || SucuriScan::is_valid_cidr( $trust_ip )
  8979. ){
  8980. $cache = new SucuriScanCache( 'trustip' );
  8981. $ip_info = SucuriScan::get_ip_info( $trust_ip );
  8982. $ip_info['added_at'] = SucuriScan::local_time();
  8983. $cache_key = md5( $ip_info['remote_addr'] );
  8984. if ( $cache->exists( $cache_key ) ) {
  8985. SucuriScanInterface::error( 'The IP address specified was already trusted.' );
  8986. } elseif ( $cache->add( $cache_key, $ip_info ) ) {
  8987. $message = 'Changes from <code>' . $trust_ip . '</code> will be ignored';
  8988. SucuriScanEvent::report_warning_event( $message );
  8989. SucuriScanInterface::info( $message );
  8990. } else {
  8991. SucuriScanInterface::error( 'The new entry was not saved in the datastore file.' );
  8992. }
  8993. }
  8994. }
  8995. // Trust and IP address to ignore notifications for a subnet.
  8996. if ( $del_trust_ip = SucuriScanRequest::post( ':del_trust_ip', '_array' ) ){
  8997. $cache = new SucuriScanCache( 'trustip' );
  8998. foreach ( $del_trust_ip as $cache_key ) {
  8999. $cache->delete( $cache_key );
  9000. }
  9001. SucuriScanInterface::info( 'The IP addresses selected were deleted successfully.' );
  9002. }
  9003. // Update the settings for the heartbeat API.
  9004. if ( $heartbeat_status = SucuriScanRequest::post( ':heartbeat_status' ) ){
  9005. $statuses_allowed = SucuriScanHeartbeat::statuses_allowed();
  9006. if ( array_key_exists( $heartbeat_status, $statuses_allowed ) ){
  9007. $message = 'Heartbeat status set to <code>' . $heartbeat_status . '</code>';
  9008. SucuriScanOption::update_option( ':heartbeat', $heartbeat_status );
  9009. SucuriScanEvent::report_info_event( $message );
  9010. SucuriScanInterface::info( $message );
  9011. } else {
  9012. SucuriScanInterface::error( 'Heartbeat status not allowed.' );
  9013. }
  9014. }
  9015. // Update the value of the heartbeat pulse.
  9016. if ( $heartbeat_pulse = SucuriScanRequest::post( ':heartbeat_pulse' ) ){
  9017. $pulses_allowed = SucuriScanHeartbeat::pulses_allowed();
  9018. if ( array_key_exists( $heartbeat_pulse, $pulses_allowed ) ){
  9019. $message = 'Heartbeat pulse set to <code>' . $heartbeat_pulse . '</code> seconds.';
  9020. SucuriScanOption::update_option( ':heartbeat_pulse', $heartbeat_pulse );
  9021. SucuriScanEvent::report_info_event( $message );
  9022. SucuriScanInterface::info( $message );
  9023. } else {
  9024. SucuriScanInterface::error( 'Heartbeat pulse not allowed.' );
  9025. }
  9026. }
  9027. // Update the value of the heartbeat interval.
  9028. if ( $heartbeat_interval = SucuriScanRequest::post( ':heartbeat_interval' ) ){
  9029. $intervals_allowed = SucuriScanHeartbeat::intervals_allowed();
  9030. if ( array_key_exists( $heartbeat_interval, $intervals_allowed ) ){
  9031. $message = 'Heartbeat interval set to <code>' . $heartbeat_interval . '</code>';
  9032. SucuriScanOption::update_option( ':heartbeat_interval', $heartbeat_interval );
  9033. SucuriScanEvent::report_info_event( $message );
  9034. SucuriScanInterface::info( $message );
  9035. } else {
  9036. SucuriScanInterface::error( 'Heartbeat interval not allowed.' );
  9037. }
  9038. }
  9039. // Enable or disable the auto-start execution of heartbeat.
  9040. if ( $heartbeat_autostart = SucuriScanRequest::post( ':heartbeat_autostart', '(en|dis)able' ) ){
  9041. $action_d = $heartbeat_autostart . 'd';
  9042. $message = 'Heartbeat auto-start was <code>' . $action_d . '</code>';
  9043. SucuriScanOption::update_option( ':heartbeat_autostart', $action_d );
  9044. SucuriScanEvent::report_info_event( $message );
  9045. SucuriScanInterface::info( $message );
  9046. }
  9047. }
  9048. }
  9049. /**
  9050. * Print a HTML code with the settings of the plugin.
  9051. *
  9052. * @return void
  9053. */
  9054. function sucuriscan_settings_page(){
  9055. SucuriScanInterface::check_permissions();
  9056. $template_variables = array(
  9057. 'PageTitle' => 'Settings',
  9058. 'Settings.General' => sucuriscan_settings_general(),
  9059. 'Settings.Scanner' => sucuriscan_settings_scanner(),
  9060. 'Settings.IgnoreScanning' => sucuriscan_settings_ignorescanning(),
  9061. 'Settings.Notifications' => sucuriscan_settings_notifications(),
  9062. 'Settings.IgnoreRules' => sucuriscan_settings_ignore_rules(),
  9063. 'Settings.TrustIP' => sucuriscan_settings_trust_ip(),
  9064. 'Settings.Heartbeat' => sucuriscan_settings_heartbeat(),
  9065. );
  9066. echo SucuriScanTemplate::get_template( 'settings', $template_variables );
  9067. }
  9068. /**
  9069. * Read and parse the content of the general settings template.
  9070. *
  9071. * @return string Parsed HTML code for the general settings panel.
  9072. */
  9073. function sucuriscan_settings_general(){
  9074. global $sucuriscan_emails_per_hour,
  9075. $sucuriscan_maximum_failed_logins,
  9076. $sucuriscan_verify_ssl_cert;
  9077. // Check the nonce here to populate the value through other functions.
  9078. $page_nonce = SucuriScanInterface::check_nonce();
  9079. // Process all form submissions.
  9080. sucuriscan_settings_form_submissions( $page_nonce );
  9081. // Register the site, get its API key, and store it locally for future usage.
  9082. $api_registered_modal = '';
  9083. // Whether the form to manually add the API key should be shown or not.
  9084. $display_manual_key_form = (bool) ( SucuriScanRequest::post( ':recover_key' ) !== false );
  9085. if ( $page_nonce && SucuriScanRequest::post( ':plugin_api_key' ) !== false ){
  9086. $registered = SucuriScanAPI::register_site();
  9087. if ( $registered ){
  9088. $api_registered_modal = SucuriScanTemplate::get_modal('settings-apiregistered', array(
  9089. 'Title' => 'Site registered successfully',
  9090. 'CssClass' => 'sucuriscan-apikey-registered',
  9091. ));
  9092. } else {
  9093. $display_manual_key_form = true;
  9094. }
  9095. }
  9096. // Get initial variables to decide some things bellow.
  9097. $api_key = SucuriScanAPI::get_plugin_key();
  9098. $emails_per_hour = SucuriScanOption::get_option( ':emails_per_hour' );
  9099. $maximum_failed_logins = SucuriScanOption::get_option( ':maximum_failed_logins' );
  9100. $verify_ssl_cert = SucuriScanOption::get_option( ':verify_ssl_cert' );
  9101. $audit_report = SucuriScanOption::get_option( ':audit_report' );
  9102. $logs4report = SucuriScanOption::get_option( ':logs4report' );
  9103. $revproxy = SucuriScanOption::get_option( ':revproxy' );
  9104. $invalid_domain = false;
  9105. // Check whether the domain name is valid or not.
  9106. if ( ! $api_key ){
  9107. $clean_domain = SucuriScan::get_top_level_domain();
  9108. $domain_address = @gethostbyname( $clean_domain );
  9109. $invalid_domain = ( $domain_address == $clean_domain ) ? true : false;
  9110. }
  9111. // Generate the HTML code for the option list in the form select fields.
  9112. $emails_per_hour_options = SucuriScanTemplate::get_select_options( $sucuriscan_emails_per_hour, $emails_per_hour );
  9113. $maximum_failed_logins_options = SucuriScanTemplate::get_select_options( $sucuriscan_maximum_failed_logins, $maximum_failed_logins );
  9114. $verify_ssl_cert_options = SucuriScanTemplate::get_select_options( $sucuriscan_verify_ssl_cert, $verify_ssl_cert );
  9115. $template_variables = array(
  9116. 'APIKey' => ( ! $api_key ? '<em>(not set)</em>' : $api_key ),
  9117. 'APIKey.RecoverVisibility' => SucuriScanTemplate::visibility( ! $api_key && ! $display_manual_key_form ),
  9118. 'APIKey.ManualKeyFormVisibility' => SucuriScanTemplate::visibility( $display_manual_key_form ),
  9119. 'APIKey.RemoveVisibility' => SucuriScanTemplate::visibility( (bool) $api_key ),
  9120. 'InvalidDomainVisibility' => SucuriScanTemplate::visibility( $invalid_domain ),
  9121. 'NotifyTo' => SucuriScanOption::get_option( ':notify_to' ),
  9122. 'EmailsPerHour' => 'Undefined',
  9123. 'EmailsPerHourOptions' => $emails_per_hour_options,
  9124. 'MaximumFailedLogins' => 'Undefined',
  9125. 'MaximumFailedLoginsOptions' => $maximum_failed_logins_options,
  9126. 'VerifySSLCert' => 'Undefined',
  9127. 'VerifySSLCertOptions' => $verify_ssl_cert_options,
  9128. 'RequestTimeout' => SucuriScanOption::get_option( ':request_timeout' ) . ' seconds',
  9129. 'DatastorePath' => SucuriScanOption::get_option( ':datastore_path' ),
  9130. 'CollectWrongPasswords' => 'No collect passwords',
  9131. 'ModalWhenAPIRegistered' => $api_registered_modal,
  9132. /* Audit Logs Report */
  9133. 'AuditReportStatus' => 'Enabled',
  9134. 'AuditReportSwitchText' => 'Disable',
  9135. 'AuditReportSwitchValue' => 'disable',
  9136. 'AuditReportSwitchCssClass' => 'button-danger',
  9137. 'AuditReportLimit' => $logs4report,
  9138. /* Support Reverse Proxy */
  9139. 'ReverseProxyStatus' => 'Enabled',
  9140. 'ReverseProxySwitchText' => 'Disable',
  9141. 'ReverseProxySwitchValue' => 'disable',
  9142. 'ReverseProxySwitchCssClass' => 'button-danger',
  9143. /* API Proxy Settings */
  9144. 'APIProxy.Host' => 'n/a',
  9145. 'APIProxy.Port' => 'n/a',
  9146. 'APIProxy.Username' => 'n/a',
  9147. 'APIProxy.Password' => 'n/a',
  9148. 'APIProxy.PasswordType' => 'default',
  9149. 'APIProxy.PasswordText' => 'empty',
  9150. );
  9151. if ( array_key_exists( $emails_per_hour, $sucuriscan_emails_per_hour ) ){
  9152. $template_variables['EmailsPerHour'] = $sucuriscan_emails_per_hour[ $emails_per_hour ];
  9153. }
  9154. if ( array_key_exists( $maximum_failed_logins, $sucuriscan_maximum_failed_logins ) ){
  9155. $template_variables['MaximumFailedLogins'] = $sucuriscan_maximum_failed_logins[ $maximum_failed_logins ];
  9156. }
  9157. if ( array_key_exists( $verify_ssl_cert, $sucuriscan_verify_ssl_cert ) ){
  9158. $template_variables['VerifySSLCert'] = $sucuriscan_verify_ssl_cert[ $verify_ssl_cert ];
  9159. }
  9160. if ( $audit_report == 'disabled' ){
  9161. $template_variables['AuditReportStatus'] = 'Disabled';
  9162. $template_variables['AuditReportSwitchText'] = 'Enable';
  9163. $template_variables['AuditReportSwitchValue'] = 'enable';
  9164. $template_variables['AuditReportSwitchCssClass'] = 'button-success';
  9165. }
  9166. if ( $revproxy == 'disabled' ){
  9167. $template_variables['ReverseProxyStatus'] = 'Disabled';
  9168. $template_variables['ReverseProxySwitchText'] = 'Enable';
  9169. $template_variables['ReverseProxySwitchValue'] = 'enable';
  9170. $template_variables['ReverseProxySwitchCssClass'] = 'button-success';
  9171. }
  9172. if ( sucuriscan_collect_wrong_passwords() === true ) {
  9173. $template_variables['CollectWrongPasswords'] = '<span class="sucuriscan-label-error">Yes, collect passwords</span>';
  9174. }
  9175. // Determine if the API calls with pass through a proxy or not.
  9176. if ( class_exists( 'WP_HTTP_Proxy' ) ) {
  9177. $wp_http_proxy = new WP_HTTP_Proxy();
  9178. if ( $wp_http_proxy->is_enabled() ) {
  9179. $proxy_host = SucuriScan::escape( $wp_http_proxy->host() );
  9180. $proxy_port = SucuriScan::escape( $wp_http_proxy->port() );
  9181. $proxy_username = SucuriScan::escape( $wp_http_proxy->username() );
  9182. $proxy_password = SucuriScan::escape( $wp_http_proxy->password() );
  9183. $template_variables['APIProxy.Host'] = $proxy_host;
  9184. $template_variables['APIProxy.Port'] = $proxy_port;
  9185. $template_variables['APIProxy.Username'] = $proxy_username;
  9186. $template_variables['APIProxy.Password'] = $proxy_password;
  9187. $template_variables['APIProxy.PasswordType'] = 'info';
  9188. $template_variables['APIProxy.PasswordText'] = 'hidden';
  9189. }
  9190. }
  9191. return SucuriScanTemplate::get_section( 'settings-general', $template_variables );
  9192. }
  9193. /**
  9194. * Read and parse the content of the scanner settings template.
  9195. *
  9196. * @return string Parsed HTML code for the scanner settings panel.
  9197. */
  9198. function sucuriscan_settings_scanner(){
  9199. global $sucuriscan_schedule_allowed,
  9200. $sucuriscan_interface_allowed;
  9201. // Get initial variables to decide some things bellow.
  9202. $fs_scanner = SucuriScanOption::get_option( ':fs_scanner' );
  9203. $scan_freq = SucuriScanOption::get_option( ':scan_frequency' );
  9204. $scan_interface = SucuriScanOption::get_option( ':scan_interface' );
  9205. $scan_modfiles = SucuriScanOption::get_option( ':scan_modfiles' );
  9206. $scan_checksums = SucuriScanOption::get_option( ':scan_checksums' );
  9207. $scan_errorlogs = SucuriScanOption::get_option( ':scan_errorlogs' );
  9208. $parse_errorlogs = SucuriScanOption::get_option( ':parse_errorlogs' );
  9209. $errorlogs_limit = SucuriScanOption::get_option( ':errorlogs_limit' );
  9210. $ignore_scanning = SucuriScanOption::get_option( ':ignore_scanning' );
  9211. $sitecheck_scanner = SucuriScanOption::get_option( ':sitecheck_scanner' );
  9212. $sitecheck_counter = SucuriScanOption::get_option( ':sitecheck_counter' );
  9213. $runtime_scan_human = SucuriScanFSScanner::get_filesystem_runtime( true );
  9214. // Get the file path of the security logs.
  9215. $integrity_log_path = SucuriScan::datastore_folder_path( 'sucuri-integrity.php' );
  9216. $lastlogins_log_path = SucuriScan::datastore_folder_path( 'sucuri-lastlogins.php' );
  9217. $failedlogins_log_path = SucuriScan::datastore_folder_path( 'sucuri-failedlogins.php' );
  9218. $sitecheck_log_path = SucuriScan::datastore_folder_path( 'sucuri-sitecheck.php' );
  9219. // Generate the HTML code for the option list in the form select fields.
  9220. $scan_freq_options = SucuriScanTemplate::get_select_options( $sucuriscan_schedule_allowed, $scan_freq );
  9221. $scan_interface_options = SucuriScanTemplate::get_select_options( $sucuriscan_interface_allowed, $scan_interface );
  9222. $template_variables = array(
  9223. /* Filesystem scanner */
  9224. 'FsScannerStatus' => 'Enabled',
  9225. 'FsScannerSwitchText' => 'Disable',
  9226. 'FsScannerSwitchValue' => 'disable',
  9227. 'FsScannerSwitchCssClass' => 'button-danger',
  9228. /* Scan modified files. */
  9229. 'ScanModfilesStatus' => 'Enabled',
  9230. 'ScanModfilesSwitchText' => 'Disable',
  9231. 'ScanModfilesSwitchValue' => 'disable',
  9232. 'ScanModfilesSwitchCssClass' => 'button-danger',
  9233. /* Scan files checksum. */
  9234. 'ScanChecksumsStatus' => 'Enabled',
  9235. 'ScanChecksumsSwitchText' => 'Disable',
  9236. 'ScanChecksumsSwitchValue' => 'disable',
  9237. 'ScanChecksumsSwitchCssClass' => 'button-danger',
  9238. /* Ignore scanning. */
  9239. 'IgnoreScanningStatus' => 'Enabled',
  9240. 'IgnoreScanningSwitchText' => 'Disable',
  9241. 'IgnoreScanningSwitchValue' => 'disable',
  9242. 'IgnoreScanningSwitchCssClass' => 'button-danger',
  9243. /* Scan error logs. */
  9244. 'ScanErrorlogsStatus' => 'Enabled',
  9245. 'ScanErrorlogsSwitchText' => 'Disable',
  9246. 'ScanErrorlogsSwitchValue' => 'disable',
  9247. 'ScanErrorlogsSwitchCssClass' => 'button-danger',
  9248. /* Parse error logs. */
  9249. 'ParseErrorLogsStatus' => 'Enabled',
  9250. 'ParseErrorLogsSwitchText' => 'Disable',
  9251. 'ParseErrorLogsSwitchValue' => 'disable',
  9252. 'ParseErrorLogsSwitchCssClass' => 'button-danger',
  9253. /* SiteCheck scanner. */
  9254. 'SiteCheckScannerStatus' => 'Enabled',
  9255. 'SiteCheckScannerSwitchText' => 'Disable',
  9256. 'SiteCheckScannerSwitchValue' => 'disable',
  9257. 'SiteCheckScannerSwitchCssClass' => 'button-danger',
  9258. /* Filsystem scanning frequency. */
  9259. 'ScanningFrequency' => 'Undefined',
  9260. 'ScanningFrequencyOptions' => $scan_freq_options,
  9261. 'ScanningInterface' => ( $scan_interface ? $sucuriscan_interface_allowed[ $scan_interface ] : 'Undefined' ),
  9262. 'ScanningInterfaceOptions' => $scan_interface_options,
  9263. /* Filesystem scanning runtime. */
  9264. 'ScanningRuntimeHuman' => $runtime_scan_human,
  9265. 'SiteCheckCounter' => $sitecheck_counter,
  9266. 'ErrorLogsLimit' => $errorlogs_limit,
  9267. 'IntegrityLogLife' => '0B',
  9268. 'LastLoginLogLife' => '0B',
  9269. 'FailedLoginLogLife' => '0B',
  9270. 'SiteCheckLogLife' => '0B',
  9271. );
  9272. if ( $fs_scanner == 'disabled' ){
  9273. $template_variables['FsScannerStatus'] = 'Disabled';
  9274. $template_variables['FsScannerSwitchText'] = 'Enable';
  9275. $template_variables['FsScannerSwitchValue'] = 'enable';
  9276. $template_variables['FsScannerSwitchCssClass'] = 'button-success';
  9277. }
  9278. if ( $scan_modfiles == 'disabled' ){
  9279. $template_variables['ScanModfilesStatus'] = 'Disabled';
  9280. $template_variables['ScanModfilesSwitchText'] = 'Enable';
  9281. $template_variables['ScanModfilesSwitchValue'] = 'enable';
  9282. $template_variables['ScanModfilesSwitchCssClass'] = 'button-success';
  9283. }
  9284. if ( $scan_checksums == 'disabled' ){
  9285. $template_variables['ScanChecksumsStatus'] = 'Disabled';
  9286. $template_variables['ScanChecksumsSwitchText'] = 'Enable';
  9287. $template_variables['ScanChecksumsSwitchValue'] = 'enable';
  9288. $template_variables['ScanChecksumsSwitchCssClass'] = 'button-success';
  9289. }
  9290. if ( $ignore_scanning == 'disabled' ){
  9291. $template_variables['IgnoreScanningStatus'] = 'Disabled';
  9292. $template_variables['IgnoreScanningSwitchText'] = 'Enable';
  9293. $template_variables['IgnoreScanningSwitchValue'] = 'enable';
  9294. $template_variables['IgnoreScanningSwitchCssClass'] = 'button-success';
  9295. }
  9296. if ( $scan_errorlogs == 'disabled' ){
  9297. $template_variables['ScanErrorlogsStatus'] = 'Disabled';
  9298. $template_variables['ScanErrorlogsSwitchText'] = 'Enable';
  9299. $template_variables['ScanErrorlogsSwitchValue'] = 'enable';
  9300. $template_variables['ScanErrorlogsSwitchCssClass'] = 'button-success';
  9301. }
  9302. if ( $parse_errorlogs == 'disabled' ){
  9303. $template_variables['ParseErrorLogsStatus'] = 'Disabled';
  9304. $template_variables['ParseErrorLogsSwitchText'] = 'Enable';
  9305. $template_variables['ParseErrorLogsSwitchValue'] = 'enable';
  9306. $template_variables['ParseErrorLogsSwitchCssClass'] = 'button-success';
  9307. }
  9308. if ( $sitecheck_scanner == 'disabled' ){
  9309. $template_variables['SiteCheckScannerStatus'] = 'Disabled';
  9310. $template_variables['SiteCheckScannerSwitchText'] = 'Enable';
  9311. $template_variables['SiteCheckScannerSwitchValue'] = 'enable';
  9312. $template_variables['SiteCheckScannerSwitchCssClass'] = 'button-success';
  9313. }
  9314. if ( array_key_exists( $scan_freq, $sucuriscan_schedule_allowed ) ){
  9315. $template_variables['ScanningFrequency'] = $sucuriscan_schedule_allowed[ $scan_freq ];
  9316. }
  9317. // Determine the age of the security log files.
  9318. $template_variables['IntegrityLogLife'] = SucuriScan::human_filesize( @filesize( $integrity_log_path ) );
  9319. $template_variables['LastLoginLogLife'] = SucuriScan::human_filesize( @filesize( $lastlogins_log_path ) );
  9320. $template_variables['FailedLoginLogLife'] = SucuriScan::human_filesize( @filesize( $failedlogins_log_path ) );
  9321. $template_variables['SiteCheckLogLife'] = SucuriScan::human_filesize( @filesize( $sitecheck_log_path ) );
  9322. return SucuriScanTemplate::get_section( 'settings-scanner', $template_variables );
  9323. }
  9324. /**
  9325. * Read and parse the content of the notification settings template.
  9326. *
  9327. * @return string Parsed HTML code for the notification settings panel.
  9328. */
  9329. function sucuriscan_settings_notifications(){
  9330. global $sucuriscan_notify_options,
  9331. $sucuriscan_email_subjects;
  9332. $template_variables = array(
  9333. 'NotificationOptions' => '',
  9334. 'EmailSubjectOptions' => '',
  9335. 'EmailSubjectCustom.Checked' => '',
  9336. 'EmailSubjectCustom.Value' => '',
  9337. 'PrettifyMailsWarningVisibility' => SucuriScanTemplate::visibility( SucuriScanMail::prettify_mails() ),
  9338. );
  9339. if ( $sucuriscan_email_subjects ) {
  9340. $email_subject = SucuriScanOption::get_option( ':email_subject' );
  9341. $is_official_subject = false;
  9342. foreach ( $sucuriscan_email_subjects as $subject_format ) {
  9343. if ( $email_subject == $subject_format ) {
  9344. $is_official_subject = true;
  9345. $checked = 'checked="checked"';
  9346. } else {
  9347. $checked = '';
  9348. }
  9349. $template_variables['EmailSubjectOptions'] .= SucuriScanTemplate::get_snippet('settings-emailsubject', array(
  9350. 'EmailSubject.Name' => $subject_format,
  9351. 'EmailSubject.Value' => $subject_format,
  9352. 'EmailSubject.Checked' => $checked,
  9353. ));
  9354. }
  9355. if ( $is_official_subject === false ) {
  9356. $template_variables['EmailSubjectCustom.Checked'] = 'checked="checked"';
  9357. $template_variables['EmailSubjectCustom.Value'] = SucuriScan::escape( $email_subject );
  9358. }
  9359. }
  9360. $counter = 0;
  9361. $alert_pattern = '/^([a-z]+:)?(.+)/';
  9362. foreach ( $sucuriscan_notify_options as $alert_type => $alert_label ){
  9363. $alert_value = SucuriScanOption::get_option( $alert_type );
  9364. $checked = ( $alert_value == 'enabled' ? 'checked="checked"' : '' );
  9365. $css_class = ( $counter % 2 == 0 ) ? 'alternate' : '';
  9366. $alert_icon = '';
  9367. if ( preg_match( $alert_pattern, $alert_label, $match ) ) {
  9368. $alert_group = str_replace( ':', '', $match[1] );
  9369. $alert_label = $match[2];
  9370. switch ( $alert_group ) {
  9371. case 'user': $alert_icon = 'dashicons-before dashicons-admin-users'; break;
  9372. case 'plugin': $alert_icon = 'dashicons-before dashicons-admin-plugins'; break;
  9373. case 'theme': $alert_icon = 'dashicons-before dashicons-admin-appearance'; break;
  9374. }
  9375. }
  9376. $template_variables['NotificationOptions'] .= SucuriScanTemplate::get_snippet('settings-notifications', array(
  9377. 'Notification.CssClass' => $css_class,
  9378. 'Notification.Name' => $alert_type,
  9379. 'Notification.Checked' => $checked,
  9380. 'Notification.Label' => $alert_label,
  9381. 'Notification.LabelIcon' => $alert_icon,
  9382. ));
  9383. $counter += 1;
  9384. }
  9385. return SucuriScanTemplate::get_section( 'settings-notifications', $template_variables );
  9386. }
  9387. /**
  9388. * Read and parse the content of the ignored-rules settings template.
  9389. *
  9390. * @return string Parsed HTML code for the ignored-rules settings panel.
  9391. */
  9392. function sucuriscan_settings_ignore_rules(){
  9393. $notify_new_site_content = SucuriScanOption::get_option( ':notify_post_publication' );
  9394. $template_variables = array(
  9395. 'IgnoreRules.MessageVisibility' => 'visible',
  9396. 'IgnoreRules.TableVisibility' => 'hidden',
  9397. 'IgnoreRules.PostTypes' => '',
  9398. );
  9399. if ( $notify_new_site_content == 'enabled' ){
  9400. $post_types = get_post_types();
  9401. $ignored_events = SucuriScanOption::get_ignored_events();
  9402. $template_variables['IgnoreRules.MessageVisibility'] = 'hidden';
  9403. $template_variables['IgnoreRules.TableVisibility'] = 'visible';
  9404. $counter = 0;
  9405. foreach ( $post_types as $post_type => $post_type_object ){
  9406. $counter += 1;
  9407. $css_class = ( $counter % 2 == 0 ) ? 'alternate' : '';
  9408. $post_type_title = ucwords( str_replace( '_', chr( 32 ), $post_type ) );
  9409. if ( array_key_exists( $post_type, $ignored_events ) ){
  9410. $is_ignored_text = 'YES';
  9411. $was_ignored_at = SucuriScan::datetime( $ignored_events[ $post_type ] );
  9412. $is_ignored_class = 'danger';
  9413. $button_action = 'remove';
  9414. $button_class = 'button-primary';
  9415. $button_text = 'Allow';
  9416. } else {
  9417. $is_ignored_text = 'NO';
  9418. $was_ignored_at = 'Not ignored';
  9419. $is_ignored_class = 'success';
  9420. $button_action = 'add';
  9421. $button_class = 'button-primary button-danger';
  9422. $button_text = 'Ignore';
  9423. }
  9424. $template_variables['IgnoreRules.PostTypes'] .= SucuriScanTemplate::get_snippet('settings-ignorerules', array(
  9425. 'IgnoreRules.CssClass' => $css_class,
  9426. 'IgnoreRules.Num' => $counter,
  9427. 'IgnoreRules.PostTypeTitle' => $post_type_title,
  9428. 'IgnoreRules.IsIgnored' => $is_ignored_text,
  9429. 'IgnoreRules.WasIgnoredAt' => $was_ignored_at,
  9430. 'IgnoreRules.IsIgnoredClass' => $is_ignored_class,
  9431. 'IgnoreRules.PostType' => $post_type,
  9432. 'IgnoreRules.Action' => $button_action,
  9433. 'IgnoreRules.ButtonClass' => 'button ' . $button_class,
  9434. 'IgnoreRules.ButtonText' => $button_text,
  9435. ));
  9436. }
  9437. }
  9438. return SucuriScanTemplate::get_section( 'settings-ignorerules', $template_variables );
  9439. }
  9440. /**
  9441. * Read and parse the content of the trust-ip settings template.
  9442. *
  9443. * @return string Parsed HTML code for the trust-ip settings panel.
  9444. */
  9445. function sucuriscan_settings_trust_ip(){
  9446. $template_variables = array(
  9447. 'TrustedIPs.List' => '',
  9448. 'TrustedIPs.NoItems.Visibility' => 'visible',
  9449. );
  9450. $cache = new SucuriScanCache( 'trustip' );
  9451. $trusted_ips = $cache->get_all();
  9452. if ( $trusted_ips ) {
  9453. $counter = 0;
  9454. foreach ( $trusted_ips as $cache_key => $ip_info ) {
  9455. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  9456. if ( $ip_info->cidr_range == 32 ) {
  9457. $ip_info->cidr_format = 'n/a';
  9458. }
  9459. $template_variables['TrustedIPs.List'] .= SucuriScanTemplate::get_snippet('settings-trustip', array(
  9460. 'TrustIP.CssClass' => $css_class,
  9461. 'TrustIP.CacheKey' => $cache_key,
  9462. 'TrustIP.RemoteAddr' => SucuriScan::escape( $ip_info->remote_addr ),
  9463. 'TrustIP.CIDRFormat' => SucuriScan::escape( $ip_info->cidr_format ),
  9464. 'TrustIP.AddedAt' => SucuriScan::datetime( $ip_info->added_at ),
  9465. ));
  9466. $counter += 1;
  9467. }
  9468. if ( $counter > 0 ) {
  9469. $template_variables['TrustedIPs.NoItems.Visibility'] = 'hidden';
  9470. }
  9471. }
  9472. return SucuriScanTemplate::get_section( 'settings-trustip', $template_variables );
  9473. }
  9474. /**
  9475. * Read and parse the content of the ignore-scanning settings template.
  9476. *
  9477. * @return string Parsed HTML code for the ignore-scanning settings panel.
  9478. */
  9479. function sucuriscan_settings_ignorescanning(){
  9480. $template_variables = array(
  9481. 'IgnoreScanning.ResourceList' => '',
  9482. 'IgnoreScanning.DisabledVisibility' => 'visible',
  9483. 'IgnoreScanning.NoItemsVisibility' => 'visible',
  9484. );
  9485. $ignore_scanning = SucuriScanFSScanner::will_ignore_scanning();
  9486. // Allow disable of this option temporarily.
  9487. if ( SucuriScanRequest::get( 'no_scan' ) == 1 ){
  9488. $ignore_scanning = false;
  9489. }
  9490. // Scan the project and get the ignored paths.
  9491. if ( $ignore_scanning === true ){
  9492. $counter = 0;
  9493. $template_variables['IgnoreScanning.DisabledVisibility'] = 'hidden';
  9494. $dir_list_list = SucuriScanFSScanner::get_ignored_directories_live();
  9495. foreach ( $dir_list_list as $group => $dir_list ){
  9496. foreach ( $dir_list as $dir_data ){
  9497. $valid_entry = false;
  9498. $snippet_data = array(
  9499. 'IgnoreScanning.CssClass' => '',
  9500. 'IgnoreScanning.Directory' => '',
  9501. 'IgnoreScanning.DirectoryPath' => '',
  9502. 'IgnoreScanning.IgnoredAt' => '',
  9503. 'IgnoreScanning.IgnoredAtText' => 'ok',
  9504. 'IgnoreScanning.IgnoredCssClass' => 'success',
  9505. );
  9506. if ( $group == 'is_ignored' ){
  9507. $valid_entry = true;
  9508. $snippet_data['IgnoreScanning.Directory'] = urlencode( $dir_data['directory_path'] );
  9509. $snippet_data['IgnoreScanning.DirectoryPath'] = SucuriScan::escape( $dir_data['directory_path'] );
  9510. $snippet_data['IgnoreScanning.IgnoredAt'] = SucuriScan::datetime( $dir_data['ignored_at'] );
  9511. $snippet_data['IgnoreScanning.IgnoredAtText'] = 'ignored';
  9512. $snippet_data['IgnoreScanning.IgnoredCssClass'] = 'warning';
  9513. }
  9514. elseif ( $group == 'is_not_ignored' ){
  9515. $valid_entry = true;
  9516. $snippet_data['IgnoreScanning.Directory'] = urlencode( $dir_data );
  9517. $snippet_data['IgnoreScanning.DirectoryPath'] = SucuriScan::escape( $dir_data );
  9518. }
  9519. if ( $valid_entry ){
  9520. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  9521. $snippet_data['IgnoreScanning.CssClass'] = $css_class;
  9522. $template_variables['IgnoreScanning.ResourceList'] .= SucuriScanTemplate::get_snippet( 'settings-ignorescanning', $snippet_data );
  9523. $counter += 1;
  9524. }
  9525. }
  9526. }
  9527. if ( $counter > 0 ){
  9528. $template_variables['IgnoreScanning.NoItemsVisibility'] = 'hidden';
  9529. }
  9530. }
  9531. return SucuriScanTemplate::get_section( 'settings-ignorescanning', $template_variables );
  9532. }
  9533. /**
  9534. * Read and parse the content of the heartbeat settings template.
  9535. *
  9536. * @return string Parsed HTML code for the heartbeat settings panel.
  9537. */
  9538. function sucuriscan_settings_heartbeat(){
  9539. // Current values set in the options table.
  9540. $heartbeat_status = SucuriScanOption::get_option( ':heartbeat' );
  9541. $heartbeat_pulse = SucuriScanOption::get_option( ':heartbeat_pulse' );
  9542. $heartbeat_interval = SucuriScanOption::get_option( ':heartbeat_interval' );
  9543. $heartbeat_autostart = SucuriScanOption::get_option( ':heartbeat_autostart' );
  9544. // Allowed values for each setting.
  9545. $statuses_allowed = SucuriScanHeartbeat::statuses_allowed();
  9546. $pulses_allowed = SucuriScanHeartbeat::pulses_allowed();
  9547. $intervals_allowed = SucuriScanHeartbeat::intervals_allowed();
  9548. // HTML select form fields.
  9549. $heartbeat_options = SucuriScanTemplate::get_select_options( $statuses_allowed, $heartbeat_status );
  9550. $heartbeat_pulse_options = SucuriScanTemplate::get_select_options( $pulses_allowed, $heartbeat_pulse );
  9551. $heartbeat_interval_options = SucuriScanTemplate::get_select_options( $intervals_allowed, $heartbeat_interval );
  9552. $template_variables = array(
  9553. 'HeartbeatStatus' => 'Undefined',
  9554. 'HeartbeatPulse' => 'Undefined',
  9555. 'HeartbeatInterval' => 'Undefined',
  9556. /* Heartbeat Options. */
  9557. 'HeartbeatStatusOptions' => $heartbeat_options,
  9558. 'HeartbeatPulseOptions' => $heartbeat_pulse_options,
  9559. 'HeartbeatIntervalOptions' => $heartbeat_interval_options,
  9560. /* Heartbeat Auto-Start. */
  9561. 'HeartbeatAutostart' => 'Enabled',
  9562. 'HeartbeatAutostartSwitchText' => 'Disable',
  9563. 'HeartbeatAutostartSwitchValue' => 'disable',
  9564. 'HeartbeatAutostartSwitchCssClass' => 'button-danger',
  9565. );
  9566. if ( array_key_exists( $heartbeat_status, $statuses_allowed ) ){
  9567. $template_variables['HeartbeatStatus'] = $statuses_allowed[ $heartbeat_status ];
  9568. }
  9569. if ( array_key_exists( $heartbeat_pulse, $pulses_allowed ) ){
  9570. $template_variables['HeartbeatPulse'] = $pulses_allowed[ $heartbeat_pulse ];
  9571. }
  9572. if ( array_key_exists( $heartbeat_interval, $intervals_allowed ) ){
  9573. $template_variables['HeartbeatInterval'] = $intervals_allowed[ $heartbeat_interval ];
  9574. }
  9575. if ( $heartbeat_autostart == 'disabled' ){
  9576. $template_variables['HeartbeatAutostart'] = 'Disabled';
  9577. $template_variables['HeartbeatAutostartSwitchText'] = 'Enable';
  9578. $template_variables['HeartbeatAutostartSwitchValue'] = 'enable';
  9579. $template_variables['HeartbeatAutostartSwitchCssClass'] = 'button-success';
  9580. }
  9581. return SucuriScanTemplate::get_section( 'settings-heartbeat', $template_variables );
  9582. }
  9583. /**
  9584. * Generate and print the HTML code for the InfoSys page.
  9585. *
  9586. * This page will contains information of the system where the site is hosted,
  9587. * also information about users in session, htaccess rules and configuration
  9588. * options.
  9589. *
  9590. * @return void
  9591. */
  9592. function sucuriscan_infosys_page(){
  9593. SucuriScanInterface::check_permissions();
  9594. // Process all form submissions.
  9595. sucuriscan_infosys_form_submissions();
  9596. // Page pseudo-variables initialization.
  9597. $template_variables = array(
  9598. 'PageTitle' => 'Site Info',
  9599. 'ServerInfo' => sucuriscan_server_info(),
  9600. 'Cronjobs' => sucuriscan_show_cronjobs(),
  9601. 'HTAccessIntegrity' => sucuriscan_infosys_htaccess(),
  9602. 'WordpressConfig' => sucuriscan_infosys_wpconfig(),
  9603. 'ErrorLogs' => sucuriscan_infosys_errorlogs(),
  9604. );
  9605. echo SucuriScanTemplate::get_template( 'infosys', $template_variables );
  9606. }
  9607. /**
  9608. * Find the main htaccess file for the site and check whether the rules of the
  9609. * main htaccess file of the site are the default rules generated by WordPress.
  9610. *
  9611. * @return string The HTML code displaying the information about the HTAccess rules.
  9612. */
  9613. function sucuriscan_infosys_htaccess(){
  9614. $htaccess_path = SucuriScan::get_htaccess_path();
  9615. $template_variables = array(
  9616. 'HTAccess.Content' => '',
  9617. 'HTAccess.Message' => '',
  9618. 'HTAccess.MessageType' => '',
  9619. 'HTAccess.MessageVisible' => 'hidden',
  9620. 'HTAccess.TextareaVisible' => 'hidden',
  9621. );
  9622. if ( $htaccess_path ){
  9623. $htaccess_rules = file_get_contents( $htaccess_path );
  9624. $template_variables['HTAccess.MessageType'] = 'updated';
  9625. $template_variables['HTAccess.MessageVisible'] = 'visible';
  9626. $template_variables['HTAccess.TextareaVisible'] = 'visible';
  9627. $template_variables['HTAccess.Content'] = $htaccess_rules;
  9628. $template_variables['HTAccess.Message'] .= 'HTAccess file found in this path <code>'.$htaccess_path.'</code>';
  9629. if ( empty($htaccess_rules) ){
  9630. $template_variables['HTAccess.TextareaVisible'] = 'hidden';
  9631. $template_variables['HTAccess.Message'] .= '</p><p>The HTAccess file found is completely empty.';
  9632. }
  9633. if ( sucuriscan_htaccess_is_standard( $htaccess_rules ) ){
  9634. $template_variables['HTAccess.Message'] .= '</p><p>
  9635. The main <code>.htaccess</code> file in your site has the standard rules for a WordPress installation. You can customize it to improve the
  9636. performance and change the behaviour of the redirections for pages and posts in your site. To get more information visit the official documentation at
  9637. <a href="http://codex.wordpress.org/Using_Permalinks#Creating_and_editing_.28.htaccess.29" target="_blank">Codex WordPrexx - Creating and editing (.htaccess)</a>';
  9638. }
  9639. }else {
  9640. $template_variables['HTAccess.Message'] = 'Your website does not contains a <code>.htaccess</code> file or it was not found in the default location.';
  9641. $template_variables['HTAccess.MessageType'] = 'error';
  9642. $template_variables['HTAccess.MessageVisible'] = 'visible';
  9643. }
  9644. return SucuriScanTemplate::get_section( 'infosys-htaccess', $template_variables );
  9645. }
  9646. /**
  9647. * Check whether the rules in a htaccess file are the default options generated
  9648. * by WordPress or if the file has custom options added by other Plugins.
  9649. *
  9650. * @param string $rules Optional parameter containing a text string with the content of the main htaccess file.
  9651. * @return boolean Either TRUE or FALSE if the rules found in the htaccess file specified are the default ones or not.
  9652. */
  9653. function sucuriscan_htaccess_is_standard( $rules = false ){
  9654. if ( $rules === false ){
  9655. $htaccess_path = SucuriScan::get_htaccess_path();
  9656. $rules = $htaccess_path ? file_get_contents( $htaccess_path ) : '';
  9657. }
  9658. if ( ! empty($rules) ){
  9659. $standard_lines = array(
  9660. '# BEGIN WordPress',
  9661. '<IfModule mod_rewrite\.c>',
  9662. 'RewriteEngine On',
  9663. 'RewriteBase \/',
  9664. 'RewriteRule .index.\.php. - \[L\]',
  9665. 'RewriteCond %\{REQUEST_FILENAME\} \!-f',
  9666. 'RewriteCond %\{REQUEST_FILENAME\} \!-d',
  9667. 'RewriteRule \. \/index\.php \[L\]',
  9668. '<\/IfModule>',
  9669. '# END WordPress',
  9670. );
  9671. $pattern = '';
  9672. $standard_lines_total = count( $standard_lines );
  9673. foreach ( $standard_lines as $i => $line ){
  9674. if ( $i < ($standard_lines_total -1) ){
  9675. $end_of_line = "\n";
  9676. }else {
  9677. $end_of_line = '';
  9678. }
  9679. $pattern .= sprintf( '%s%s', $line, $end_of_line );
  9680. }
  9681. if ( preg_match( "/{$pattern}/", $rules ) ){
  9682. return true;
  9683. }
  9684. }
  9685. return false;
  9686. }
  9687. /**
  9688. * Retrieve all the constants and variables with their respective values defined
  9689. * in the WordPress configuration file, only the database password constant is
  9690. * omitted for security reasons.
  9691. *
  9692. * @return string The HTML code displaying the constants and variables found in the wp-config file.
  9693. */
  9694. function sucuriscan_infosys_wpconfig(){
  9695. $template_variables = array(
  9696. 'WordpressConfig.Rules' => '',
  9697. 'WordpressConfig.Total' => 0,
  9698. );
  9699. $ignore_wp_rules = array( 'DB_PASSWORD' );
  9700. $wp_config_path = SucuriScan::get_wpconfig_path();
  9701. if ( $wp_config_path ){
  9702. $wp_config_rules = array();
  9703. $wp_config_content = SucuriScanFileInfo::file_lines( $wp_config_path );
  9704. // Parse the main configuration file and look for constants and global variables.
  9705. foreach ( (array) $wp_config_content as $line ){
  9706. // Ignore commented lines.
  9707. if ( preg_match( '/^\s?(#|\/\/)/', $line ) ) { continue; }
  9708. // Detect PHP constants even if the line if indented.
  9709. elseif ( preg_match( '/define\(/', $line ) ) {
  9710. $line = preg_replace( '/.*define\((.+)\);.*/', '$1', $line );
  9711. $line_parts = explode( ',', $line, 2 );
  9712. }
  9713. // Detect global variables like the database table prefix.
  9714. elseif ( preg_match( '/^\$[a-zA-Z_]+/', $line ) ){
  9715. $line = preg_replace( '/;\s\/\/.*/', ';', $line );
  9716. $line_parts = explode( '=', $line, 2 );
  9717. }
  9718. // Ignore other lines.
  9719. else { continue; }
  9720. // Clean and append the rule to the wp_config_rules variable.
  9721. if ( isset($line_parts) && count( $line_parts ) == 2 ){
  9722. $key_name = '';
  9723. $key_value = '';
  9724. // TODO: A foreach loop is not really necessary, find a better way.
  9725. foreach ( $line_parts as $i => $line_part ){
  9726. $line_part = trim( $line_part );
  9727. $line_part = ltrim( $line_part, '$' );
  9728. $line_part = rtrim( $line_part, ';' );
  9729. // Remove single/double quotes at the beginning and end of the string.
  9730. $line_part = ltrim( $line_part, "'" );
  9731. $line_part = rtrim( $line_part, "'" );
  9732. $line_part = ltrim( $line_part, '"' );
  9733. $line_part = rtrim( $line_part, '"' );
  9734. // Assign the clean strings to specific variables.
  9735. if ( $i == 0 ){ $key_name = $line_part; }
  9736. if ( $i == 1 ){
  9737. if ( defined( $key_name ) ){
  9738. $key_value = constant( $key_name );
  9739. if ( is_bool( $key_value ) ){
  9740. $key_value = ( $key_value === true ) ? 'TRUE' : 'FALSE';
  9741. }
  9742. } else {
  9743. $key_value = $line_part;
  9744. }
  9745. }
  9746. }
  9747. // Remove the value of sensitive variables like the database password.
  9748. if ( in_array( $key_name, $ignore_wp_rules ) ){
  9749. $key_value = 'hidden';
  9750. }
  9751. // Append the value to the configuration rules.
  9752. $wp_config_rules[ $key_name ] = $key_value;
  9753. }
  9754. }
  9755. // Pass the WordPress configuration rules to the template and show them.
  9756. $counter = 0;
  9757. foreach ( $wp_config_rules as $var_name => $var_value ){
  9758. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  9759. $label_css = 'sucuriscan-monospace';
  9760. if ( empty($var_value) ){
  9761. $var_value = 'empty';
  9762. $label_css = 'sucuriscan-label-default';
  9763. }
  9764. elseif ( $var_value == 'hidden' ){
  9765. $label_css = 'sucuriscan-label-info';
  9766. }
  9767. $template_variables['WordpressConfig.Total'] += 1;
  9768. $template_variables['WordpressConfig.Rules'] .= SucuriScanTemplate::get_snippet('infosys-wpconfig', array(
  9769. 'WordpressConfig.VariableName' => SucuriScan::escape( $var_name ),
  9770. 'WordpressConfig.VariableValue' => SucuriScan::escape( $var_value ),
  9771. 'WordpressConfig.VariableCssClass' => $label_css,
  9772. 'WordpressConfig.CssClass' => $css_class,
  9773. ));
  9774. $counter += 1;
  9775. }
  9776. }
  9777. return SucuriScanTemplate::get_section( 'infosys-wpconfig', $template_variables );
  9778. }
  9779. /**
  9780. * Retrieve a list with the scheduled tasks configured for the site.
  9781. *
  9782. * @return array A list of pseudo-variables and values that will replace them in the HTML template.
  9783. */
  9784. function sucuriscan_show_cronjobs(){
  9785. $template_variables = array(
  9786. 'Cronjobs.List' => '',
  9787. 'Cronjobs.Total' => 0,
  9788. );
  9789. $cronjobs = _get_cron_array();
  9790. $schedules = wp_get_schedules();
  9791. $counter = 0;
  9792. foreach ( $cronjobs as $timestamp => $cronhooks ){
  9793. foreach ( (array) $cronhooks as $hook => $events ){
  9794. foreach ( (array) $events as $key => $event ){
  9795. if ( empty($event['args']) ){
  9796. $event['args'] = array( '<em>empty</em>' );
  9797. }
  9798. $template_variables['Cronjobs.Total'] += 1;
  9799. $template_variables['Cronjobs.List'] .= SucuriScanTemplate::get_snippet('infosys-cronjobs', array(
  9800. 'Cronjob.Hook' => $hook,
  9801. 'Cronjob.Schedule' => $event['schedule'],
  9802. 'Cronjob.NextTime' => SucuriScan::datetime( $timestamp ),
  9803. 'Cronjob.Arguments' => SucuriScan::implode( ', ', $event['args'] ),
  9804. 'Cronjob.CssClass' => ( $counter % 2 == 0 ) ? '' : 'alternate',
  9805. ));
  9806. $counter += 1;
  9807. }
  9808. }
  9809. }
  9810. return SucuriScanTemplate::get_section( 'infosys-cronjobs', $template_variables );
  9811. }
  9812. /**
  9813. * Process the requests sent by the form submissions originated in the infosys
  9814. * page, all forms must have a nonce field that will be checked against the one
  9815. * generated in the template render function.
  9816. *
  9817. * @param boolean $page_nonce True if the nonce is valid, False otherwise.
  9818. * @return void
  9819. */
  9820. function sucuriscan_infosys_form_submissions(){
  9821. if ( SucuriScanInterface::check_nonce() ){
  9822. // Modify the scheduled tasks (run now, remove, re-schedule).
  9823. $allowed_actions = '(runnow|hourly|twicedaily|daily|remove)';
  9824. if ( $cronjob_action = SucuriScanRequest::post( ':cronjob_action', $allowed_actions ) ){
  9825. $cronjobs = SucuriScanRequest::post( ':cronjobs', '_array' );
  9826. if ( ! empty($cronjobs) ){
  9827. $total_tasks = count( $cronjobs );
  9828. // Force execution of the selected scheduled tasks.
  9829. if ( $cronjob_action == 'runnow' ) {
  9830. SucuriScanInterface::info( $total_tasks . ' tasks were scheduled to run in the next ten seconds.' );
  9831. SucuriScanEvent::report_notice_event( sprintf(
  9832. 'Force execution of scheduled tasks: (multiple entries): %s',
  9833. @implode( ',', $cronjobs )
  9834. ) );
  9835. foreach ( $cronjobs as $task_name ){
  9836. wp_schedule_single_event( time() + 10, $task_name );
  9837. }
  9838. }
  9839. // Force deletion of the selected scheduled tasks.
  9840. elseif ( $cronjob_action == 'remove' ) {
  9841. SucuriScanInterface::info( $total_tasks . ' scheduled tasks were removed.' );
  9842. SucuriScanEvent::report_notice_event( sprintf(
  9843. 'Delete scheduled tasks: (multiple entries): %s',
  9844. @implode( ',', $cronjobs )
  9845. ) );
  9846. foreach ( $cronjobs as $task_name ){
  9847. wp_clear_scheduled_hook( $task_name );
  9848. }
  9849. }
  9850. // Re-schedule the selected scheduled tasks.
  9851. elseif (
  9852. $cronjob_action == 'hourly'
  9853. || $cronjob_action == 'twicedaily'
  9854. || $cronjob_action == 'daily'
  9855. ) {
  9856. SucuriScanInterface::info( $total_tasks . ' tasks were re-scheduled to run <code>' . $cronjob_action . '</code>.' );
  9857. SucuriScanEvent::report_notice_event( sprintf(
  9858. 'Re-configure scheduled tasks %s: (multiple entries): %s',
  9859. $cronjob_action,
  9860. @implode( ',', $cronjobs )
  9861. ) );
  9862. foreach ( $cronjobs as $task_name ){
  9863. wp_clear_scheduled_hook( $task_name );
  9864. $next_due = wp_next_scheduled( $task_name );
  9865. wp_schedule_event( $next_due, $cronjob_action, $task_name );
  9866. }
  9867. }
  9868. } else {
  9869. SucuriScanInterface::error( 'No scheduled tasks were selected from the list.' );
  9870. }
  9871. }
  9872. }
  9873. }
  9874. /**
  9875. * Locate, parse and display the latest error logged in the main error_log file.
  9876. *
  9877. * @return array A list of pseudo-variables and values that will replace them in the HTML template.
  9878. */
  9879. function sucuriscan_infosys_errorlogs(){
  9880. $template_variables = array(
  9881. 'ErrorLog.Path' => '',
  9882. 'ErrorLog.Exists' => 'No',
  9883. 'ErrorLog.NoItemsVisibility' => 'hidden',
  9884. 'ErrorLog.DisabledVisibility' => 'hidden',
  9885. 'ErrorLog.InvalidFormatVisibility' => 'hidden',
  9886. 'ErrorLog.LogsLimit' => '0',
  9887. 'ErrorLog.FileSize' => '0B',
  9888. 'ErrorLog.List' => '',
  9889. );
  9890. $error_log_path = false;
  9891. $log_filename = SucuriScan::ini_get( 'error_log' );
  9892. $errorlogs_limit = SucuriScanOption::get_option( ':errorlogs_limit' );
  9893. $template_variables['ErrorLog.LogsLimit'] = $errorlogs_limit;
  9894. $errorlogs_counter = 0;
  9895. if ( $log_filename ) {
  9896. $error_log_path = @realpath( ABSPATH . '/' . $log_filename );
  9897. }
  9898. if ( SucuriScanOption::get_option( ':parse_errorlogs' ) === 'disabled' ) {
  9899. $template_variables['ErrorLog.DisabledVisibility'] = 'visible';
  9900. }
  9901. if ( $error_log_path ) {
  9902. $template_variables['ErrorLog.Path'] = $error_log_path;
  9903. $template_variables['ErrorLog.Exists'] = 'Yes';
  9904. $template_variables['ErrorLog.FileSize'] = SucuriScan::human_filesize( filesize( $error_log_path ) );
  9905. $last_lines = SucuriScanFileInfo::tail_file( $error_log_path, $errorlogs_limit );
  9906. $error_logs = SucuriScanFSScanner::parse_error_logs( $last_lines );
  9907. $error_logs = array_reverse( $error_logs );
  9908. $errorlogs_counter = 0;
  9909. foreach ( $error_logs as $error_log ) {
  9910. $css_class = ( $errorlogs_counter % 2 == 0 ) ? '' : 'alternate';
  9911. $template_variables['ErrorLog.List'] .= SucuriScanTemplate::get_snippet('infosys-errorlogs', array(
  9912. 'ErrorLog.CssClass' => $css_class,
  9913. 'ErrorLog.DateTime' => SucuriScan::datetime( $error_log->timestamp ),
  9914. 'ErrorLog.ErrorType' => SucuriScan::escape( $error_log->error_type ),
  9915. 'ErrorLog.ErrorCode' => SucuriScan::escape( $error_log->error_code ),
  9916. 'ErrorLog.ErrorAbbr' => strtoupper( substr( $error_log->error_code, 0, 1 ) ),
  9917. 'ErrorLog.ErrorMessage' => SucuriScan::escape( $error_log->error_message ),
  9918. 'ErrorLog.FilePath' => SucuriScan::escape( $error_log->file_path ),
  9919. 'ErrorLog.LineNumber' => SucuriScan::escape( $error_log->line_number ),
  9920. ));
  9921. $errorlogs_counter += 1;
  9922. }
  9923. if ( $errorlogs_counter <= 0 ) {
  9924. $template_variables['ErrorLog.InvalidFormatVisibility'] = 'visible';
  9925. }
  9926. } else {
  9927. $template_variables['ErrorLog.NoItemsVisibility'] = 'visible';
  9928. }
  9929. return SucuriScanTemplate::get_section( 'infosys-errorlogs', $template_variables );
  9930. }
  9931. /**
  9932. * Gather information from the server, database engine, and PHP interpreter.
  9933. *
  9934. * @return array A list of pseudo-variables and values that will replace them in the HTML template.
  9935. */
  9936. function sucuriscan_server_info(){
  9937. global $wpdb;
  9938. $template_variables = array(
  9939. 'ServerInfo.Variables' => '',
  9940. );
  9941. $info_vars = array(
  9942. 'Plugin_version' => SUCURISCAN_VERSION,
  9943. 'Plugin_checksum' => SUCURISCAN_PLUGIN_CHECKSUM,
  9944. 'Last_filesystem_scan' => SucuriScanFSScanner::get_filesystem_runtime( true ),
  9945. 'Using_CloudProxy' => 'Unknown',
  9946. 'Support_Reverse_Proxy' => 'Unknown',
  9947. 'Host_Address' => 'Unknown',
  9948. 'HTTP_Host' => 'Unknown',
  9949. 'Host_Name' => 'Unknown',
  9950. 'Site_URL' => 'Unknown',
  9951. 'Top_Level_Domain' => 'Unknown',
  9952. 'Remote_Address' => SucuriScan::get_remote_addr(),
  9953. 'Remote_Address_Header' => SucuriScan::get_remote_addr_header(),
  9954. 'Operating_system' => sprintf( '%s (%d Bit)', PHP_OS, PHP_INT_SIZE * 8 ),
  9955. 'Server' => 'Unknown',
  9956. 'Developer_mode' => 'OFF',
  9957. 'Memory_usage' => 'N/A',
  9958. 'MySQL_version' => '0.0',
  9959. 'SQL_mode' => 'Not set',
  9960. 'PHP_version' => PHP_VERSION,
  9961. );
  9962. $proxy_info = SucuriScan::is_behind_cloudproxy( true );
  9963. $reverse_proxy = SucuriScan::support_reverse_proxy();
  9964. $info_vars['HTTP_Host'] = $proxy_info['http_host'];
  9965. $info_vars['Host_Name'] = $proxy_info['host_name'];
  9966. $info_vars['Host_Address'] = $proxy_info['host_addr'];
  9967. $info_vars['Site_URL'] = SucuriScan::get_domain();
  9968. $info_vars['Top_Level_Domain'] = SucuriScan::get_domain( true );
  9969. $info_vars['Using_CloudProxy'] = $proxy_info['status'] ? 'Yes' : 'No';
  9970. $info_vars['Support_Reverse_Proxy'] = $reverse_proxy ? 'Yes' : 'No';
  9971. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ){
  9972. $info_vars['Developer_mode'] = 'ON';
  9973. }
  9974. if ( function_exists( 'memory_get_usage' ) ){
  9975. $info_vars['Memory_usage'] = round( memory_get_usage() / 1024 / 1024, 2 ).' MB';
  9976. }
  9977. if ( isset($_SERVER['SERVER_SOFTWARE']) ){
  9978. $info_vars['Server'] = SucuriScan::escape( $_SERVER['SERVER_SOFTWARE'] );
  9979. }
  9980. if ( $wpdb ){
  9981. $info_vars['MySQL_version'] = $wpdb->get_var( 'SELECT VERSION() AS version' );
  9982. $mysql_info = $wpdb->get_results( 'SHOW VARIABLES LIKE "sql_mode"' );
  9983. if ( is_array( $mysql_info ) && ! empty($mysql_info[0]->Value) ){
  9984. $info_vars['SQL_mode'] = $mysql_info[0]->Value;
  9985. }
  9986. }
  9987. $field_names = array(
  9988. 'safe_mode',
  9989. 'expose_php',
  9990. 'allow_url_fopen',
  9991. 'memory_limit',
  9992. 'upload_max_filesize',
  9993. 'post_max_size',
  9994. 'max_execution_time',
  9995. 'max_input_time',
  9996. );
  9997. foreach ( $field_names as $php_flag ){
  9998. $php_flag_value = SucuriScan::ini_get( $php_flag );
  9999. $php_flag_name = 'PHP_' . $php_flag;
  10000. $info_vars[ $php_flag_name ] = $php_flag_value ? $php_flag_value : 'N/A';
  10001. }
  10002. $counter = 0;
  10003. foreach ( $info_vars as $var_name => $var_value ){
  10004. $css_class = ( $counter % 2 == 0 ) ? '' : 'alternate';
  10005. $var_name = str_replace( '_', chr( 32 ), $var_name );
  10006. $template_variables['ServerInfo.Variables'] .= SucuriScanTemplate::get_snippet('infosys-serverinfo', array(
  10007. 'ServerInfo.CssClass' => $css_class,
  10008. 'ServerInfo.Title' => $var_name,
  10009. 'ServerInfo.Value' => $var_value,
  10010. ));
  10011. $counter += 1;
  10012. }
  10013. return SucuriScanTemplate::get_section( 'infosys-serverinfo', $template_variables );
  10014. }