Tail::__construct()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 3
nop 1
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware - Ajax log file viewer (tail -f)
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <[email protected]>
7
 * @copyright 2012-16 by [email protected]
8
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
9
 * @package api
10
 * @subpackage json
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Json;
15
16
use EGroupware\Api;
17
18
/**
19
 * Ajax log file viewer (tail -f)
20
 *
21
 * To not allow to view arbitrary files, allowed filenames are stored in the session.
22
 * Class fetches log-file periodically in chunks for 8k.
23
 * If fetch returns no new content next request will be in 2s, otherwise in 200ms.
24
 * As logfiles can be quiet huge, we display at max the last 32k of it!
25
 *
26
 * Example usage:
27
 *
28
 * $error_log = new Api\Json\Tail('/var/log/apache2/error_log');
29
 * echo $error_log->show();
30
 *
31
 * Strongly prefered for security reasons is to use a path relative to EGroupware's files_dir,
32
 * eg. new Api\Json\Tail('groupdav/somelog')!
33
 */
34
class Tail
35
{
36
	/**
37
	 * Maximum size of single ajax request
38
	 *
39
	 * Currently also maximum size / 4 of displayed logfile content!
40
	 */
41
	const MAX_CHUNK_SIZE = 8192;
42
43
	/**
44
	 * Contains allowed filenames to display, we can NOT allow to display arbitrary files!
45
	 *
46
	 * @param array
47
	 */
48
	protected $filenames;
49
50
	/**
51
	 * Filename class is instanciated to view, set by constructor
52
	 *
53
	 * @param string
54
	 */
55
	protected $filename;
56
57
	/**
58
	 * Methods allowed to call via menuaction
59
	 *
60
	 * @var array
61
	 */
62
	public $public_functions = array(
63
		'download' => true,
64
	);
65
66
	/**
67
	 * Constructor
68
	 *
69
	 * @param string $filename =null if not starting with as slash relative to EGw files dir (this is strongly prefered for security reasons)
70
	 */
71
	public function __construct($filename=null)
72
	{
73
		$this->filenames =& Api\Cache::getSession('phpgwapi', __CLASS__);
74
75
		if ($filename)
76
		{
77
			// do NOT allow path-traversal
78
			$filename = str_replace('../', '', $filename);
79
80
			$this->filename = $filename;
81
82
			if (!$this->filenames || !in_array($filename,$this->filenames)) $this->filenames[] = $filename;
83
		}
84
	}
85
86
	/**
87
	 * Ajax callback to load next chunk of log-file
88
	 *
89
	 * @param string $filename
90
	 * @param int $start =0 last position in log-file
91
	 * @throws Api\Exception\WrongParameter
92
	 */
93
	public function ajax_chunk($filename,$start=0)
94
	{
95
		if (!in_array($filename,$this->filenames))
96
		{
97
			throw new Api\Exception\WrongParameter("Not allowed to view '$filename'!");
98
		}
99
		if ($filename[0] != '/') $filename = $GLOBALS['egw_info']['server']['files_dir'].'/'.$filename;
100
101
		if (file_exists($filename))
102
		{
103
			$size = filesize($filename);
104
			if (!$start || $start < 0 || $start > $size || $size-$start > 4*self::MAX_CHUNK_SIZE)
105
			{
106
				$start = $size - 4*self::MAX_CHUNK_SIZE;
107
				if ($start < 0) $start = 0;
108
			}
109
			$hsize = Api\Vfs::hsize($size);
110
			$content = file_get_contents($filename, false, null, $start, self::MAX_CHUNK_SIZE);
111
			$length = bytes($content);
112
			$writable = is_writable($filename) || is_writable(dirname($filename));
113
		}
114
		else
115
		{
116
			$start = $length = 0;
117
			$content = '';
118
			$writable = $hsize = false;
119
		}
120
		$response = Response::get();
121
		$response->data(array(	// send all responses as data
122
			'size' => $hsize,
123
			'writable' => $writable,
124
			'next' => $start + $length,
125
			'length' => $length,
126
			'content' => $content,
127
		));
128
	}
129
130
	/**
131
	 * Ajax callback to delete log-file
132
	 *
133
	 * @param string $filename
134
	 * @param boolean $truncate =false true: truncate file, false: delete file
135
	 * @throws Api\Exception\WrongParameter
136
	 */
137
	public function ajax_delete($filename,$truncate=false)
138
	{
139
		if (!in_array($filename,$this->filenames))
140
		{
141
			throw new Api\Exception\WrongParameter("Not allowed to view '$filename'!");
142
		}
143
		if ($filename[0] != '/') $filename = $GLOBALS['egw_info']['server']['files_dir'].'/'.$filename;
144
		if ($truncate)
145
		{
146
			file_put_contents($filename, '');
147
		}
148
		else
149
		{
150
			unlink($filename);
151
		}
152
	}
153
154
	/**
155
	 * Return html & javascript for logviewer
156
	 *
157
	 * @param string $header =null default $this->filename
158
	 * @return string
159
	 * @throws Api\Exception\WrongParameter
160
	 */
161
	public function show($header=null)
162
	{
163
		if (!isset($this->filename))
164
		{
165
			throw new Api\Exception\WrongParameter("Must be instanciated with filename!");
166
		}
167
		if (is_null($header)) $header = $this->filename;
168
169
		return '
170
<p style="float: left; margin: 5px"><b>'.htmlspecialchars($header).'</b></p>
171
<div style="float: right; margin: 2px; margin-right: 5px">
172
	'.Api\Html::form(
173
		Api\Html::input('clear_log',lang('Clear window'),'button','id="clear_log"')."\n".
174
		Api\Html::input('delete_log',lang('Delete file'),'button','id="purge_log"')."\n".
175
		Api\Html::input('empty_log',lang('Empty file'),'button','id="empty_log"')."\n".
176
		Api\Html::input('download_log',lang('Download'),'submit','id="download_log"'),
177
		'','/index.php',array(
178
		'menuaction' => 'api.'.__CLASS__.'.download',
179
		'filename' => $this->filename,
180
	)).'
181
</div>
182
<pre class="tail" id="log" data-filename="'.htmlspecialchars($this->filename).'" style="clear: both; width: 99.5%; border: 2px groove silver; margin-bottom: 0; overflow: auto;"></pre>';
183
	}
184
185
	/**
186
	 * Download a file specified per GET parameter (must be in $this->filesnames!)
187
	 *
188
	 * @throws Api\Exception\WrongParameter
189
	 */
190
	public function download()
191
	{
192
		$filename = $_GET['filename'];
193
		if (!in_array($filename,$this->filenames))
194
		{
195
			throw new Api\Exception\WrongParameter("Not allowed to download '$filename'!");
196
		}
197
		Api\Header\Content::type(basename($filename), 'text/plain');
198
		if ($filename[0] != '/') $filename = $GLOBALS['egw_info']['server']['files_dir'].'/'.$filename;
199
		for($n=ob_get_level(); $n > 0; --$n)
200
		{
201
			ob_end_clean();	// stop all output buffering, to NOT run into memory_limit
202
		}
203
		readfile($filename);
204
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
205
	}
206
}
207
208
// some testcode, if this file is called via it's URL (you need to uncomment and adapt filename!)
209
/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__)
210
{
211
	$GLOBALS['egw_info'] = array(
212
		'flags' => array(
213
			'currentapp' => 'admin',
214
			'nonavbar' => true,
215
		),
216
	);
217
	include_once '../../header.inc.php';
218
219
	$error_log = new Tail('/opt/local/apache2/logs/error_log');
220
	echo $error_log->show();
221
}*/
222