class-wc-log-handler-file.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. <?php
  2. /**
  3. * Class WC_Log_Handler_File file.
  4. *
  5. * @package WooCommerce\Log Handlers
  6. */
  7. if ( ! defined( 'ABSPATH' ) ) {
  8. exit; // Exit if accessed directly.
  9. }
  10. /**
  11. * Handles log entries by writing to a file.
  12. *
  13. * @class WC_Log_Handler_File
  14. * @version 1.0.0
  15. * @package WooCommerce/Classes/Log_Handlers
  16. */
  17. class WC_Log_Handler_File extends WC_Log_Handler {
  18. /**
  19. * Stores open file handles.
  20. *
  21. * @var array
  22. */
  23. protected $handles = array();
  24. /**
  25. * File size limit for log files in bytes.
  26. *
  27. * @var int
  28. */
  29. protected $log_size_limit;
  30. /**
  31. * Cache logs that could not be written.
  32. *
  33. * If a log is written too early in the request, pluggable functions may be unavailable. These
  34. * logs will be cached and written on 'plugins_loaded' action.
  35. *
  36. * @var array
  37. */
  38. protected $cached_logs = array();
  39. /**
  40. * Constructor for the logger.
  41. *
  42. * @param int $log_size_limit Optional. Size limit for log files. Default 5mb.
  43. */
  44. public function __construct( $log_size_limit = null ) {
  45. if ( null === $log_size_limit ) {
  46. $log_size_limit = 5 * 1024 * 1024;
  47. }
  48. $this->log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit );
  49. add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) );
  50. }
  51. /**
  52. * Destructor.
  53. *
  54. * Cleans up open file handles.
  55. */
  56. public function __destruct() {
  57. foreach ( $this->handles as $handle ) {
  58. if ( is_resource( $handle ) ) {
  59. fclose( $handle ); // @codingStandardsIgnoreLine.
  60. }
  61. }
  62. }
  63. /**
  64. * Handle a log entry.
  65. *
  66. * @param int $timestamp Log timestamp.
  67. * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
  68. * @param string $message Log message.
  69. * @param array $context {
  70. * Additional information for log handlers.
  71. *
  72. * @type string $source Optional. Determines log file to write to. Default 'log'.
  73. * @type bool $_legacy Optional. Default false. True to use outdated log format
  74. * originally used in deprecated WC_Logger::add calls.
  75. * }
  76. *
  77. * @return bool False if value was not handled and true if value was handled.
  78. */
  79. public function handle( $timestamp, $level, $message, $context ) {
  80. if ( isset( $context['source'] ) && $context['source'] ) {
  81. $handle = $context['source'];
  82. } else {
  83. $handle = 'log';
  84. }
  85. $entry = self::format_entry( $timestamp, $level, $message, $context );
  86. return $this->add( $entry, $handle );
  87. }
  88. /**
  89. * Builds a log entry text from timestamp, level and message.
  90. *
  91. * @param int $timestamp Log timestamp.
  92. * @param string $level emergency|alert|critical|error|warning|notice|info|debug.
  93. * @param string $message Log message.
  94. * @param array $context Additional information for log handlers.
  95. *
  96. * @return string Formatted log entry.
  97. */
  98. protected static function format_entry( $timestamp, $level, $message, $context ) {
  99. if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) {
  100. if ( isset( $context['source'] ) && $context['source'] ) {
  101. $handle = $context['source'];
  102. } else {
  103. $handle = 'log';
  104. }
  105. $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle );
  106. $time = date_i18n( 'm-d-Y @ H:i:s' );
  107. $entry = "{$time} - {$message}";
  108. } else {
  109. $entry = parent::format_entry( $timestamp, $level, $message, $context );
  110. }
  111. return $entry;
  112. }
  113. /**
  114. * Open log file for writing.
  115. *
  116. * @param string $handle Log handle.
  117. * @param string $mode Optional. File mode. Default 'a'.
  118. * @return bool Success.
  119. */
  120. protected function open( $handle, $mode = 'a' ) {
  121. if ( $this->is_open( $handle ) ) {
  122. return true;
  123. }
  124. $file = self::get_log_file_path( $handle );
  125. if ( $file ) {
  126. if ( ! file_exists( $file ) ) {
  127. $temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine.
  128. @fclose( $temphandle ); // @codingStandardsIgnoreLine.
  129. if ( defined( 'FS_CHMOD_FILE' ) ) {
  130. @chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine.
  131. }
  132. }
  133. $resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine.
  134. if ( $resource ) {
  135. $this->handles[ $handle ] = $resource;
  136. return true;
  137. }
  138. }
  139. return false;
  140. }
  141. /**
  142. * Check if a handle is open.
  143. *
  144. * @param string $handle Log handle.
  145. * @return bool True if $handle is open.
  146. */
  147. protected function is_open( $handle ) {
  148. return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] );
  149. }
  150. /**
  151. * Close a handle.
  152. *
  153. * @param string $handle Log handle.
  154. * @return bool success
  155. */
  156. protected function close( $handle ) {
  157. $result = false;
  158. if ( $this->is_open( $handle ) ) {
  159. $result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine.
  160. unset( $this->handles[ $handle ] );
  161. }
  162. return $result;
  163. }
  164. /**
  165. * Add a log entry to chosen file.
  166. *
  167. * @param string $entry Log entry text.
  168. * @param string $handle Log entry handle.
  169. *
  170. * @return bool True if write was successful.
  171. */
  172. protected function add( $entry, $handle ) {
  173. $result = false;
  174. if ( $this->should_rotate( $handle ) ) {
  175. $this->log_rotate( $handle );
  176. }
  177. if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) {
  178. $result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine.
  179. } else {
  180. $this->cache_log( $entry, $handle );
  181. }
  182. return false !== $result;
  183. }
  184. /**
  185. * Clear entries from chosen file.
  186. *
  187. * @param string $handle Log handle.
  188. *
  189. * @return bool
  190. */
  191. public function clear( $handle ) {
  192. $result = false;
  193. // Close the file if it's already open.
  194. $this->close( $handle );
  195. /**
  196. * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at
  197. * the beginning of the file, and truncate the file to zero length.
  198. */
  199. if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) {
  200. $result = true;
  201. }
  202. do_action( 'woocommerce_log_clear', $handle );
  203. return $result;
  204. }
  205. /**
  206. * Remove/delete the chosen file.
  207. *
  208. * @param string $handle Log handle.
  209. *
  210. * @return bool
  211. */
  212. public function remove( $handle ) {
  213. $removed = false;
  214. $file = trailingslashit( WC_LOG_DIR ) . $handle;
  215. if ( $file ) {
  216. if ( is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
  217. $this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked.
  218. $removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink
  219. }
  220. do_action( 'woocommerce_log_remove', $handle, $removed );
  221. }
  222. return $removed;
  223. }
  224. /**
  225. * Check if log file should be rotated.
  226. *
  227. * Compares the size of the log file to determine whether it is over the size limit.
  228. *
  229. * @param string $handle Log handle.
  230. * @return bool True if if should be rotated.
  231. */
  232. protected function should_rotate( $handle ) {
  233. $file = self::get_log_file_path( $handle );
  234. if ( $file ) {
  235. if ( $this->is_open( $handle ) ) {
  236. $file_stat = fstat( $this->handles[ $handle ] );
  237. return $file_stat['size'] > $this->log_size_limit;
  238. } elseif ( file_exists( $file ) ) {
  239. return filesize( $file ) > $this->log_size_limit;
  240. } else {
  241. return false;
  242. }
  243. } else {
  244. return false;
  245. }
  246. }
  247. /**
  248. * Rotate log files.
  249. *
  250. * Logs are rotated by prepending '.x' to the '.log' suffix.
  251. * The current log plus 10 historical logs are maintained.
  252. * For example:
  253. * base.9.log -> [ REMOVED ]
  254. * base.8.log -> base.9.log
  255. * ...
  256. * base.0.log -> base.1.log
  257. * base.log -> base.0.log
  258. *
  259. * @param string $handle Log handle.
  260. */
  261. protected function log_rotate( $handle ) {
  262. for ( $i = 8; $i >= 0; $i-- ) {
  263. $this->increment_log_infix( $handle, $i );
  264. }
  265. $this->increment_log_infix( $handle );
  266. }
  267. /**
  268. * Increment a log file suffix.
  269. *
  270. * @param string $handle Log handle.
  271. * @param null|int $number Optional. Default null. Log suffix number to be incremented.
  272. * @return bool True if increment was successful, otherwise false.
  273. */
  274. protected function increment_log_infix( $handle, $number = null ) {
  275. if ( null === $number ) {
  276. $suffix = '';
  277. $next_suffix = '.0';
  278. } else {
  279. $suffix = '.' . $number;
  280. $next_suffix = '.' . ( $number + 1 );
  281. }
  282. $rename_from = self::get_log_file_path( "{$handle}{$suffix}" );
  283. $rename_to = self::get_log_file_path( "{$handle}{$next_suffix}" );
  284. if ( $this->is_open( $rename_from ) ) {
  285. $this->close( $rename_from );
  286. }
  287. if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
  288. return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename
  289. } else {
  290. return false;
  291. }
  292. }
  293. /**
  294. * Get a log file path.
  295. *
  296. * @param string $handle Log name.
  297. * @return bool|string The log file path or false if path cannot be determined.
  298. */
  299. public static function get_log_file_path( $handle ) {
  300. if ( function_exists( 'wp_hash' ) ) {
  301. return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle );
  302. } else {
  303. wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' );
  304. return false;
  305. }
  306. }
  307. /**
  308. * Get a log file name.
  309. *
  310. * File names consist of the handle, followed by the date, followed by a hash, .log.
  311. *
  312. * @since 3.3
  313. * @param string $handle Log name.
  314. * @return bool|string The log file name or false if cannot be determined.
  315. */
  316. public static function get_log_file_name( $handle ) {
  317. if ( function_exists( 'wp_hash' ) ) {
  318. $date_suffix = date( 'Y-m-d', current_time( 'timestamp', true ) );
  319. $hash_suffix = wp_hash( $handle );
  320. return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' );
  321. } else {
  322. wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' );
  323. return false;
  324. }
  325. }
  326. /**
  327. * Cache log to write later.
  328. *
  329. * @param string $entry Log entry text.
  330. * @param string $handle Log entry handle.
  331. */
  332. protected function cache_log( $entry, $handle ) {
  333. $this->cached_logs[] = array(
  334. 'entry' => $entry,
  335. 'handle' => $handle,
  336. );
  337. }
  338. /**
  339. * Write cached logs.
  340. */
  341. public function write_cached_logs() {
  342. foreach ( $this->cached_logs as $log ) {
  343. $this->add( $log['entry'], $log['handle'] );
  344. }
  345. }
  346. /**
  347. * Delete all logs older than a defined timestamp.
  348. *
  349. * @since 3.4.0
  350. * @param integer $timestamp Timestamp to delete logs before.
  351. */
  352. public static function delete_logs_before_timestamp( $timestamp = 0 ) {
  353. if ( ! $timestamp ) {
  354. return;
  355. }
  356. $log_files = self::get_log_files();
  357. foreach ( $log_files as $log_file ) {
  358. $last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file );
  359. if ( $last_modified < $timestamp ) {
  360. @unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine.
  361. }
  362. }
  363. }
  364. /**
  365. * Get all log files in the log directory.
  366. *
  367. * @since 3.4.0
  368. * @return array
  369. */
  370. public static function get_log_files() {
  371. $files = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine.
  372. $result = array();
  373. if ( ! empty( $files ) ) {
  374. foreach ( $files as $key => $value ) {
  375. if ( ! in_array( $value, array( '.', '..' ), true ) ) {
  376. if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) {
  377. $result[ sanitize_title( $value ) ] = $value;
  378. }
  379. }
  380. }
  381. }
  382. return $result;
  383. }
  384. }