Passed
Push — master ( c02ff8...8cf6c5 )
by Esteban De La Fuente
06:05
created

DocumentUploader::validateUploadXmlResponse()   C

Complexity

Conditions 13
Paths 22

Size

Total Lines 70
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 47
c 1
b 0
f 0
nc 22
nop 1
dl 0
loc 70
ccs 0
cts 50
cp 0
crap 182
rs 6.6166

How to fix   Long Method    Complexity   

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
11
 * por la Fundación para el Software Libre, ya sea la versión 3 de la Licencia,
12
 * o (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\WebService;
26
27
use libredte\lib\Core\Helper\Rut;
28
use libredte\lib\Core\Signature\Certificate;
29
use libredte\lib\Core\Sii\HttpClient\ConnectionConfig;
30
use libredte\lib\Core\Sii\HttpClient\SiiClientException;
31
use libredte\lib\Core\Sii\HttpClient\TokenManager;
32
use libredte\lib\Core\Xml\XmlConverter;
33
use libredte\lib\Core\Xml\XmlDocument;
34
use UnexpectedValueException;
35
36
/**
37
 * Clase para el envío de documentos al SII.
38
 *
39
 * Principalmente es para el envío y consulta de estado del envío de documentos
40
 * tributarios electrónicos en formato XML.
41
 */
42
class DocumentUploader
43
{
44
    /**
45
     * Certificado digital.
46
     *
47
     * @var Certificate
48
     */
49
    private Certificate $certificate;
50
51
    /**
52
     * Configuración de la conexión al SII.
53
     *
54
     * @var ConnectionConfig
55
     */
56
    private ConnectionConfig $config;
57
58
    /**
59
     * Administrador de tokens de autenticación del SII.
60
     *
61
     * @var TokenManager
62
     */
63
    private TokenManager $tokenManager;
64
65
    /**
66
     * Constructor de la clase que consume servicios web mediante WSDL del SII.
67
     *
68
     * @param Certificate $certificate
69
     * @param ConnectionConfig $config
70
     * @param TokenManager $tokenManager
71
     */
72 6
    public function __construct(
73
        Certificate $certificate,
74
        ConnectionConfig $config,
75
        TokenManager $tokenManager,
76
    ) {
77 6
        $this->certificate = $certificate;
78 6
        $this->config = $config;
79 6
        $this->tokenManager = $tokenManager;
80
    }
81
82
    /**
83
     * Realiza el envío de un XML al SII.
84
     *
85
     * @param XmlDocument $doc Documento XML que se desea enviar al SII.
86
     * @param string $company RUT de la empresa emisora del XML.
87
     * @param bool $compress Indica si se debe enviar comprimido el XML.
88
     * @param int|null $retry Intentos que se realizarán como máximo al enviar.
89
     * @return int Número de seguimiento (Track ID) del envío del XML al SII.
90
     * @throws UnexpectedValueException Si alguno de los RUT son inválidos.
91
     */
92 1
    public function sendXml(
93
        XmlDocument $doc,
94
        string $company,
95
        bool $compress = false,
96
        ?int $retry = null
97
    ): int {
98
        // Crear string del documento XML.
99 1
        $xml = $doc->saveXML();
100 1
        if (empty($xml) || $xml == '<?xml version="1.0" encoding="ISO-8859-1"?>'."\n") {
101 1
            throw new SiiClientException(
102 1
                'El XML que se desea enviar al SII no puede ser vacío.'
103 1
            );
104
        }
105
106
        // Validar los RUT que se utilizarán para el envío y descomponerlos.
107
        $sender = $this->certificate->getID();
108
        Rut::validate($sender);
109
        Rut::validate($company);
110
        [$rutSender, $dvSender] = Rut::toArray($sender);
111
        [$rutCompany, $dvCompany] = Rut::toArray($company);
112
113
        // Crear el archivo que se enviará en el sistema de archivos para poder
114
        // adjuntarlo en la solicitud mediante curl al SII.
115
        [$filepath, $mimetype] = $this->createXmlFile($xml, $compress);
116
        $filename = $company . '_' . basename($filepath);
117
118
        // Preparar los datos que se enviarán mediante POST al SII.
119
        $data = [
120
            'rutSender' => $rutSender,
121
            'dvSender' => $dvSender,
122
            'rutCompany' => $rutCompany,
123
            'dvCompany' => $dvCompany,
124
            'archivo' => curl_file_create($filepath, $mimetype, $filename),
125
        ];
126
127
        // Si no se especificó $retry se obtiene el valor por defecto.
128
        $retry = max(0, min(
129
            $retry ?? $this->config->getReintentos(),
130
            ConnectionConfig::REINTENTOS
131
        ));
132
133
        // Realizar la solicitud mediante POST al SII para subir el archivo.
134
        $xmlResponse = $this->uploadXml($data, $retry);
135
136
        // Eliminar el archivo temporal con el XML.
137
        unlink($filepath);
138
139
        // Procesar respuesta recibida desde el SII.
140
        $response = XmlConverter::xmlToArray($xmlResponse);
141
        $this->validateUploadXmlResponse($response);
142
143
        // Entregar el número de seguimiendo (Track ID) del envío al SII.
144
        $trackId = $response['RECEPCIONDTE']['TRACKID'] ?? 0;
145
        return (int) $trackId;
146
    }
147
148
    /**
149
     * Valida la respuesta recibida desde el SII al enviar un XML.
150
     *
151
     * @param array $response Arreglo con los datos del XML de la respuesta.
152
     * @return void
153
     * @throws SiiClientException Si el envío tuvo algún problema.
154
     */
155
    private function validateUploadXmlResponse(array $response): void
156
    {
157
        $status = $response['RECEPCIONDTE']['STATUS'] ?? null;
158
159
        // Si el estado es `null` la respuesta del SII no es válida. Lo cual
160
        // indicaría que el SII no contestó correctamente a la solicitud o bien
161
        // la misma solicitud se hizo de manera incorrecta produciendo que el
162
        // SII no contestase adecuadamente.
163
        if ($status === null) {
164
            throw new SiiClientException(
165
                'La respuesta del envío del XML al SII no trae un código de estado válido.'
166
            );
167
        }
168
169
        // Si el estado es 0, el envío fue OK.
170
        if ($status == 0) {
171
            return;
172
        }
173
174
        // Se define un mensaje de error según el código de estado.
175
        switch ($status) {
176
            case 1:
177
                $message = sprintf(
178
                    'El usuario %s no tiene permisos para enviar XML al SII.',
179
                    $this->certificate->getId()
180
                );
181
                break;
182
            case 2:
183
                $message = 'Error en el tamaño del archivo enviado con el XML, muy grande o muy chico.';
184
                break;
185
            case 3:
186
                $message = 'El archivo enviado está cortado, el tamaño es diferente al parámetro "size".';
187
                break;
188
            case 5:
189
                $message = sprintf(
190
                    'El usuario %s no está autenticado (posible token expirado).',
191
                    $this->certificate->getId()
192
                );
193
                break;
194
            case 6:
195
                $message = 'La empresa no está autorizada a enviar archivos XML al SII.';
196
                break;
197
            case 7:
198
                $message = 'El esquema del XML es inválido.';
199
                break;
200
            case 8:
201
                $message = 'Existe un error en la firma del documento XML.';
202
                break;
203
            case 9:
204
                $message = 'Los servidores del SII están con problemas internos.';
205
                break;
206
            case 99:
207
                $message = 'El XML enviado ya fue previamente recibido por el SII.';
208
                break;
209
            default:
210
                $message = sprintf(
211
                    'Ocurrió un error con código de estado "%s", que es desconocido por LibreDTE.',
212
                    $status
213
                );
214
                break;
215
        }
216
217
        // Ver si vienen detalles del error.
218
        $error = $response['DETAIL']['ERROR'] ?? null;
219
        if ($error !== null) {
220
            $message .= ' ' . implode(' ', $error);
221
        }
222
223
        // Lanzar una excepción con el mensaje de error determinado.
224
        throw new SiiClientException($message);
225
    }
226
227
    /**
228
     * Sube el archivo XML al SII y retorna la respuesta de este.
229
     *
230
     * Este método emula la subida mendiante los siguientes formularios:
231
     *
232
     *   - Producción: https://palena.sii.cl/cgi_dte/UPL/DTEauth?1
233
     *   - Certificación: https://maullin.sii.cl/cgi_dte/UPL/DTEauth?1
234
     *
235
     * @param array $data Arreglo con los datos del formulario del SII,
236
     * incluyendo el archivo XML que se subirá.
237
     * @param integer $retry
238
     * @return XmlDocument Respuesta del SII al enviar el XML.
239
     * @throws SiiClientException Si no se puede obtener el token para enviar
240
     * el XML al SII o si hubo un problema (error) al enviar el XML al SII.
241
     */
242
    private function uploadXml(array $data, int $retry): XmlDocument
243
    {
244
        // URL que se utilizará para subir el XML al SII.
245
        $url = $this->config->getUrl('/cgi_dte/UPL/DTEUpload');
246
247
        // Obtener el token asociado al certificado digital.
248
        $token = $this->tokenManager->getToken($this->certificate);
249
250
        // Cabeceras HTTP de la solicitud que se hará al SII.
251
        $headers = [
252
            'User-Agent: Mozilla/4.0 (compatible; PROG 1.0; LibreDTE)',
253
            'Referer: https://www.libredte.cl',
254
            'Cookie: TOKEN=' . $token,
255
        ];
256
257
        // Inicializar curl.
258
        $curl = curl_init();
259
        curl_setopt($curl, CURLOPT_POST, true);
260
        curl_setopt($curl, CURLOPT_URL, $url);
261
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
262
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
263
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
264
265
        // Si no se debe verificar el certificado SSL del servidor del SII se
266
        // agrega la opción a curl.
267
        if (!$this->config->getVerificarSsl()) {
268
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
269
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
270
        }
271
272
        // Realizar el envío del XML al SII con $retry intentos.
273
        $responseBody = null;
274
        for ($i = 0; $i < $retry; $i++) {
275
            // Realizar consulta al SII enviando el XML.
276
            $responseBody = curl_exec($curl);
277
278
            // Si se logró obtener una respuesta, y no es un "Error 500",
279
            // entonces se logró enviar el XML al SII y se rompe el ciclo para
280
            // parar los reintentos.
281
            if ($responseBody && $responseBody !== 'Error 500') {
282
                break;
283
            }
284
285
            // El reitento será con "exponential backoff", por lo que se hace
286
            // una pausa de 0.2 * $retry segundos antes de volver a intentar el
287
            // envio del XML al SII.
288
            usleep(200000 * $retry);
289
        }
290
291
        // Validar si hubo un error en la respuesta.
292
        if (!$responseBody || $responseBody === 'Error 500') {
293
            $message = 'Falló el envío del XML al SII. ';
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
294
            $message = !$responseBody
295
                ? curl_error($curl)
296
                : 'El SII tiene problemas en sus servidores (Error 500).'
297
            ;
298
            curl_close($curl); // Se cierra conexión curl acá por error.
299
            throw new SiiClientException($message);
300
        }
301
302
        // Cerrar conexión curl.
303
        curl_close($curl);
304
305
        // Entregar el resultado como un documento XML.
306
        $xmlDocument = new XmlDocument();
307
        $xmlDocument->loadXML($responseBody);
0 ignored issues
show
Bug introduced by
It seems like $responseBody can also be of type true; however, parameter $source of libredte\lib\Core\Xml\XmlDocument::loadXML() does only seem to accept string, 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

307
        $xmlDocument->loadXML(/** @scrutinizer ignore-type */ $responseBody);
Loading history...
308
        return $xmlDocument;
309
    }
310
311
    /**
312
     * Guarda el XML en un archivo temporal y, si es necesario, lo comprime.
313
     *
314
     * @param string $xml Documento XML que se guardará en el archivo..
315
     * @param bool $compress Indica si se debe crear un archivo comprimido.
316
     * @return array Arreglo con la ruta al archivo creado y su mimetype.
317
     */
318
    private function createXmlFile(string $xml, bool $compress): array
319
    {
320
        // Normalizar el XML agregando el encabezado si no viene en el
321
        // documento. El SII recibe los documentos en ISO-8859-1 por lo que se
322
        // asume (y no valida) que el contenido del XML en $xml viene ya
323
        // codificado en ISO-8859-1.
324
        if (!str_contains($xml, '<?xml')) {
325
            $xml = '<?xml version="1.0" encoding="ISO-8859-1"?>' . "\n" . $xml;
326
        }
327
328
        // Comprimir el XML si es necesario. En caso de error al comprimir se
329
        // creará el archivo XML igualmente, pero sin comprimir. Ya que no
330
        // debería fallar la creación del XML para el envío si falló la
331
        // compresión al ser una funcionalidad opcional del SII.
332
        if ($compress) {
333
            $xmlGzEncoded = gzencode($xml);
334
            if ($xmlGzEncoded !== false) {
335
                $xml = $xmlGzEncoded;
336
            } else {
337
                $compress = false;
338
            }
339
        }
340
341
        // Crear archivo temporal y guardar los datos del XML en el archivo.
342
        $filepath = $this->getXmlFilePath($compress);
343
        file_put_contents($filepath, $xml);
344
345
        // Determinar mimetype que tiene el archivo.
346
        $mimetype = $compress ? 'application/gzip' : 'application/xml';
347
348
        // Entregar la ruta al archivo creado con el contenido del XML y su
349
        // mimetype final (pues no necesariamente será gzip aunque se haya así
350
        // solicitado).
351
        return [$filepath, $mimetype];
352
    }
353
354
    /**
355
     * Obtiene un nombre único para el archivo del XML que se desea crear.
356
     *
357
     * @param bool $compress Indica si se debe crear un archivo comprimido.
358
     * @return string Arreglo con la ruta al archivo.
359
     */
360
    private function getXmlFilePath(bool $compress): string
361
    {
362
        // Genera un archivo temporal para el XML.
363
        $tempDir = sys_get_temp_dir();
364
        $prefix = 'libredte_xml_document_for_upload_to_sii_';
365
        $filepath = tempnam($tempDir, $prefix);
366
367
        // Renombrar la ruta asignando la extensión al archivo.
368
        $realFilepath = $filepath . ($compress ? '.xml.gz' : '.xml');
369
        rename($filepath, $realFilepath);
370
371
        // Entregar la ruta que se determinó para el archivo.
372
        return $realFilepath;
373
    }
374
}
375