Passed
Push — master ( 9d79d3...4bb85e )
by Esteban De La Fuente
08:41
created

AuthenticateJob   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 73.08%

Importance

Changes 0
Metric Value
wmc 15
eloc 69
dl 0
loc 228
c 0
b 0
f 0
ccs 57
cts 78
cp 0.7308
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getCache() 0 25 3
A __construct() 0 11 2
A authenticate() 0 27 3
A getSeedFromSii() 0 18 3
A getTokenFromSii() 0 53 4
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * LibreDTE: Biblioteca PHP (Núcleo).
7
 * Copyright (C) LibreDTE <https://www.libredte.cl>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de
20
 * GNU junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace libredte\lib\Core\Package\Billing\Component\Integration\Worker\SiiLazy\Job;
26
27
use Derafu\Lib\Core\Foundation\Abstract\AbstractJob;
28
use Derafu\Lib\Core\Foundation\Contract\JobInterface;
29
use Derafu\Lib\Core\Package\Prime\Component\Signature\Contract\SignatureComponentInterface;
30
use Derafu\Lib\Core\Package\Prime\Component\Signature\Exception\SignatureException;
31
use Derafu\Lib\Core\Package\Prime\Component\Xml\Contract\XmlComponentInterface;
32
use libredte\lib\Core\Package\Billing\Component\Integration\Contract\SiiRequestInterface;
33
use libredte\lib\Core\Package\Billing\Component\Integration\Exception\SiiAuthenticateException;
34
use LogicException;
35
use Psr\SimpleCache\CacheInterface;
36
use Symfony\Component\Cache\Adapter\ArrayAdapter;
37
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
38
use Symfony\Component\Cache\Psr16Cache;
39
40
/**
41
 * Clase para gestionar las solicitudes de token para autenticación al SII.
42
 */
43
class AuthenticateJob extends AbstractJob implements JobInterface
44
{
45
    /**
46
     * Componente para firma electrónica.
47
     *
48
     * @var SignatureComponentInterface
49
     */
50
    private SignatureComponentInterface $signatureComponent;
51
52
    /**
53
     * Componente para manejo de documentos XML.
54
     *
55
     * @var XmlComponentInterface
56
     */
57
    private XmlComponentInterface $xmlComponent;
58
59
    /**
60
     * Trabajo que realiza consultas a la API SOAP del SII.
61
     *
62
     * @var ConsumeWebserviceJob
63
     */
64
    private ConsumeWebserviceJob $consumeWebserviceJob;
65
66
    /**
67
     * Instancia con la implementación de la caché que se utilizará para el
68
     * almacenamiento de los tokens.
69
     *
70
     * @var CacheInterface
71
     */
72
    private CacheInterface $cache;
73
74
    /**
75
     * Constructor y sus dependencias.
76
     */
77 1
    public function __construct(
78
        SignatureComponentInterface $signatureComponent,
79
        XmlComponentInterface $xmlComponent,
80
        ConsumeWebserviceJob $consumeWebserviceJob,
81
        ?CacheInterface $cache = null
82
    ) {
83 1
        $this->signatureComponent = $signatureComponent;
84 1
        $this->xmlComponent = $xmlComponent;
85 1
        $this->consumeWebserviceJob = $consumeWebserviceJob;
86 1
        if ($cache !== null) {
87
            $this->cache = $cache;
88
        }
89
    }
90
91
    /**
92
     * Obtiene un token de autenticación asociado al certificado digital.
93
     *
94
     * El token se busca primero en la caché, si existe, se reutilizará, si no
95
     * existe se solicitará uno nuevo al SII.
96
     *
97
     * @param SiiRequestInterface $request Datos de la solicitud al SII.
98
     * @return string El token asociado al certificado digital de la solicitud.
99
     * @throws SiiAuthenticateException Si hubo algún error al obtener el token.
100
     */
101 1
    public function authenticate(SiiRequestInterface $request): string
102
    {
103 1
        $cache = $this->getCache($request->getTokenDefaultCache());
104
105
        // Armar clave de la caché para el token asociado al certificado.
106 1
        $cacheKey = $request->getTokenKey();
107
108
        // Verificar si hay un token en la caché y si no ha expirado.
109 1
        if ($cache->has($cacheKey)) {
110
            return $cache->get($cacheKey);
111
        }
112
113
        // Si no hay un token o está expirado, solicitar uno nuevo.
114
        // Esto falla con excepción que se deja pasara a quien haya llamado a
115
        // este método getToken().
116 1
        if ($request->getCertificate() === null) {
117
            throw new LogicException(
118
                'Para autenticar en el SII se debe proveer un certificado digital.'
119
            );
120
        }
121 1
        $newToken = $this->getTokenFromSii($request);
122
123
        // Si se logró obtener un token, se guarda en la caché.
124
        $cache->set($cacheKey, $newToken, $request->getTokenTtl());
125
126
        // Entregar el nuevo token obtenido.
127
        return $newToken;
128
    }
129
130
    /**
131
     * Método para obtener el token de la sesión en el SII.
132
     *
133
     * Primero se obtiene una semilla, luego se firma la semilla con el
134
     * certificado digital y con esta semilla firmada se hace la solicitud del
135
     * token al SII.
136
     *
137
     * Referencia: http://www.sii.cl/factura_electronica/autenticacion.pdf
138
     *
139
     * WSDL producción: https://palena.sii.cl/DTEWS/GetTokenFromSeed.jws?WSDL
140
     * WSDL certificación: https://maullin.sii.cl/DTEWS/GetTokenFromSeed.jws?WSDL
141
     *
142
     * @param SiiRequestInterface $request Datos de la solicitud al SII.
143
     * @return string Token para autenticación en SII.
144
     * @throws SiiAuthenticateException En caso de error.
145
     */
146 1
    private function getTokenFromSii(SiiRequestInterface $request): string
147
    {
148
        // Obtener semilla.
149 1
        $semilla = $this->getSeedFromSii($request);
150
151
        // Crear solicitud del token con la semilla, parámetro getTokenRequest
152
        // de la función getToken() en el servicio web GetTokenFromSeed.
153 1
        $xmlRequest = $this->xmlComponent->getEncoderWorker()->encode([
154 1
            'getToken' => [
155 1
                'item' => [
156 1
                    'Semilla' => $semilla,
157 1
                ],
158 1
            ],
159 1
        ]);
160
161
        // Firmar el XML de la solicitud del token.
162
        try {
163 1
            $xmlRequestSigned = $this->signatureComponent->getGeneratorWorker()->signXml(
164 1
                $xmlRequest,
165 1
                $request->getCertificate()
0 ignored issues
show
Bug introduced by
It seems like $request->getCertificate() can also be of type null; however, parameter $certificate of Derafu\Lib\Core\Package\...kerInterface::signXml() does only seem to accept Derafu\Lib\Core\Package\...ct\CertificateInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
                /** @scrutinizer ignore-type */ $request->getCertificate()
Loading history...
166 1
            );
167
        } catch (SignatureException $e) {
168
            throw new SiiAuthenticateException(sprintf(
169
                'No fue posible firmar getToken. %s',
170
                $e->getMessage()
171
            ));
172
        }
173
174
        // Realizar la solicitud del token al SII.
175 1
        $xmlResponse = $this->consumeWebserviceJob->sendRequest(
176 1
            $request,
177 1
            'GetTokenFromSeed',
178 1
            'getToken',
179 1
            ['pszXml' => $xmlRequestSigned]
180 1
        );
181
182
        // Extraer respuesta de la solicitud del token.
183 1
        $response = $this->xmlComponent->getDecoderWorker()->decode($xmlResponse);
184 1
        $estado = $response['SII:RESPUESTA']['SII:RESP_HDR']['ESTADO'] ?? null;
185 1
        $token = $response['SII:RESPUESTA']['SII:RESP_BODY']['TOKEN'] ?? null;
186
187
        // Validar respuesta de la solicitud del token.
188 1
        if ($estado !== '00' || $token === null) {
189 1
            $glosa = $response['SII:RESPUESTA']['SII:RESP_HDR']['GLOSA'] ?? null;
190 1
            throw new SiiAuthenticateException(sprintf(
191 1
                'No fue posible obtener el token para autenticar en el SII al usuario %s. %s',
192 1
                $request->getCertificate()->getId(),
193 1
                $glosa
194 1
            ));
195
        }
196
197
        // Entregar el token obtenido desde el SII para la sesión.
198
        return $token;
199
    }
200
201
    /**
202
     * Obtiene una semilla desde el SII para luego usarla en la obtención del
203
     * token para la autenticación.
204
     *
205
     * Este es el único servicio web que se puede llamar sin utilizar el
206
     * certificado digital. Es de libre consumo y se usa para obtener la
207
     * semilla necesaria para luego, usando el certificado, obtener un token
208
     * válido para la sesión en el SII.
209
     *
210
     * Nota: la semilla tiene una validez de 2 minutos.
211
     *
212
     * WSDL producción: https://palena.sii.cl/DTEWS/CrSeed.jws?WSDL
213
     * WSDL certificación: https://maullin.sii.cl/DTEWS/CrSeed.jws?WSDL
214
     *
215
     * @param SiiRequestInterface $request Datos de la solicitud al SII.
216
     * @return int La semilla si se logró obtener.
217
     * @throws SiiAuthenticateException En caso de error.
218
     */
219 1
    private function getSeedFromSii(SiiRequestInterface $request): int
220
    {
221 1
        $xmlResponse = $this->consumeWebserviceJob->sendRequest(
222 1
            $request,
223 1
            'CrSeed',
224 1
            'getSeed'
225 1
        );
226 1
        $response = $this->xmlComponent->getDecoderWorker()->decode($xmlResponse);
227 1
        $estado = $response['SII:RESPUESTA']['SII:RESP_HDR']['ESTADO'] ?? null;
228 1
        $semilla = $response['SII:RESPUESTA']['SII:RESP_BODY']['SEMILLA'] ?? null;
229
230 1
        if ($estado !== '00' || $semilla === null) {
231
            throw new SiiAuthenticateException(
232
                'No fue posible obtener la semilla.'
233
            );
234
        }
235
236 1
        return (int) $semilla;
237
    }
238
239
    /**
240
     * Entrega una instancia con la implementación de una caché para ser
241
     * utilizada en la biblioteca.
242
     *
243
     * @param string $defaultCache Caché por defecto que se debe crear.
244
     * @return CacheInterface Implementación de caché PSR-16.
245
     */
246 1
    private function getCache(string $defaultCache): CacheInterface
247
    {
248
        // Si no está asignada la caché se asigna a una por defecto.
249 1
        if (!isset($this->cache)) {
250
            // Asignar una implementación de caché en el sistema de archivos.
251 1
            if ($defaultCache === 'filesystem') {
252
                $adapter = new FilesystemAdapter(
253
                    'libredte_lib',
254
                    3600, // TTL por defecto a una hora (3600 segundos).
255
                    sys_get_temp_dir()
256
                );
257
            }
258
259
            // Asignar una implementación de caché en memoria.
260
            else {
261 1
                $adapter = new ArrayAdapter();
262
            }
263
264
            // Asignar el adaptador de la caché que se utilizará convirtiéndolo
265
            // a una instancia válida de PSR-16.
266 1
            $this->cache = new Psr16Cache($adapter);
267
        }
268
269
        // Entregar la instancia de la caché.
270 1
        return $this->cache;
271
    }
272
}
273