PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/better-wp-security/core/class-itsec-logger.php

https://gitlab.com/gabdark/aceit
PHP | 556 lines | 291 code | 173 blank | 92 comment | 65 complexity | 5c79c39a46fcd03e714a0a5b45350fb1 MD5 | raw file
  1. <?php
  2. /**
  3. * Handles the writing, maintenance and display of log files
  4. *
  5. * @package iThemes-Security
  6. * @since 4.0
  7. */
  8. final class ITSEC_Logger {
  9. private
  10. $log_file,
  11. $logger_displays,
  12. $logger_modules,
  13. $module_path;
  14. /**
  15. * @access private
  16. *
  17. * @var array Events that need to be logged to a file but couldn't
  18. */
  19. private $_events_to_log_to_file = array();
  20. function __construct() {
  21. $this->logger_modules = array(); //array to hold information on modules using this feature
  22. $this->logger_displays = array(); //array to hold metabox information
  23. $this->module_path = ITSEC_Lib::get_module_path( __FILE__ );
  24. add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
  25. add_action( 'plugins_loaded', array( $this, 'write_pending_events_to_file' ) );
  26. //Run database cleanup daily with cron
  27. if ( ! wp_next_scheduled( 'itsec_purge_logs' ) ) {
  28. wp_schedule_event( time(), 'daily', 'itsec_purge_logs' );
  29. }
  30. add_action( 'itsec_purge_logs', array( $this, 'purge_logs' ) );
  31. }
  32. /**
  33. * Gets events from the logs for a specified module
  34. *
  35. * @param string $module module or type of events to fetch
  36. * @param array $params array of extra query parameters
  37. * @param int $limit the maximum number of rows to retrieve
  38. * @param int $offset the offset of the data
  39. * @param string $order order by column
  40. * @param bool $direction false for descending or true for ascending
  41. *
  42. * @return bool|mixed false on error, null if no events or array of events
  43. */
  44. public function get_events( $module, $params = array(), $limit = null, $offset = null, $order = null, $direction = false ) {
  45. global $wpdb;
  46. if ( isset( $module ) !== true || strlen( $module ) < 1 ) {
  47. return false;
  48. }
  49. if ( sizeof( $params ) > 0 || $module != 'all' ) {
  50. $where = " WHERE ";
  51. } else {
  52. $where = '';
  53. }
  54. $param_search = '';
  55. if ( $module == 'all' ) {
  56. $module_sql = '';
  57. $and = '';
  58. } else {
  59. $module_sql = "`log_type` = '" . esc_sql( $module ) . "'";
  60. $and = ' AND ';
  61. }
  62. if ( $direction === false ) {
  63. $order_direction = ' DESC';
  64. } else {
  65. $order_direction = ' ASC';
  66. }
  67. if ( $order !== null ) {
  68. $order_statement = ' ORDER BY `' . esc_sql( $order ) . '`';
  69. } else {
  70. $order_statement = ' ORDER BY `log_id`';
  71. }
  72. if ( $limit !== null ) {
  73. if ( $offset !== null ) {
  74. $result_limit = ' LIMIT ' . absint( $offset ) . ', ' . absint( $limit );
  75. } else {
  76. $result_limit = ' LIMIT ' . absint( $limit );
  77. }
  78. } else {
  79. $result_limit = '';
  80. }
  81. if ( sizeof( $params ) > 0 ) {
  82. foreach ( $params as $field => $value ) {
  83. if ( gettype( $value ) != 'integer' ) {
  84. $param_search .= $and . "`" . esc_sql( $field ) . "`='" . esc_sql( $value ) . "'";
  85. } else {
  86. $param_search .= $and . "`" . esc_sql( $field ) . "`=" . esc_sql( $value ) . "";
  87. }
  88. }
  89. }
  90. $items = $wpdb->get_results( "SELECT * FROM `" . $wpdb->base_prefix . "itsec_log`" . $where . $module_sql . $param_search . $order_statement . $order_direction . $result_limit . ";", ARRAY_A );
  91. return $items;
  92. }
  93. /**
  94. * Logs events sent by other modules or systems
  95. *
  96. * @param string $module the module requesting the log entry
  97. * @param int $priority the priority of the log entry (1-10)
  98. * @param array $data extra data to log (non-indexed data would be good here)
  99. * @param string $host the remote host triggering the event
  100. * @param string $username the username triggering the event
  101. * @param string $user the user id triggering the event
  102. * @param string $url the url triggering the event
  103. * @param string $referrer the referrer to the url (if applicable)
  104. *
  105. * @return void
  106. */
  107. public function log_event( $module, $priority = 5, $data = array(), $host = '', $username = '', $user = '', $url = '', $referrer = '' ) {
  108. if ( isset( $this->logger_modules[ $module ] ) ) {
  109. $type = ITSEC_Modules::get_setting( 'global', 'log_type' );
  110. if ( 'database' === $type || 'both' === $type ) {
  111. $this->_log_event_to_db( $module, $priority, $data, $host, $username, $user, $url, $referrer );
  112. }
  113. if ( 'file' === $type || 'both' === $type ) {
  114. $this->_log_event_to_file( $module, $priority, $data, $host, $username, $user, $url, $referrer );
  115. }
  116. }
  117. do_action( 'itsec_log_event', $module, $priority, $data, $host, $username, $user, $url, $referrer );
  118. }
  119. private function _log_event_to_db( $module, $priority = 5, $data = array(), $host = '', $username = '', $user = '', $url = '', $referrer = '' ) {
  120. global $wpdb, $itsec_globals;
  121. $options = $this->logger_modules[ $module ];
  122. $values = array(
  123. 'log_type' => $options['type'],
  124. 'log_priority' => intval( $priority ),
  125. 'log_function' => $options['function'],
  126. 'log_date' => date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ),
  127. 'log_date_gmt' => date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ),
  128. 'log_host' => sanitize_text_field( $host ),
  129. 'log_username' => sanitize_text_field( $username ),
  130. 'log_user' => intval( $user ),
  131. 'log_url' => $url,
  132. 'log_referrer' => $referrer,
  133. 'log_data' => serialize( $data ),
  134. );
  135. $columns = '`' . implode( '`, `', array_keys( $values ) ) . '`';
  136. $placeholders = '%s, %d, %s, %s, %s, %s, %s, %s, %s, %s, %s';
  137. $query_format = "INSERT INTO `{$wpdb->base_prefix}itsec_log` ($columns) VALUES ($placeholders)";
  138. $cached_show_errors_setting = $wpdb->hide_errors();
  139. $result = $wpdb->query( $wpdb->prepare( $query_format, $values ) );
  140. if ( ! $result ) {
  141. $wpdb->show_errors();
  142. ITSEC_Lib::create_database_tables();
  143. // Attempt the query again. Since errors will now be shown, a remaining issue will be display an error.
  144. $result = $wpdb->query( $wpdb->prepare( $query_format, $values ) );
  145. }
  146. // Set $wpdb->show_errors back to its original setting.
  147. $wpdb->show_errors( $cached_show_errors_setting );
  148. }
  149. private function _log_event_to_file( $module, $priority = 5, $data = array(), $host = '', $username = '', $user = '', $url = '', $referrer = '' ) {
  150. global $itsec_globals;
  151. // If the file can't be prepared, store the events up to write later (at plugins_loaded)
  152. if ( false === $this->_prepare_log_file() ) {
  153. $this->_events_to_log_to_file[] = compact( 'module', 'priority', 'data', 'host', 'username', 'user', 'url', 'referrer' );
  154. return;
  155. }
  156. $options = $this->logger_modules[ $module ];
  157. $file_data = $this->sanitize_array( $data, true );
  158. $message =
  159. $options['type'] . ',' .
  160. intval( $priority ) . ',' .
  161. $options['function'] . ',' .
  162. date( 'Y-m-d H:i:s', $itsec_globals['current_time'] ) . ',' .
  163. date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] ) . ',' .
  164. sanitize_text_field( $host ) . ',' .
  165. sanitize_text_field( $username ) . ',' .
  166. ( intval( $user ) === 0 ? '' : intval( $user ) ) . ',' .
  167. esc_sql( $url ) . ',' .
  168. esc_sql( $referrer ) . ',' .
  169. maybe_serialize( $file_data );
  170. $this->add_to_log_file( $message );
  171. }
  172. public function write_pending_events_to_file() {
  173. foreach ( $this->_events_to_log_to_file as $event ) {
  174. call_user_func_array( array( $this, '_log_event_to_file' ), $event );
  175. }
  176. }
  177. /**
  178. * Displays into box for logs page
  179. *
  180. * @since 4.0
  181. *
  182. * @return void
  183. */
  184. public function metabox_all_logs() {
  185. $log_filter = isset( $_GET['itsec_log_filter'] ) ? sanitize_text_field( $_GET['itsec_log_filter'] ) : 'all-log-data';
  186. $callback = null;
  187. echo '<p>' . __( 'To adjust logging options visit the global settings page.', 'better-wp-security' ) . '</p>';
  188. echo '<label for="itsec_log_filter"><strong>' . __( 'Select Filter: ', 'better-wp-security' ) . '</strong></label>';
  189. echo '<select id="itsec_log_filter" name="itsec_log_filter">';
  190. echo '<option value="all-log-data" ' . selected( $log_filter, 'all-log-data' ) . '>' . __( 'All Log Data', 'better-wp-security' ) . '</option>';
  191. if ( sizeof( $this->logger_displays ) > 0 ) {
  192. foreach ( $this->logger_displays as $display ) {
  193. if ( $display['module'] === $log_filter ) {
  194. $callback = $display['callback'];
  195. }
  196. echo '<option value="' . $display['module'] . '" ' . selected( $log_filter, $display['module'] ) . '>' . $display['title'] . '</option>';
  197. }
  198. }
  199. echo '</select>';
  200. if ( $log_filter === 'all-log-data' || $callback === null ) {
  201. $this->all_logs_content();
  202. } else {
  203. call_user_func_array( $callback, array() );
  204. }
  205. }
  206. /**
  207. * A better print array function to display array data in the logs
  208. *
  209. * @since 4.2
  210. *
  211. * @param array $array_items array to print or return
  212. * @param bool $return true to return the data false to echo it
  213. */
  214. public function print_array( $array_items, $return = true ) {
  215. $items = '';
  216. //make sure we're working with an array
  217. if ( ! is_array( $array_items ) ) {
  218. return false;
  219. }
  220. if ( sizeof( $array_items ) > 0 ) {
  221. $items .= '<ul>';
  222. foreach ( $array_items as $key => $item ) {
  223. if ( is_array( $item ) ) {
  224. $items .= '<li>';
  225. if ( ! is_numeric( $key ) ) {
  226. $items .= '<h3>' . $key . '</h3>';
  227. }
  228. $items .= $this->print_array( $item, true ) . PHP_EOL;
  229. $items .= '</li>';
  230. } else {
  231. if ( strlen( trim( $item ) ) > 0 ) {
  232. $items .= '<li><h3>' . $key . ' = ' . $item . '</h3></li>' . PHP_EOL;
  233. }
  234. }
  235. }
  236. $items .= '</ul>';
  237. }
  238. return $items;
  239. }
  240. /**
  241. * Purges database logs and rotates file logs (when needed)
  242. *
  243. * @return void
  244. */
  245. public function purge_logs( $purge_all = false ) {
  246. global $wpdb, $itsec_globals, $itsec_clear_all_logs;
  247. if ( true === $purge_all ) {
  248. if ( ! wp_verify_nonce( $_POST['wp_nonce'], 'itsec_clear_logs' ) ) {
  249. return;
  250. }
  251. $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_log`;" );
  252. } else {
  253. //Clean up the database log first
  254. $type = ITSEC_Modules::get_setting( 'global', 'log_type' );
  255. if ( 'database' === $type || 'both' === $type ) {
  256. $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_log` WHERE `log_date_gmt` < '" . date( 'Y-m-d H:i:s', $itsec_globals['current_time_gmt'] - ( ITSEC_Modules::get_setting( 'global', 'log_rotation' ) * DAY_IN_SECONDS ) ) . "';" );
  257. } else {
  258. $wpdb->query( "DELETE FROM `" . $wpdb->base_prefix . "itsec_log`;" );
  259. }
  260. $this->rotate_log();
  261. }
  262. }
  263. /**
  264. * Register modules that will use the logger service
  265. *
  266. * @return void
  267. */
  268. public function register_modules() {
  269. $this->logger_modules = apply_filters( 'itsec_logger_modules', $this->logger_modules );
  270. $this->logger_displays = apply_filters( 'itsec_logger_displays', $this->logger_displays );
  271. }
  272. /**
  273. * Rotates the event-log.log file when called
  274. *
  275. * Adapted from http://www.phpclasses.org/browse/file/49471.html
  276. *
  277. * @return void
  278. */
  279. private function rotate_log() {
  280. $log_file = $this->get_log_file();
  281. if ( ! @file_exists( $log_file ) || @filesize( $log_file ) < 10485760 ) { // 10485760 is 1 mebibyte
  282. return;
  283. }
  284. // rotate
  285. $path_info = pathinfo( $log_file );
  286. $base_directory = $path_info['dirname'];
  287. $base_name = $path_info['basename'];
  288. $num_map = array();
  289. foreach ( new DirectoryIterator( $base_directory ) as $fInfo ) {
  290. if ( $fInfo->isDot() || ! $fInfo->isFile() ) {
  291. continue;
  292. }
  293. if ( preg_match( '/^' . $base_name . '\.?([0-9]*)$/', $fInfo->getFilename(), $matches ) ) {
  294. $num = $matches[1];
  295. $old_file = $fInfo->getFilename();
  296. if ( $num == '' ) {
  297. $num = - 1;
  298. }
  299. $num_map[ $num ] = $old_file;
  300. }
  301. }
  302. krsort( $num_map );
  303. foreach ( $num_map as $num => $old_file ) {
  304. $new_file = $num + 1;
  305. @rename( $base_directory . DIRECTORY_SEPARATOR . $old_file, $log_file . '.' . $new_file );
  306. }
  307. $this->_prepare_log_file();
  308. }
  309. /**
  310. * Sanitizes strings in a given array recursively
  311. *
  312. * @param array $array array to sanitize
  313. * @param bool $to_string true if output should be string or false for array output
  314. *
  315. * @return mixed sanitized array or string
  316. */
  317. private function sanitize_array( $array, $to_string = false ) {
  318. $sanitized_array = array();
  319. $string = '';
  320. //Loop to sanitize each piece of data
  321. foreach ( $array as $key => $value ) {
  322. if ( is_array( $value ) ) {
  323. if ( $to_string === false ) {
  324. $sanitized_array[ esc_sql( $key ) ] = $this->sanitize_array( $value );
  325. } else {
  326. $string .= esc_sql( $key ) . '=' . $this->sanitize_array( $value, true );
  327. }
  328. } else {
  329. $sanitized_array[ esc_sql( $key ) ] = esc_sql( $value );
  330. $string .= esc_sql( $key ) . '=' . esc_sql( $value );
  331. }
  332. }
  333. if ( $to_string === false ) {
  334. return $sanitized_array;
  335. } else {
  336. return $string;
  337. }
  338. }
  339. private function get_log_file() {
  340. if ( isset( $this->log_file ) ) {
  341. return $this->log_file;
  342. $this->rotate_log();
  343. }
  344. $log_location = ITSEC_Modules::get_setting( 'global', 'log_location' );
  345. $log_info = ITSEC_Modules::get_setting( 'global', 'log_info' );
  346. if ( empty( $log_info ) ) {
  347. // We need wp_generate_password() to create a cryptographically secure file name
  348. if ( ! function_exists( 'wp_generate_password' ) ) {
  349. $this->log_file = false;
  350. return false;
  351. }
  352. $log_info = substr( sanitize_title( get_bloginfo( 'name' ) ), 0, 20 ) . '-' . wp_generate_password( 30, false );
  353. ITSEC_Modules::set_setting( 'global', 'log_info', $log_info );
  354. }
  355. $this->log_file = "$log_location/event-log-$log_info.log";
  356. return $this->log_file;
  357. }
  358. /**
  359. * Creates a new log file and adds header information (if needed)
  360. *
  361. * @return void
  362. */
  363. private function _prepare_log_file() {
  364. $log_file = $this->get_log_file();
  365. // We can't prepare a file if we can't get the file name
  366. if ( false === $log_file ) {
  367. return false;
  368. }
  369. if ( ! file_exists( $log_file ) ) { //only if current log file doesn't exist
  370. $header = 'log_type,log_priority,log_function,log_date,log_date_gmt,log_host,log_username,log_user,log_url,log_referrer,log_data';
  371. $this->add_to_log_file( $header );
  372. }
  373. return true;
  374. }
  375. private function add_to_log_file( $details ) {
  376. $log_file = $this->get_log_file();
  377. if ( false === $log_file ) {
  378. return false;
  379. }
  380. @error_log( $details . PHP_EOL, 3, $log_file );
  381. return true;
  382. }
  383. }