Ei kuvausta

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

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