Skip to content

Bugfix/v4.4.1#3

Open
droa6 wants to merge 30 commits intov4.4from
bugfix/v4.4.1
Open

Bugfix/v4.4.1#3
droa6 wants to merge 30 commits intov4.4from
bugfix/v4.4.1

Conversation

@droa6
Copy link
Copy Markdown
Owner

@droa6 droa6 commented Aug 29, 2025

Summary by CodeRabbit

  • New Features

    • Separate endpoints for recepción and comprobante checks; XML generation now validates against XSDs and returns validation details.
  • Security

    • Argon2id used for passwords and session keys.
    • Signature process enforces certificate matching to document clave and emisor.
  • Improvements

    • Clearer error messages including provided values; consistent HTTP status handling.
    • Automatic per-line total calculations and payment/total consistency checks.
    • DB port configurable via environment; informacion_referencia made optional; clave uses Costa Rica date.
  • Breaking Changes

    • Params bulk-set overwrite/return behavior changed; some XSDs no longer require embedded signature; consulta endpoint renamed.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings August 29, 2025 17:28
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 29, 2025

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • api/contrib/genXML/genXML.php

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Replaced manual date parts with DateTime for clave; expanded consultar with recepcion and comprobante endpoints and curl refactor; switched selected hashing to Argon2id; large genXML refactor to auto-calc totals, trim and validate XML via ValidadorXML; added certificate checks in signing; DB connect refactor and port config; params_set default override changed; ImageResize tool call removed.

Changes

Cohort / File(s) Summary of Changes
Clave generation
api/contrib/clave/clave.php
Use DateTime (America/Costa_Rica) for fechaClave (dmy); map tipoDocumento via $tipos[$tipoDoc] ?? null; assemble clave using fechaClave; update consecutivo non-numeric error message to include [$consecutivo].
Consultar (recepción / comprobante)
api/contrib/consultar/README.md, api/contrib/consultar/consultar.php, api/contrib/consultar/module.php
Rename consutar()consultarRecepcion(); add consultarComprobante(); README wording changes; refactor curl to build $args + curl_setopt_array, add CURLOPT_HEADER => true, remove Postman-Token header; standardized curl error handling and consistent associative return format; module routes updated for recepcion and comprobante.
Hashing → Argon2id
api/contrib/facturador/companny_user.php, api/modules/users/module.php, api/contrib/facturador/companny_user.php
Replace PASSWORD_DEFAULT with PASSWORD_ARGON2ID for session key generation, user registration, and password recovery paths.
genXML refactor, helpers & XML validation
api/contrib/genXML/genXML.php, api/tools/ValidadorXML.php, www/xsd/TiqueteElectronico_V4.4.xsd, www/xsd/NotaCreditoElectronica_V4.4.xsd
Add AUTO_CALCULAR, DECIMALES; new helpers array_all, array_any, validarLinea, autoCalcularLinea, limpiarTexto; per-line auto-calculation of totals/taxes and document aggregation; trim whitespace from final XML; integrate ValidadorXML::validateXml and return validacion with genXML responses; remove mandatory ds:Signature from two XSDs.
Signing validations
api/contrib/signXML/Firmadohaciendacr.php
insertaFirma rejects empty xml/keys (returns 400); extract <Clave> and Emisor Identificacion Numero from XML; normalize certificate serialNumber (cedula) and verify presence in CLAVE and equality with EMISOR; return 400 on mismatches; include certificate path in some error messages.
Core params & response handling
api/core/params.php, api/core/tools.php
params_set default override now true; inline bulk-set logic and remove _params_set; functions return persisted value(s). tools changes: use $httpStatus variable, propagate numeric Status, adjust kill logic, throw JSON payload in CLI path, centralize JSON emission via tools_returnJson.
DB connect & config
api/modules/db/module.php, www/settings.php
Rename db_Connect()conectarBd(); build $dbConfig (host,port,user,pwd,name), log it, construct mysqli with port, set utf8mb4; add $config['db']['port'] (env DB_PORT, default 3306).
Files module: ImageResize removal
api/modules/files/module.php
Remove active tools_useTool('ImageResize.php') call and leave commented line; upload behavior unchanged.
genXML param optional
api/contrib/crlibreall/module.php, api/contrib/genXML/module.php
Change informacion_referencia from required (req => true) to optional (req => false) across genXML-related routes.
Misc small fixes
api/contrib/consultar/README.md, api/modules/wirez/messagesSend.php
README wording clarifications; fix syntax in wirez_conversationsCreate SQL insert closing parenthesis.
XML signature policy
api/contrib/firmarXML/xmlseclibs/src/XMLSecurityDSig.php
Add branch detecting v4.3 namespace to initialize signPolicy with specific URL and digest; v4.4 branch retained.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant API as consultar.php
  participant H as Hacienda

  rect rgb(232,245,255)
  note right of API: Recepción lookup flow
  C->>API: r=recepcion (clave, token, client_id)
  API->>API: Build curl $args & headers
  API->>H: GET /recepcion/{clave}
  alt Network error
    H--xAPI: curl_error
    API-->>C: {Status: error, text: error}
  else 200
    H-->>API: body (maybe respuesta-xml)
    API->>API: Decode, decode respuesta-xml if present, populate DetalleMensaje
    API-->>C: {Status: code, text: objeto}
  end
  end

  rect rgb(240,255,240)
  note right of API: Comprobante lookup
  C->>API: r=comprobante (clave, token, client_id)
  API->>H: GET /comprobantes/{clave}
  H-->>API: JSON/err
  API-->>C: {Status: code, text: data|error}
  end
Loading
sequenceDiagram
  autonumber
  participant S as Submitter
  participant G as genXML.php
  participant V as ValidadorXML
  participant F as Firmadohaciendacr

  rect rgb(255,250,230)
  S->>G: genXML* (datos)
  G->>G: Auto-calc per-line totals & taxes
  G->>G: Assemble and trim XML
  G->>V: validateXml(xml, consecutivo, version)
  alt Invalid
    V-->>G: {status:error, details}
    G-->>S: {clave, xml, validacion:error}
  else OK
    V-->>G: {status:ok}
    G-->>S: {clave, xml, validacion:ok}
  end
  end

  rect rgb(255,240,245)
  S->>F: insertaFirma(xml, publicKey, privateKey)
  F->>F: Validate inputs present
  F->>F: Extract Clave & Emisor ID from XML
  F->>F: Read certificate cedula
  alt Mismatch
    F-->>S: 400 error (cedula/clave/emisor)
  else Match
    F-->>S: Signed XML
  end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

I hop through tags and padded lines,
I count cada monto, sumo fines.
Argon2 warms my hashing den,
I match cédulas, then stamp again.
A rabbit trims XML and hums. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Bugfix/v4.4.1' is vague and uses a generic versioning format that does not convey meaningful information about the specific changes or fixes in the pull request. Replace the generic version-based title with a descriptive summary of the main fix or feature, such as 'Add XML validation and refactor generated totals calculation' or identify the primary issue being resolved.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements version 4.4.1 bugfixes for an electronic invoice system, focusing on XML generation improvements, password security upgrades, and database connectivity enhancements.

  • Updates password hashing from PASSWORD_DEFAULT to PASSWORD_ARGON2ID for improved security
  • Adds database port configuration support and auto-calculation features for XML invoice generation
  • Implements comprehensive XML validation and enhanced API response handling

Reviewed Changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
www/settings.php Adds database port configuration with default fallback
api/tools/ValidadorXML.php New XML validation tool for invoice documents
api/modules/users/module.php Updates session key generation to use ARGON2ID hashing
api/modules/files/module.php Comments out ImageResize tool usage
api/modules/db/module.php Refactors database connection to support port configuration
api/core/tools.php Enhances HTTP response handling and CLI mode detection
api/core/params.php Simplifies parameter setting by removing override functionality
api/contrib/signXML/Firmadohaciendacr.php Adds certificate validation against XML content
api/contrib/genXML/genXML.php Major refactor with auto-calculation, validation, and XML optimization
api/contrib/facturador/companny_user.php Updates password hashing to ARGON2ID
api/contrib/consultar/module.php Adds new consultation endpoints
api/contrib/consultar/consultar.php Implements separate functions for reception and invoice queries
api/contrib/consultar/README.md Updates documentation for new endpoints
api/contrib/clave/clave.php Improves error message formatting

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

unset($r["xml"]);
}
return (object) $r;
}
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validateXml method is missing a return statement when validation succeeds. This will return null instead of the expected success object.

Copilot uses AI. Check for mistakes.
$xmlString .= '
<OtrasSenas>' . $emisorOtrasSenas . '</OtrasSenas>
</Ubicacion>';
$xmlString .= '<OtrasSenas>123456</OtrasSenas>';
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded value '123456' for OtrasSenas should be replaced with a configurable parameter or removed if not needed.

Suggested change
$xmlString .= '<OtrasSenas>123456</OtrasSenas>';
// Use a configurable value for OtrasSenas instead of a hardcoded one
if (isset($emisorOtrasSenas) && $emisorOtrasSenas !== '') {
$xmlString .= '<OtrasSenas>' . htmlspecialchars($emisorOtrasSenas, ENT_XML1 | ENT_COMPAT, 'UTF-8') . '</OtrasSenas>';
}

Copilot uses AI. Check for mistakes.

// Se calcula de la siguiente manera: se obtiene de la sumatoria de los campos “Subtotal”, “Impuesto Neto”.

$calculados[$numLinea]['montoTotalLinea'] += ($calculados[$numLinea]['subTotal'] + $calculados[$numLinea]['montoTotalLinea']);
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calculation is incorrect. It's adding montoTotalLinea to itself, which will double the value. It should be: $calculados[$numLinea]['montoTotalLinea'] = $calculados[$numLinea]['subTotal'] + $calculados[$numLinea]['impuestoNeto'];

Suggested change
$calculados[$numLinea]['montoTotalLinea'] += ($calculados[$numLinea]['subTotal'] + $calculados[$numLinea]['montoTotalLinea']);
$calculados[$numLinea]['montoTotalLinea'] = $calculados[$numLinea]['subTotal'] + $calculados[$numLinea]['impuestoNeto'];

