Completed
Push — master ( 7014bb...a38bcf )
by James
15s
created

LogfileCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 0
cts 4
cp 0
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace BrowscapPHP\Command;
5
6
use BrowscapPHP\Browscap;
7
use BrowscapPHP\Exception\InvalidArgumentException;
8
use BrowscapPHP\Exception\ReaderException;
9
use BrowscapPHP\Exception\UnknownBrowserException;
10
use BrowscapPHP\Exception\UnknownBrowserTypeException;
11
use BrowscapPHP\Exception\UnknownDeviceException;
12
use BrowscapPHP\Exception\UnknownEngineException;
13
use BrowscapPHP\Exception\UnknownPlatformException;
14
use BrowscapPHP\Helper\Filesystem;
15
use BrowscapPHP\Helper\LoggerHelper;
16
use BrowscapPHP\Util\Logfile\ReaderCollection;
17
use BrowscapPHP\Util\Logfile\ReaderFactory;
18
use Doctrine\Common\Cache\FilesystemCache;
19
use Roave\DoctrineSimpleCache\SimpleCacheAdapter;
20
use Symfony\Component\Console\Command\Command;
21
use Symfony\Component\Console\Input\InputArgument;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Input\InputOption;
24
use Symfony\Component\Console\Output\OutputInterface;
25
use Symfony\Component\Filesystem\Exception\IOException;
26
use Symfony\Component\Finder\Finder;
27
use Symfony\Component\Finder\SplFileInfo;
28
29
/**
30
 * Commands to parse a log file and parse the useragents in it
31
 */
