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
|
|
|
public function __construct( |
62
|
|
|
Certificate $certificate, |
63
|
|
|
ConnectionConfig $config, |
64
|
|
|
) { |
65
|
|
|
$this->certificate = $certificate; |
66
|
|
|
$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
|
|
|
public function getToken(): string |
85
|
|
|
{ |
86
|
|
|
// Obtener semilla. |
87
|
|
|
$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
|
|
|
$xmlRequest = XmlConverter::arrayToXml([ |
92
|
|
|
'getToken' => [ |
93
|
|
|
'item' => [ |
94
|
|
|
'Semilla' => $semilla, |
95
|
|
|
], |
96
|
|
|
], |
97
|
|
|
]); |
98
|
|
|
|
99
|
|
|
// Firmar el XML de la solicitud del token. |
100
|
|
|
try { |
101
|
|
|
$xmlRequestSigned = SignatureGenerator::signXML( |
102
|
|
|
$xmlRequest, |
103
|
|
|
$this->certificate |
104
|
|
|
); |
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
|
|
|
$xmlResponse = $this->sendRequest( |
114
|
|
|
'GetTokenFromSeed', |
115
|
|
|
'getToken', |
116
|
|
|
['pszXml' => $xmlRequestSigned] |
117
|
|
|
); |
118
|
|
|
|
119
|
|
|
// Extraer respuesta de la solicitud del token. |
120
|
|
|
$response = XmlConverter::xmlToArray($xmlResponse); |
121
|
|
|
$estado = $response['SII:RESPUESTA']['SII:RESP_HDR']['ESTADO'] ?? null; |
122
|
|
|
$token = $response['SII:RESPUESTA']['SII:RESP_BODY']['TOKEN'] ?? null; |
123
|
|
|
|
124
|
|
|
// Validar respuesta de la solicitud del token. |
125
|
|
|
if ($estado !== '00' || $token === null) { |
126
|
|
|
$glosa = $response['SII:RESPUESTA']['SII:RESP_HDR']['GLOSA'] ?? null; |
127
|
|
|
throw new SiiClientException(sprintf( |
128
|
|
|
'No fue posible obtener el token para autenticar en el SII al usuario %s. %s', |
129
|
|
|
$this->certificate->getId(), |
130
|
|
|
$glosa |
131
|
|
|
)); |
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
|
|
|
public function getSeed(): int |
156
|
|
|
{ |
157
|
|
|
$xmlResponse = $this->sendRequest('CrSeed', 'getSeed'); |
158
|
|
|
$response = XmlConverter::xmlToArray($xmlResponse); |
159
|
|
|
$estado = $response['SII:RESPUESTA']['SII:RESP_HDR']['ESTADO'] ?? null; |
160
|
|
|
$semilla = $response['SII:RESPUESTA']['SII:RESP_BODY']['SEMILLA'] ?? null; |
161
|
|
|
|
162
|
|
|
if ($estado !== '00' || $semilla === null) { |
163
|
|
|
throw new SiiClientException('No fue posible obtener la semilla.'); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
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
|
|
|
public function getWsdlUri(string $servicio): string |
180
|
|
|
{ |
181
|
|
|
$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
|
|
|
if ($ambiente === ConnectionConfig::CERTIFICACION) { |
192
|
|
|
$servidor = $this->config->getServidor(); |
193
|
|
|
$wsdl = PathManager::getWsdlPath($servidor, $servicio); |
194
|
|
|
if ($wsdl !== null) { |
195
|
|
|
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
|
|
|
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
|
|
|
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
|
|
|
if (is_int($args)) { |
|
|
|
|
231
|
|
|
$retry = $args; |
232
|
|
|
$args = []; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
// Si no se especificó $retry se obtiene el valor por defecto. |
236
|
|
|
$retry = max(0, min( |
237
|
|
|
$retry ?? $this->config->getReintentos(), |
238
|
|
|
ConnectionConfig::REINTENTOS |
239
|
|
|
)); |
240
|
|
|
|
241
|
|
|
// Definir el WSDL que se debe utilizar. |
242
|
|
|
$wsdl = $this->getWsdlUri($service); |
243
|
|
|
|
244
|
|
|
// Realizar la llamada a la función en el servicio web del SII. |
245
|
|
|
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
|
|
|
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
|
|
|
$soapClientOptions = $this->createSoapClientOptions(); |
266
|
|
|
|
267
|
|
|
// Preparar cliente SOAP. |
268
|
|
|
try { |
269
|
|
|
$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
|
|
|
$options = null; |
288
|
|
|
|
289
|
|
|
// En el WSDL indicadas como soap:header. |
290
|
|
|
$requestHeaders = []; |
291
|
|
|
|
292
|
|
|
// Si el SII enviase cabeceras SOAP devuelta. |
293
|
|
|
$responseHeaders = []; |
294
|
|
|
|
295
|
|
|
// Para almacenar la respuesta de la llamada a la API SOAP del SII. |
296
|
|
|
$responseBody = null; |
297
|
|
|
|
298
|
|
|
// Para ir almacenando los errores, si existen, de cada intento. |
299
|
|
|
$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
|
|
|
for ($i = 0; $i < $retry; $i++) { |
306
|
|
|
try { |
307
|
|
|
// Se realiza la llamada a la función en el servicio web. |
308
|
|
|
$responseBody = $soap->__soapCall( |
309
|
|
|
$function, |
310
|
|
|
$args, |
311
|
|
|
$options, |
312
|
|
|
$requestHeaders, |
313
|
|
|
$responseHeaders |
314
|
|
|
); |
315
|
|
|
// Si la llamada no falló (no hubo excepción), se rompe el |
316
|
|
|
// ciclo de reintentos. |
317
|
|
|
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
|
|
|
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
|
|
|
$responseBody = str_replace(['�', '�'], '?', $responseBody); |
359
|
|
|
|
360
|
|
|
// Entregar el resultado como un documento XML. |
361
|
|
|
$xmlDocument = new XmlDocument(); |
362
|
|
|
$xmlDocument->loadXML($responseBody); |
363
|
|
|
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
|
|
|
private function createSoapClientOptions(): array |
372
|
|
|
{ |
373
|
|
|
// Configuración de caché para SOAP. |
374
|
|
|
ini_set('soap.wsdl_cache_enabled', 3600); |
375
|
|
|
ini_set('soap.wsdl_cache_ttl', 3600); |
376
|
|
|
|
377
|
|
|
// Opciones base. |
378
|
|
|
$options = [ |
379
|
|
|
'encoding' => 'ISO-8859-1', |
380
|
|
|
//'trace' => true, // Permite usar __getLastResponse(). |
381
|
|
|
'exceptions' => true, // Lanza SoapFault en caso de error. |
382
|
|
|
'cache_wsdl' => WSDL_CACHE_MEMORY, // WSDL_CACHE_DISK o WSDL_CACHE_MEMORY. |
383
|
|
|
'keep_alive' => false, |
384
|
|
|
'stream_context' => [ |
385
|
|
|
'http' => [ |
386
|
|
|
'header' => [ |
387
|
|
|
'User-Agent: Mozilla/5.0 (compatible; PROG 1.0; +https://www.libredte.cl)', |
388
|
|
|
], |
389
|
|
|
], |
390
|
|
|
], |
391
|
|
|
]; |
392
|
|
|
|
393
|
|
|
// Si no se debe verificar el certificado SSL del servidor del SII se |
394
|
|
|
// asigna al "stream_context" dicha configuración. |
395
|
|
|
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
|
|
|
$options['stream_context'] = stream_context_create( |
405
|
|
|
$options['stream_context'] |
406
|
|
|
); |
407
|
|
|
|
408
|
|
|
// Retornar las opciones para el SoapClient. |
409
|
|
|
return $options; |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
|