Passed
Push — master ( a8bdfa...0cad3e )
by Esteban De La Fuente
09:10
created

WsdlConsumer::getToken()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 52
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 4.0879

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 27
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 52
ccs 28
cts 34
cp 0.8235
crap 4.0879
rs 9.488

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Sii\HttpClient;
26
27
use libredte\lib\Core\Service\PathManager;
28
use libredte\lib\Core\Signature\Certificate;
29
use libredte\lib\Core\Signature\SignatureException;
30
use libredte\lib\Core\Signature\SignatureGenerator;
31
use libredte\lib\Core\Xml\XmlConverter;
32
use libredte\lib\Core\Xml\XmlDocument;
33
use SoapClient;
34
use SoapFault;
35
36
/**
37
 * Clase para consumir los servicios web SOAP del SII.
38
 */
39
class WsdlConsumer
40
{
41
    /**
42
     * Certificado digital.
43
     *
44
     * @var Certificate
45
     */
46
    private Certificate $certificate;
47
48
    /**
49
     * Configuración de la conexión al SII.
50
     *
51
     * @var ConnectionConfig
52
     */
53
    private ConnectionConfig $config;
54
55
    /**
56
     * Constructor de la clase que consume servicios web mediante WSDL del SII.
57
     *
58
     * @param Certificate $certificate
59
     * @param ConnectionConfig $config
60
     */
61 6
    public function __construct(
62
        Certificate $certificate,
63
        ConnectionConfig $config,
64
    ) {
65 6
        $this->certificate = $certificate;
66 6
        $this->config = $config;
67
    }
68
69
    /**
70
     * Método para obtener el token de la sesión en el SII.
71
     *
72
     * Primero se obtiene una semilla, luego se firma la semilla con el
73
     * certificado digital y con esta semilla firmada se hace la solicitud del
74
     * token al SII.
75
     *
76
     * Referencia: http://www.sii.cl/factura_electronica/autenticacion.pdf
77
     *
78
     * WSDL producción: https://palena.sii.cl/DTEWS/GetTokenFromSeed.jws?WSDL
79
     * WSDL certificación: https://maullin.sii.cl/DTEWS/GetTokenFromSeed.jws?WSDL
80
     *
81
     * @return string Token para autenticación en SII.
82
     * @throws SiiClientException En caso de error.
83
     */
84 1
    public function getToken(): string
85
    {
86
        // Obtener semilla.
87 1
        $semilla = $this->getSeed();
88
89
        // Crear solicitud del token con la semilla, parámetro getTokenRequest
90
        // de la función getToken() en el servicio web GetTokenFromSeed.
91 1
        $xmlRequest = XmlConverter::arrayToXml([
92 1
            'getToken' => [
93 1
                'item' => [
94 1
                    'Semilla' => $semilla,
95 1
                ],
96 1
            ],
97 1
        ]);
98
99
        // Firmar el XML de la solicitud del token.
100
        try {
101 1
            $xmlRequestSigned = SignatureGenerator::signXML(
102 1
                $xmlRequest,
103 1
                $this->certificate
104 1
            );
105
        } catch (SignatureException $e) {
106
            throw new SiiClientException(sprintf(
107
                'No fue posible firmar getToken. %s',
108
                $e->getMessage()
109
            ));
110
        }
111
112
        // Realizar la solicitud del token al SII.
113 1
        $xmlResponse = $this->sendRequest(
114 1
            'GetTokenFromSeed',
115 1
            'getToken',
116 1
            ['pszXml' => $xmlRequestSigned]
117 1
        );
118
119
        // Extraer respuesta de la solicitud del token.
120 1
        $response = XmlConverter::xmlToArray($xmlResponse);
121 1
        $estado = $response['SII:RESPUESTA']['SII:RESP_HDR']['ESTADO'] ?? null;
122 1
        $token = $response['SII:RESPUESTA']['SII:RESP_BODY']['TOKEN'] ?? null;
123
124
        // Validar respuesta de la solicitud del token.
125 1
        if ($estado !== '00' || $token === null) {
126 1
            $glosa = $response['SII:RESPUESTA']['SII:RESP_HDR']['GLOSA'] ?? null;
127 1
            throw new SiiClientException(sprintf(
128 1
                'No fue posible obtener el token para autenticar en el SII al usuario %s. %s',
129 1
                $this->certificate->getId(),
130 1
                $glosa
131 1
            ));
132
        }
133
134
        // Entregar el token obtenido desde el SII para la sesión.
135
        return $token;
136
    }
137
138
    /**
139
     * Obtiene una semilla desde el SII para luego usarla en la obtención del
140
     * token para la autenticación.
141
     *
142
     * Este es el único servicio web que se puede llamar sin utilizar el
143
     * certificado digital. Es de libre consumo y se usa para obtener la
144
     * semilla necesaria para luego, usando el certificado, obtener un token
145
     * válido para la sesión en el SII.
146
     *
147
     * Nota: la semilla tiene una validez de 2 minutos.
148
     *
149
     * WSDL producción: https://palena.sii.cl/DTEWS/CrSeed.jws?WSDL
150
     * WSDL certificación: https://maullin.sii.cl/DTEWS/CrSeed.jws?WSDL
151
     *
152
     * @return int La semilla si se logró obtener.
153
     * @throws SiiClientException En caso de error.
154
     */
155 2
    public function getSeed(): int
156
    {
157 2
        $xmlResponse = $this->sendRequest('CrSeed', 'getSeed');
158 2
        $response = XmlConverter::xmlToArray($xmlResponse);
159 2
        $estado = $response['SII:RESPUESTA']['SII:RESP_HDR']['ESTADO'] ?? null;
160 2
        $semilla = $response['SII:RESPUESTA']['SII:RESP_BODY']['SEMILLA'] ?? null;
161
162 2
        if ($estado !== '00' || $semilla === null) {
163
            throw new SiiClientException('No fue posible obtener la semilla.');
164
        }
165
166 2
        return (int) $semilla;
167
    }
168
169
    /**
170
     * Método para obtener el XML del WSDL (Web Services Description Language)
171
     * del servicio del SII que se desea consumir.
172
     *
173
     * @param string $servicio Servicio para el cual se desea obtener su WSDL.
174
     * @return string Ubicación del WSDL del servicio según el ambiente que
175
     * esté configurado. Entrega, normalmente, un archivo local para un WSDL
176
     * del ambiente de certificación y siempre una URL para un WSDL del
177
     * ambiente de producción.
178
     */
179 4
    public function getWsdlUri(string $servicio): string
180
    {
181 4
        $ambiente = $this->config->getAmbiente();
182
183
        // Algunos WSDL del ambiente de certificación no funcionan tal cual los
184
        // provee SII. Lo anterior ya que apuntan a un servidor llamado
185
        // nogal.sii.cl el cual no es accesible desde Internet. Posiblemente es
186
        // un servidor local del SII para desarrollo. Así que LibreDTE tiene
187
        // para el ambiente de certificación WSDL modificados para funcionar
188
        // con el servidor de pruebas (maullin.sii.cl). Estos WSDL se usan
189
        // siempre al solicitar el WSDL del ambiente de certificación.
190
        // Cambios basados en: http://stackoverflow.com/a/28464354/3333009
191 4
        if ($ambiente === ConnectionConfig::CERTIFICACION) {
192 1
            $servidor = $this->config->getServidor();
193 1
            $wsdl = PathManager::getWsdlPath($servidor, $servicio);
194 1
            if ($wsdl !== null) {
195 1
                return $wsdl;
196
            }
197
        }
198
199
        // Los WSDL para el ambiente de producción son directamente los
200
        // proporcionados por el SII y que están definidos en la configuración.
201
        // Si por cualquier motivo un WSDL de un servicio para el ambiente de
202
        // certificación no existe localmente en LibreDTE, también se entregará
203
        // el WSDL oficial del SII.
204 3
        return $this->config->getWsdl($servicio);
205
    }
206
207
    /**
208
     * Realiza una solicitud a un servicio web del SII mediante el uso de WSDL.
209
     *
210
     * Este método prepara y normaliza los datos recibidos y llama al método
211
     * que realmente hace la consulta al SII: callServiceFunction().
212
     *
213
     * @param string $service Nombre del servicio del SII que se consumirá.
214
     * @param string $function Nombre de la función que se ejecutará en el
215
     * servicio web del SII.
216
     * @param array|int $args Argumentos que se pasarán al servicio web.
217
     * @param int|null $retry Intentos que se realizarán como máximo para
218
     * obtener respuesta.
219
     * @return XmlDocument Documento XML con la respuesta del servicio web.
220
     * @throws SiiClientException En caso de error.
221
     */
222 2
    public function sendRequest(
223
        string $service,
224
        string $function,
225
        array|int $args = [],
226
        ?int $retry = null
227
    ): XmlDocument {
228
        // Revisar si se pasó en $args el valor de $retry.
229
        // @scrutinizer ignore-type-check
230 2
        if (is_int($args)) {
0 ignored issues
show
introduced by
The condition is_int($args) is always false.
Loading history...
231
            $retry = $args;
232
            $args = [];
233
        }
234
235
        // Si no se especificó $retry se obtiene el valor por defecto.
236 2
        $retry = max(0, min(
237 2
            $retry ?? $this->config->getReintentos(),
238 2
            ConnectionConfig::REINTENTOS
239 2
        ));
240
241
        // Definir el WSDL que se debe utilizar.
242 2
        $wsdl = $this->getWsdlUri($service);
243
244
        // Realizar la llamada a la función en el servicio web del SII.
245 2
        return $this->callServiceFunction($wsdl, $function, $args, $retry);
246
    }
247
248
    /**
249
     * Ejecuta una función en un servicio web del SII mediante el uso de WSDL.
250
     *
251
     * @param string $wsdl El WSDL del servicio web donde está la función.
252
     * @param string $function Nombre de la función que se ejecutará,
253
     * @param array $args Argumentos que se pasarán al servicio web.
254
     * @param int $retry Intentos que se realizarán como máximo.
255
     * @return XmlDocument Documento XML con la respuesta del servicio web.
256
     * @throws SiiClientException En caso de error.
257
     */
258 2
    private function callServiceFunction(
259
        string $wsdl,
260
        string $function,
261
        array $args,
262
        int $retry
263
    ): XmlDocument {
264
        // Definir las opciones para consumir el servicio web.
265 2
        $soapClientOptions = $this->createSoapClientOptions();
266
267
        // Preparar cliente SOAP.
268
        try {
269 2
            $soap = new SoapClient($wsdl, $soapClientOptions);
270
        } catch (SoapFault $e) {
271
            $message = $e->getMessage();
272
            if (
273
                isset($e->getTrace()[0]['args'][1])
274
                && is_string($e->getTrace()[0]['args'][1])
275
            ) {
276
                $message .= ': ' . $e->getTrace()[0]['args'][1];
277
            }
278
            throw new SiiClientException(sprintf(
279
                'Ocurrió un error al crear el cliente SOAP para la API del SII con el WSDL %s. %s',
280
                $wsdl,
281
                $message
282
            ));
283
        }
284
285
        // Argumentos adicionales para la llamada al servicio web SOAP mediante
286
        // __soapCall().
287 2
        $options = null;
288
289
        // En el WSDL indicadas como soap:header.
290 2
        $requestHeaders = [];
291
292
        // Si el SII enviase cabeceras SOAP devuelta.
293 2
        $responseHeaders = [];
294
295
        // Para almacenar la respuesta de la llamada a la API SOAP del SII.
296 2
        $responseBody = null;
297
298
        // Para ir almacenando los errores, si existen, de cada intento.
299 2
        $errors = [];
300
301
        // Ejecutar la función que se ha solicitado del servicio web a través
302
        // del cliente SOAP preparado previamente.
303
        // Se realizarán $retry intentos de consulta. O sea, si $retry > 0 se
304
        // hará una consulta más $retry - 1 reintentos.
305 2
        for ($i = 0; $i < $retry; $i++) {
306
            try {
307
                // Se realiza la llamada a la función en el servicio web.
308 2
                $responseBody = $soap->__soapCall(
309 2
                    $function,
310 2
                    $args,
311 2
                    $options,
312 2
                    $requestHeaders,
313 2
                    $responseHeaders
314 2
                );
315
                // Si la llamada no falló (no hubo excepción), se rompe el
316
                // ciclo de reintentos.
317 2
                break;
318
            } catch (SoapFault $e) {
319
                $message = $e->getMessage();
320
                if (
321
                    isset($e->getTrace()[0]['args'][1])
322
                    && is_string($e->getTrace()[0]['args'][1])
323
                ) {
324
                    $message .= ': ' . $e->getTrace()[0]['args'][1];
325
                }
326
                $errors[] = sprintf(
327
                    'Error al ejecutar la función %s en el servicio web SOAP del SII ($i = %d): %s',
328
                    $function,
329
                    $i,
330
                    $message
331
                );
332
                $responseBody = null;
333
                // El reitento será con "exponential backoff", por lo que se
334
                // hace una pausa de 0.2 * $retry segundos antes de volver a
335
                // intentar llamar a la función del servicio web.
336
                usleep(200000 * $retry);
337
            }
338
        }
339
340
        // Si la respuesta es `null` significa que ninguno de los intentos de
341
        // llamadas a la función del servicio web fue exitoso.
342 2
        if ($responseBody === null) {
343
            throw new SiiClientException(sprintf(
344
                'No se obtuvo respuesta de la función %s del servicio web SOAP del SII después de %d intentos. %s',
345
                $function,
346
                $retry,
347
                implode(' ', $errors)
348
            ));
349
        }
350
351
        // El SII indica que la respuesta que envía es:
352
        //   Content-Type: text/xml;charset=utf-8
353
        // Sin embargo, parece que indica que es UTF-8 pero envía contenido
354
        // codificado con ISO-8859-1 (que es lo esperable del SII). Lo que hace
355
        // que los caracteres especiales se obtengan como "�", por lo cual se
356
        // reemplazan en la respuesta por "?" para que sea "un poco" más
357
        // legible la respuesta del SII.
358 2
        $responseBody = str_replace(['�', '�'], '?', $responseBody);
359
360
        // Entregar el resultado como un documento XML.
361 2
        $xmlDocument = new XmlDocument();
362 2
        $xmlDocument->loadXML($responseBody);
363 2
        return $xmlDocument;
364
    }
365
366
    /**
367
     * Define las opciones para consumir el servicio web del SII mediante SOAP.
368
     *
369
     * @return array Arreglo con las opciones para SoapClient.
370
     */
371 2
    private function createSoapClientOptions(): array
372
    {
373
        // Configuración de caché para SOAP.
374 2
        ini_set('soap.wsdl_cache_enabled', 3600);
375 2
        ini_set('soap.wsdl_cache_ttl', 3600);
376
377
        // Opciones base.
378 2
        $options = [
379 2
            'encoding' => 'ISO-8859-1',
380
            //'trace' => true, // Permite usar __getLastResponse().
381 2
            'exceptions' => true, // Lanza SoapFault en caso de error.
382 2
            'cache_wsdl' => WSDL_CACHE_MEMORY, // WSDL_CACHE_DISK o WSDL_CACHE_MEMORY.
383 2
            'keep_alive' => false,
384 2
            'stream_context' => [
385 2
                'http' => [
386 2
                    'header' => [
387 2
                        'User-Agent: Mozilla/5.0 (compatible; PROG 1.0; +https://www.libredte.cl)',
388 2
                    ],
389 2
                ],
390 2
            ],
391 2
        ];
392
393
        // Si no se debe verificar el certificado SSL del servidor del SII se
394
        // asigna al "stream_context" dicha configuración.
395 2
        if (!$this->config->getVerificarSsl()) {
396
            $options['stream_context']['ssl'] = [
397
                'verify_peer' => false,
398
                'verify_peer_name' => false,
399
                'allow_self_signed' => true,
400
            ];
401
        }
402
403
        // Crear el "stream context" verdadero con las opciones definidas.
404 2
        $options['stream_context'] = stream_context_create(
405 2
            $options['stream_context']
406 2
        );
407
408
        // Retornar las opciones para el SoapClient.
409 2
        return $options;
410
    }
411
}
412