32
class LogfileCommand extends Command
33
{
34
    /**
35
     * @var array
36
     */
37
    private $undefinedClients = [];
38
39
    /**
40
     * @var array
41
     */
42
    private $uas = [];
43
44
    /**
45
     * @var array
46
     */
47
    private $uasWithType = [];
48
49
    /**
50
     * @var int
51
     */
52
    private $countOk = 0;
53
54
    /**
55
     * @var int
56
     */
57
    private $countNok = 0;
58
59
    /**
60
     * @var int
61
     */
62
    private $totalCount = 0;
63
64
    /**
65
     * @var string
66
     */
67
    private $defaultCacheFolder;
68
69
    public function __construct(string $defaultCacheFolder)
70
    {
71
        $this->defaultCacheFolder = $defaultCacheFolder;
72
73
        parent::__construct();
74
    }
75
76 1
    protected function configure() : void
77
    {
78
        $this
79 1
            ->setName('browscap:log')
80 1
            ->setDescription('Parses the supplied webserver log file.')
81 1
            ->addArgument(
82 1
                'output',
83 1
                InputArgument::REQUIRED,
84 1
                'Path to output log file',
85 1
                null
86
            )
87 1
            ->addOption(
88 1
                'log-file',
89 1
                'f',
90 1
                InputOption::VALUE_REQUIRED,
91 1
                'Path to a webserver log file'
92
            )
93 1
            ->addOption(
94 1
                'log-dir',
95 1
                'd',
96 1
                InputOption::VALUE_REQUIRED,
97 1
                'Path to webserver log directory'
98
            )
99 1
            ->addOption(
100 1
                'include',
101 1
                'i',
102 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
103 1
                'Include glob expressions for log files in the log directory',
104 1
                ['*.log', '*.log*.gz', '*.log*.bz2']
105
            )
106 1
            ->addOption(
107 1
                'exclude',
108 1
                'e',
109 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
110 1
                'Exclude glob expressions for log files in the log directory',
111 1
                ['*error*']
112
            )
113 1
            ->addOption(
114 1
                'cache',
115 1
                'c',
116 1
                InputOption::VALUE_OPTIONAL,
117 1
                'Where the cache files are located',
118 1
                $this->defaultCacheFolder
119
            );
120 1
    }
121
122
    protected function execute(InputInterface $input, OutputInterface $output) : void
123
    {
124
        if (! $input->getOption('log-file') && ! $input->getOption('log-dir')) {
125
            throw InvalidArgumentException::oneOfCommandArguments('log-file', 'log-dir');
126
        }
127
128
        $logger = LoggerHelper::createDefaultLogger($output);
129
130
        $fileCache = new FilesystemCache($input->getOption('cache'));
131
        $cache = new SimpleCacheAdapter($fileCache);
132
133
        $browscap = new Browscap($cache, $logger);
134
        $collection = ReaderFactory::factory();
135
        $fs = new Filesystem();
136
137
        /** @var $file \Symfony\Component\Finder\SplFileInfo */
138
        foreach ($this->getFiles($input) as $file) {
139
            $this->uas = [];
140
            $path = $this->getPath($file);
141
142
            $this->countOk = 0;
143
            $this->countNok = 0;
144
145
            $logger->info('Analyzing file "' . $file->getPathname() . '"');
146
147
            $lines = file($path);
148
149
            if (empty($lines)) {
150
                $logger->info('Skipping empty file "' . $file->getPathname() . '"');
151
                continue;
152
            }
153
154
            $this->totalCount = count($lines);
155
156
            foreach ($lines as $line) {
157
                $this->handleLine(
158
                    $output,
159
                    $collection,
160
                    $browscap,
161
                    $line
162
                );
163
            }
164
165
            $this->outputProgress($output, '', true);
166
167
            arsort($this->uas, SORT_NUMERIC);
168
169
            try {
170
                $fs->dumpFile(
171
                    $input->getArgument('output') . '/output.txt',
172
                    implode(PHP_EOL, array_unique($this->undefinedClients))
173
                );
174
            } catch (IOException $e) {
175
                // do nothing
176
            }
177
178
            try {
179
                $fs->dumpFile(
180
                    $input->getArgument('output') . '/output-with-amount.txt',
181
                    $this->createAmountContent()
182
                );
183
            } catch (IOException $e) {
184
                // do nothing
185
            }
186
187
            try {
188
                $fs->dumpFile(
189
                    $input->getArgument('output') . '/output-with-amount-and-type.txt',
190
                    $this->createAmountTypeContent()
191
                );
192
            } catch (IOException $e) {
193
                // do nothing
194
            }
195
        }
196
197
        $outputFile = $input->getArgument('output') . '/output.txt';
198
199
        try {
200
            $fs->dumpFile(
201
                $outputFile,
202
                implode(PHP_EOL, array_unique($this->undefinedClients))
203
            );
204
        } catch (IOException $e) {
205
            throw new \UnexpectedValueException('writing to file "' . $outputFile . '" failed', 0, $e);
206
        }
207
208
        try {
209
            $fs->dumpFile(
210
                $input->getArgument('output') . '/output-with-amount.txt',
211
                $this->createAmountContent()
212
            );
213
        } catch (IOException $e) {
214
            // do nothing
215
        }
216
217
        try {
218
            $fs->dumpFile(
219
                $input->getArgument('output') . '/output-with-amount-and-type.txt',
220
                $this->createAmountTypeContent()
221
            );
222
        } catch (IOException $e) {
223
            // do nothing
224
        }
225
    }
226
227
    private function createAmountContent() : string
228
    {
229
        $counts = [];
230
231
        foreach ($this->uasWithType as $uas) {
232
            foreach ($uas as $userAgentString => $count) {
233
                if (isset($counts[$userAgentString])) {
234
                    $counts[$userAgentString] += $count;
235
                } else {
236
                    $counts[$userAgentString] = $count;
237
                }
238
            }
239
        }
240
241
        $content = '';
242
243
        arsort($counts, SORT_NUMERIC);
244
245
        foreach ($counts as $agentOfLine => $count) {
246
            $content .= "$count\t$agentOfLine\n";
247
        }
248
249
        return $content;
250
    }
251
252
    private function createAmountTypeContent() : string
253
    {
254
        $content = '';
255
        $types = ['B', 'T', 'P', 'D', 'N', 'U'];
256
257
        foreach ($types as $type) {
258
            if (! isset($this->uasWithType[$type])) {
259
                continue;
260
            }
261
262
            arsort($this->uasWithType[$type], SORT_NUMERIC);
263
264
            foreach ($this->uasWithType[$type] as $agentOfLine => $count) {
265
                $content .= "$type\t$count\t$agentOfLine\n";
266
            }
267
        }
268
269
        return $content;
270
    }
271
272
    private function handleLine(
273
        OutputInterface $output,
274
        ReaderCollection $collection,
275
        Browscap $browscap,
276
        string $line
277
    ) : void {
278
        $userAgentString = '';
279
280
        try {
281
            $userAgentString = $collection->read($line);
282
283
            try {
284
                $this->getResult($browscap->getBrowser($userAgentString));
285
            } catch (\Exception $e) {
286
                $this->undefinedClients[] = $userAgentString;
287
288
                throw $e;
289
            }
290
291
            $type = '.';
292
            ++$this->countOk;
293
        } catch (ReaderException $e) {
294
            $type = 'E';
295
            ++$this->countNok;
296
        } catch (UnknownBrowserTypeException $e) {
297
            $type = 'T';
298
            ++$this->countNok;
299
        } catch (UnknownBrowserException $e) {
300
            $type = 'B';
301
            ++$this->countNok;
302
        } catch (UnknownPlatformException $e) {
303
            $type = 'P';
304
            ++$this->countNok;
305
        } catch (UnknownDeviceException $e) {
306
            $type = 'D';
307
            ++$this->countNok;
308
        } catch (UnknownEngineException $e) {
309
            $type = 'N';
310
            ++$this->countNok;
311
        } catch (\Exception $e) {
312
            $type = 'U';
313
            ++$this->countNok;
314
        }
315
316
        $this->outputProgress($output, $type);
317
318
        // count all useragents
319
        if (isset($this->uas[$userAgentString])) {
320
            ++$this->uas[$userAgentString];
321
        } else {
322
            $this->uas[$userAgentString] = 1;
323
        }
324
325
        if ('.' !== $type && 'E' !== $type) {
326
            // count all undetected useragents grouped by detection error
327
            if (! isset($this->uasWithType[$type])) {
328
                $this->uasWithType[$type] = [];
329
            }
330
331
            if (isset($this->uasWithType[$type][$userAgentString])) {
332
                ++$this->uasWithType[$type][$userAgentString];
333
            } else {
334
                $this->uasWithType[$type][$userAgentString] = 1;
335
            }
336
        }
337
    }
338
339
    private function outputProgress(OutputInterface $output, string $result, bool $end = false) : void
340
    {
341
        if (($this->totalCount % 70) === 0 || $end) {
342
            $formatString = '  %' . strlen($this->countOk) . 'd OK, %' . strlen($this->countNok) . 'd NOK, Summary %'
343
                . strlen($this->totalCount) . 'd';
344
345
            if ($end) {
346
                $result = str_pad($result, 70 - ($this->totalCount % 70), ' ', STR_PAD_RIGHT);
347
            }
348
349
            $endString = sprintf($formatString, $this->countOk, $this->countNok, $this->totalCount);
350
351
            $output->writeln($result . $endString);
352
353
            return;
354
        }
355
356
        $output->write($result);
357
    }
358
359
    private function getResult(\stdClass $result) : string
360
    {
361
        if ('Default Browser' === $result->browser) {
362
            throw new UnknownBrowserException('Unknown browser found');
363
        }
364
365
        if ('unknown' === $result->browser_type) {
366
            throw new UnknownBrowserTypeException('Unknown browser type found');
367
        }
368
369
        if (in_array($result->browser_type, ['Bot/Crawler', 'Library'])) {
370
            return '.';
371
        }
372
373
        if ('unknown' === $result->platform) {
374
            throw new UnknownPlatformException('Unknown platform found');
375
        }
376
377
        if ('unknown' === $result->device_type) {
378
            throw new UnknownDeviceException('Unknown device type found');
379
        }
380
381
        if ('unknown' === $result->renderingengine_name) {
382
            throw new UnknownEngineException('Unknown rendering engine found');
383
        }
384
385
        return '.';
386
    }
387
388
    private function getFiles(InputInterface $input) : Finder
389
    {
390
        $finder = Finder::create();
391
392
        if ($input->getOption('log-file')) {
393
            $file = $input->getOption('log-file');
394
            $finder->append(Finder::create()->in(dirname($file))->name(basename($file)));
395
        }
396
397
        if ($input->getOption('log-dir')) {
398
            $dirFinder = Finder::create()
399
                ->in($input->getOption('log-dir'));
400
            array_map([$dirFinder, 'name'], $input->getOption('include'));
401
            array_map([$dirFinder, 'notName'], $input->getOption('exclude'));
402
403
            $finder->append($dirFinder);
404
        }
405
406
        return $finder;
407
    }
408
409
    private function getPath(SplFileInfo $file) : string
410
    {
411
        switch ($file->getExtension()) {
412
            case 'gz':
413
                $path = 'compress.zlib://' . $file->getPathname();
414
                break;
415
            case 'bz2':
416
                $path = 'compress.bzip2://' . $file->getPathname();
417
                break;
418
            default:
419
                $path = $file->getPathname();
420
                break;
421
        }
422
423
        return $path;
424
    }
425
}
426