Completed
Push — min-php71 ( f5a25a )
by James
05:53
created

LogfileCommand::setCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace BrowscapPHP\Command;
5
6
use BrowscapPHP\Browscap;
7
use BrowscapPHP\Cache\BrowscapCache;
8
use BrowscapPHP\Cache\BrowscapCacheInterface;
9
use BrowscapPHP\Exception\InvalidArgumentException;
10
use BrowscapPHP\Exception\ReaderException;
11
use BrowscapPHP\Exception\UnknownBrowserException;
12
use BrowscapPHP\Exception\UnknownBrowserTypeException;
13
use BrowscapPHP\Exception\UnknownDeviceException;
14
use BrowscapPHP\Exception\UnknownEngineException;
15
use BrowscapPHP\Exception\UnknownPlatformException;
16
use BrowscapPHP\Helper\Filesystem;
17
use BrowscapPHP\Helper\LoggerHelper;
18
use BrowscapPHP\Util\Logfile\ReaderCollection;
19
use BrowscapPHP\Util\Logfile\ReaderFactory;
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
use WurflCache\Adapter\File;
29
30
/**
31
 * Commands to parse a log file and parse the useragents in it
32
 */
33
class LogfileCommand extends Command
34
{
35
    /**
36
     * @var array
37
     */
38
    private $undefinedClients = [];
39
40
    /**
41
     * @var array
42
     */
43
    private $uas = [];
44
45
    /**
46
     * @var array
47
     */
48
    private $uasWithType = [];
49
50
    /**
51
     * @var int
52
     */
53
    private $countOk = 0;
54
55
    /**
56
     * @var int
57
     */
58
    private $countNok = 0;
59
60
    /**
61
     * @var int
62
     */
63
    private $totalCount = 0;
64
65
    /**
66
     * @var ?BrowscapCacheInterface
67
     */
68
    private $cache;
69
70
    /**
71
     * @var string
72
     */
73
    private $defaultCacheFolder;
74
75 1
    public function __construct(string $defaultCacheFolder, ?BrowscapCacheInterface $cache = null)
76
    {
77 1
        $this->defaultCacheFolder = $defaultCacheFolder;
78 1
        $this->cache = $cache;
79
80 1
        parent::__construct();
81 1
    }
82
83 1
    protected function configure() : void
84
    {
85
        $this
86 1
            ->setName('browscap:log')
87 1
            ->setDescription('Parses the supplied webserver log file.')
88 1
            ->addArgument(
89 1
                'output',
90 1
                InputArgument::REQUIRED,
91 1
                'Path to output log file',
92 1
                null
93
            )
94 1
            ->addOption(
95 1
                'log-file',
96 1
                'f',
97 1
                InputOption::VALUE_REQUIRED,
98 1
                'Path to a webserver log file'
99
            )
100 1
            ->addOption(
101 1
                'log-dir',
102 1
                'd',
103 1
                InputOption::VALUE_REQUIRED,
104 1
                'Path to webserver log directory'
105
            )
106 1
            ->addOption(
107 1
                'include',
108 1
                'i',
109 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
110 1
                'Include glob expressions for log files in the log directory',
111 1
                ['*.log', '*.log*.gz', '*.log*.bz2']
112
            )
113 1
            ->addOption(
114 1
                'exclude',
115 1
                'e',
116 1
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
117 1
                'Exclude glob expressions for log files in the log directory',
118 1
                ['*error*']
119
            )
120 1
            ->addOption(
121 1
                'debug',
122 1
                null,
123 1
                InputOption::VALUE_NONE,
124 1
                'Should the debug mode entered?'
125
            )
126 1
            ->addOption(
127 1
                'cache',
128 1
                'c',
129 1
                InputOption::VALUE_OPTIONAL,
130 1
                'Where the cache files are located',
131 1
                $this->defaultCacheFolder
132
            );
133 1
    }
134
135
    protected function execute(InputInterface $input, OutputInterface $output) : void
136
    {
137
        if (!$input->getOption('log-file') && !$input->getOption('log-dir')) {
138
            throw InvalidArgumentException::oneOfCommandArguments('log-file', 'log-dir');
139
        }
140
141
        $loggerHelper = new LoggerHelper();
142
        $logger       = $loggerHelper->create($input->getOption('debug'));
143
144
        $browscap   = new Browscap();
145
        $collection = ReaderFactory::factory();
146
        $fs         = new Filesystem();
147
148
        $browscap
149
            ->setLogger($logger)
150
            ->setCache($this->getCache($input));
151
152
        /** @var $file \Symfony\Component\Finder\SplFileInfo */
153
        foreach ($this->getFiles($input) as $file) {
154
            $this->uas = [];
155
            $path      = $this->getPath($file);
156
157
            $this->countOk  = 0;
158
            $this->countNok = 0;
159
160
            $logger->info('Analyzing file "' . $file->getPathname() . '"');
161
162
            $lines = file($path);
163
164
            if (empty($lines)) {
165
                $logger->info('Skipping empty file "' . $file->getPathname() . '"');
166
                continue;
167
            }
168
169
            $this->totalCount = count($lines);
170
171
            foreach ($lines as $line) {
172
                $this->handleLine(
173
                    $output,
174
                    $collection,
175
                    $browscap,
176
                    $line
177
                );
178
            }
179
180
            $this->outputProgress($output, '', true);
181
182
            arsort($this->uas, SORT_NUMERIC);
183
184
            try {
185
                $fs->dumpFile(
186
                    $input->getArgument('output') . '/output.txt',
187
                    implode(PHP_EOL, array_unique($this->undefinedClients))
188
                );
189
            } catch (IOException $e) {
190
                // do nothing
191
            }
192
193
            try {
194
                $fs->dumpFile(
195
                    $input->getArgument('output') . '/output-with-amount.txt',
196
                    $this->createAmountContent()
197
                );
198
            } catch (IOException $e) {
199
                // do nothing
200
            }
201
202
            try {
203
                $fs->dumpFile(
204
                    $input->getArgument('output') . '/output-with-amount-and-type.txt',
205
                    $this->createAmountTypeContent()
206
                );
207
            } catch (IOException $e) {
208
                // do nothing
209
            }
210
        }
211
212
        $outputFile = $input->getArgument('output') . '/output.txt';
213
214
        try {
215
            $fs->dumpFile(
216
                $outputFile,
217
                implode(PHP_EOL, array_unique($this->undefinedClients))
218
            );
219
        } catch (IOException $e) {
220
            throw new \UnexpectedValueException('writing to file "' . $outputFile . '" failed', 0, $e);
221
        }
222
223
        try {
224
            $fs->dumpFile(
225
                $input->getArgument('output') . '/output-with-amount.txt',
226
                $this->createAmountContent()
227
            );
228
        } catch (IOException $e) {
229
            // do nothing
230
        }
231
232
        try {
233
            $fs->dumpFile(
234
                $input->getArgument('output') . '/output-with-amount-and-type.txt',
235
                $this->createAmountTypeContent()
236
            );
237
        } catch (IOException $e) {
238
            // do nothing
239
        }
240
    }
241
242
    private function createAmountContent() : string
243
    {
244
        $counts = [];
245
246
        foreach ($this->uasWithType as $uas) {
247
            foreach ($uas as $userAgentString => $count) {
248
                if (isset($counts[$userAgentString])) {
249
                    $counts[$userAgentString] += $count;
250
                } else {
251
                    $counts[$userAgentString] = $count;
252
                }
253
            }
254
        }
255
256
        $content = '';
257
258
        arsort($counts, SORT_NUMERIC);
259
260
        foreach ($counts as $agentOfLine => $count) {
261
            $content .= "$count\t$agentOfLine\n";
262
        }
263
264
        return $content;
265
    }
266
267
    private function createAmountTypeContent() : string
268
    {
269
        $content = '';
270
        $types   = ['B', 'T', 'P', 'D', 'N', 'U'];
271
272
        foreach ($types as $type) {
273
            if (!isset($this->uasWithType[$type])) {
274
                continue;
275
            }
276
277
            arsort($this->uasWithType[$type], SORT_NUMERIC);
278
279
            foreach ($this->uasWithType[$type] as $agentOfLine => $count) {
280
                $content .= "$type\t$count\t$agentOfLine\n";
281
            }
282
        }
283
284
        return $content;
285
    }
286
287
    private function handleLine(
288
        OutputInterface $output,
289
        ReaderCollection $collection,
290
        Browscap $browscap,
291
        int $line
292
    ) : void {
293
        $userAgentString = '';
294
295
        try {
296
            $userAgentString = $collection->read($line);
297
298
            try {
299
                $this->getResult($browscap->getBrowser($userAgentString));
0 ignored issues
show
Bug introduced by
It seems like $browscap->getBrowser($userAgentString) can be null; however, getResult() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
300
            } catch (\Exception $e) {
301
                $this->undefinedClients[] = $userAgentString;
302
303
                throw $e;
304
            }
305
306
            $type = '.';
307
            ++$this->countOk;
308
        } catch (ReaderException $e) {
309
            $type = 'E';
310
            ++$this->countNok;
311
        } catch (UnknownBrowserTypeException $e) {
312
            $type = 'T';
313
            ++$this->countNok;
314
        } catch (UnknownBrowserException $e) {
315
            $type = 'B';
316
            ++$this->countNok;
317
        } catch (UnknownPlatformException $e) {
318
            $type = 'P';
319
            ++$this->countNok;
320
        } catch (UnknownDeviceException $e) {
321
            $type = 'D';
322
            ++$this->countNok;
323
        } catch (UnknownEngineException $e) {
324
            $type = 'N';
325
            ++$this->countNok;
326
        } catch (\Exception $e) {
327
            $type = 'U';
328
            ++$this->countNok;
329
        }
330
331
        $this->outputProgress($output, $type);
332
333
        // count all useragents
334
        if (isset($this->uas[$userAgentString])) {
335
            ++$this->uas[$userAgentString];
336
        } else {
337
            $this->uas[$userAgentString] = 1;
338
        }
339
340
        if ('.' !== $type && 'E' !== $type) {
341
            // count all undetected useragents grouped by detection error
342
            if (!isset($this->uasWithType[$type])) {
343
                $this->uasWithType[$type] = [];
344
            }
345
346
            if (isset($this->uasWithType[$type][$userAgentString])) {
347
                ++$this->uasWithType[$type][$userAgentString];
348
            } else {
349
                $this->uasWithType[$type][$userAgentString] = 1;
350
            }
351
        }
352
    }
353
354
    private function outputProgress(OutputInterface $output, string $result, bool $end = false) : void
355
    {
356
        if (($this->totalCount % 70) === 0 || $end) {
357
            $formatString = '  %' . strlen($this->countOk) . 'd OK, %' . strlen($this->countNok) . 'd NOK, Summary %'
358
                . strlen($this->totalCount) . 'd';
359
360
            if ($end) {
361
                $result = str_pad($result, 70 - ($this->totalCount % 70), ' ', STR_PAD_RIGHT);
362
            }
363
364
            $endString = sprintf($formatString, $this->countOk, $this->countNok, $this->totalCount);
365
366
            $output->writeln($result . $endString);
367
368
            return;
369
        }
370
371
        $output->write($result);
372
    }
373
374
    private function getResult(\stdClass $result) : string
375
    {
376
        if ('Default Browser' === $result->browser) {
377
            throw new UnknownBrowserException('Unknown browser found');
378
        }
379
380
        if ('unknown' === $result->browser_type) {
381
            throw new UnknownBrowserTypeException('Unknown browser type found');
382
        }
383
384
        if (in_array($result->browser_type, ['Bot/Crawler', 'Library'])) {
385
            return '.';
386
        }
387
388
        if ('unknown' === $result->platform) {
389
            throw new UnknownPlatformException('Unknown platform found');
390
        }
391
392
        if ('unknown' === $result->device_type) {
393
            throw new UnknownDeviceException('Unknown device type found');
394
        }
395
396
        if ('unknown' === $result->renderingengine_name) {
397
            throw new UnknownEngineException('Unknown rendering engine found');
398
        }
399
400
        return '.';
401
    }
402
403
    private function getFiles(InputInterface $input) : Finder
404
    {
405
        $finder = Finder::create();
406
407
        if ($input->getOption('log-file')) {
408
            $file = $input->getOption('log-file');
409
            $finder->append(Finder::create()->in(dirname($file))->name(basename($file)));
410
        }
411
412
        if ($input->getOption('log-dir')) {
413
            $dirFinder = Finder::create()
414
                ->in($input->getOption('log-dir'));
415
            array_map([$dirFinder, 'name'], $input->getOption('include'));
416
            array_map([$dirFinder, 'notName'], $input->getOption('exclude'));
417
418
            $finder->append($dirFinder);
419
        }
420
421
        return $finder;
422
    }
423
424
    private function getPath(SplFileInfo $file) : string
425
    {
426
        switch ($file->getExtension()) {
427
            case 'gz':
428
                $path = 'compress.zlib://' . $file->getPathname();
429
                break;
430
            case 'bz2':
431
                $path = 'compress.bzip2://' . $file->getPathname();
432
                break;
433
            default:
434
                $path = $file->getPathname();
435
                break;
436
        }
437
438
        return $path;
439
    }
440
441
    private function getCache(InputInterface $input) : BrowscapCacheInterface
442
    {
443
        if (null === $this->cache) {
444
            $cacheAdapter = new File([File::DIR => $input->getOption('cache')]);
445
            $this->cache  = new BrowscapCache($cacheAdapter);
446
        }
447
448
        return $this->cache;
449
    }
450
}
451