1<?php 2/** 3 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org) 4 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 5 * 6 * Licensed under The MIT License 7 * For full copyright and license information, please see the LICENSE.txt 8 * Redistributions of files must retain the above copyright notice. 9 * 10 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) 11 * @link https://cakefoundation.org CakePHP(tm) Project 12 * @since 1.3.0 13 * @license https://opensource.org/licenses/mit-license.php MIT License 14 */ 15namespace Cake\Log\Engine; 16 17use Cake\Core\Configure; 18use Cake\Utility\Text; 19 20/** 21 * File Storage stream for Logging. Writes logs to different files 22 * based on the level of log it is. 23 */ 24class FileLog extends BaseLog 25{ 26 /** 27 * Default config for this class 28 * 29 * - `levels` string or array, levels the engine is interested in 30 * - `scopes` string or array, scopes the engine is interested in 31 * - `file` Log file name 32 * - `path` The path to save logs on. 33 * - `size` Used to implement basic log file rotation. If log file size 34 * reaches specified size the existing file is renamed by appending timestamp 35 * to filename and new log file is created. Can be integer bytes value or 36 * human readable string values like '10MB', '100KB' etc. 37 * - `rotate` Log files are rotated specified times before being removed. 38 * If value is 0, old versions are removed rather then rotated. 39 * - `mask` A mask is applied when log files are created. Left empty no chmod 40 * is made. 41 * 42 * @var array 43 */ 44 protected $_defaultConfig = [ 45 'path' => null, 46 'file' => null, 47 'types' => null, 48 'levels' => [], 49 'scopes' => [], 50 'rotate' => 10, 51 'size' => 10485760, // 10MB 52 'mask' => null, 53 ]; 54 55 /** 56 * Path to save log files on. 57 * 58 * @var string|null 59 */ 60 protected $_path; 61 62 /** 63 * The name of the file to save logs into. 64 * 65 * @var string|null 66 */ 67 protected $_file; 68 69 /** 70 * Max file size, used for log file rotation. 71 * 72 * @var int|null 73 */ 74 protected $_size; 75 76 /** 77 * Sets protected properties based on config provided 78 * 79 * @param array $config Configuration array 80 */ 81 public function __construct(array $config = []) 82 { 83 parent::__construct($config); 84 85 if (!empty($this->_config['path'])) { 86 $this->_path = $this->_config['path']; 87 } 88 if ( 89 $this->_path !== null && 90 Configure::read('debug') && 91 !is_dir($this->_path) 92 ) { 93 mkdir($this->_path, 0775, true); 94 } 95 96 if (!empty($this->_config['file'])) { 97 $this->_file = $this->_config['file']; 98 if (substr($this->_file, -4) !== '.log') { 99 $this->_file .= '.log'; 100 } 101 } 102 103 if (!empty($this->_config['size'])) { 104 if (is_numeric($this->_config['size'])) { 105 $this->_size = (int)$this->_config['size']; 106 } else { 107 $this->_size = Text::parseFileSize($this->_config['size']); 108 } 109 } 110 } 111 112 /** 113 * Implements writing to log files. 114 * 115 * @param string $level The severity level of the message being written. 116 * See Cake\Log\Log::$_levels for list of possible levels. 117 * @param string $message The message you want to log. 118 * @param array $context Additional information about the logged message 119 * @return bool success of write. 120 */ 121 public function log($level, $message, array $context = []) 122 { 123 $message = $this->_format($message, $context); 124 $output = date('Y-m-d H:i:s') . ' ' . ucfirst($level) . ': ' . $message . "\n"; 125 $filename = $this->_getFilename($level); 126 if ($this->_size) { 127 $this->_rotateFile($filename); 128 } 129 130 $pathname = $this->_path . $filename; 131 $mask = $this->_config['mask']; 132 if (!$mask) { 133 return file_put_contents($pathname, $output, FILE_APPEND); 134 } 135 136 $exists = file_exists($pathname); 137 $result = file_put_contents($pathname, $output, FILE_APPEND); 138 static $selfError = false; 139 140 if (!$selfError && !$exists && !chmod($pathname, (int)$mask)) { 141 $selfError = true; 142 trigger_error(vsprintf( 143 'Could not apply permission mask "%s" on log file "%s"', 144 [$mask, $pathname] 145 ), E_USER_WARNING); 146 $selfError = false; 147 } 148 149 return $result; 150 } 151 152 /** 153 * Get filename 154 * 155 * @param string $level The level of log. 156 * @return string File name 157 */ 158 protected function _getFilename($level) 159 { 160 $debugTypes = ['notice', 'info', 'debug']; 161 162 if ($this->_file) { 163 $filename = $this->_file; 164 } elseif ($level === 'error' || $level === 'warning') { 165 $filename = 'error.log'; 166 } elseif (in_array($level, $debugTypes)) { 167 $filename = 'debug.log'; 168 } else { 169 $filename = $level . '.log'; 170 } 171 172 return $filename; 173 } 174 175 /** 176 * Rotate log file if size specified in config is reached. 177 * Also if `rotate` count is reached oldest file is removed. 178 * 179 * @param string $filename Log file name 180 * @return bool|null True if rotated successfully or false in case of error. 181 * Null if file doesn't need to be rotated. 182 */ 183 protected function _rotateFile($filename) 184 { 185 $filePath = $this->_path . $filename; 186 clearstatcache(true, $filePath); 187 188 if ( 189 !file_exists($filePath) || 190 filesize($filePath) < $this->_size 191 ) { 192 return null; 193 } 194 195 $rotate = $this->_config['rotate']; 196 if ($rotate === 0) { 197 $result = unlink($filePath); 198 } else { 199 $result = rename($filePath, $filePath . '.' . time()); 200 } 201 202 $files = glob($filePath . '.*'); 203 if ($files) { 204 $filesToDelete = count($files) - $rotate; 205 while ($filesToDelete > 0) { 206 unlink(array_shift($files)); 207 $filesToDelete--; 208 } 209 } 210 211 return $result; 212 } 213} 214