Copilot uses AI. Check for mistakes.
<TotalDescuentos>' . $totalDescuentos . '</TotalDescuentos>';
// agrega los campos del resumen que esten en el array calculados y no esten vacios
foreach (AUTO_CALCULAR as $campoResumen) {
if($calculados[$campoResumen]!='') {
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison should check if the value is not equal to 0 or empty, not string comparison. Use: if($calculados[$campoResumen] != 0) {

Suggested change
if($calculados[$campoResumen]!='') {
if($calculados[$campoResumen] != 0 && $calculados[$campoResumen] != '') {

Copilot uses AI. Check for mistakes.
if( is_object($responseObj) ) {
// si hay respuesta de hacienda, procesar detalle de mensaje
if(isset($responseObj->{'respuesta-xml'})) {
$xmlRespuesta = base64_decode($response->{'respuesta-xml'});
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable $response should be $responseObj since $response is a string and $responseObj is the decoded JSON object.

Suggested change
$xmlRespuesta = base64_decode($response->{'respuesta-xml'});
$xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'});

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 21

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
api/core/params.php (3)

32-35: Avoid trim() on non-strings to prevent TypeError and mishandling of false/array values

If $params[$p] is false or an array (possible after recent setter changes), trim() will raise a TypeError on PHP 8+. Return non-string values as-is; only trim strings.

-    if (isset($params[$p]) && trim($params[$p]) != '')
-        return $params[$p];
-    else
-        return $def;
+    if (array_key_exists($p, $params)) {
+        $v = $params[$p];
+        if (is_string($v)) {
+            return trim($v) !== '' ? $v : $def;
+        }
+        return $v;
+    }
+    return $def;

80-83: Remove stray stdout write in CLI path

echo "loading"; emits unsolicited output and can break scripts that parse CLI output. Use grace_debug only.

-    echo "loading";
+    // no-op: rely on grace_debug above

102-102: Do not dump parameter definitions to stdout; leaks secrets

print_r($params); may expose sensitive config on CI logs or terminals.

-    print_r($params);
+    // Debug print removed to avoid leaking configuration
api/contrib/facturador/companny_user.php (4)

134-141: Fix two issues: insecure session token and SQL injection via dynamic table name.

  • Session key: same weakness as users module—don’t derive from time()/rand() or use password_hash for tokens.
  • Dynamic table: db_escape() is for values, not identifiers; current string concatenation allows identifier injection.

Apply:

-function companny_users_generateSessionKey($idUser, $idMasterUser) {
-    $q = sprintf("delete from " . db_escape($idMasterUser) . "_master_sessions where idUser='" . db_escape($idUser) . "'");
-    db_query($q, 0);
-
-    $sessionKey = password_hash(time() * rand(0, 1000), PASSWORD_ARGON2ID);
-
-    $q = sprintf("INSERT INTO " . db_escape($idMasterUser) . "_master_sessions (idUser, sessionKey, ip, lastAccess) "
-            . "VALUES('%s', '%s', '%s', '%s')", db_escape($idUser), db_escape($sessionKey), db_escape($_SERVER['REMOTE_ADDR']), time());
+function companny_users_generateSessionKey($idUser, $idMasterUser) {
+    // Strictly validate identifier suffix to prevent SQL injection via table name
+    if (!preg_match('/^\d+$/', (string)$idMasterUser)) {
+        return ERROR_BAD_REQUEST;
+    }
+    $table = sprintf("`%s_master_sessions`", $idMasterUser);
+    $q = sprintf("DELETE FROM %s WHERE idUser='%s'", $table, db_escape($idUser));
+    db_query($q, 0);
+
+    // High-entropy session token
+    $sessionKey = bin2hex(random_bytes(32));
+
+    $q = sprintf(
+        "INSERT INTO %s (idUser, sessionKey, ip, lastAccess) VALUES('%s','%s','%s','%s')",
+        $table, db_escape($idUser), db_escape($sessionKey), db_escape($_SERVER['REMOTE_ADDR']), time()
+    );
     db_query($q, 0);
 
     return $sessionKey;
 }

Follow-up: Apply the same identifier hardening to SELECT/DELETE in companny_users_confirmSessionKey (Lines 216–221) and companny_users_destroySession (Line 245).


105-106: Do not log plaintext or hashed passwords. Remove this debug line.

This leaks sensitive data into logs.

Apply:

-    grace_debug("### VAlidacion: " . $compannyUser->pwd . " Lo que ingresa" . params_get("pwd"));
+    // Removed sensitive debug logging

107-115: Migrate legacy hashes to Argon2id on successful login and rehash if needed.

You accept MD5 for backward compatibility but don’t upgrade the stored hash. Rehash on both Argon2id verify (if parameters changed) and MD5 path.

Apply:

-    if (password_verify(params_get('pwd', ''), $compannyUser->pwd)) {
+    if (password_verify(params_get('pwd', ''), $compannyUser->pwd)) {
+        // Upgrade hash if parameters/algorithm changed
+        if (password_needs_rehash($compannyUser->pwd, PASSWORD_ARGON2ID)) {
+            $compannyUser->pwd = password_hash(params_get('pwd', ''), PASSWORD_ARGON2ID);
+            _companny_users_update((array)$compannyUser, $idMasterUser);
+        }
         // Create a token
         grace_debug("Able to login");
         return array('sessionKey' => companny_users_generateSessionKey($compannyUser->idUser, $idMasterUser), 'userName' => $compannyUser->userName, 'idUser' => $compannyUser->idUser);
-    } else if ($compannyUser->pwd == md5(params_get('pwd', ''))) {
+    } else if ($compannyUser->pwd == md5(params_get('pwd', ''))) {
         // Create a token
         grace_debug("Able to login");
+        // Migrate from MD5 to Argon2id
+        $compannyUser->pwd = password_hash(params_get('pwd', ''), PASSWORD_ARGON2ID);
+        _companny_users_update((array)$compannyUser, $idMasterUser);
         return array('sessionKey' => companny_users_generateSessionKey($compannyUser->idUser, $idMasterUser), 'userName' => $compannyUser->userName, 'idUser' => $compannyUser->idUser);

211-238: Sessions expire by lastAccess, but you never update lastAccess—either re-enable the updater or update here.

Without touching lastAccess, sessions may expire prematurely.

Apply:

-    } else {
+    } else {
         # Lets confirm the time frame   
         if (conf_get('sessionLifetime', 'users') != -1) {
             if ((time() - $r->lastAccess) > conf_get('sessionLifetime', 'users')) {
                 grace_debug("User last access is to old");
                 return false;
             }
-            return $r->idUser;
+            // Refresh last access on valid use
+            if (!preg_match('/^\d+$/', (string)$idMasterUser)) {
+                return ERROR_BAD_REQUEST;
+            }
+            $table = sprintf("`%s_master_sessions`", $idMasterUser);
+            $upd = sprintf("UPDATE %s SET lastAccess = '%s' WHERE sessionKey = '%s'", $table, time(), db_escape(params_get('sessionKey','')));
+            db_query($upd, 0);
+            return $r->idUser;
         } else {
             return $r->idUser;
         }
     }
api/contrib/signXML/Firmadohaciendacr.php (1)

201-206: Wrong call to member method and hex prefix handling.

  • stringHex2StringDec must be called as $this->stringHex2StringDec.
  • Strip the 0x prefix before conversion.
-        if (strpos($certData['serialNumber'], "0x") === false) {
+        if (strpos($certData['serialNumber'], "0x") === false) {
             // https://bugs.php.net/bug.php?id=77411
             $serialNumber = $certData['serialNumber'];
         } else {
-            $serialNumber = stringHex2StringDec($certData['serialNumber']);
+            $serialNumber = $this->stringHex2StringDec(preg_replace('/^0x/i', '', $certData['serialNumber']));
        }
api/contrib/consultar/consultar.php (1)

144-145: Undefined variable $apiTo in error response.

The variable $apiTo is undefined in the error response array. It should be $url.

-            "to" => $apiTo,
+            "to" => $url,
🧹 Nitpick comments (11)
api/core/params.php (1)

88-116: Rename local $params (param definitions) to avoid shadowing global $params (actual config)

Shadowing increases bug risk and readability cost. Use $paramDefs (or similar) for the CLI schema.

-    # The actual params to be considered
-    $params = array();
+    # The actual param definitions to be considered
+    $paramDefs = array();

-    # Extract only the params
-    foreach ($allParams as $param) {
-        if (isset($param['params']))
-            $params[] = $param['params'];
+    # Extract only the params
+    foreach ($allParams as $param) {
+        if (isset($param['params']))
+            $paramDefs[] = $param['params'];
     }

-    # For some reason they are stored in pos 0 of the array
-    $params = $params[0];
+    # For some reason they are stored in pos 0 of the array
+    $paramDefs = $paramDefs[0];

-    foreach ($params as $p) {
+    foreach ($paramDefs as $p) {
         $longOpt = $p['key'];
         $opts .= $p['cli'];
         # Is it mandatory?
         if ($p['req']) {
             $opts .= ":";
             $longOpt .= ":";
         } else {
             $opts .= "::";
             $longOpt .= "::";
         }

         $longOpts[] = $longOpt;
     }
@@
-    foreach ($params as $p) {
+    foreach ($paramDefs as $p) {
         # Was it sent as longOpt?
         if (isset($args[$p['key']]))
             params_set($p['key'], $args[$p['key']]);
         else if (isset($args[$p['cli']]))
             params_set($p['key'], $args[$p['cli']]);
         else if ($p['req'] == false) # If it is optional, I will load the default value?
             params_set($p['key'], $p['def']);
     }
@@
-    params_verifyRequest($params);
+    params_verifyRequest($paramDefs);

Also applies to: 124-132

api/contrib/facturador/companny_user.php (1)

5-10: Harden getIdUser() against empty results.

db_query($q, 2) may return an empty array; accessing $result[0] will notice/warn.

Apply:

 function getIdUser($sessionKey) {
     $q = "SELECT `idUser` FROM `sessions` WHERE `sessionKey`='" . db_escape($sessionKey) . "'";
     $result = db_query($q, 2);
-    $idUser = $result[0]->idUser;
-    return $idUser;
+    if (!is_array($result) || count($result) === 0) {
+        return 0;
+    }
+    return (int)$result[0]->idUser;
 }
api/contrib/clave/clave.php (1)

88-88: Echoing raw user input in error string; consider sanitization or consistent pattern.

Including [$consecutivo] in the message helps debugging but can introduce noisy logs or, if rendered unsafely in HTML, XSS risk. Either:

  • keep JSON-only responses and ensure frontend escapes, or
  • wrap the value with a safe formatter, or
  • remove the echo to match other numeric checks.
api/modules/files/module.php (1)

159-159: Removing ImageResize loader may break files_resizeImg at runtime.

files_resizeImg instantiates \Eventviva\ImageResize. If autoloading isn’t configured, this will now fatal.

  • Option A: lazy-load inside files_resizeImg.
  • Option B: ensure Composer autoload provides Eventviva\ImageResize.

Example lazy-load (outside this hunk, at start of files_resizeImg):

if (!class_exists('\Eventviva\ImageResize')) {
    tools_useTool('ImageResize.php');
}
api/tools/ValidadorXML.php (1)

37-44: Docblock parameter mismatch and naming clarity.

@params mention $tipo_doc, but the signature receives $consecutivo and infers tipo from it. Update docblock to avoid confusion, or accept $tipo_doc explicitly.

-     * @param string $tipo_doc - tipo de documento a validar
+     * @param string $consecutivo - consecutivoFinal (20 dígitos) del comprobante; se usa para derivar el tipo_doc
api/contrib/signXML/Firmadohaciendacr.php (2)

162-166: Timezone literal is hardcoded; prefer from config.

Consider conf_get('timezone', 'core', '-06:00') to avoid surprises across deployments.


256-299: String-based XML assembly/insertion is brittle; prefer DOM for signature injection.

Direct string replace of closing tags can break on formatting/namespace differences. Consider building nodes with DOMDocument and appendChild at the document element.

I can provide a DOM-based injection patch if desired.

api/contrib/consultar/consultar.php (1)

115-116: Inconsistent error message for client_id validation.

The error message in consultarComprobante() is generic ("Ha ocurrido un error en el client_id") while consultarRecepcion() provides the actual client_id value. Consider making them consistent.

-        return "Ha ocurrido un error en el client_id.";
+        return "El client_id proporcionado (".params_get("client_id").") no es válido.";
api/contrib/genXML/genXML.php (3)

769-783: Consider extracting tax rate mapping to a constant.

The tax rate mapping could be defined as a class constant for better maintainability and reusability across different document types.

+const TARIFA_IMPUESTO_MAP = [
+    "01" => 0,      // 0% (Artículo 32, num 1, RLIVA)
+    "02" => 0.01,   // Tarifa reducida 1%
+    "03" => 0.02,   // Tarifa reducida 2%
+    "04" => 0.04,   // Tarifa reducida 4%
+    "05" => 0,      // Transitorio 0%
+    "06" => 0.04,   // Transitorio 4%   
+    "07" => 0.08,   // Tarifa transitoria 8%
+    "08" => 0.13,   // Tarifa general 13%
+    "09" => 0.005,  // Tarifa reducida 0.5%
+    "10" => 0,      // Tarifa Exenta
+    "11" => 0       // Tarifa 0% sin derecho a crédito
+];

 // Then use it as:
-$impuestoPorTarifa = [/* ... */];
-$calculados[$l]['tarifa'] = round($impuestoPorTarifa[$i->codigoTarifa], 2)*100;
+$calculados[$l]['tarifa'] = round(self::TARIFA_IMPUESTO_MAP[$i->codigoTarifa], 2)*100;

189-205: Consider removing commented-out code blocks.

There are large blocks of commented-out code for the manual total parameters. Since you're moving to auto-calculation, these should be removed to keep the code clean.

-    /*
-    $totalServGravados = params_get("total_serv_gravados");
-    // ... (all the commented parameters)
-    $totalComprobante = params_get("total_comprobante");
-    */
-
-    $totalDescuentos = params_get("total_descuentos");
+    $totalDescuentos = params_get("total_descuentos");

Also applies to: 1237-1256


776-776: Fix typo in comment.

There's a small typo in the tarifa comment.

-                        "09" => 0.05,   // Tarifa reducida 0.5%
+                        "09" => 0.005,  // Tarifa reducida 0.5%
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2729d40 and 57d295a.

📒 Files selected for processing (14)
  • api/contrib/clave/clave.php (1 hunks)
  • api/contrib/consultar/README.md (1 hunks)
  • api/contrib/consultar/consultar.php (3 hunks)
  • api/contrib/consultar/module.php (1 hunks)
  • api/contrib/facturador/companny_user.php (3 hunks)
  • api/contrib/genXML/genXML.php (48 hunks)
  • api/contrib/signXML/Firmadohaciendacr.php (3 hunks)
  • api/core/params.php (1 hunks)
  • api/core/tools.php (2 hunks)
  • api/modules/db/module.php (2 hunks)
  • api/modules/files/module.php (1 hunks)
  • api/modules/users/module.php (1 hunks)
  • api/tools/ValidadorXML.php (1 hunks)
  • www/settings.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
api/contrib/signXML/Firmadohaciendacr.php (1)
api/core/tools.php (1)
  • tools_reply (19-111)
api/modules/users/module.php (1)
api/modules/crypto/crypto.php (1)
  • crypto_encrypt (32-48)
api/modules/db/module.php (2)
api/core/conf.php (1)
  • conf_get (26-34)
api/core/grace.php (1)
  • grace_debug (101-105)
api/core/tools.php (1)
api/core/conf.php (1)
  • conf_get (26-34)
api/contrib/consultar/consultar.php (1)
api/core/params.php (1)
  • params_get (24-36)
api/tools/ValidadorXML.php (1)
api/core/conf.php (1)
  • conf_get (26-34)
api/contrib/genXML/genXML.php (3)
api/core/tools.php (2)
  • tools_useTool (175-183)
  • tools_reply (19-111)
api/core/params.php (1)
  • params_get (24-36)
api/tools/ValidadorXML.php (2)
  • ValidadorXML (3-97)
  • validateXml (43-96)
🪛 LanguageTool
api/contrib/consultar/README.md

[misspelling] ~1-~1: Si es adjetivo o nombre, se escribe con tilde.
Context: # Modulo para hacer comprobar estado de los comp...

(DIACRITICS_02)


[uncategorized] ~1-~1: Probablemente falta un determinante.
Context: # Modulo para hacer comprobar estado de los comprobantes y su estado de rece...

(AI_ES_GGEC_MISSING_DETERMINER)


[uncategorized] ~1-~1: Probablemente falta un signo diacrítico.
Context: ...tado de los comprobantes y su estado de recepcion Se envia: * w : consultar * r : r...

(AI_ES_GGEC_MISSING_ORTHOGRAPHY_DIACRITIC)


[uncategorized] ~3-~3: Probablemente falta un signo diacrítico.
Context: ...mprobantes y su estado de recepcion Se envia: * w : consultar * r : recepcion o...

(AI_ES_GGEC_MISSING_ORTHOGRAPHY_DIACRITIC)


[uncategorized] ~6-~6: Probablemente falta un signo diacrítico.
Context: ...n Se envia: * w : consultar * r : recepcion o comprobante * clave : Clave numeric...

(AI_ES_GGEC_MISSING_ORTHOGRAPHY_DIACRITIC)


[uncategorized] ~7-~7: Probablemente falta un signo diacrítico.
Context: ...cepcion o comprobante * clave : Clave numerica del comprobante * token: Token obteni...

(AI_ES_GGEC_MISSING_ORTHOGRAPHY_DIACRITIC)


[typographical] ~8-~8: Mayúsculas y minúsculas recomendadas.
Context: ...ave numerica del comprobante * token: Token obtenido de Hacienda

(AI_ES_GGEC_REPLACEMENT_CASING_UPPERCASE)

🪛 GitHub Actions: PHPUnit Tests
api/contrib/genXML/genXML.php

[error] 33-33: Call to undefined function tools_useTool() in api/contrib/genXML/genXML.php:33. Command: './vendor/bin/phpunit --stderr --debug --bootstrap vendor/autoload.php tests/api_contrib_genXML_FE.php' (exit code 255).

🔇 Additional comments (17)
api/modules/db/module.php (1)

51-52: Remove BC alias; no references to db_Connect() found
Ran

rg -nP '\bdb_Connect\s*\(' -C2

and found zero matches—renaming to conectarBd() is safe.

api/core/params.php (1)

41-41: Verify backward compatibility for removed $override parameter

At api/core/params.php (line 41), the signature now reads

function params_set($p, $val = false)

removing the third $override argument. An internal search found no three-argument calls, but this is a public API change that will trigger PHP 8+ ArgumentCountError for any downstream callers and drops the original “do-not-overwrite” semantics. Confirm no external/plugins depend on this parameter or reintroduce it (with deprecation) to prevent runtime errors and behavior drift.

api/contrib/facturador/companny_user.php (1)

412-415: Argon2id adoption looks good—ensure environment support and set a rehash policy.

  • Keep Argon2id, but verify the runtime supports it; fall back to PASSWORD_DEFAULT if not.
  • Pair with the login-side migration (see Lines 107–115).

Run-time check to confirm support:

// At bootstrap
if (!defined('PASSWORD_ARGON2ID')) { /* log/warn */ }

Optional hardening:

-$pwd = password_hash($compannyUserDets['pwd'], PASSWORD_ARGON2ID);
+// Optionally tune costs to your infra; defaults are fine if unsure.
+$pwd = password_hash($compannyUserDets['pwd'], PASSWORD_ARGON2ID);
api/core/tools.php (1)

88-99: Behavior change: tools_reply now returns JSON in web mode instead of exiting. Verify all call sites.

Previously, callers could assume tools_reply printed and terminated. Now in non-CLI it returns a string and does not exit, which can cause duplicate outputs or continued execution after an intended error.

  • Either keep exiting in both modes, or
  • Document this change and update all call sites to echo/return the JSON and stop execution.

Would you like me to scan the repository for direct calls to tools_reply and flag those that rely on termination semantics?

api/contrib/consultar/consultar.php (3)

33-34: Include the client_id value in the error message.

Good improvement for debugging - now the error message includes the actual client_id that was provided, making troubleshooting easier.


36-53: Good refactoring of curl options into a separate array.

Moving curl options to a separate $args array improves code readability and maintainability. The removal of the Postman-Token header is also appropriate for production code.


59-67: Improved error handling with standardized response structure.

The error handling now returns a consistent associative array with Status, to, and text fields, which provides better structure for API responses.

api/contrib/consultar/module.php (2)

34-44: Routing updates align correctly with renamed functions.

The routing configuration correctly updates the endpoint from consultarCom to recepcion with the corresponding action consultarRecepcion, maintaining backward compatibility concerns should be considered.


45-56: New comprobante endpoint properly configured.

The new comprobante route is correctly configured with the same required parameters and access controls as the reception endpoint.

api/contrib/consultar/README.md (1)

6-8: Documentation correctly reflects the API changes.

The updated documentation accurately describes the new endpoints (recepcion and comprobante) and clarifies that the token is obtained from Hacienda.

api/contrib/genXML/genXML.php (7)

35-133: Excellent addition of auto-calculation helper functions.

The new helper functions (array_all, array_any, validarLinea, autoCalcularLinea) and the AUTO_CALCULAR constant provide a robust framework for automatic calculation of totals. This reduces manual calculation errors and improves maintainability.


165-165: Removal of deprecated parameter is good cleanup.

Removing the deprecated omitir_receptor parameter helps clean up the codebase.


269-279: Good validation for required medios_pago field.

The validation ensures that medios_pago is provided when the payment condition requires it (not credit, consignment, or other special conditions).


549-910: Excellent implementation of automatic tax calculation.

The automatic calculation of taxes based on codigoTarifa is well-implemented. The tax rate mapping is correct for Costa Rica's tax system, and the validation ensures calculated values match provided values when both exist.


1018-1043: Excellent validation of payment methods total.

The validation ensures that the sum of all payment methods equals the total invoice amount, preventing accounting discrepancies.


1166-1184: Good addition of XML validation and response structure.

The XML validation using ValidadorXML and the consistent response structure with validation results improves data quality and error handling. The XML trimming also helps reduce file size.


4077-4081: Email validation with proper error handling in genXMLFec.

Good that you're returning an error response instead of just logging invalid emails. This provides immediate feedback to the API consumer.

@@ -1,7 +1,8 @@
# Modulo para hacer comprobar estado de los comprobantes
# Modulo para hacer comprobar estado de los comprobantes y su estado de recepcion
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix Spanish spelling and grammar issues.

The header has spelling errors that should be corrected.

-# Modulo para hacer comprobar estado de los comprobantes y su estado de recepcion
+# Módulo para comprobar el estado de los comprobantes y su estado de recepción
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Modulo para hacer comprobar estado de los comprobantes y su estado de recepcion
# Módulo para comprobar el estado de los comprobantes y su estado de recepción
🧰 Tools
🪛 LanguageTool

[misspelling] ~1-~1: Si es adjetivo o nombre, se escribe con tilde.
Context: # Modulo para hacer comprobar estado de los comp...

(DIACRITICS_02)


[uncategorized] ~1-~1: Probablemente falta un determinante.
Context: # Modulo para hacer comprobar estado de los comprobantes y su estado de rece...

(AI_ES_GGEC_MISSING_DETERMINER)


[uncategorized] ~1-~1: Probablemente falta un signo diacrítico.
Context: ...tado de los comprobantes y su estado de recepcion Se envia: * w : consultar * r : r...

(AI_ES_GGEC_MISSING_ORTHOGRAPHY_DIACRITIC)

🤖 Prompt for AI Agents
In api/contrib/consultar/README.md around line 1, the header contains Spanish
spelling/grammar errors; replace it with a corrected, accented and concise
version such as: "Módulo para comprobar el estado de los comprobantes y su
recepción" (ensure "Módulo" and "recepción" include accents and remove the
redundant "hacer").

Comment on lines 512 to 516
$temp = rand(0, 1000) + time();

$compannyUser->pwd = password_hash($temp, PASSWORD_DEFAULT);
$compannyUser->pwd = password_hash($temp, PASSWORD_ARGON2ID);
grace_debug("New tmp pwd: " . $compannyUser->pwd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t email a new plaintext password; issue a time-bound reset token instead.

Current flow generates and emails a password, which is insecure and hard to audit. Prefer a reset token + link.

Minimal improvement if you must keep this flow:

  • Store a forced-change flag and require update on next login.
  • Set very short validity and invalidate all sessions.

Better approach (recommended):

  • Create a reset token with random_bytes(32), store hashed, email a one-time link, and let the user set a new password.

Comment on lines +46 to +47
$baseFolder = conf_get('coreInstall', 'modules', '');
$baseFolder = str_replace('/api', '/www', $baseFolder);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Path construction is brittle (str_replace '/api'→'/www').

Conf values may not include '/api', yielding wrong paths. Prefer a dedicated config key (e.g., xsdBasePath) or compute relative to known anchor.

Minimal hardening:

-        $baseFolder = conf_get('coreInstall', 'modules', '');
-        $baseFolder = str_replace('/api', '/www', $baseFolder);
+        $baseFolder = rtrim(conf_get('xsdBasePath', 'core', str_replace('/api', '/www', conf_get('coreInstall', 'modules', ''))), '/');

🏁 Script executed:

#!/bin/bash
# Locate conf_get implementation and usages
rg -n "function\s\+conf_get" -C3 --type=php
rg -n "conf_get\(" -C3 --type=php
# Find where 'coreInstall' config is defined or referenced
rg -n "coreInstall" -C3 --type=php

Length of output: 28066


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' api/core/conf.php

Length of output: 1385


Use a dedicated config key for the XSD base path instead of brittle str_replace('/api', '/www', …).
Replacing '/api''/www' will break whenever coreInstall doesn’t include '/api'. Define a new config['xsd']['basePath'] in your settings and call conf_get('basePath','xsd', …) (with rtrim) here.

🤖 Prompt for AI Agents
In api/tools/ValidadorXML.php around lines 46-47, replace the brittle
str_replace-based derivation of the XSD base path with a dedicated config key:
read the path via conf_get('basePath','xsd','') (or equivalent config API),
apply rtrim to remove any trailing slashes, and use that value instead of
manipulating conf_get('coreInstall','modules',...) with str_replace; update
config files to include config['xsd']['basePath'] and ensure callers use this
canonical, trimmed base path.

Comment on lines +48 to +55
$schemas = [
"01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
"02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
"03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
"04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
"05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

XSD mapping likely incorrect for '02' and message types; verify filenames.

  • '02' (Nota de Débito Electrónica) should point to NotaDebitoElectronica_V$version.xsd, not FacturaCompra.
  • Hacienda messages for 05–07 are typically MensajeReceptor*.xsd in v4.4.

Proposed mapping:

-            "02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
+            "02" => "$baseFolder/xsd/NotaDebitoElectronica_V$version.xsd",
-            "05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
+            "05" => "$baseFolder/xsd/MensajeReceptor_V$version.xsd",

🏁 Script executed:

#!/bin/bash
# List available XSDs to confirm expected names for v4.4
fd -a --glob 'www/xsd/*V4.4*.xsd' || true
rg -n "schemaLocation|xsd" -C1 www/xsd 2>/dev/null | head -n 200 || true

Length of output: 10915


Fix XSD schema mappings for codes 02 and 05

  • Code "02": reference NotaDebitoElectronica_V$version.xsd (Debit Note) instead of FacturaCompra_V$version.xsd.
  • Code "05": reference MensajeReceptor_V$version.xsd (Acceptance Confirmation) instead of MensajeHacienda_V$version.xsd.
-            "02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
+            "02" => "$baseFolder/xsd/NotaDebitoElectronica_V$version.xsd",
-            "05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
+            "05" => "$baseFolder/xsd/MensajeReceptor_V$version.xsd",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$schemas = [
"01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
"02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
"03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
"04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
"05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
];
$schemas = [
"01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
"02" => "$baseFolder/xsd/NotaDebitoElectronica_V$version.xsd",
"03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
"04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
"05" => "$baseFolder/xsd/MensajeReceptor_V$version.xsd",
];
🤖 Prompt for AI Agents
In api/tools/ValidadorXML.php around lines 48 to 55, update the $schemas mapping
so that code "02" points to
"$baseFolder/xsd/NotaDebitoElectronica_V$version.xsd" instead of
"FacturaCompra_V$version.xsd" and code "05" points to
"$baseFolder/xsd/MensajeReceptor_V$version.xsd" instead of
"MensajeHacienda_V$version.xsd"; preserve the same $baseFolder and $version
interpolation and keep the rest of the mapping entries unchanged.

Comment on lines +77 to +80
libxml_use_internal_errors(true);
$xml = new DOMDocument();
$xml->loadXML($contenido_xml);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden XML loading and handle load failures.

Prevent XXE/network fetch and bail early on invalid XML:

-        $xml = new DOMDocument();
-        $xml->loadXML($contenido_xml);
+        $xml = new DOMDocument();
+        if (@$xml->loadXML($contenido_xml, LIBXML_NONET) === false) {
+            return (object)[
+                "status" => "error",
+                "message" => "Error: XML inválido o mal formado"
+            ];
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
libxml_use_internal_errors(true);
$xml = new DOMDocument();
$xml->loadXML($contenido_xml);
libxml_use_internal_errors(true);
$xml = new DOMDocument();
if (@$xml->loadXML($contenido_xml, LIBXML_NONET) === false) {
return (object)[
"status" => "error",
"message" => "Error: XML inválido o mal formado"
];
}
🤖 Prompt for AI Agents
In api/tools/ValidadorXML.php around lines 77-80, the XML is loaded without
blocking external entity/network fetches and without checking for load failures;
update load to use LIBXML_NONET to prevent XXE/network fetches (and call
libxml_disable_entity_loader(true) for older PHP versions if needed), keep
libxml_use_internal_errors(true), check the boolean return of $xml->loadXML(...,
LIBXML_NONET), collect libxml_get_errors() and clear them with
libxml_clear_errors() on failure, and bail/return/throw with a clear error
message so invalid XML is handled early.

Comment on lines +49 to +50
# Database port
$config['db']['port'] = getenv('DB_PORT') ?: 3306; // Default to 3306 if not set
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Sanitize and validate DB_PORT; cast to int and enforce valid range

Avoid treating numeric strings loosely and ensure an integer within 1–65535. This prevents bad ports and type issues downstream.

-# Database port
-$config['db']['port'] = getenv('DB_PORT') ?: 3306; // Default to 3306 if not set
+# Database port
+$__port = getenv('DB_PORT');
+$config['db']['port'] = (is_numeric($__port) ? max(1, min(65535, (int)$__port)) : 3306);
+unset($__port);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Database port
$config['db']['port'] = getenv('DB_PORT') ?: 3306; // Default to 3306 if not set
# Database port
$__port = getenv('DB_PORT');
$config['db']['port'] = (is_numeric($__port)
? max(1, min(65535, (int)$__port))
: 3306
);
unset($__port);
🤖 Prompt for AI Agents
In www/settings.php around lines 49 to 50, the DB_PORT value is taken directly
from getenv and left as a string or fallback value; change this to cast the
environment value to an integer, validate it is a number within the TCP port
range 1–65535, and fall back to 3306 if missing or out of range. Specifically,
read getenv('DB_PORT'), trim and check is_numeric, cast to (int), enforce 1 <=
port <= 65535, and assign the validated int to $config['db']['port']; if
validation fails, use 3306 (optionally emit a warning/log).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
api/contrib/consultar/consultar.php (4)

24-26: Standardize return type and avoid strlen(false) warning for clave.

Use a robust empty check and return the structured payload used elsewhere.

-    if ($clave == "" || strlen($clave) == 0)
-        return "La clave no puede ser en blanco";
+    if (empty($clave) || !is_string($clave)) {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "La clave no puede estar vacía"
+        );
+    }

36-51: Enable TLS verification and adjust headers for GET.

Disabling SSL verification is unsafe; also prefer Accept for GET.

         CURLOPT_MAXREDIRS       => 10,
-        CURLOPT_SSL_VERIFYHOST  => 0,
-        CURLOPT_SSL_VERIFYPEER  => 0,
-        CURLOPT_TIMEOUT         => 30,
+        CURLOPT_SSL_VERIFYHOST  => 2,
+        CURLOPT_SSL_VERIFYPEER  => 1,
+        CURLOPT_CONNECTTIMEOUT  => 10,
+        CURLOPT_TIMEOUT         => 30,
         CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
-        CURLOPT_CUSTOMREQUEST   => "GET",
+        CURLOPT_HTTPGET         => true,
         CURLOPT_HTTPHEADER      => array(
             "Authorization: Bearer " . params_get('token'),
             "Cache-Control: no-cache",
-            "Content-Type: application/x-www-form-urlencoded"
+            "Accept: application/json"
         ),

141-149: Undefined variable $apiTo; use the actual request URL.

This breaks the error path.

-            "to" => $apiTo,
+            "to" => $args[CURLOPT_URL],

150-154: Return a standardized response object (Status/to/text).

Keep API outputs consistent with consultarRecepcion.

-        $response = json_decode($response);
-        return $response;
+        $responseObj = json_decode($response);
+        return array(
+            "Status" => $status,
+            "to" => $url,
+            "text" => $responseObj
+        );
♻️ Duplicate comments (1)
api/contrib/consultar/consultar.php (1)

79-81: Return structured payload instead of raw $response.

This was already flagged; see fix above returning the array with Status/to/text.

🧹 Nitpick comments (3)
api/contrib/consultar/consultar.php (3)

59-67: Return the full request URL on cURL error.

Helps debugging which resource failed.

-        $arrayResp = array(
+        $arrayResp = array(
             "Status" => $status,
-            "to" => $url,
+            "to" => $args[CURLOPT_URL],
             "text" => $err
         );

46-48: Validate token before making the request.

Avoid a pointless 401 by rejecting empty tokens early (apply similarly in consultarComprobante()).

-            "Authorization: Bearer " . params_get('token'),
+            // Validar token antes de construir headers
+            "Authorization: Bearer " . params_get('token'),

Follow-up (example guard to add near the top):

$token = params_get('token');
if (empty($token) || !is_string($token)) {
    return array("Status"=>400,"to"=>null,"text"=>"El token no puede estar vacío");
}

45-46: Prefer CURLOPT_HTTPGET over CUSTOMREQUEST for GET.

Clearer intent and lets cURL set method semantics.

-        CURLOPT_CUSTOMREQUEST   => "GET",
+        CURLOPT_HTTPGET         => true,

Also applies to: 127-128

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 57d295a and ce227fc.

📒 Files selected for processing (2)
  • api/contrib/consultar/consultar.php (3 hunks)
  • api/core/tools.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/core/tools.php
🧰 Additional context used
🧬 Code graph analysis (1)
api/contrib/consultar/consultar.php (1)
api/core/params.php (1)
  • params_get (24-36)

Comment on lines 33 to 35
if ($url == null)
return "Ha ocurrido un error en el client_id.";
return "El client_id proprocionado (".params_get("client_id").") no es válido. ";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo and return a structured error for invalid client_id.

Keep responses consistent and correct the Spanish typo.

-    if ($url == null)
-        return "El client_id proprocionado (".params_get("client_id").") no es válido. ";
+    if ($url == null) {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "El client_id proporcionado (" . params_get("client_id") . ") no es válido."
+        );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($url == null)
return "Ha ocurrido un error en el client_id.";
return "El client_id proprocionado (".params_get("client_id").") no es válido. ";
if ($url == null) {
return array(
"Status" => 400,
"to" => null,
"text" => "El client_id proporcionado (" . params_get("client_id") . ") no es válido."
);
}
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 33 to 35, fix the Spanish
typo ("proprocionado" → "proporcionado") and replace the plain string return
with a structured JSON error response (e.g., an object with keys like "error"
and "message" that includes the provided client_id via params_get("client_id")).
Also set the response Content-Type to application/json and return an appropriate
HTTP status (400) for invalid client_id.

Comment on lines +71 to +86
if( is_object($responseObj) ) {
// si hay respuesta de hacienda, procesar detalle de mensaje
if(isset($responseObj->{'respuesta-xml'})) {
$xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'});
$xmlRespuesta = preg_replace('/>\s+</', '><', $xmlRespuesta);
$startLength = strlen("tiene los siguientes errores: ")+7;
$startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ", 0);
if(!$startPos) {
// si no hay errores, retornar la respuesta normal
return $response;
}
$endPos = strpos($xmlRespuesta, "</DetalleMensaje>", $startPos);
$detalleMensaje = substr($xmlRespuesta, $startPos+$startLength, $endPos -($startPos+$startLength+1));
$separadorMensaje = "&#13;\n";
$responseObj->{'DetalleMensaje'} = explode($separadorMensaje,trim($detalleMensaje,$separadorMensaje));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix falsey strpos check, guard base64 decode, and off-by-one in substring.

Current logic can misclassify position 0 as “not found” and may slice incorrectly.

-        if(isset($responseObj->{'respuesta-xml'})) {
-            $xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'});
+        if (isset($responseObj->{'respuesta-xml'})) {
+            $xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'}, true);
+            if ($xmlRespuesta === false) {
+                return array("Status" => $status, "to" => $url, "text" => $responseObj);
+            }
             $xmlRespuesta = preg_replace('/>\s+</', '><', $xmlRespuesta);
-            $startLength = strlen("tiene los siguientes errores: ")+7;
-            $startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ", 0);
-            if(!$startPos) {
-                // si no hay errores, retornar la respuesta normal
-                return $response;
-            }
+            $startLength = strlen("tiene los siguientes errores: ");
+            $startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ");
+            if ($startPos === false) {
+                // no hay errores, devolver respuesta normal
+                return array("Status" => $status, "to" => $url, "text" => $responseObj);
+            }
             $endPos = strpos($xmlRespuesta, "</DetalleMensaje>", $startPos);
-            $detalleMensaje = substr($xmlRespuesta, $startPos+$startLength, $endPos -($startPos+$startLength+1));
+            $detalleMensaje = substr($xmlRespuesta, $startPos + $startLength, $endPos - ($startPos + $startLength));
             $separadorMensaje = "&#13;\n";
             $responseObj->{'DetalleMensaje'} = explode($separadorMensaje,trim($detalleMensaje,$separadorMensaje));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if( is_object($responseObj) ) {
// si hay respuesta de hacienda, procesar detalle de mensaje
if(isset($responseObj->{'respuesta-xml'})) {
$xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'});
$xmlRespuesta = preg_replace('/>\s+</', '><', $xmlRespuesta);
$startLength = strlen("tiene los siguientes errores: ")+7;
$startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ", 0);
if(!$startPos) {
// si no hay errores, retornar la respuesta normal
return $response;
}
$endPos = strpos($xmlRespuesta, "</DetalleMensaje>", $startPos);
$detalleMensaje = substr($xmlRespuesta, $startPos+$startLength, $endPos -($startPos+$startLength+1));
$separadorMensaje = "&#13;\n";
$responseObj->{'DetalleMensaje'} = explode($separadorMensaje,trim($detalleMensaje,$separadorMensaje));
}
if (is_object($responseObj)) {
// si hay respuesta de hacienda, procesar detalle de mensaje
if (isset($responseObj->{'respuesta-xml'})) {
// strict decode and bail on failure
$xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'}, true);
if ($xmlRespuesta === false) {
return array(
"Status" => $status,
"to" => $url,
"text" => $responseObj
);
}
$xmlRespuesta = preg_replace('/>\s+</', '><', $xmlRespuesta);
// find the error section
$startLength = strlen("tiene los siguientes errores: ");
$startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ");
if ($startPos === false) {
// no hay errores, devolver respuesta normal
return array(
"Status" => $status,
"to" => $url,
"text" => $responseObj
);
}
$endPos = strpos($xmlRespuesta, "</DetalleMensaje>", $startPos);
$detalleMensaje = substr(
$xmlRespuesta,
$startPos + $startLength,
$endPos - ($startPos + $startLength)
);
$separadorMensaje = "&#13;\n";
$responseObj->{'DetalleMensaje'} = explode(
$separadorMensaje,
trim($detalleMensaje, $separadorMensaje)
);
}
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 71 to 86, the code treats a
strpos result of 0 as “not found”, doesn't guard base64_decode for failure, and
computes the substring length with an off-by-one; change the strpos check to
strict comparison (=== false) so position 0 is treated as found, only call
base64_decode if respuesta-xml exists and check its return is not false before
using it, and compute the substring length as $endPos - ($startPos +
$startLength) (remove the extra +1) so substr extracts the full message; keep
the separator handling but trim using the separator characters properly before
explode.

Comment on lines +101 to +107
function consultarComprobante()
{
$curl = curl_init();
$clave = params_get('clave');

if ($clave == "" || strlen($clave) == 0)
return "La clave no puede ser en blanco";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same clave handling as consultarRecepcion: standardize and avoid warnings.

-    if ($clave == "" || strlen($clave) == 0)
-        return "La clave no puede ser en blanco";
+    if (empty($clave) || !is_string($clave)) {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "La clave no puede estar vacía"
+        );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function consultarComprobante()
{
$curl = curl_init();
$clave = params_get('clave');
if ($clave == "" || strlen($clave) == 0)
return "La clave no puede ser en blanco";
function consultarComprobante()
{
$curl = curl_init();
$clave = params_get('clave');
- if ($clave == "" || strlen($clave) == 0)
if (empty($clave) || !is_string($clave)) {
return array(
"Status" => 400,
"to" => null,
"text" => "La clave no puede estar vacía"
);
}
// …rest of function…
}
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 101 to 107, the current
clave handling uses params_get('clave') and then calls strlen which can emit
warnings if clave is null; standardize to the same approach used in
consultarRecepcion by normalizing and trimming the input: assign $clave =
trim(params_get('clave') ?? ''); then check with if ($clave === '') return "La
clave no puede ser en blanco"; this avoids warnings from strlen and null values
and ensures consistent validation.

Comment on lines +115 to +116
if ($url == null)
return "Ha ocurrido un error en el client_id.";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Return structured error and include the provided client_id.

-    if ($url == null)
-        return "Ha ocurrido un error en el client_id.";
+    if ($url == null) {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "El client_id proporcionado (" . params_get("client_id") . ") no es válido."
+        );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($url == null)
return "Ha ocurrido un error en el client_id.";
// Replace the one-line error return with a structured response including the invalid client_id
if ($url == null) {
return array(
"Status" => 400,
"to" => null,
"text" => "El client_id proporcionado (" . params_get("client_id") . ") no es válido."
);
}
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 115-116, the function
currently returns a plain string when $url is null; replace this with a
structured JSON error response that includes the provided client_id (e.g.,
{"error":"invalid_client_id","message":"Ha ocurrido un error en el
client_id.","client_id":<client_id>}), set the proper Content-Type:
application/json header and an appropriate HTTP status code (e.g., 400), and
ensure you reference the actual client_id variable rather than omitting it.

Comment on lines +118 to +134
$args = array(
CURLOPT_URL => $url . $clave,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"Authorization: Bearer " . params_get('token'),
"Cache-Control: no-cache",
"Content-Type: application/x-www-form-urlencoded"
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Mirror cURL hardening from consultarRecepcion.

Enable TLS verification, add connect timeout, prefer HTTPGET, and use Accept.

         CURLOPT_MAXREDIRS       => 10,
-        CURLOPT_SSL_VERIFYHOST  => 0,
-        CURLOPT_SSL_VERIFYPEER  => 0,
-        CURLOPT_TIMEOUT         => 30,
+        CURLOPT_SSL_VERIFYHOST  => 2,
+        CURLOPT_SSL_VERIFYPEER  => 1,
+        CURLOPT_CONNECTTIMEOUT  => 10,
+        CURLOPT_TIMEOUT         => 30,
         CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
-        CURLOPT_CUSTOMREQUEST   => "GET",
+        CURLOPT_HTTPGET         => true,
         CURLOPT_HTTPHEADER      => array(
             "Authorization: Bearer " . params_get('token'),
             "Cache-Control: no-cache",
-            "Content-Type: application/x-www-form-urlencoded"
+            "Accept: application/json"
         ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$args = array(
CURLOPT_URL => $url . $clave,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"Authorization: Bearer " . params_get('token'),
"Cache-Control: no-cache",
"Content-Type: application/x-www-form-urlencoded"
),
);
$args = array(
CURLOPT_URL => $url . $clave,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_SSL_VERIFYPEER => 1,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_HTTPGET => true,
CURLOPT_HTTPHEADER => array(
"Authorization: Bearer " . params_get('token'),
"Cache-Control: no-cache",
"Accept: application/json"
),
);
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 118 to 134, mirror the
hardening used in consultarRecepcion by enabling TLS verification (set
CURLOPT_SSL_VERIFYHOST to 2 and CURLOPT_SSL_VERIFYPEER to true), add a connect
timeout (CURLOPT_CONNECTTIMEOUT, e.g. 10 seconds), prefer using CURLOPT_HTTPGET
=> true instead of CURLOPT_CUSTOMREQUEST => "GET", and replace/augment headers
to include an Accept header (e.g. "Accept: application/json") while removing or
leaving Content-Type only if required for GET requests; ensure the Authorization
header remains unchanged.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
api/contrib/consultar/consultar.php (2)

24-26: Null-safe clave check and structured error response.

Avoid strlen on null and keep responses consistent with the rest of the module.

-    if ($clave == "" || strlen($clave) == 0)
-        return "La clave no puede ser en blanco";
+    $clave = trim((string)(params_get('clave') ?? ''));
+    if ($clave === '') {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "La clave no puede estar vacía"
+        );
+    }

165-178: Undefined variable $apiTo; also standardize return shape to match consultarRecepcion.

-    if ($err)
-    {
-        $arrayResp = array(
-            "Status" => $status,
-            "to" => $apiTo,
-            "text" => $err
-        );
-        return $arrayResp;
-    }
-    else
-    {
-         $response = json_decode($response);
-        return $response;
-    }
+    if ($err) {
+        return array(
+            "Status" => $status,
+            "to" => $url,
+            "text" => $err
+        );
+    }
+
+    $decoded = json_decode($response);
+    return array(
+        "Status" => $status,
+        "to" => $url,
+        "text" => $decoded !== null ? $decoded : $response
+    );
api/contrib/genXML/genXML.php (2)

343-349: Fix phone validation: comparing numeric value instead of length.

You’re comparing the phone number’s numeric value to min/max length constants, causing false negatives.

Apply this diff:

-if ($emisorCodPaisTel != '' && $emisorTel != '' && $emisorTel >= EMISORNUMEROTELMIN && $emisorTel <= EMISORNUMEROTELMAX) {
+if ($emisorCodPaisTel != '' && $emisorTel != '' && ctype_digit((string)$emisorTel)
+    && strlen((string)$emisorTel) >= EMISORNUMEROTELMIN && strlen((string)$emisorTel) <= EMISORNUMEROTELMAX) {

736-757: Don’t multiply MontoDescuento by cantidad.

Spec and example show montoDescuento is a line amount, not per-unit. Multiplying doubles the discount and breaks SubTotal.

Apply this diff:

-                    $c['montoDescuento'] = floatval($c['montoDescuento']) * $d->cantidad;
+                    $c['montoDescuento'] = round((float)$c['montoDescuento'], DECIMALES);
♻️ Duplicate comments (10)
api/core/params.php (1)

44-44: Default override=true restores unconditional-overwrite semantics — LGTM.

Matches prior recommendation to reintroduce override semantics with a safe default.

To catch any callers that relied on a previous default of false, scan usages:

#!/usr/bin/env bash
# All call sites (with context)
rg -nC2 '\bparams_set\s*\('

# Calls that explicitly pass false (good to double-check intent)
rg -nP '\bparams_set\s*\(\s*[^,]+,\s*[^,]+,\s*false\b'

# Potential bulk-set call sites (return shape may matter)
rg -nP '\bparams_set\s*\(\s*(\[|array\s*\()' -nC2
api/core/tools.php (1)

52-78: Guard $response['text']; reindex after unset and don't assume existence.

Avoid notices and preserve array JSON shape when removing index 0. Also guard the final assignment.

-        if (is_numeric($response['Status'])) {
+        if (is_numeric($response['Status'])) {
             $httpStatus = $response['Status'];
             $killMe = $response['Status'] >= 299;
-            if(is_array($response['text']) && count($response['text']) > 1) {
+            if (isset($response['text']) && is_array($response['text']) && count($response['text']) > 1) {
                 // cuando la respuesta es un array de Hacienda, el primer elemento es el código de estado
                 // y el resto es el mensaje
-                unset($response['text'][0]);
+                unset($response['text'][0]);
+                $response['text'] = array_values($response['text']); // reindexar
             }
         } else {
@@
-        $response = $response['text'];
+        $response = $response['text'] ?? $response;
api/contrib/consultar/consultar.php (6)

36-52: Harden cURL: enable TLS verification, add connect timeout, prefer HTTPGET, use Accept header.

         CURLOPT_URL             => $url . $clave,
         CURLOPT_RETURNTRANSFER  => true,
-        CURLOPT_HEADER          => true,
+        CURLOPT_HEADER          => true,
         CURLOPT_ENCODING        => "",
         CURLOPT_MAXREDIRS       => 10,
-        CURLOPT_SSL_VERIFYHOST  => 0,
-        CURLOPT_SSL_VERIFYPEER  => 0,
+        CURLOPT_SSL_VERIFYHOST  => 2,
+        CURLOPT_SSL_VERIFYPEER  => 1,
+        CURLOPT_CONNECTTIMEOUT  => 10,
         CURLOPT_TIMEOUT         => 30,
         CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
-        CURLOPT_CUSTOMREQUEST   => "GET",
+        CURLOPT_HTTPGET         => true,
         CURLOPT_HTTPHEADER      => array(
             "Authorization: Bearer " . params_get('token'),
             "Cache-Control: no-cache",
-            "Content-Type: application/x-www-form-urlencoded"
+            "Accept: application/json"
         ),

33-35: Invalid client_id: fix typo and return a structured 400.

-    if ($url == null)
-        return "El client_id proprocionado (".params_get("client_id").") no es válido. ";
+    if ($url === null) {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "El client_id proporcionado (" . params_get("client_id") . ") no es válido."
+        );
+    }

139-141: Invalid client_id: return structured 400 with provided client_id.

-    if ($url == null)
-        return "Ha ocurrido un error en el client_id.";
+    if ($url === null) {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "El client_id proporcionado (" . params_get("client_id") . ") no es válido."
+        );
+    }

142-157: Harden cURL (TLS verify, connect timeout, GET, Accept header).

         CURLOPT_URL             => $url . $clave,
         CURLOPT_RETURNTRANSFER  => true,
         CURLOPT_ENCODING        => "",
         CURLOPT_MAXREDIRS       => 10,
-        CURLOPT_SSL_VERIFYHOST  => 0,
-        CURLOPT_SSL_VERIFYPEER  => 0,
+        CURLOPT_SSL_VERIFYHOST  => 2,
+        CURLOPT_SSL_VERIFYPEER  => 1,
+        CURLOPT_CONNECTTIMEOUT  => 10,
         CURLOPT_TIMEOUT         => 30,
         CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
-        CURLOPT_CUSTOMREQUEST   => "GET",
+        CURLOPT_HTTPGET         => true,
         CURLOPT_HTTPHEADER      => array(
             "Authorization: Bearer " . params_get('token'),
             "Cache-Control: no-cache",
-            "Content-Type: application/x-www-form-urlencoded"
+            "Accept: application/json"
         ),

75-93: respuesta-xml parsing: guard base64_decode, fix strpos falsey check and substring math.

-        if(isset($responseObj->{'respuesta-xml'})) {
-            
-            $xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'});
-            $xmlRespuesta = preg_replace('/>\s+</', '><', $xmlRespuesta);
-            $startLength = strlen("tiene los siguientes errores: ")+7;
-            $startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ", 0);
-            if(!$startPos) {
-                // si no hay errores, retornar la respuesta normal
-                return $responseObj;
-            }
-            $endPos = strpos($xmlRespuesta, "</DetalleMensaje>", $startPos);
-            $detalleMensaje = substr($xmlRespuesta, $startPos+$startLength, $endPos -($startPos+$startLength+1));
-            $separadorMensaje = "&#13;\n";
-            $responseObj->{'DetalleMensaje'} = explode($separadorMensaje,trim($detalleMensaje,$separadorMensaje));
-        }
+        if (isset($responseObj->{'respuesta-xml'})) {
+            $xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'}, true);
+            if ($xmlRespuesta === false) {
+                return array("Status" => $status, "to" => $url, "text" => $responseObj);
+            }
+            $xmlRespuesta = preg_replace('/>\s+</', '><', $xmlRespuesta);
+            $needle = "tiene los siguientes errores: ";
+            $startPos = strpos($xmlRespuesta, $needle);
+            if ($startPos === false) {
+                return array("Status" => $status, "to" => $url, "text" => $responseObj);
+            }
+            $startLength = strlen($needle);
+            $endPos = strpos($xmlRespuesta, "</DetalleMensaje>", $startPos);
+            if ($endPos !== false) {
+                $detalleMensaje = substr($xmlRespuesta, $startPos + $startLength, $endPos - ($startPos + $startLength));
+                $separadorMensaje = "&#13;\n";
+                $responseObj->{'DetalleMensaje'} = explode($separadorMensaje, trim($detalleMensaje, $separadorMensaje));
+            }
+        }

130-132: Duplicate clave check: apply the same null-safe, structured response.

-    if ($clave == "" || strlen($clave) == 0)
-        return "La clave no puede ser en blanco";
+    $clave = trim((string)(params_get('clave') ?? ''));
+    if ($clave === '') {
+        return array(
+            "Status" => 400,
+            "to" => null,
+            "text" => "La clave no puede estar vacía"
+        );
+    }
api/contrib/genXML/genXML.php (2)

330-341: Do not hardcode OtrasSenas; use provided value and escape it.

Hardcoded "123456" leaks into all FE XMLs and breaks data integrity. Also, align the Distrito check with the others.

Apply this diff:

-    if ($emisorProv != '' && $emisorCanton != '' && $emisorDistrito) {
+    if ($emisorProv != '' && $emisorCanton != '' && $emisorDistrito != '') {
         $xmlString .= '<Ubicacion>
             <Provincia>' . $emisorProv . '</Provincia>
             <Canton>' . $emisorCanton . '</Canton>
             <Distrito>' . $emisorDistrito . '</Distrito>';
         if ($emisorBarrio != '') {
             $xmlString .= '<Barrio>' . $emisorBarrio . '</Barrio>';
         }
-        $xmlString .= '<OtrasSenas>123456</OtrasSenas>';
+        if ($emisorOtrasSenas != '') {
+            $xmlString .= '<OtrasSenas>' . htmlspecialchars($emisorOtrasSenas, ENT_XML1 | ENT_COMPAT, 'UTF-8') . '</OtrasSenas>';
+        }
         
         $xmlString .= '</Ubicacion>';
     }

33-34: Blocking: load core libs before calling tools_useTool().

This triggers the pipeline failure (undefined function). Require core/conf and core/tools before using tools_useTool.

Apply this diff:

+require_once dirname(__DIR__, 2) . '/core/conf.php';
+require_once dirname(__DIR__, 2) . '/core/tools.php';
 tools_useTool('ValidadorXML.php');
🧹 Nitpick comments (12)
api/contrib/clave/clave.php (4)

57-58: Allow optional fechaEmision override; “now” may misalign contingency/back-dated docs

Using CR timezone is good. Consider allowing an optional fechaEmision param (defaults to now) and validate it to keep the clave date consistent with the document’s emission date.

-    $date = new DateTime('now', new DateTimeZone('America/Costa_Rica'));
-    $fechaClave = $date->format('dmy');
+    $fechaEmision = params_get('fechaEmision'); // opcional: ISO 8601 o 'Y-m-d H:i:s'
+    $tz = new DateTimeZone('America/Costa_Rica');
+    if ($fechaEmision) {
+        try {
+            $date = new DateTime($fechaEmision, $tz);
+        } catch (Exception $e) {
+            return "El parametro fechaEmision no es valido";
+        }
+    } else {
+        $date = new DateTime('now', $tz);
+    }
+    $fechaClave = $date->format('dmy');

87-87: Avoid echoing raw consecutivo in error message

Prevents long/unsanitized input from being reflected in responses/logs.

-        return "El parametro consecutivo [$consecutivo] no es numeral";
+        return "El parametro consecutivo no es numeral";

189-195: Validate final clave (length=50, numeric) before returning

Adds a cheap integrity guard and clearer error if construction ever drifts.

-    $clave = $codigoPais . $fechaClave . $identificacion . $consecutivoFinal . $codSituacion . $codigoSeguridad;
+    $clave = $codigoPais . $fechaClave . $identificacion . $consecutivoFinal . $codSituacion . $codigoSeguridad;
+    if (strlen($clave) !== 50 || !ctype_digit($clave)) {
+        return "Clave generada invalida";
+    }

I can add a small unit test asserting structure and length (50) across a matrix of inputs if helpful.


117-117: Remove or guard debug call in production path

Avoids accidental leakage/noise in API responses/logs.

-    grace_debug($tipoDoc);
+    if (defined('APP_DEBUG') && APP_DEBUG) {
+        grace_debug($tipoDoc);
+    }
api/core/params.php (2)

39-43: Docblock is clearer; add PHPDoc tags for toolability.

Specify param/return types and note that $p is ignored on bulk sets.

 /**
  * Set config parameters.
  * - When $val is array, performs a bulk set.
  * - $override=true overwrites existing keys; false preserves existing values.
  * Returns the persisted value(s) from $params.
+ *
+ * @param string|mixed $p Key for scalar set (ignored when $val is array)
+ * @param mixed|array $val Value for scalar set, or assoc array for bulk set
+ * @param bool $override When true, overwrite existing keys
+ * @return mixed|array|null Persisted value(s); null if scalar write skipped and key absent
  */

50-56: Preserve input key order in bulk-set return.

array_intersect_key returns keys in $params’ order. If callers expect the order of provided $val, return in that order without changing semantics.

-        // Return the slice that reflects what is now stored
-        return array_intersect_key($params, $val);
+        // Return the slice that reflects what is now stored, preserving input key order
+        $out = [];
+        foreach (array_keys($val) as $k) {
+            if (array_key_exists($k, $params)) {
+                $out[$k] = $params[$k];
+            }
+        }
+        return $out;
api/core/tools.php (2)

85-86: Do not override a successful non-200 status to 200.

Preserve upstream 2xx codes (e.g., 201/204) rather than forcing 200.

-    } else {
-        $httpStatus = 200;
-    }
+    }

121-125: Set a single JSON Content-Type header (with charset).

Remove the text/html header and emit only application/json.

-        http_response_code($response['httpStatus']);
-        header('Content-Type: text/html; charset=utf-8');
-        header('Content-Type: application/json');
+        http_response_code($response['httpStatus']);
+        header('Content-Type: application/json; charset=utf-8');
api/contrib/consultar/consultar.php (1)

142-178: DRY: extract common cURL args into a helper to avoid divergence.

Both consultarRecepcion and consultarComprobante share nearly identical cURL setup and error handling. Consider a small internal helper (e.g., buildCurlArgs($url, $headers, $withHeaders = false)) to keep options consistent.

api/contrib/genXML/genXML.php (3)

1467-1471: Consistent error handling for emisor email in NC.

Here you return 400 (good), but FE and others only log. Consider aligning behavior across generators.

Do you want a unifying helper for email validation + reply?


351-356: Escape/sanitize receptor email and other free-text fields.

CorreoElectronico and free-text tags should be trimmed/escaped to avoid invalid XML on special chars.

I can generate a small helper to safely append optional string tags with htmlspecialchars and length checks.

Also applies to: 407-410


23-23: Typo in constant name or unused constant.

CODIDOREFVALUES looks misspelled and is unused; consider renaming or removing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ce227fc and 967f82a.

📒 Files selected for processing (7)
  • api/contrib/clave/clave.php (3 hunks)
  • api/contrib/consultar/consultar.php (3 hunks)
  • api/contrib/crlibreall/module.php (1 hunks)
  • api/contrib/genXML/genXML.php (62 hunks)
  • api/contrib/genXML/module.php (6 hunks)
  • api/core/params.php (1 hunks)
  • api/core/tools.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
api/core/tools.php (1)
api/core/conf.php (1)
  • conf_get (26-34)
api/contrib/consultar/consultar.php (1)
api/core/params.php (1)
  • params_get (24-36)
api/contrib/genXML/genXML.php (4)
api/core/tools.php (2)
  • tools_useTool (169-177)
  • tools_reply (19-102)
api/core/params.php (1)
  • params_get (24-36)
api/tools/ValidadorXML.php (2)
  • ValidadorXML (3-97)
  • validateXml (43-96)
api/core/conf.php (1)
  • conf_get (26-34)
🪛 GitHub Actions: PHPUnit Tests
api/contrib/genXML/genXML.php

[error] 33-33: Call to undefined function tools_useTool() in api/contrib/genXML/genXML.php:33. Command: './vendor/bin/phpunit --stderr --debug --bootstrap vendor/autoload.php tests/api_contrib_genXML_FE.php'.

🔇 Additional comments (4)
api/core/params.php (1)

57-60: Drop null-handing check—params_set never returns null.

Likely an incorrect or invalid review comment.

api/contrib/crlibreall/module.php (1)

113-114: Making informacion_referencia optional for FE path looks good.

Confirm downstream genXML handles missing reference info gracefully (no required XSD nodes emitted).

api/contrib/genXML/module.php (1)

121-123: Optionalizing informacion_referencia across all genXML routes: OK.

Ensure genXML.php defaults omit the node entirely when absent to satisfy XSD, and validators don’t expect it by default.

Also applies to: 196-198, 271-273, 345-347, 418-420, 478-480

api/contrib/genXML/genXML.php (1)

279-289: Good: medios_pago required when condicion_venta not in {02,08,10}.

This prevents inconsistent payments vs totals.

Comment on lines +102 to +116
if(empty($body)) {
$header_lines = explode("\r\n", trim($headers));
$header_array = [];
foreach ($header_lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(': ', $line, 2);
$header_array[$key] = $value;
}
}
return array(
"Status" => $header_array['x-http-status'],
"to" => $url,
"text" => $header_array['x-error-cause']
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Header-only error path: guard missing Hacienda headers and fall back to $status.

-        return array(
-            "Status" => $header_array['x-http-status'],
-            "to" => $url,
-            "text" => $header_array['x-error-cause']
-        );
+        return array(
+            "Status" => isset($header_array['x-http-status']) ? (int)$header_array['x-http-status'] : $status,
+            "to" => $url,
+            "text" => $header_array['x-error-cause'] ?? 'Error desconocido'
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(empty($body)) {
$header_lines = explode("\r\n", trim($headers));
$header_array = [];
foreach ($header_lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(': ', $line, 2);
$header_array[$key] = $value;
}
}
return array(
"Status" => $header_array['x-http-status'],
"to" => $url,
"text" => $header_array['x-error-cause']
);
}
if (empty($body)) {
$header_lines = explode("\r\n", trim($headers));
$header_array = [];
foreach ($header_lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(': ', $line, 2);
$header_array[$key] = $value;
}
}
return array(
"Status" => isset($header_array['x-http-status'])
? (int)$header_array['x-http-status']
: $status,
"to" => $url,
"text" => $header_array['x-error-cause'] ?? 'Error desconocido'
);
}
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 102 to 116, the header-only
error path assumes Hacienda headers x-http-status and x-error-cause always
exist; add guards to check those keys in the parsed $header_array before using
them and fall back to the existing $status variable (or a sensible default like
'unknown') for "Status" and a generic message for "text" when headers are
missing; ensure header parsing is resilient (trim lines, handle headers without
space after colon) and return the array using the guarded values.

Comment on lines +908 to +914
if(isset($d->codigoCABYS) && in_array($d->codigoCABYS[0], ['5','6','7','8','9'], true)) {
// Servicio
$calculados[$resumenKeys['servicio']] += ($calculados[$l]['montoTotal']);
} else {
// Mercancia
$calculados[$resumenKeys['mercancia']] += ($calculados[$l]['montoTotal']);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard CABYS index access; cast to string.

Accessing $d->codigoCABYS[0] on numeric input can emit notices.

Apply this diff:

-                    if(isset($d->codigoCABYS) && in_array($d->codigoCABYS[0], ['5','6','7','8','9'], true)) {
+                    $codigoCABYSStr = (string)$d->codigoCABYS;
+                    if ($codigoCABYSStr !== '' && in_array($codigoCABYSStr[0], ['5','6','7','8','9'], true)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(isset($d->codigoCABYS) && in_array($d->codigoCABYS[0], ['5','6','7','8','9'], true)) {
// Servicio
$calculados[$resumenKeys['servicio']] += ($calculados[$l]['montoTotal']);
} else {
// Mercancia
$calculados[$resumenKeys['mercancia']] += ($calculados[$l]['montoTotal']);
}
$codigoCABYSStr = (string)$d->codigoCABYS;
if ($codigoCABYSStr !== '' && in_array($codigoCABYSStr[0], ['5','6','7','8','9'], true)) {
// Servicio
$calculados[$resumenKeys['servicio']] += ($calculados[$l]['montoTotal']);
} else {
// Mercancia
$calculados[$resumenKeys['mercancia']] += ($calculados[$l]['montoTotal']);
}

Comment on lines 1344 to 1345
// $totalComprobante = params_get("total_comprobante");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

NC: $totalComprobante is undefined (commented out) but later used.

This will produce notices and invalid XML. Restore the param or compute it.

Apply one of the following:

  • Minimal restore:
-    // $totalComprobante = params_get("total_comprobante");
+    $totalComprobante = params_get("total_comprobante");
  • Prefer computed:
+    $totalComprobanteCalc = (float)$totalVentasNeta + (float)$totalImp + (float)($totalOtrosCargos ?: 0) - (float)($totalIVADevuelto ?: 0);
@@
-        <TotalComprobante>' . $totalComprobante . '</TotalComprobante>
+        <TotalComprobante>' . number_format($totalComprobanteCalc, DECIMALES, '.', '') . '</TotalComprobante>

Also applies to: 2036-2038

🤖 Prompt for AI Agents
In api/contrib/genXML/genXML.php around lines 1344-1345 (and similarly at
2036-2038), $totalComprobante is referenced but commented out/undefined; restore
it or compute it before use. Fix by either reintroducing the
params_get("total_comprobante") assignment where appropriate, or (preferred)
calculate $totalComprobante from the invoice line items (sum of each line's
total = quantity * unit_price + taxes/adjustments as used elsewhere), assign the
correctly formatted numeric string (same decimal places/locale as other monetary
values) to $totalComprobante before it’s inserted into the XML, and ensure any
related code paths use this computed value to avoid notices and invalid XML.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
api/contrib/signXML/Firmadohaciendacr.php (2)

201-206: Fix fatal: call instance method via $this->stringHex2StringDec.

Currently it calls an undefined global function, causing a fatal error when serialNumber is hex.

-            $serialNumber = stringHex2StringDec($certData['serialNumber']);
+            $serialNumber = $this->stringHex2StringDec($certData['serialNumber']);

114-116: Validate base64 and XML load errors.

base64_decode can return false; loadXML can fail. Return a 400 early in those cases.

-        $xml1                     = base64_decode($xmlsinfirma);
+        $xml1 = base64_decode($xmlsinfirma, true);
+        if ($xml1 === false) {
+            tools_reply(["Status" => 400, "text" => "XML base64 inválido"]);
+            return base64_encode("");
+        }
 ...
-        $d->loadXML($xml);
+        if (@$d->loadXML($xml) === false) {
+            tools_reply(["Status" => 400, "text" => "XML mal formado"]);
+            return;
+        }

Also applies to: 135-139

♻️ Duplicate comments (3)
api/contrib/signXML/Firmadohaciendacr.php (3)

128-133: Return immediately after tools_reply to stop further processing.

Without an early return, execution continues and attempts to sign with invalid state.

         if (is_null($this->publicKey) || is_null($this->privateKey) || is_null($xml)) {
             tools_reply([
                 "Status" => 400,
                 "text" => "Al firmar, el documento o las claves no pueden estar vacías",
             ]);
+            return;
         }

174-186: Replace fragile parsing with DOM/XPath and compare the exact 12-digit identificación segment. Add early return.

strpos/substr is brittle and checking “contains” can pass false positives. Extract the 12-digit identificación segment from CLAVE (pos 9, len 12) and compare to the certificate ID normalized and left-padded to 12.

-        $startPos = strpos($xml, "<Clave>", 0); 
-        $clave = substr($xml, $startPos+7, 50);
-        
-        $issuerCedula = $certData['subject']['serialNumber'];
-        $issuerCedula = preg_replace('/^(CPF|CPJ)/', '', $issuerCedula);
-        $issuerCedula = str_replace('-', '', $issuerCedula);
-        $issuerCedula = ltrim($issuerCedula, '0');
-        if(strpos($clave, $issuerCedula) === false){
-            tools_reply([
-                "Status" => 400,
-                "text" => "El número de cédula del certificado ($issuerCedula) no coincide con el indicado en la CLAVE en el XML ($clave)",
-            ]);
-        }
+        $xpath = new DOMXPath($d);
+        $claveNode = $xpath->query('/*/*[local-name()="Clave"]')->item(0);
+        if (!$claveNode) {
+            tools_reply(["Status" => 400, "text" => "El XML no contiene la etiqueta <Clave>"]);
+            return;
+        }
+        $claveRaw = trim($claveNode->textContent);
+        $claveNorm = preg_replace('/\s+/', '', $claveRaw);
+        if (strlen($claveNorm) < 21) { // requiere al menos 21 para extraer desde pos 9, largo 12
+            tools_reply(["Status" => 400, "text" => "El valor de <Clave> es inválido o incompleto"]);
+            return;
+        }
+        $issuerCedula = $certData['subject']['serialNumber'];
+        $issuerCedula = preg_replace('/^(CPF|CPJ)/', '', $issuerCedula);
+        $issuerCedula = str_replace('-', '', $issuerCedula);
+        $issuerCedula = ltrim($issuerCedula, '0');
+        $issuerCedula12 = str_pad($issuerCedula, 12, '0', STR_PAD_LEFT);
+        $claveIdent = substr($claveNorm, 9, 12);
+        if ($claveIdent !== $issuerCedula12) {
+            tools_reply([
+                "Status" => 400,
+                "text" => "La cédula del certificado ($issuerCedula12) no coincide con la identificación en la CLAVE ($claveIdent)"
+            ]);
+            return;
+        }

188-199: Use XPath for Emisor/Identificacion/Numero (and Receptor fallback). Add early return.

The current strpos/substr will break on formatting changes or missing tags. Also consider MensajeReceptor where Receptor may be relevant.

-        $startPos = strpos($xml, "<Emisor>", 0);
-        $startPos = strpos($xml, "<Identificacion>", $startPos); 
-        $startPos = strpos($xml, "<Numero>", $startPos);
-        $endPos = strpos($xml, "</Numero>", $startPos);
-        $emisorNumero = substr($xml, $startPos+8, $endPos   -($startPos+8));
-
-        if($emisorNumero !== $issuerCedula){
-            tools_reply([
-                "Status" => 400,
-                "text" => "El número de cédula del certificado ($issuerCedula) no coincide con el indicado en el EMISOR en el XML ($emisorNumero)",
-            ]);
-        }
+        $emisorNumeroNode = $xpath->query('/*/*[local-name()="Emisor"]/*[local-name()="Identificacion"]/*[local-name()="Numero"]')->item(0);
+        if (!$emisorNumeroNode) {
+            // fallback para documentos donde solo exista Receptor
+            $emisorNumeroNode = $xpath->query('/*/*[local-name()="Receptor"]/*[local-name()="Identificacion"]/*[local-name()="Numero"]')->item(0);
+        }
+        if (!$emisorNumeroNode) {
+            tools_reply(["Status" => 400, "text" => "El XML no contiene Emisor/Receptor con Identificacion/Numero"]);
+            return;
+        }
+        $emisorNumero = trim($emisorNumeroNode->textContent);
+        if ($emisorNumero !== $issuerCedula) {
+            tools_reply([
+                "Status" => 400,
+                "text" => "El número de cédula del certificado ($issuerCedula) no coincide con el indicado en el XML ($emisorNumero)",
+            ]);
+            return;
+        }
🧹 Nitpick comments (3)
api/contrib/signXML/Firmadohaciendacr.php (3)

208-247: Ensure the SignedProperties digest matches the embedded element.

You compute $propDigest from $aconop (with added xmlns) but embed $prop. If those differ in namespace declarations under canonicalization, digest verification can fail. Either embed the same string you hashed or hash what you embed.

Minimal tweak:

-       $prop = '<xades:SignedProperties Id="' . $this->SignedProperties .  '">' .
+       $prop = '<xades:SignedProperties Id="' . $this->SignedProperties .  '">' .
         ...
        $aconop     = str_replace('<xades:SignedProperties', '<xades:SignedProperties ' . $xmnls_signedprops, $prop);
        $propDigest = $this->retC14DigestSha1($aconop);
+       // Embed the exact content that was hashed
+       $prop = $aconop;

And ensure the ds:Object later embeds $prop (unchanged).


52-60: Rename or document: retC14DigestSha1 uses SHA-256, not SHA-1.

Name is misleading; either rename to retC14DigestSha256 or update the comment to avoid confusion.

-    public function retC14DigestSha1($strcadena)
+    public function retC14DigestSha256($strcadena)
 ...
-        return base64_encode(hash('sha256', $strcadena, true));
+        return base64_encode(hash('sha256', $strcadena, true));

And update the few call sites accordingly.


162-162: Optional: derive timezone dynamically or from config.

Hardcoding -06:00 is fine for CR, but consider conf_get to keep this portable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between aa8e7ee and 613b71c.

📒 Files selected for processing (1)
  • api/contrib/signXML/Firmadohaciendacr.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
api/contrib/signXML/Firmadohaciendacr.php (1)
api/core/tools.php (1)
  • tools_reply (19-102)

Comment on lines +98 to 100
echo "Error: No se puede leer el almacén de certificados o la clave no es la correcta [$clavecertificado].\n";
exit;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not log secrets (certificate passphrase leaked).

The error message includes [$clavecertificado], which is sensitive. Avoid printing passphrases anywhere (stdout, logs).

Apply:

-            echo "Error: No se puede leer el almacén de certificados o la clave no es la correcta [$clavecertificado].\n";
+            echo "Error: No se puede leer el almacén de certificados o la clave no es la correcta.\n";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "Error: No se puede leer el almacén de certificados o la clave no es la correcta [$clavecertificado].\n";
exit;
}
echo "Error: No se puede leer el almacén de certificados o la clave no es la correcta.\n";
exit;
🤖 Prompt for AI Agents
In api/contrib/signXML/Firmadohaciendacr.php around lines 98-100 the error
prints the certificate passphrase variable [$clavecertificado], leaking a
secret; remove the passphrase from any output and replace the echo with a
generic error message (e.g. "Error: No se puede leer el almacén de certificados
o la clave no es la correcta.") and exit; if additional context is needed for
debugging, log a non-sensitive identifier (certificate filename or fingerprint)
or store the detailed error in a secure audit log accessible only to authorized
operators, never include the passphrase in stdout/logs.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
api/tools/ValidadorXML.php (3)

81-96: Add explicit success return and harden XML load/validation (XXE-safe, base64 support, restore libxml state).

On success, the function returns null. Also, XML is loaded without guarding against XXE/network fetch and without checking load failures. Patch below consolidates: base64 decode if provided, use LIBXML_NONET, return early on load failure, restore libxml error state on all paths, and return a success object.

-        libxml_use_internal_errors(true);
-        $xml = new DOMDocument();
-        $xml->loadXML($contenido_xml);
-
-        if (!$xml->schemaValidate($schemas[$tipo_doc])) {
-            $errors = ValidadorXML::libxml_display_errors();
-            $r = [
-                "status"  => "error",
-                "schema"  => $schemas[$tipo_doc],
-                "message" => $errors,
-                "xml"     => $contenido_xml
-            ];
-            if(count($errors) === 0) {
-                $r["status"] = "ok";
-                $r["message"] = "El XML es válido";
-                unset($r["xml"]);
-            }
-            return (object) $r;
-        }
+        $prevUseInternal = libxml_use_internal_errors(true);
+        $xml = new DOMDocument();
+        // Acepta XML en base64 o como cadena UTF-8 directa
+        $decoded = base64_decode($contenido_xml, true);
+        $xmlString = ($decoded !== false) ? $decoded : $contenido_xml;
+        if (@$xml->loadXML($xmlString, LIBXML_NONET) === false) {
+            $errors = ValidadorXML::libxml_display_errors();
+            libxml_use_internal_errors($prevUseInternal);
+            return (object)[
+                "status"  => "error",
+                "message" => $errors ?: "Error: XML inválido o mal formado"
+            ];
+        }
+
+        if (!$xml->schemaValidate($schemas[$tipo_doc])) {
+            $errors = ValidadorXML::libxml_display_errors();
+            $r = [
+                "status"  => "error",
+                "schema"  => $schemas[$tipo_doc],
+                "message" => $errors ?: "Error: el XML no cumple con el XSD especificado",
+            ];
+            // Solo incluir el XML con una bandera de configuración explícita
+            if (conf_get('includeXmlOnValidationError', 'xsd', false)) {
+                $r["xml"] = $xmlString;
+            }
+            libxml_use_internal_errors($prevUseInternal);
+            return (object) $r;
+        }
+        libxml_use_internal_errors($prevUseInternal);
+        return (object) ["status" => "ok", "message" => "El XML es válido"];

46-47: Avoid brittle path rewriting; add a dedicated config and robust fallback.

The current regex only matches “…/api/” trailing slash and can yield “…/www/” double segment or miss “…/api” without slash. Prefer a dedicated xsd base path with a safe sibling fallback.

-        $baseFolder = conf_get('coreInstall', 'modules', '');
-        $baseFolder = preg_replace('/\/api\/$/', '/www/', $baseFolder); 
+        // Prefer a dedicated config; fallback derives sibling 'www' next to 'api'
+        $baseFolder = rtrim(conf_get('xsdBasePath', 'core', ''), '/');
+        if ($baseFolder === '') {
+            $root = rtrim(conf_get('coreInstall', 'modules', ''), '/');
+            $baseFolder = preg_replace('#/api$#', '/www', $root);
+        }

48-55: Fix XSD mappings for "02" and "05".

Debit Note ("02") and Message ("05") point to likely wrong XSDs. Update to NotaDebitoElectronica and MensajeReceptor respectively. Please verify filenames in www/xsd.

             "01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
-            "02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
+            "02" => "$baseFolder/xsd/NotaDebitoElectronica_V$version.xsd",
             "03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
             "04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
-            "05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
+            "05" => "$baseFolder/xsd/MensajeReceptor_V$version.xsd",

To verify filenames exist for the selected version:

#!/bin/bash
# List XSDs for v4.4 and check the two expected files
fd -a --glob 'www/xsd/*V4.4*.xsd' || true
🧹 Nitpick comments (3)
api/tools/ValidadorXML.php (3)

6-23: Enrich error details with line and column numbers.

Include line/column/code to speed up pinpointing schema violations.

         switch ($error->level) {
@@
         }
-        $r["mensaje"] = $error->message;
+        $r["mensaje"] = trim($error->message);
+        if (isset($error->line))   $r["linea"]   = $error->line;
+        if (isset($error->column)) $r["columna"] = $error->column;
+        if (isset($error->code))   $r["codigo"]  = $error->code;

37-42: DocBlock mismatches parameters and encoding.

Update docs to match the actual signature and accepted encodings (raw or base64).

-     * @param string $contenido_xml - XML sin firmar en base64
-     * @param string $tipo_doc - tipo de documento a validar
-     * @return type
+     * @param string $contenido_xml - XML sin firmar (cadena UTF-8 o base64)
+     * @param string $consecutivo   - consecutivo; de aquí se deriva el tipo de documento
+     * @return object               - {status: "ok"|"error", message: string|array, schema?: string, xml?: string}

45-56: Validate consecutivo before extracting tipo_doc.

Insert the following check before the existing substr call in api/tools/ValidadorXML.php:

-        $tipo_doc = substr($consecutivo, 8, 2);
+        if (!is_string($consecutivo) || strlen($consecutivo) < 10) {
+            return (object)[
+                "status"  => "error",
+                "message" => "Error: consecutivo inválido; no se puede derivar el tipo de documento"
+            ];
+        }
+        $tipo_doc = substr($consecutivo, 8, 2);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 613b71c and 223b51c.

📒 Files selected for processing (1)
  • api/tools/ValidadorXML.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
api/tools/ValidadorXML.php (1)
api/core/conf.php (1)
  • conf_get (26-34)

Comment on lines +43 to +55
public static function validateXml($contenido_xml, $consecutivo, $version = "4.4")
{
$tipo_doc = substr($consecutivo, 8, 2);
$baseFolder = conf_get('coreInstall', 'modules', '');
$baseFolder = preg_replace('/\/api\/$/', '/www/', $baseFolder);
$schemas = [
"01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
"02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
"03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
"04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
"05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate and sanitize the $version input to prevent path traversal.

Since $version interpolates into a filesystem path, constrain it to digits and dots.

-        $schemas = [
+        if (!preg_match('/^\d+(?:\.\d+)*$/', $version)) {
+            return (object)[
+                "status"  => "error",
+                "message" => "Error: versión inválida ($version)"
+            ];
+        }
+        $schemas = [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static function validateXml($contenido_xml, $consecutivo, $version = "4.4")
{
$tipo_doc = substr($consecutivo, 8, 2);
$baseFolder = conf_get('coreInstall', 'modules', '');
$baseFolder = preg_replace('/\/api\/$/', '/www/', $baseFolder);
$schemas = [
"01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
"02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
"03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
"04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
"05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
];
public static function validateXml($contenido_xml, $consecutivo, $version = "4.4")
{
$tipo_doc = substr($consecutivo, 8, 2);
$baseFolder = conf_get('coreInstall', 'modules', '');
$baseFolder = preg_replace('/\/api\/$/', '/www/', $baseFolder);
// Ensure $version contains only digits and dots to avoid path traversal
if (!preg_match('/^\d+(?:\.\d+)*$/', $version)) {
return (object)[
"status" => "error",
"message" => "Error: versión inválida ($version)"
];
}
$schemas = [
"01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
"02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
"03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
"04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
"05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
];
🤖 Prompt for AI Agents
In api/tools/ValidadorXML.php around lines 43 to 55, the $version value is
interpolated directly into filesystem paths which allows path traversal;
validate and sanitize $version before use by restricting it to an allowlist or a
strict pattern (e.g. only digits and dots) and reject or normalize any other
input, or map allowed version strings to known filenames; then use the
sanitized/mapped version when composing the $schemas paths so only safe,
expected paths are constructed.

Comment on lines +83 to +89
$r = [
"status" => "error",
"schema" => $schemas[$tipo_doc],
"message" => $errors,
"xml" => $contenido_xml
];
if(count($errors) === 0) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Do not echo back full XML by default (PII leakage risk).

Returning the entire XML in errors can leak sensitive invoice data. Gate it behind an explicit config flag (as in the consolidated patch) or remove it.

-            $r = [
+            $r = [
                 "status"  => "error",
                 "schema"  => $schemas[$tipo_doc],
                 "message" => $errors,
-                "xml"     => $contenido_xml
             ];
+            // Opcional: solo incluir el XML si está habilitado explícitamente
+            if (conf_get('includeXmlOnValidationError', 'xsd', false)) {
+                $r["xml"] = $contenido_xml;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$r = [
"status" => "error",
"schema" => $schemas[$tipo_doc],
"message" => $errors,
"xml" => $contenido_xml
];
if(count($errors) === 0) {
$r = [
"status" => "error",
"schema" => $schemas[$tipo_doc],
"message" => $errors,
];
// Opcional: solo incluir el XML si está habilitado explícitamente
if (conf_get('includeXmlOnValidationError', 'xsd', false)) {
$r["xml"] = $contenido_xml;
}
if (count($errors) === 0) {
🤖 Prompt for AI Agents
In api/tools/ValidadorXML.php around lines 83 to 89, the code currently includes
the full XML content in the error response which risks leaking PII; change this
so the "xml" field is only included when an explicit configuration flag (e.g.
SHOW_FULL_XML_ON_ERROR or similar) is enabled, defaulting to false. Implement a
config lookup at the top of the function or class, remove "xml" from the default
$r array, and conditionally add $r['xml'] = $contenido_xml (or a
redacted/truncated version) only if the config flag is true; ensure the flag is
documented and defaults to off so production does not expose full XML by
default.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
api/contrib/genXML/module.php (1)

523-523: Remove trailing blank line.

There's an unnecessary trailing blank line at the end of the file.

Apply this diff to remove the trailing blank line:

-
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 223b51c and 2eb5219.

📒 Files selected for processing (2)
  • api/contrib/genXML/module.php (7 hunks)
  • www/xsd/NotaCreditoElectronica_V4.4.xsd (0 hunks)
💤 Files with no reviewable changes (1)
  • www/xsd/NotaCreditoElectronica_V4.4.xsd
🔇 Additional comments (6)
api/contrib/genXML/module.php (6)

121-121: LGTM - Consistent with system-wide optimization.

The change to make informacion_referencia non-required aligns with the broader genXML refactor mentioned in the AI summary. This parameter is now consistently marked as optional across all document types (FE, NC, ND, TE, FEC, FEE).


196-196: LGTM - Consistent parameter optimization.

Making informacion_referencia optional for credit notes (NC) maintains consistency with the FE path changes and supports the broader genXML validation improvements.


271-271: LGTM - Consistent parameter optimization.

Making informacion_referencia optional for debit notes (ND) maintains consistency with other document types in this refactor.


345-345: LGTM - Consistent parameter optimization.

Making informacion_referencia optional for electronic tickets (TE) maintains consistency with other document types in this refactor.


418-418: LGTM - Consistent parameter optimization.

Making informacion_referencia optional for purchase invoices (FEC) maintains consistency with other document types in this refactor.


478-478: LGTM - Consistent parameter optimization.

Making informacion_referencia optional for export invoices (FEE) maintains consistency with other document types in this refactor.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (7)
api/core/tools.php (2)

57-61: Guard array access and reindex after unset.

The code assumes $response['text'] exists and is an array without verification, and after unset($response['text'][0]), the array may not be zero-indexed for JSON encoding.

Based on learnings

Apply this diff:

-        if (is_numeric($response['Status'])) {
+        if (is_numeric($response['Status'])) {
             $httpStatus = $response['Status'];
             $killMe = $response['Status'] >= 299;
-            if(is_array($response['text']) && count($response['text']) > 1) {
+            if (isset($response['text']) && is_array($response['text']) && count($response['text']) > 1) {
                 // cuando la respuesta es un array de Hacienda, el primer elemento es el código de estado
                 // y el resto es el mensaje
                 unset($response['text'][0]);
+                $response['text'] = array_values($response['text']); // reindexar para JSON
             }
         }

77-77: Guard against missing 'text' key.

The assignment assumes $response['text'] exists, which may not be true for all response structures.

Apply this diff:

-        $response = $response['text'];
+        $response = $response['text'] ?? $response;
api/contrib/consultar/consultar.php (5)

84-88: Fix falsy strpos check and inconsistent return format.

  • Line 84: !$startPos treats position 0 as "not found"
  • Line 87: Returns $responseObj directly instead of the wrapped array format used elsewhere

Based on learnings

Apply this diff:

             $startLength = strlen("tiene los siguientes errores: ") + 7;
             $startPos = strpos($xmlRespuesta, "tiene los siguientes errores: ", 0);
-            if (!$startPos) {
+            if ($startPos === false) {
                 // si no hay errores, retornar la respuesta normal
-                return $responseObj;
+                return array(
+                    "Status" => $status,
+                    "to" => $url,
+                    "text" => $responseObj
+                );
             }

90-90: Fix off-by-one error in substr length calculation.

The calculation adds an extra +1 that shortens the extracted message by one character.

Based on learnings

Apply this diff:

-            $detalleMensaje = substr($xmlRespuesta, $startPos + $startLength, $endPos - ($startPos + $startLength + 1));
+            $detalleMensaje = substr($xmlRespuesta, $startPos + $startLength, $endPos - ($startPos + $startLength));

111-115: Guard against missing Hacienda headers.

The code assumes x-http-status and x-error-cause headers always exist, which may not be true.

Based on learnings

Apply this diff:

         return array(
-            "Status" => $header_array['x-http-status'],
+            "Status" => isset($header_array['x-http-status']) ? (int)$header_array['x-http-status'] : $status,
             "to" => $url,
-            "text" => $header_array['x-error-cause']
+            "text" => $header_array['x-error-cause'] ?? 'Error desconocido'
         );

151-152: Enable TLS verification for production security.

Same security issue as in consultarRecepcion - SSL verification is disabled.

Apply this diff:

-        CURLOPT_SSL_VERIFYHOST => 0,
-        CURLOPT_SSL_VERIFYPEER => 0,
+        CURLOPT_SSL_VERIFYHOST => 2,
+        CURLOPT_SSL_VERIFYPEER => 1,
+        CURLOPT_CONNECTTIMEOUT => 10,

42-43: Enable TLS verification for production security.

Disabling SSL verification (VERIFYHOST=0, VERIFYPEER=0) exposes the application to man-in-the-middle attacks.

Apply this diff:

-        CURLOPT_SSL_VERIFYHOST => 0,
-        CURLOPT_SSL_VERIFYPEER => 0,
+        CURLOPT_SSL_VERIFYHOST => 2,
+        CURLOPT_SSL_VERIFYPEER => 1,
+        CURLOPT_CONNECTTIMEOUT => 10,
🧹 Nitpick comments (4)
api/contrib/consultar/consultar.php (4)

24-25: Use empty() to avoid strlen warnings on null.

Using strlen() on a potentially null value can emit warnings. The validation also has redundant checks.

Apply this diff:

-    if ($clave == "" || strlen($clave) == 0)
+    if (empty($clave) || !is_string($clave))
         return "La clave no puede ser en blanco";

81-81: Use strict mode for base64_decode.

Without strict mode, base64_decode returns false for invalid input but may also return an empty string, making error detection ambiguous.

Apply this diff:

-            $xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'});
+            $xmlRespuesta = base64_decode($responseObj->{'respuesta-xml'}, true);
+            if ($xmlRespuesta === false) {
+                return array("Status" => $status, "to" => $url, "text" => $responseObj);
+            }

131-133: Use empty() to avoid strlen warnings on null.

Same issue as in consultarRecepcion - strlen() can warn on null values.

Apply this diff:

-    if ($clave == "" || strlen($clave) == 0) {
+    if (empty($clave) || !is_string($clave)) {
         return "La clave no puede ser en blanco";
     }

199-205: Remove unused JSON decode or use decoded object.

Line 199 decodes $body into $responseObj but line 204 returns the raw $body string instead. Either remove the unused decode or return $responseObj as the text field.

If you intend to return the decoded object:

     $responseObj = json_decode($body);

     return tools_reply(array(
         "Status" => $status,
         "to" => $url,
-        "text" => $body
+        "text" => $responseObj ?? $body
     ), true);

Or if raw body is intended, remove the unused decode:

-    $responseObj = json_decode($body);
-
     return tools_reply(array(
         "Status" => $status,
         "to" => $url,
         "text" => $body
     ), true);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb5219 and f10f714.

📒 Files selected for processing (2)
  • api/contrib/consultar/consultar.php (2 hunks)
  • api/core/tools.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
api/core/tools.php (1)
api/core/conf.php (1)
  • conf_get (26-34)
api/contrib/consultar/consultar.php (2)
api/core/params.php (1)
  • params_get (24-36)
api/core/tools.php (1)
  • tools_reply (19-103)
🪛 PHPMD (2.15.0)
api/contrib/consultar/consultar.php

199-199: Avoid unused local variables such as '$responseObj'. (undefined)

(UnusedLocalVariable)

Comment on lines +192 to +196
return tools_reply(array(
"Status" => $header_array['x-http-status'] ?? $status,
"to" => $url,
"text" => $header_array['x-error-cause']
), true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard x-error-cause access consistently.

Line 193 correctly guards x-http-status with null coalescing, but line 195 still accesses x-error-cause without verification.

Apply this diff:

         return tools_reply(array(
             "Status" => $header_array['x-http-status'] ?? $status,
             "to" => $url,
-            "text" => $header_array['x-error-cause']
+            "text" => $header_array['x-error-cause'] ?? 'Error desconocido'
         ), true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return tools_reply(array(
"Status" => $header_array['x-http-status'] ?? $status,
"to" => $url,
"text" => $header_array['x-error-cause']
), true);
return tools_reply(array(
"Status" => $header_array['x-http-status'] ?? $status,
"to" => $url,
"text" => $header_array['x-error-cause'] ?? 'Error desconocido'
), true);
🤖 Prompt for AI Agents
In api/contrib/consultar/consultar.php around lines 192 to 196, the return
builds a response but accesses $header_array['x-error-cause'] without guarding
it; change that access to use a null-coalescing fallback (or isset) like
$header_array['x-error-cause'] ?? null (or empty string) so the key is checked
before use and prevents undefined index notices, leaving the rest of the return
structure unchanged.

Comment on lines 84 to 86
} else {
http_response_code(200);
$httpStatus = 200;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove status override that discards computed HTTP status.

Line 85 unconditionally sets $httpStatus = 200 for non-error paths, discarding the status computed earlier (lines 54-76). This means success responses always return 200 even if a different success status was intended.

Apply this diff:

     if ($killMe) {
         if (is_string($response)) {
             $response = "ERROR: " . $response;
         }
-    } else {
-        $httpStatus = 200;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else {
http_response_code(200);
$httpStatus = 200;
}
if ($killMe) {
if (is_string($response)) {
$response = "ERROR: " . $response;
}
}
🤖 Prompt for AI Agents
In api/core/tools.php around lines 84 to 86, remove the unconditional override
that sets $httpStatus = 200; so that previously computed status values are
preserved; either delete the else block entirely or change it to only set a
default when $httpStatus is not already set (e.g., check for null/empty) so
success responses keep their intended HTTP status.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/modules/wirez/messagesSend.php (1)

56-56: Fix incorrect json_decode usage and double-escaping.

Line 56 has a critical bug:

  • ENT_QUOTES is not a valid parameter for json_decode. The second parameter expects a boolean (associative), not an HTML entity constant.
  • When coerced to true, json_decode returns an array, causing addslashes() to fail with a type error.
  • Using both addslashes() and db_escape() is redundant since db_escape() already handles escaping.

Apply this diff to fix the issue:

-            db_escape(addslashes(json_decode(params_get('text',''), ENT_QUOTES))),
+            db_escape(json_decode(params_get('text',''))),
♻️ Duplicate comments (2)
api/core/tools.php (2)

52-78: Previous review feedback not addressed: guard $response['text'] access and reindex array.

The issues flagged in the previous review remain:

  1. Line 57 accesses $response['text'] without verifying the key exists
  2. Line 60's unset($response['text'][0]) leaves a gap in array indices, causing json_encode to produce an object instead of an array
  3. Line 77 assigns $response['text'] without a fallback

Apply this diff:

     if (is_array($response) && isset($response['Status'])) {
         // propagar el codigo de estado HTTP cuando se recibe en el error
         if (is_numeric($response['Status'])) {
             $httpStatus = $response['Status'];
             $killMe = $response['Status'] >= 299;
-            if(is_array($response['text']) && count($response['text']) > 1) {
+            if(isset($response['text']) && is_array($response['text']) && count($response['text']) > 1) {
                 // cuando la respuesta es un array de Hacienda, el primer elemento es el código de estado
                 // y el resto es el mensaje
                 unset($response['text'][0]);
+                $response['text'] = array_values($response['text']); // reindex for JSON array
             }
         } else {
             switch ($response['Status']) {
                 case 'error':
                     $httpStatus = 500;
                     $killMe = true;
                     break;
                 case 'ok':
                     $httpStatus = 200;
                     break;
                 default:
                     $httpStatus = 400;
                     $killMe = true;
             }
             
         }
-        $response = $response['text'];
+        $response = $response['text'] ?? $response;
     }

80-86: Previous review feedback not addressed: remove status override that discards computed HTTP status.

Line 85 unconditionally sets $httpStatus = 200, discarding any success status computed earlier (e.g., a 201 or 202 from Hacienda). This means all non-error responses return 200 regardless of the original status.

Apply this diff:

     if ($killMe) {
         if (is_string($response)) {
             $response = "ERROR: " . $response;
         }
-    } else {
-        $httpStatus = 200;
     }
🧹 Nitpick comments (2)
api/modules/wirez/messagesSend.php (1)

74-78: Consider using prepared statements for better security.

While db_escape() provides basic SQL injection protection, prepared statements with parameterized queries offer stronger security guarantees and are the recommended best practice for database queries.

Example refactor using prepared statements:

$stmt = $dbConn->prepare("INSERT INTO conversations (idUser, idRecipient, timestamp, subject) VALUES(?, ?, ?, ?)");
$stmt->bind_param("ssss", $idUser, $idRecipient, $timestamp, $subject);
$stmt->execute();
api/core/tools.php (1)

88-101: CLI path uses exception for response - verify this is intentional.

Throwing an exception containing the JSON response (lines 89-93) is unusual for CLI mode. Typically, CLI should print output and exit. If this pattern is intentional (e.g., for testing frameworks that catch exceptions), consider adding a comment explaining the design choice.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f10f714 and 81f6dbf.

📒 Files selected for processing (2)
  • api/core/tools.php (2 hunks)
  • api/modules/wirez/messagesSend.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
api/core/tools.php (1)
api/core/conf.php (1)
  • conf_get (26-34)
api/modules/wirez/messagesSend.php (1)
api/modules/db/module.php (1)
  • db_escape (151-155)
🔇 Additional comments (3)
api/modules/wirez/messagesSend.php (1)

78-78: Critical syntax fix - LGTM!

This change correctly closes the sprintf call by adding the missing closing parenthesis. Without this fix, the code would fail with a PHP parse error.

api/core/tools.php (2)

21-50: Initial error mapping looks reasonable.

The switch statement correctly maps error constants to HTTP status codes. Note that HTTP 440 (line 34) is a non-standard Microsoft extension; consider using 401 or 419 for broader client compatibility if needed, though this is acceptable for internal use.


104-117: LGTM!

The function correctly handles output, logging, and exit. The TODO comment about relocating helper functions is noted.

Comment on lines 119 to 128
function tools_returnJson($response, $addHeaders = true)
{
if ($addHeaders && conf_get('mode', 'core', 'web') != 'cli') {
http_response_code($response['httpStatus']);
header('Content-Type: text/html; charset=utf-8');
header('Content-Type: application/json');
}

return json_encode($response);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard httpStatus access and remove duplicate Content-Type header.

Two issues:

  1. Line 122 accesses $response['httpStatus'] without verifying the key exists—callers outside tools_reply may omit it.
  2. Lines 123-124 set Content-Type twice; the second header overwrites the first, making line 123 redundant.

Apply this diff:

 function tools_returnJson($response, $addHeaders = true)
 {
     if ($addHeaders && conf_get('mode', 'core', 'web') != 'cli') {
-        http_response_code($response['httpStatus']);
-        header('Content-Type: text/html; charset=utf-8');
+        http_response_code($response['httpStatus'] ?? 200);
         header('Content-Type: application/json');
     }
-    
+
     return json_encode($response);
 }
🤖 Prompt for AI Agents
In api/core/tools.php around lines 119 to 128, guard access to
$response['httpStatus'] before calling http_response_code and remove the
duplicate Content-Type header: check with isset($response['httpStatus']) (and
optionally is_int/is_numeric) before calling
http_response_code($response['httpStatus']), and replace the two header() calls
with a single header('Content-Type: application/json; charset=utf-8') so
Content-Type is set only once.

Comment on lines +129 to +132
tools_reply([
"Status" => 400,
"text" => "Al firmar, el documento o las claves no pueden estar vacías",
]);

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (5)
api/tools/ValidadorXML.php (5)

66-67: Path construction remains brittle despite regex improvement.

The preg_replace('/\/api\/$/', '/www/', ...) approach only works when the path ends exactly with /api/. If the configuration changes or doesn't match this pattern, the XSD files won't be found. As previously suggested, use a dedicated config key like conf_get('basePath','xsd','') to avoid fragile string manipulation.


63-74: Critical: Validate $version parameter to prevent path traversal.

The $version parameter is interpolated directly into filesystem paths (lines 69-73) without validation. An attacker could pass values like "../../../etc/passwd" to access arbitrary files. Validate that $version matches an expected pattern (e.g., /^\d+\.\d+$/) before use.

🔎 Proposed fix
 public static function validateXml(string $contenido_xml, string $consecutivo, $version = "4.4"): object
 {
+    // Validate version to prevent path traversal
+    if (!preg_match('/^\d+\.\d+$/', $version)) {
+        return (object) [
+            "status" => "error",
+            "message" => "Error: versión inválida ($version)"
+        ];
+    }
+
     $tipo_doc = substr($consecutivo, 8, 2);

68-74: Incorrect XSD mappings for document types "02" and "05".

Based on Costa Rica's electronic invoicing specifications:

  • Code "02" represents Nota de Débito Electrónica (Debit Note), not Factura de Compra
  • Code "05" represents Mensaje Receptor (Acceptance Message), not Mensaje Hacienda

These incorrect mappings will cause validation failures for legitimate documents.

🔎 Proposed fix
     $schemas = [
         "01" => "$baseFolder/xsd/FacturaElectronica_V$version.xsd",
-        "02" => "$baseFolder/xsd/FacturaCompra_V$version.xsd",
+        "02" => "$baseFolder/xsd/NotaDebitoElectronica_V$version.xsd",
         "03" => "$baseFolder/xsd/NotaCreditoElectronica_V$version.xsd",
         "04" => "$baseFolder/xsd/TiqueteElectronico_V$version.xsd",
-        "05" => "$baseFolder/xsd/MensajeHacienda_V$version.xsd",
+        "05" => "$baseFolder/xsd/MensajeReceptor_V$version.xsd",
     ];

Verify the correct XSD filenames exist in the repository:

#!/bin/bash
# Check for XSD files to confirm correct naming conventions
fd -t f -e xsd . www/xsd/ | grep -E "(NotaDebito|MensajeReceptor|FacturaCompra)" | head -20

97-99: Critical: XML loading vulnerable to XXE attacks and missing error handling.

The loadXML call lacks the LIBXML_NONET flag, allowing external entity/network fetches (XXE vulnerability). Additionally, there's no check for load failures, which could cause unexpected behavior with malformed XML.

🔎 Proposed fix
     libxml_use_internal_errors(true);
     $xml = new DOMDocument();
-    $xml->loadXML($contenido_xml);
+    if (@$xml->loadXML($contenido_xml, LIBXML_NONET) === false) {
+        $errors = self::libxml_display_errors();
+        return (object) [
+            "status" => "error",
+            "message" => empty($errors) ? "XML inválido o mal formado" : $errors
+        ];
+    }

101-115: PII leakage risk: Full XML content exposed in error responses.

Line 107 includes the entire XML document in error responses, which may contain sensitive invoice data (customer names, tax IDs, amounts, etc.). This poses a privacy and compliance risk, especially under regulations like GDPR or Costa Rica's data protection laws.

Gate this behind an explicit configuration flag that defaults to false, or remove it entirely.

🔎 Proposed fix
         $errors = self::libxml_display_errors();
         $r = [
             "status" => "error",
             "schema" => $schemas[$tipo_doc],
             "message" => $errors,
-            "xml" => $contenido_xml
         ];
+        // Only include XML if explicitly enabled for debugging
+        if (conf_get('includeXmlOnValidationError', 'xsd', false)) {
+            $r["xml"] = $contenido_xml;
+        }
🧹 Nitpick comments (2)
api/contrib/firmarXML/xmlseclibs/src/XMLSecurityDSig.php (1)

283-303: Refactor switch-case to if-elseif-else for clarity.

The use of strpos() !== false as case expressions in a switch statement is non-idiomatic and confusing. While it works due to PHP's loose comparison, an if-elseif-else structure would be clearer and more maintainable. Additionally, both v4.3 and v4.4 cases have identical implementation, which could be consolidated.

🔎 Proposed refactor
 public function setSignPolicy(){
+    if ($this->xmlFirstChild === null) {
+        throw new Exception("xmlFirstChild is not initialized");
+    }
     $xmlns = $this->xmlFirstChild->getAttribute('xmlns');
-    switch ($xmlns){
-        case (strpos($xmlns, 'v4.3') !== false):
-            $this->signPolicy = [
-                "name" 		=> "",
-                "url" 		=> "https://cdn.comprobanteselectronicos.go.cr/xml-schemas/Resoluci%C3%B3n_General_sobre_disposiciones_t%C3%A9cnicas_comprobantes_electr%C3%B3nicos_para_efectos_tributarios.pdf",
-                "digest" 	=> "DWxin1xWOeI8OuWQXazh4VjLWAaCLAA954em7DMh0h8=" // Base64_Encode(Hash_File(SHA_256))
-            ];
-            break;
-        case (strpos($xmlns, 'v4.4') !== false):
-            $this->signPolicy = [
-                "name" 		=> "",
-                "url" 		=> "https://cdn.comprobanteselectronicos.go.cr/xml-schemas/Resoluci%C3%B3n_General_sobre_disposiciones_t%C3%A9cnicas_comprobantes_electr%C3%B3nicos_para_efectos_tributarios.pdf",
-                "digest" 	=> "DWxin1xWOeI8OuWQXazh4VjLWAaCLAA954em7DMh0h8=" // Base64_Encode(Hash_File(SHA_256))
-            ];
-            break;
-        default:
-            throw new Exception("Cannot validate version: Unsupported Version");
+    if (strpos($xmlns, 'v4.3') !== false || strpos($xmlns, 'v4.4') !== false) {
+        $this->signPolicy = [
+            "name" 		=> "",
+            "url" 		=> "https://cdn.comprobanteselectronicos.go.cr/xml-schemas/Resoluci%C3%B3n_General_sobre_disposiciones_t%C3%A9cnicas_comprobantes_electr%C3%B3nicos_para_efectos_tributarios.pdf",
+            "digest" 	=> "DWxin1xWOeI8OuWQXazh4VjLWAaCLAA954em7DMh0h8=" // Base64_Encode(Hash_File(SHA_256))
+        ];
+    } else {
+        throw new Exception("Cannot validate version: Unsupported Version");
     }
 }
api/tools/ValidadorXML.php (1)

144-147: Add input length validation to prevent warnings.

The method extracts positions 8-9 from $consecutivo without validating the string length. If a short or invalid consecutivo is passed, this could produce warnings or unexpected results.

🔎 Proposed fix
 public static function getDocumentTypeFromConsecutivo(string $consecutivo): string
 {
+    if (strlen($consecutivo) < 10) {
+        return ''; // or throw an exception
+    }
     return substr($consecutivo, 8, 2);
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81f6dbf and ceab7a5.

📒 Files selected for processing (3)
  • api/contrib/firmarXML/xmlseclibs/src/XMLSecurityDSig.php
  • api/contrib/genXML/genXML.php
  • api/tools/ValidadorXML.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Seer Code Review
🔇 Additional comments (3)
api/contrib/firmarXML/xmlseclibs/src/XMLSecurityDSig.php (1)

286-299: Verify whether v4.3 and v4.4 should share the same policy document or if v4.3 requires its own.

v4.3 and v4.4 cases use identical URL and digest values. v4.3 appears only in this switch statement with no other references in the codebase, while v4.4 is actively used throughout the schema generation code. Confirm whether:

  1. Both versions intentionally reference the same policy document, or
  2. v4.3 should have a distinct policy URL/digest (or be removed if no longer supported)
api/tools/ValidadorXML.php (2)

19-53: LGTM: Error formatting helpers are well-structured.

The private helper methods correctly format libxml errors into structured arrays and properly clear errors after collection.


117-120: Well done: Success return statement correctly added.

This properly addresses the previous issue where the function would return null for valid XML. The success case now returns a consistent object structure.

@@ -283,6 +283,13 @@ public function setCanonicalMethod($method)
public function setSignPolicy(){
$xmlns = $this->xmlFirstChild->getAttribute('xmlns');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add null check before accessing xmlFirstChild.

The xmlFirstChild property is nullable (declared as DOMElement|null at Line 99), but there's no null check before calling getAttribute(). This could cause a fatal error if xmlFirstChild is not initialized.

🔎 Proposed fix
 public function setSignPolicy(){
+    if ($this->xmlFirstChild === null) {
+        throw new Exception("xmlFirstChild is not initialized");
+    }
     $xmlns = $this->xmlFirstChild->getAttribute('xmlns');
🤖 Prompt for AI Agents
In api/contrib/firmarXML/xmlseclibs/src/XMLSecurityDSig.php around line 284, the
code accesses $this->xmlFirstChild without checking for null; add a null check
before calling getAttribute() — if xmlFirstChild is null, handle it by returning
a sensible default (e.g., empty string), throwing a clear exception, or
otherwise short-circuiting the method; update the code to only call
$this->xmlFirstChild->getAttribute('xmlns') when xmlFirstChild is an instance of
DOMElement and document the chosen behavior in a brief comment.

Comment on lines +127 to +136
public static function getSupportedDocumentTypes(): array
{
return [
"01" => "Factura Electrónica",
"02" => "Factura de Compra",
"03" => "Nota de Crédito Electrónica",
"04" => "Tiquete Electrónico",
"05" => "Mensaje Hacienda",
];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Document type label for "05" inconsistent with corrected mapping.

The label "Mensaje Hacienda" should be "Mensaje Receptor" to match the corrected XSD mapping for acceptance confirmation messages.

🔎 Proposed fix
     return [
         "01" => "Factura Electrónica",
         "02" => "Factura de Compra",
         "03" => "Nota de Crédito Electrónica",
         "04" => "Tiquete Electrónico",
-        "05" => "Mensaje Hacienda",
+        "05" => "Mensaje Receptor",
     ];

Note: The label for "02" should also be updated to "Nota de Débito Electrónica" once the XSD mapping is corrected.

🤖 Prompt for AI Agents
In api/tools/ValidadorXML.php around lines 127 to 136, update the document type
labels to match the corrected XSD mapping: change the label for key "05" from
"Mensaje Hacienda" to "Mensaje Receptor", and proactively change the label for
key "02" from "Factura de Compra" to "Nota de Débito Electrónica" so the mapping
stays consistent once the XSD is updated.

Comment on lines +192 to +196
return tools_reply(array(
"Status" => $header_array['x-http-status'] ?? $status,
"to" => $url,
"text" => $header_array['x-error-cause']
), true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The function consultarComprobante calls tools_reply, which contains an exit statement. This causes the script to terminate abruptly instead of returning a value to the caller.
Severity: HIGH | Confidence: High

🔍 Detailed Analysis

The function consultarComprobante has several paths, including on cURL error or when the response body is empty, where it calls return tools_reply(...). The tools_reply function, via _tools_reply, executes an exit; statement, which terminates the entire PHP script. This prevents the return statement from ever executing and violates the expectation of the calling function tools_proccesPath, which anticipates a return value. Instead of a graceful error response, the request will terminate unexpectedly, making the actual error difficult to diagnose.

💡 Suggested Fix

In consultarComprobante, instead of calling tools_reply, construct the response array and return it directly. This allows the response to propagate back to the calling function tools_proccesPath for proper handling, consistent with the behavior of the sibling function consultarRecepcion.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: api/contrib/consultar/consultar.php#L192-L196

Potential issue: The function `consultarComprobante` has several paths, including on
cURL error or when the response body is empty, where it calls `return tools_reply(...)`.
The `tools_reply` function, via `_tools_reply`, executes an `exit;` statement, which
terminates the entire PHP script. This prevents the `return` statement from ever
executing and violates the expectation of the calling function `tools_proccesPath`,
which anticipates a return value. Instead of a graceful error response, the request will
terminate unexpectedly, making the actual error difficult to diagnose.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8114258

Comment on lines +55 to +56
$httpStatus = $response['Status'];
$killMe = $response['Status'] >= 299;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The $killMe error flag is incorrectly overwritten based on the Status code, causing connection failures to be reported as HTTP 200 OK successes.
Severity: HIGH

Suggested Fix

The logic should not overwrite $killMe if it's already true. Change the assignment to only set $killMe if it's not already set, for example: if ($killMe !== true) { $killMe = $response['Status'] >= 299; }.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: api/core/tools.php#L55-L56

Potential issue: The `tools_reply()` function unconditionally overwrites the `$killMe`
parameter if the response array contains a numeric `Status`. When a caller, such as
`consultarComprobante()`, experiences a cURL connection failure, it passes `Status: 0`
and `$killMe: true`. The line `$killMe = $response['Status'] >= 299;` then overwrites
`$killMe` to `false` (since `0 >= 299` is false). This causes the function to
incorrectly report a successful HTTP 200 OK response to the client, masking the
underlying connection failure.

Did we get this right? 👍 / 👎 to inform future reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants