Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Nieto2020/portalhub/llms.txt

Use this file to discover all available pages before exploring further.

The document repository (modules/documentos/) is the central file store for the consultancy portal. Clients upload their accounting files, advisors and admins can attach documents to any assigned client’s record, and every file is tracked with a version number so historical uploads are never silently overwritten. XML files receive an automatic CFDI validation flag on upload, supporting Mexican fiscal compliance workflows.

Document Record Fields

Every uploaded file creates a row in the documentos table with the following structure:
ColumnTypeDescription
id_documentointAuto-increment primary key
id_usuario_propietarioint (FK)Owner of the document (usually the client)
id_tipo_docint (FK)References cat_tipos_documentos catalogue
ruta_archivostringRelative path on disk under uploads/
nombre_originalstringOriginal filename as submitted by the user
versionintIncrements with each re-upload of the same filename and type
validacion_cfdibool1 if the file is an XML CFDI; 0 otherwise
fecha_subidadatetimeServer timestamp of the upload

Uploading a Document

Endpoint: POST modules/documentos/subir.php (multipart/form-data) subir.php is the primary upload handler. It enforces a 10 MB size limit, a strict allowlist of extensions and MIME types, path-traversal prevention via basename(), and file-name obfuscation on disk using bin2hex(random_bytes(16)). Allowed file types:
ExtensionExpected MIME Type
pdfapplication/pdf
xmltext/xml or application/xml
docapplication/msword
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
xlsapplication/vnd.ms-excel
xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
jpgimage/jpeg or image/pjpeg
pngimage/png
Both the file extension and the real MIME type (detected via finfo) must match. Uploading a .pdf file that contains executable content, for example, will be rejected with 400.
Form fields:
FieldRequiredDescription
archivoYesThe file binary (multipart/form-data)
id_tipo_docYesDocument type ID from the cat_tipos_documentos catalogue
id_clienteAdmin/Asesor onlyClient to upload on behalf of; not required for clients uploading their own files
curl -X POST https://portalhub.example.com/modules/documentos/subir.php \
  -b "PHPSESSID=your_session_id" \
  -F "archivo=@declaracion_anual.pdf" \
  -F "id_tipo_doc=3"

Automatic Versioning

Before saving a new file, subir.php queries the highest existing version number for the same owner, document type, and original filename:
$sqlVersion = "SELECT MAX(version) as ultima_version FROM documentos
               WHERE id_usuario_propietario = :id_user
                 AND id_tipo_doc = :id_tipo
                 AND nombre_original = :nombre_orig";

$stmtV = $conexion->prepare($sqlVersion);
$stmtV->execute([...]);

$resultadoV = $stmtV->fetch(PDO::FETCH_ASSOC);
$version = ($resultadoV['ultima_version'] ?? 0) + 1;
The obfuscated disk name encodes the version to avoid collisions:
$nombreHash = bin2hex(random_bytes(16)) . "_v" . $version . "." . $extension;
$rutaRelativa = "uploads/" . $nombreHash;
This means re-uploading the same document type creates a new record (version n+1) while the previous version’s file and record remain intact.

CFDI Validation Flag

When the uploaded file has an .xml extension, validacion_cfdi is automatically set to 1:
$cfdi = ($extension === 'xml') ? 1 : 0;
The flag is stored alongside the document record and can be used by the frontend or reporting layer to identify CFDI fiscal receipts that may require additional validation steps.

Role-Based Upload Permissions

RoleBehaviour
ROL_CLIENTE (3)Can upload only to their own record; id_usuario_propietario is set from the session
ROL_ASESOR (2)Must supply id_cliente; the system verifies an active assignment exists in cliente_asesor before allowing the upload
ROL_ADMIN (1)Can upload to any client by providing id_cliente

Listing Documents

Endpoint: GET modules/documentos/listar.php Returns all document records visible to the logged-in user, joined with cat_tipos_documentos and usuarios to include the type name and owner email.
RoleFilter Applied
ROL_CLIENTE (3)Only rows where id_usuario_propietario = id_usuario
ROL_ASESOR (2)Documents owned by any client in their active cliente_asesor assignments
ROL_ADMIN (1)All documents
Response item shape:
{
  "id_documento": 204,
  "id_usuario_propietario": 12,
  "id_tipo_doc": 3,
  "ruta_archivo": "uploads/3a7f...b2_v2.pdf",
  "nombre_original": "declaracion_anual.pdf",
  "version": 2,
  "validacion_cfdi": false,
  "fecha_subida": "2025-07-15 09:42:11",
  "nombre_tipo": "Declaración Anual",
  "propietario_correo": "cliente@empresa.mx"
}

Downloading a Document

Endpoint: GET modules/documentos/descargar.php?id={id_documento} descargar.php resolves the physical path from the ruta_archivo column and streams the file to the client using readfile() with appropriate Content-Disposition: attachment headers. The original filename (not the hashed disk name) is used in the Content-Disposition header. Access is gated by the same ownership rules as listing:
1

Fetch Document Record

The document row is loaded by id_documento. A 404 is returned if it does not exist.
2

Permission Check

  • Admin: always permitted
  • Client: permitted only if id_usuario_propietario matches the session user
  • Asesor: permitted only if the document owner is an actively assigned client
3

File Existence Check

The resolved path is verified with file_exists(). If the physical file is missing, 404 is returned.
4

Stream File

Output buffering is cleared, headers are set, and readfile() streams the binary content.
header("Content-Disposition: attachment; filename=\"" . $documento['nombre_original'] . "\"");
header("Content-Length: " . filesize($rutaAbsoluta));
readfile($rutaAbsoluta);
exit;

Uploads Directory Security

The uploads/ directory is protected by an .htaccess file that prevents direct web access to uploaded files. All downloads must go through descargar.php, which enforces authentication and authorization before streaming any file.
Because disk filenames are randomized with bin2hex(random_bytes(16)), even if the .htaccess protection were bypassed, an attacker could not guess the path of a specific user’s file.

Registering Document Metadata Directly

Endpoint: modules/documentos/registrar.php registrar.php is a lower-level utility for inserting a document record into the documentos table without a multipart file upload. It accepts values directly for id_usuario_propietario, id_tipo_doc, ruta_archivo, version, and validacion_cfdi, and returns the new id_documento on success. This script is intended for programmatic or batch-registration scenarios where the file has already been placed on disk by other means.
$sql = "INSERT INTO documentos
        (id_usuario_propietario, id_tipo_doc, ruta_archivo, version, validacion_cfdi)
        VALUES (:id_usuario_propietario, :id_tipo_doc, :ruta_archivo, :version, :validacion_cfdi)";

Deleting a Document

Endpoint: modules/documentos/eliminar.php eliminar.php removes a document record from the documentos table by id_documento:
$sql = "DELETE FROM documentos WHERE id_documento = :id_documento";
On success, the endpoint returns 200 with the message "Documento eliminado correctamente". On database error it returns 500.

Post-Upload Notifications

After a successful upload, subir.php uses NotificationService to alert the relevant party:
  • Client uploads → the client’s active advisor is notified with event type "Documento".
  • Advisor or admin uploads → the document owner (client) is notified.
See Notifications for the NotificationService API.

Build docs developers (and LLMs) love