Passed
Pull Request — master (#144)
by Sebastiao
02:32
created

SoapBase::certIsExpired()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 2
nop 1
crap 4
1
<?php
2
namespace NFePHP\Common\Soap;
3
4
use NFePHP\Common\Certificate;
5
use NFePHP\Common\Exception\RuntimeException;
6
use NFePHP\Common\Strings;
7
use League\Flysystem\Filesystem;
8
use League\Flysystem\Adapter\Local;
9
use Psr\Log\LoggerInterface;
10
11
/**
12
 * Soap base class
13
 *
14
 * @category  NFePHP
15
 * @package   NFePHP\Common\Soap\SoapBase
16
 * @copyright NFePHP Copyright (c) 2017
17
 * @author    Roberto L. Machado <linux.rlm at gmail dot com>
18
 * @license   http://www.gnu.org/licenses/lgpl.txt LGPLv3+
19
 * @license   https://opensource.org/licenses/MIT MIT
20
 * @license   http://www.gnu.org/licenses/gpl.txt GPLv3+
21
 * @link      http://github.com/nfephp-org/sped-nfse for the canonical source repository
22
 */
23
abstract class SoapBase implements SoapInterface
24
{
25
    /**
26
     * @var int
27
     */
28
    protected $soapprotocol = self::SSL_DEFAULT;
29
    /**
30
     * @var int
31
     */
32
    protected $soaptimeout = 20;
33
    /**
34
     * @var string
35
     */
36
    protected $proxyIP;
37
    /**
38
     * @var int
39
     */
40
    protected $proxyPort;
41
    /**
42
     * @var string
43
     */
44
    protected $proxyUser;
45
    /**
46
     * @var string
47
     */
48
    protected $proxyPass;
49
    /**
50
     * @var array
51
     */
52
    protected $prefixes = [1 => 'soapenv', 2 => 'soap'];
53
    /**
54
     * @var Certificate
55
     */
56
    protected $certificate;
57
    /**
58
     * @var LoggerInterface
59
     */
60
    protected $logger;
61
     /**
62
     * @var string
63
     */
64
    protected $tempdir;
65
    /**
66
     * @var string
67
     */
68
    protected $certsdir;
69
    /**
70
     * @var string
71
     */
72
    protected $debugdir;
73
    /**
74
     * @var string
75
     */
76
    protected $prifile;
77
    /**
78
     * @var string
79
     */
80
    protected $pubfile;
81
    /**
82
     * @var string
83
     */
84
    protected $certfile;
85
    /**
86
     * @var string
87
     */
88
    protected $casefaz;
89
    /**
90
     * @var bool
91
     */
92
    protected $disablesec = false;
93
    /**
94
     * @var bool
95
     */
96
    protected $disableCertValidation = false;
97
    /**
98
     * @var \League\Flysystem\Adapter\Local
99
     */
100
    protected $adapter;
101
    /**
102
     * @var \League\Flysystem\Filesystem
103
     */
104
    protected $filesystem;
105
    /**
106
     * @var string
107
     */
108
    protected $temppass = '';
109
    /**
110
     * @var bool
111
     */
112
    protected $encriptPrivateKey = true;
113
    /**
114
     * @var bool
115
     */
116
    protected $debugmode = false;
117
    /**
118
     * @var string
119
     */
120
    public $responseHead;
121
    /**
122
     * @var string
123
     */
124
    public $responseBody;
125
    /**
126
     * @var string
127
     */
128
    public $requestHead;
129
    /**
130
     * @var string
131
     */
132
    public $requestBody;
133
    /**
134
     * @var string
135
     */
136
    public $soaperror;
137
    /**
138
     * @var array
139
     */
140
    public $soapinfo = [];
141
    /**
142
     * @var int
143
     */
144
    public $waitingTime = 45;
145
146
    /**
147
     * SoapBase constructor.
148
     * @param Certificate|null     $certificate Certificate from client
149
     * @param LoggerInterface|null $logger      Logger object for logger
150
     */
151 5
    public function __construct(
152
        Certificate $certificate = null,
153
        LoggerInterface $logger = null
154
    ) {
155 5
        $this->logger = $logger;
156 5
        $this->certificate = $this->certIsExpired($certificate);
157 4
        $this->setTemporaryFolder(sys_get_temp_dir() . '/sped/');
158
159 4
        if (null !== $certificate) {
160
            $this->saveTemporarilyKeyFiles();
161
        }
162 4
    }
163
164
    /**
165
     * Check if certificate is valid to currently used date
166
     * @param Certificate $certificate
167
     * @return Certificate
168
     * @throws RuntimeException
169
     */
170 5
    private function certIsExpired(Certificate $certificate = null)
171
    {
172 5
        if (false === $this->disableCertValidation
173 5
            && null !== $certificate
174 5
            && $certificate->isExpired()
175
        ) {
176 1
            throw new RuntimeException(
177 1
                'The validity of the certificate has expired.'
178
            );
179
        }
180
181 4
        return $certificate;
182
    }
183
    
184
    /**
185
     * Destructor
186
     * Clean temporary files
187
     */
188 4
    public function __destruct()
189
    {
190 4
        $this->removeTemporarilyFiles();
191 4
    }
192
193
    /**
194
     * Disables the security checking of host and peer certificates
195
     * @param bool $flag
196
     * @return bool
197
     */
198 1
    public function disableSecurity($flag = false)
199
    {
200 1
        return $this->disablesec = $flag;
201
    }
202
    
203
    /**
204
     * ONlY for tests
205
     * @param bool $flag
206
     * @return bool
207
     */
208 1
    public function disableCertValidation($flag = true)
209
    {
210 1
        return $this->disableCertValidation = $flag;
211
    }
212
213
    /**
214
     * Load path to CA and enable to use on SOAP
215
     * @param string $capath
216
     * @return void
217
     */
218
    public function loadCA($capath)
219
    {
220
        if (is_file($capath)) {
221
            $this->casefaz = $capath;
222
        }
223
    }
224
    
225
    /**
226
     * Set option to encrypt private key before save in filesystem
227
     * for an additional layer of protection
228
     * @param bool $encript
229
     * @return bool
230
     */
231
    public function setEncriptPrivateKey($encript = true)
232
    {
233
        return $this->encriptPrivateKey = $encript;
234
    }
235
    
236
    /**
237
     * Set another temporayfolder for saving certificates for SOAP utilization
238
     * @param string $folderRealPath
239
     * @return void
240
     */
241 4
    public function setTemporaryFolder($folderRealPath)
242
    {
243 4
        $this->tempdir = $folderRealPath;
244 4
        $this->setLocalFolder($folderRealPath);
245 4
    }
246
    
247
    /**
248
     * Set Local folder for flysystem
249
     * @param string $folder
250
     */
251 4
    protected function setLocalFolder($folder = '')
252
    {
253 4
        $this->adapter = new Local($folder);
254 4
        $this->filesystem = new Filesystem($this->adapter);
255 4
    }
256
257
    /**
258
     * Set debug mode, this mode will save soap envelopes in temporary directory
259
     * @param bool $value
260
     * @return bool
261
     */
262
    public function setDebugMode($value = false)
263
    {
264
        return $this->debugmode = $value;
265
    }
266
    
267
    /**
268
     * Set certificate class for SSL communications
269
     * @param Certificate $certificate
270
     * @return void
271
     */
272 1
    public function loadCertificate(Certificate $certificate)
273
    {
274 1
        $this->certificate = $this->certIsExpired($certificate);
275 1
    }
276
    
277
    /**
278
     * Set logger class
279
     * @param LoggerInterface $logger
280
     * @return LoggerInterface
281
     */
282
    public function loadLogger(LoggerInterface $logger)
283
    {
284
        return $this->logger = $logger;
285
    }
286
    
287
    /**
288
     * Set timeout for communication
289
     * @param int $timesecs
290
     * @return int
291
     */
292
    public function timeout(int $timesecs)
293
    {
294
        return $this->soaptimeout = $timesecs;
295
    }
296
    
297
    /**
298
     * Set security protocol
299
     * @param int $protocol
300
     * @return int
301
     */
302
    public function protocol($protocol = self::SSL_DEFAULT)
303
    {
304
        return $this->soapprotocol = $protocol;
305
    }
306
    
307
    /**
308
     * Set prefixes
309
     * @param array $prefixes
310
     * @return string[]
311
     */
312
    public function setSoapPrefix($prefixes = [])
313
    {
314
        return $this->prefixes = $prefixes;
315
    }
316
317
    /**
318
     * Set proxy parameters
319
     * @param string $ip
320
     * @param int    $port
321
     * @param string $user
322
     * @param string $password
323
     * @return void
324
     */
325
    public function proxy($ip, $port, $user, $password)
326
    {
327
        $this->proxyIP = $ip;
328
        $this->proxyPort = $port;
329
        $this->proxyUser = $user;
330
        $this->proxyPass = $password;
331
    }
332
333
    /**
334
     * @param string $url
335
     * @param string $operation
336
     * @param string $action
337
     * @param int $soapver
338
     * @param array $parameters
339
     * @param array $namespaces
340
     * @param string $request
341
     * @param null $soapheader
342
     * @return mixed
343
     */
344
    abstract public function send(
345
        $url,
346
        $operation = '',
347
        $action = '',
348
        $soapver = SOAP_1_2,
349
        $parameters = [],
350
        $namespaces = [],
351
        $request = '',
352
        $soapheader = null
353
    );
354
    
355
    /**
356
     * Mount soap envelope
357
     * @param string $request
358
     * @param array $namespaces
359
     * @param $soapVer int
360
     * @param \SOAPHeader $header
361
     * @return string
362
     */
363
    protected function makeEnvelopeSoap(
364
        $request,
365
        $namespaces,
366
        $soapVer = SOAP_1_2,
367
        \SoapHeader $header = null
368
    ) {
369
        $prefix = $this->prefixes[$soapVer];
370
371
        $envelopeAttributes = $this->getStringAttributes($namespaces);
372
373
        return $this->mountEnvelopString(
374
            $prefix,
375
            $envelopeAttributes,
376
            $this->mountSoapHeaders($prefix, $header),
377
            $request
378
        );
379
    }
380
381
    /**
382
     * @param string $envelopPrefix
383
     * @param string $envelopAttributes
384
     * @param string $header
385
     * @param string $bodyContent
386
     * @return string
387
     */
388
    private function mountEnvelopString(
389
        $envelopPrefix,
390
        $envelopAttributes = '',
391
        $header = '',
392
        $bodyContent = ''
393
    ) {
394
        return sprintf(
395
            '<%s:Envelope %s >'.$header.'<%s:Body>%s</%s:Body></%s:Envelop>',
396
            $envelopPrefix,
397
            $envelopAttributes,
398
            $envelopPrefix,
399
            $bodyContent,
400
            $envelopPrefix,
401
            $envelopPrefix
402
        );
403
    }
404
405
    /**
406
     * @param string $envelopPrefix
407
     * @param \SoapHeader $header
408
     * @return string
409
     */
410
    private function mountSoapHeaders($envelopPrefix, $header = null)
411
    {
412
        $headerItems = '';
413
        foreach ($header->data as $key => $value) {
0 ignored issues
show
Bug introduced by
The property data does not seem to exist in SoapHeader.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
414
            $headerItems = '<'.$key.'>'.$value.'<'.$key.'>';
415
        }
416
417
        return
418
            $header ? sprintf(
419
                '<%s:Header><%s xmlns="%s">%s</%s></%s:Header>',
420
                $envelopPrefix,
421
                $header->name,
0 ignored issues
show
Bug introduced by
The property name does not seem to exist in SoapHeader.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
422
                $header->ns ?? '',
0 ignored issues
show
Bug introduced by
The property ns does not seem to exist in SoapHeader.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
423
                $headerItems
424
            ) : '';
425
    }
426
427
    /**
428
     * @param array $namespaces
429
     * @return string
430
     */
431
    private function getStringAttributes($namespaces = [])
432
    {
433
        foreach ($namespaces as $key => $value) {
434
            $envelopeAttributes = $key.'="'.$value.'"';
435
        }
436
437
        return $envelopeAttributes ?? '';
438
    }
439
440
    
441
    /**
442
     * Temporarily saves the certificate keys for use cURL or SoapClient
443
     */
444
    public function saveTemporarilyKeyFiles()
445
    {
446
        if (!is_object($this->certificate)) {
447
            throw new RuntimeException(
448
                'Certificate not found.'
449
            );
450
        }
451
        $this->certsdir = $this->certificate->getCnpj() . '/certs/';
452
        $this->prifile = $this->certsdir. Strings::randomString(10).'.pem';
453
        $this->pubfile = $this->certsdir . Strings::randomString(10).'.pem';
454
        $this->certfile = $this->certsdir . Strings::randomString(10).'.pem';
455
        $ret = true;
456
        $private = $this->certificate->privateKey;
457
        if ($this->encriptPrivateKey) {
458
            //cria uma senha temporária ALEATÓRIA para salvar a chave primaria
459
            //portanto mesmo que localizada e identificada não estará acessível
460
            //pois sua senha não existe além do tempo de execução desta classe
461
            $this->temppass = Strings::randomString(16);
462
            //encripta a chave privada entes da gravação do filesystem
463
            openssl_pkey_export(
464
                $this->certificate->privateKey,
465
                $private,
466
                $this->temppass
467
            );
468
        }
469
        $ret &= $this->filesystem->put(
470
            $this->prifile,
471
            $private
472
        );
473
        $ret &= $this->filesystem->put(
474
            $this->pubfile,
475
            $this->certificate->publicKey
476
        );
477
        $ret &= $this->filesystem->put(
478
            $this->certfile,
479
            $private."{$this->certificate}"
480
        );
481
        if (!$ret) {
482
            throw new RuntimeException(
483
                'Unable to save temporary key files in folder.'
484
            );
485
        }
486
    }
487
    
488
    /**
489
     * Delete all files in folder
490
     */
491 4
    public function removeTemporarilyFiles()
492
    {
493 4
        $contents = $this->filesystem->listContents($this->certsdir, true);
494
        //define um limite de $waitingTime min, ou seja qualquer arquivo criado a mais
495
        //de $waitingTime min será removido
496
        //NOTA: quando ocorre algum erro interno na execução do script, alguns
497
        //arquivos temporários podem permanecer
498
        //NOTA: O tempo default é de 45 minutos e pode ser alterado diretamente nas
499
        //propriedades da classe, esse tempo entre 5 a 45 min é recomendável pois
500
        //podem haver processos concorrentes para um mesmo usuário. Esses processos
501
        //como o DFe podem ser mais longos, dependendo a forma que o aplicativo
502
        //utilize a API. Outra solução para remover arquivos "perdidos" pode ser
503
        //encontrada oportunamente.
504 4
        $dt = new \DateTime();
505 4
        $tint = new \DateInterval("PT".$this->waitingTime."M");
506 4
        $tint->invert = true;
0 ignored issues
show
Documentation Bug introduced by
The property $invert was declared of type integer, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
507 4
        $tsLimit = $dt->add($tint)->getTimestamp();
508 4
        foreach ($contents as $item) {
509
            if ($item['type'] == 'file') {
510
                if ($item['path'] == $this->prifile
511
                    || $item['path'] == $this->pubfile
512
                    || $item['path'] == $this->certfile
513
                ) {
514
                    $this->filesystem->delete($item['path']);
515
                    continue;
516
                }
517
                $timestamp = $this->filesystem->getTimestamp($item['path']);
518
                if ($timestamp < $tsLimit) {
519
                    //remove arquivos criados a mais de 45 min
520
                    $this->filesystem->delete($item['path']);
521
                }
522
            }
523
        }
524 4
    }
525
    
526
    /**
527
     * Save request envelope and response for debug reasons
528
     * @param string $operation
529
     * @param string $request
530
     * @param string $response
531
     * @return void
532
     */
533
    public function saveDebugFiles($operation, $request, $response)
534
    {
535
        if (!$this->debugmode) {
536
            return;
537
        }
538
        $this->debugdir = $this->certificate->getCnpj() . '/debug/';
539
        $now = \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', ''));
540
        $time = substr($now->format("ymdHisu"), 0, 16);
541
        try {
542
            $this->filesystem->put(
543
                $this->debugdir . $time . "_" . $operation . "_sol.txt",
544
                $request
545
            );
546
            $this->filesystem->put(
547
                $this->debugdir . $time . "_" . $operation . "_res.txt",
548
                $response
549
            );
550
        } catch (\Exception $e) {
551
            throw new RuntimeException(
552
                'Unable to create debug files.'
553
            );
554
        }
555
    }
556
}
557