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