Passed
Push — develop ( 879147...9ec092 )
by Nikolay
05:27 queued 12s
created

SysLogsManagementProcessor::scanDirRecursively()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 20
rs 9.2222
c 0
b 0
f 0
cc 6
nc 5
nop 1
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\PBXCoreREST\Lib;
21
22
use MikoPBX\Core\System\Network;
23
use MikoPBX\Core\System\Processes;
24
use MikoPBX\Core\System\System;
25
use MikoPBX\Core\System\Util;
26
use MikoPBX\PBXCoreREST\Workers\WorkerMakeLogFilesArchive;
27
use Phalcon\Di;
28
use Phalcon\Di\Injectable;
29
30
/**
31
 * Class SysLogsManagementProcessor
32
 *
33
 * @package MikoPBX\PBXCoreREST\Lib
34
 *
35
 */
36
class SysLogsManagementProcessor extends Injectable
37
{
38
    public const DEFAULT_FILENAME = 'asterisk/messages';
39
40
    /**
41
     * Processes syslog requests
42
     *
43
     * @param array $request
44
     *
45
     * @return PBXApiResult An object containing the result of the API call.
46
     *
47
     */
48
    public static function callBack(array $request): PBXApiResult
49
    {
50
        $action         = $request['action'];
51
        $data           = $request['data'];
52
        $res            = new PBXApiResult();
53
        $res->processor = __METHOD__;
54
        switch ($action) {
55
            case 'getLogFromFile':
56
                $res = self::getLogFromFile($data['filename'], $data['filter'], $data['lines'], $data['offset']);
57
                break;
58
            case 'prepareLog':
59
                $res = self::prepareLog(false);
60
                $res->processor = $action;
61
                break;
62
            case 'startLog':
63
                $res = self::startLog();
64
                break;
65
            case 'stopLog':
66
                $res = self::prepareLog(true);
67
                $res->processor = $action;
68
                break;
69
            case 'downloadLogsArchive':
70
                $res = self::downloadLogsArchive($data['filename']);
71
                break;
72
            case 'downloadLogFile':
73
                $res = self::downloadLogFile($data['filename']);
74
                break;
75
            case 'getLogsList':
76
                $res = self::getLogsList();
77
                break;
78
            case 'eraseFile':
79
                $res = self::eraseFile($data['filename']);
80
                break;
81
            default:
82
                $res->messages['error'][] = "Unknown action - $action in ".__CLASS__;
83
        }
84
85
        $res->function = $action;
86
87
        return $res;
88
    }
89
90
    /**
91
     * Gets partially filtered log file strings.
92
     *
93
     * @param string $filename
94
     * @param string $filter
95
     * @param int    $lines
96
     * @param int    $offset
97
     *
98
     * @return PBXApiResult An object containing the result of the API call.
99
     */
100
    public static function getLogFromFile(string $filename, string $filter = '', $lines = 500, $offset = 0): PBXApiResult
101
    {
102
        $res            = new PBXApiResult();
103
        $res->processor = __METHOD__;
104
        $filename       = System::getLogDir() . '/' . $filename;
105
        if ( ! file_exists($filename)) {
106
            $res->success    = false;
107
            $res->messages[] = 'No access to the file ' . $filename;
108
        } else {
109
            $res->success = true;
110
            $head         = Util::which('head');
111
            $grep         = '/bin/grep';
112
            if (!is_executable($grep)) {
113
                $grep         = Util::which('grep');
114
            }
115
            $tail         = Util::which('tail');
116
            $filter       = escapeshellarg($filter);
117
            $offset       = (int)$offset;
118
            $lines        = (int)$lines;
119
            $linesPlusOffset = $lines+$offset;
120
121
            $di          = Di::getDefault();
122
            $dirsConfig  = $di->getShared('config');
123
            $filenameTmp = $dirsConfig->path('www.downloadCacheDir') . '/' . __FUNCTION__ . '_' . time() . '.log';
124
            if (empty($filter)){
125
                $cmd         = "{$tail} -n {$linesPlusOffset} {$filename}";
126
            } else {
127
                $cmd         = "{$grep} --text -h -e ".str_replace('&',"' -e '", $filter)." -F {$filename} | $tail -n {$linesPlusOffset}";
128
            }
129
            if ($offset>0){
130
                $cmd .= " | {$head} -n {$lines}";
131
            }
132
            $cmd .= " > $filenameTmp";
133
134
            Processes::mwExec("$cmd; chown www:www $filenameTmp");
135
            $res->data['cmd']=$cmd;
136
            $res->data['filename'] = $filenameTmp;
137
        }
138
139
        return $res;
140
    }
141
142
    /**
143
     * Starts the collection of logs and captures TCP packets.
144
     *
145
     * @return PBXApiResult An object containing the result of the API call.
146
     */
147
    private static function startLog(): PBXApiResult
148
    {
149
        $res            = new PBXApiResult();
150
        $res->processor = __METHOD__;
151
        $logDir         = System::getLogDir();
152
153
        // TCP dump
154
        $tcpDumpDir  = "{$logDir}/tcpDump";
155
        Util::mwMkdir($tcpDumpDir);
156
        $network     = new Network();
157
        $arr_eth     = $network->getInterfacesNames();
158
        $tcpdumpPath = Util::which('tcpdump');
159
        $timeout = 300;
160
        foreach ($arr_eth as $eth) {
161
            Processes::mwExecBgWithTimeout(
162
                "{$tcpdumpPath} -i {$eth} -n -s 0 -vvv -w {$tcpDumpDir}/{$eth}.pcap",
163
                $timeout,
164
                "{$tcpDumpDir}/{$eth}_out.log"
165
            );
166
        }
167
        $res->success = true;
168
169
        return $res;
170
    }
171
172
    /**
173
     * Stops tcpdump and starts creating a log files archive for download.
174
     *
175
     * @param bool $tcpdumpOnly Indicates whether to include only tcpdump logs.
176
     *
177
     * @return PBXApiResult An object containing the result of the API call.
178
     */
179
    private static function prepareLog(bool $tcpdumpOnly): PBXApiResult
180
    {
181
        $res            = new PBXApiResult();
182
        $res->processor = __METHOD__;
183
        $di             = Di::getDefault();
184
        $dirsConfig     = $di->getShared('config');
185
        $temp_dir       = $dirsConfig->path('core.tempDir');
186
187
        $prefix = $tcpdumpOnly?'tcpdump':'sys';
188
        $futureFileName        = $temp_dir . '/log-'.$prefix.'-' . time() . '.zip';
189
        $res->data['filename'] = $futureFileName;
190
        $res->success          = true;
191
        // Create background task
192
        $merge_settings                     = [];
193
        $merge_settings['result_file']      = $futureFileName;
194
        $merge_settings['tcpdump_only']     = $tcpdumpOnly;
195
196
        if($tcpdumpOnly){
197
            Processes::killByName('timeout');
198
            Processes::killByName('tcpdump');
199
        }
200
        $settings_file                      = "{$temp_dir}/log-settings-".$prefix.".json";
201
        file_put_contents($settings_file, json_encode($merge_settings, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
202
        $phpPath               = Util::which('php');
203
        $workerFilesMergerPath = Util::getFilePathByClassName(WorkerMakeLogFilesArchive::class);
204
        Processes::mwExecBg("{$phpPath} -f {$workerFilesMergerPath} start '{$settings_file}'");
205
206
        return $res;
207
    }
208
209
    /**
210
     * Requests a zipped archive containing logs and PCAP file
211
     * Checks if archive ready it returns download link.
212
     *
213
     * @param string $resultFile
214
     *
215
     * @return PBXApiResult An object containing the result of the API call.
216
     */
217
    private static function downloadLogsArchive(string $resultFile): PBXApiResult
218
    {
219
        $res            = new PBXApiResult();
220
        $res->processor = __METHOD__;
221
222
        $progress_file = "{$resultFile}.progress";
223
        if ( !file_exists($progress_file)) {
224
            $res->messages[] = 'Archive does not exist. Try again!';
225
        } elseif (file_exists($progress_file) && file_get_contents($progress_file) === '100') {
226
            $uid          = Util::generateRandomString(36);
227
            $di           = Di::getDefault();
228
            $downloadLink = $di->getShared('config')->path('www.downloadCacheDir');
229
            $result_dir   = "{$downloadLink}/{$uid}";
230
            Util::mwMkdir($result_dir);
231
            $link_name = 'MikoPBXLogs_' . basename($resultFile);
232
            Util::createUpdateSymlink($resultFile, "{$result_dir}/{$link_name}");
233
            Util::addRegularWWWRights("{$result_dir}/{$link_name}");
234
            $res->success          = true;
235
            $res->data['status']   = "READY";
236
            $res->data['filename'] = "{$uid}/{$link_name}";
237
        } else {
238
            $res->success           = true;
239
            $res->data['status']    = "PREPARING";
240
            $res->data['progress']  = file_get_contents($progress_file);
241
        }
242
243
        return $res;
244
    }
245
246
    /**
247
     * Prepares a downloadable link for a log file with the provided name.
248
     *
249
     * @param string $filename The name of the log file.
250
     *
251
     * @return PBXApiResult An object containing the result of the API call.
252
     *
253
     */
254
    private static function downloadLogFile(string $filename): PBXApiResult
255
    {
256
        $res            = new PBXApiResult();
257
        $res->processor = __METHOD__;
258
        $filename       = System::getLogDir() . '/' . $filename;
259
        if ( ! file_exists($filename)) {
260
            $res->success    = false;
261
            $res->messages[] = 'File does not exist ' . $filename;
262
        } else {
263
            $uid          = Util::generateRandomString(36);
264
            $di           = Di::getDefault();
265
            $downloadLink = $di->getShared('config')->path('www.downloadCacheDir');
266
            $result_dir   = "{$downloadLink}/{$uid}";
267
            Util::mwMkdir($result_dir);
268
            $link_name = basename($filename);
269
            $lnPath    = Util::which('ln');
270
            $chownPath = Util::which('chown');
271
            Processes::mwExec("{$lnPath} -s {$filename} {$result_dir}/{$link_name}");
272
            Processes::mwExec("{$chownPath} www:www {$result_dir}/{$link_name}");
273
            $res->success          = true;
274
            $res->data['filename'] = "{$uid}/{$link_name}";
275
        }
276
277
        return $res;
278
    }
279
280
    /**
281
     * Erase log file with the provided name.
282
     *
283
     * @param string $filename The name of the log file.
284
     *
285
     * @return PBXApiResult An object containing the result of the API call.
286
     *
287
     */
288
    private static function eraseFile(string $filename): PBXApiResult
289
    {
290
        $res            = new PBXApiResult();
291
        $res->processor = __METHOD__;
292
        $filename       = System::getLogDir() . '/' . $filename;
293
        if ( ! file_exists($filename)) {
294
            $res->success    = false;
295
            $res->messages[] = 'File does not exist ' . $filename;
296
        } else {
297
            $echoPath = Util::which('echo');
298
            Processes::mwExec("$echoPath ' ' > $filename");
299
            $res->success          = true;
300
        }
301
302
        return $res;
303
    }
304
305
    /**
306
     * Returns list of log files to show them on web interface
307
     *
308
     * @return PBXApiResult An object containing the result of the API call.
309
     */
310
    private static function getLogsList(): PBXApiResult
311
    {
312
        $res            = new PBXApiResult();
313
        $res->processor = __METHOD__;
314
        $logDir         = System::getLogDir();
315
        $filesList      = [];
316
        $entries        = self::scanDirRecursively($logDir);
317
        $entries        = Util::flattenArray($entries);
318
        $defaultFound   = false;
319
        foreach ($entries as $entry) {
320
            $fileSize = filesize($entry);
321
            $now      = time();
322
            if ($fileSize === 0
323
                || $now - filemtime($entry) > 604800 // Older than 10 days
324
            ) {
325
                continue;
326
            }
327
            $relativePath             = str_ireplace($logDir . '/', '', $entry);
328
            $fileSizeKB               = ceil($fileSize / 1024);
329
            $default = ($relativePath === self::DEFAULT_FILENAME);
330
            $filesList[$relativePath] =
331
                [
332
                    'path'    => $relativePath,
333
                    'size'    => "{$fileSizeKB} kb",
334
                    'default' => $default,
335
                ];
336
            if($default){
337
                $defaultFound = true;
338
            }
339
        }
340
        if(!$defaultFound){
0 ignored issues
show
introduced by
The condition $defaultFound is always false.
Loading history...
341
            if(isset($filesList['system/messages'])){
342
                $filesList['system/messages']['default'] = true;
343
344
            }else{
345
                $filesList[array_key_first($filesList)]['default'] = true;
346
            }
347
        }
348
        ksort($filesList);
349
        $res->success       = true;
350
        $res->data['files'] = $filesList;
351
        return $res;
352
    }
353
354
    /**
355
     * Scans a directory just like scandir(), only recursively
356
     * returns a hierarchical array representing the directory structure
357
     *
358
     * @param string $dir directory to scan
359
     *
360
     * @return array
361
     */
362
    private static function scanDirRecursively(string $dir): array
363
    {
364
        $list = [];
365
366
        //get directory contents
367
        foreach (scandir($dir) as $d) {
368
            //ignore any of the files in the array
369
            if (in_array($d, ['.', '..'])) {
370
                continue;
371
            }
372
            //if current file ($d) is a directory, call scanDirRecursively
373
            if (is_dir($dir . '/' . $d)) {
374
                $list[] = self::scanDirRecursively($dir . '/' . $d);
375
                //otherwise, add the file to the list
376
            } elseif (is_file($dir . '/' . $d) || is_link($dir . '/' . $d)) {
377
                $list[] = $dir . '/' . $d;
378
            }
379
        }
380
381
        return $list;
382
    }
383
}