Proyectos de Subversion Moodle

Rev

Rev 1414 | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |

<?php

// NOTA IMPORTANTE:
// Las credenciales y claves RSA NO DEBEN ESTAR HARDCODEADAS EN UN ENTORNO DE PRODUCCIÓN.
// Considere usar variables de entorno, un archivo de configuración externo, o las propias configuraciones de Moodle
// para almacenar LLWS_USERNAME, LLWS_PASSWORD, LLWS_RSA_N, LLWS_RSA_D, LLWS_RSA_E.
// Por simplicidad en este ejemplo, se mantienen aquí para la demostración de los cambios.

define('LLWS_USERNAME', 'leaderdslinked-ws');
define('LLWS_PASSWORD', 'KFB3lFsLp&*CpB0uc2VNc!zVn@w!EZqZ*jr$J0AlE@sYREUcyP');
define('LLWS_RSA_N', 34589927);     // Clave pública RSA N
define('LLWS_RSA_D', 4042211);      // Clave privada RSA D
define('LLWS_RSA_E', 7331);         // Clave pública RSA E
define('LLWS_CATEGORY_ID', 6);      // ID de categoría para cursos

// Inclusión de archivos necesarios de Moodle
require_once(__DIR__ . '/../config.php');
global $DB, $CFG;

// Inclusión de librerías de Moodle
require_once($CFG->libdir . '/moodlelib.php');
require_once($CFG->libdir . '/externallib.php');
require_once($CFG->libdir . '/authlib.php');
require_once($CFG->libdir . '/gdlib.php'); // Para procesamiento de imágenes
require_once($CFG->dirroot . '/user/lib.php'); // Para funciones de usuario
require_once(__DIR__ . '/rsa.php'); // Asegúrate de que esta ruta sea correcta para tu clase RSA
require_once(__DIR__ . '/lib.php'); // Asegúrate de que esta ruta sea correcta para tus funciones ll_get_user_by_email, ll_get_username_available, ll_create_user

// Helper function to output JSON error and exit
// NOTA: Estos headers solo se configuran cuando hay un error para una respuesta JSON.
// Para un login exitoso, no se necesitan estos headers.
function output_json_error($errorCode)
{
    // Es buena práctica asegurar que la sesión se cierre antes de la salida,
    // aunque el manejo de salida de Moodle podría cubrir esto.
    if (function_exists('session_write_close')) {
        session_write_close();
    }
    header('Content-Type: application/json');
    // Para entornos de producción, considera restringir Access-Control-Allow-Origin
    // a dominios específicos si esta vista será accedida vía AJAX desde otros orígenes.
    // Si es una redirección directa, CORS es irrelevante para una respuesta JSON.
    header('Access-Control-Allow-Origin: *');
    echo json_encode(['success' => false, 'data' => $errorCode]);
    exit;
}

// Obtención y sanitización de parámetros de la petición
$username   = trim(isset($_REQUEST['username']) ? filter_var($_REQUEST['username'], FILTER_SANITIZE_STRING) : '');
$password   = trim(isset($_REQUEST['password']) ? filter_var($_REQUEST['password'], FILTER_SANITIZE_STRING) : '');
$timestamp  = trim(isset($_REQUEST['timestamp']) ? filter_var($_REQUEST['timestamp'], FILTER_SANITIZE_STRING) : '');
// Usamos FILTER_VALIDATE_INT para asegurar que sea un entero y no un string numérico grande.
$rand       = intval(isset($_REQUEST['rand']) ? filter_var($_REQUEST['rand'], FILTER_VALIDATE_INT) : 0, 10);
$data       = trim(isset($_REQUEST['data']) ? filter_var($_REQUEST['data'], FILTER_SANITIZE_STRING) : '');

// --- Validaciones de Seguridad ---

// Validación de parámetros de seguridad mínimos
if (empty($username) || empty($password) || empty($timestamp) || empty($rand) || !is_integer($rand)) {
    output_json_error('ERROR_SECURITY1');
}

// Validación del nombre de usuario de la API
if ($username !== LLWS_USERNAME) { // Usamos !== para comparación estricta
    output_json_error('ERROR_SECURITY2');
}

// Validación del formato del timestamp
$dt = \DateTime::createFromFormat('Y-m-d\TH:i:s', $timestamp);
if (!$dt) {
    output_json_error('ERROR_SECURITY3');
}

// Validación del rango de tiempo del timestamp (±5 minutos) para prevenir Replay Attacks
$t0 = $dt->getTimestamp();
$t1 = strtotime('-5 minutes');
$t2 = strtotime('+5 minutes');

if ($t0 < $t1 || $t0 > $t2) {
    output_json_error('ERROR_SECURITY4'); // Habilitado para prevenir Replay Attacks
}

// Validación de la contraseña (hash de la contraseña de la API + rand + timestamp)
$expectedPasswordString = $username . '-' . LLWS_PASSWORD . '-' . $rand . '-' . $timestamp;
if (!password_verify($expectedPasswordString, $password)) {
    output_json_error('ERROR_SECURITY5');
}

// --- Validación y Desencriptación de Datos de Usuario ---

// Validación de datos
if (empty($data)) {
    output_json_error('ERROR_PARAMETERS1');
}

// Decodificación de datos en base64
$data = base64_decode($data);
if ($data === false || empty($data)) { // base64_decode puede retornar false
    output_json_error('ERROR_PARAMETERS2');
}

// Desencriptación de datos usando RSA
try {
    $rsa = new rsa(); // Asegúrate de que la clase 'rsa' esté correctamente cargada.
    $rsa->setKeys(LLWS_RSA_N, LLWS_RSA_D, LLWS_RSA_E);
    $data = $rsa->decrypt($data);
} catch (Throwable $e) {
    // Registra el error para depuración, pero no lo expongas al cliente.
    error_log("RSA Decryption Error: " . $e->getMessage());
    output_json_error('ERROR_PARAMETERS3');
}

// Conversión de datos a array
$data = (array) json_decode($data, true); // Añadido 'true' para asegurar un array asociativo
if (empty($data)) {
    output_json_error('ERROR_PARAMETERS4');
}

// Extracción y validación de datos del usuario
$email      = trim(isset($data['email']) ? filter_var($data['email'], FILTER_SANITIZE_EMAIL) : '');
$first_name = trim(isset($data['first_name']) ? filter_var($data['first_name'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) : '');
$last_name  = trim(isset($data['last_name']) ? filter_var($data['last_name'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) : '');

// Asegúrate de que los datos esenciales del usuario estén presentes y sean válidos.
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || empty($first_name) || empty($last_name)) {
    output_json_error('ERROR_PARAMETERS5');
}

// --- Búsqueda o Creación de Usuario en Moodle ---

$user = ll_get_user_by_email($email); // Asume que esta función busca al usuario por email
$new_user = false;

if ($user) {
    // Usuario encontrado
} else {
    // Usuario no encontrado, proceder a la creación
    $new_user = true;
    $username_moodle = ll_get_username_available($first_name, $last_name); // Genera un nombre de usuario disponible para Moodle
    $user = ll_create_user($username_moodle, $email, $first_name, $last_name); // Crea el usuario

    // Procesamiento de imagen de perfil si se proporciona y el usuario fue creado
    if ($user) {
        $filename   = trim(isset($data['image_filename']) ? filter_var($data['image_filename'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) : '');
        $content    = trim(isset($data['image_content']) ? filter_var($data['image_content'], FILTER_SANITIZE_STRING) : '');

        // Validación básica de nombre de archivo para evitar path traversal
        if ($filename && strpos($filename, '..') === false && strpos($filename, DIRECTORY_SEPARATOR) === false) {
            $tempfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename; // Usar directorio temporal del sistema
            try {
                $decoded_content = base64_decode($content);
                if ($decoded_content !== false && !empty($decoded_content)) {
                    file_put_contents($tempfile, $decoded_content);

                    if (file_exists($tempfile)) {
                        $usericonid = process_new_icon(context_user::instance($user->id, MUST_EXIST), 'user', 'icon', 0, $tempfile);
                        if ($usericonid) {
                            $DB->set_field('user', 'picture', $usericonid, array('id' => $user->id));
                        }
                    }
                }
            } catch (\Throwable $e) {
                // IMPORTANT: Registra el error de la imagen, pero no abortes el login del usuario si la imagen falla.
                error_log("Error processing user image for user ID {$user->id}: " . $e->getMessage());
            } finally {
                // Asegúrate de eliminar el archivo temporal
                if (file_exists($tempfile)) {
                    unlink($tempfile);
                }
            }
        }
    }
}

// Verificación de creación/recuperación de usuario
if (!$user) {
    output_json_error('ERROR_MOODLE1');
}

// --- Inscripción en Cursos para Usuarios Nuevos ---

if ($new_user) {
    $role = $DB->get_record('role', array('archetype' => 'student'));
    $enrolmethod = 'manual';

    $courses = get_courses(); // Obtiene todos los cursos
    foreach ($courses as $course) {
        // CORRECCIÓN: Error tipográfico 'categoy_id' a 'category_id'
        if ($course->category_id == LLWS_CATEGORY_ID) {
            $context = context_course::instance($course->id);
            if (!is_enrolled($context, $user)) { // Verifica si el usuario ya está inscrito
                $enrol = enrol_get_plugin($enrolmethod);
                if ($enrol === null) {
                    // Si el método de matriculación manual no existe, loguea un error y continúa.
                    // No debería abortar el login del usuario por esto.
                    error_log("Enrolment method '{$enrolmethod}' not found for course ID {$course->id}.");
                    continue; // Pasa al siguiente curso
                }

                // Obtener o crear una instancia de matriculación manual para el curso
                $instance = $enrol->add_default_instance($course); // Esto intenta encontrar o crear una instancia por defecto
                if ($instance === null) {
                    // Si add_default_instance falla (ej. no hay permisos), intenta crear una nueva instancia.
                    // Esto es menos común si add_default_instance no funciona, pero es una alternativa.
                    $instanceid = $enrol->add_instance($course);
                    $instance = $DB->get_record('enrol', array('id' => $instanceid));
                }

                if ($instance) {
                    $enrol->enrol_user($instance, $user->id, $role->id);
                } else {
                    error_log("Failed to get or create manual enrolment instance for course ID {$course->id}.");
                }
            }
        }
    }
}

// --- Completar el Proceso de Inicio de Sesión ---

// Obtener datos completos del usuario (asegura que todos los campos de Moodle estén cargados)
$user = get_complete_user_data('id', $user->id);

if ($user) {
    // Verificar si la cuenta está confirmada
    if (empty($user->confirmed)) {
        output_json_error('ACCOUNT_NOT_CONFIRMED');
    }

    // Verificar si la contraseña ha expirado (solo para autenticación LDAP)
    $userauth = get_auth_plugin($user->auth);
    if (!isguestuser() && !empty($userauth->config->expiration) && $userauth->config->expiration == 1) {
        $days2expire = $userauth->password_expire($user->username);
        if (intval($days2expire) < 0) {
            output_json_error('PASSWORD_EXPIRED');
        }
    }

    // CORRECCIÓN: Para un flujo de login web, Moodle maneja la sesión existente.
    // complete_user_login() es suficiente para establecer la sesión del usuario.
    // Eliminar el cierre agresivo de todas las sesiones a menos que sea un requisito muy específico.
    // Si la sesión actual ya está autenticada, complete_user_login() la actualizará.
    // Si necesitas forzar un re-login de la sesión actual, puedes hacerlo de forma más granular:
    /*
    if (isloggedin() && get_userid() !== $user->id) { // Si un usuario diferente ya está logueado en esta sesión
        \core\session\manager::terminate_current();
        session_destroy();
        setcookie('MoodleSession', '', time() - 3600, '/'); // Eliminar la cookie de la sesión anterior
    }
    */

    // Completar el proceso de inicio de sesión de Moodle.
    // Esto establecerá la sesión del usuario $user.
    complete_user_login($user);

    // Aplicar límite de inicio de sesión concurrente (descomentado si es necesario)
    // \core\session\manager::apply_concurrent_login_limit($user->id, session_id());

    // Configurar cookie de nombre de usuario (Moodle lo maneja internamente)
    if (!empty($CFG->nolastloggedin)) {
        // No almacenar último usuario conectado en cookie
    } else if (empty($CFG->rememberusername)) {
        // Sin cookies permanentes, eliminar la anterior si existe
        set_moodle_cookie('');
    } else {
        set_moodle_cookie($user->username);
    }

    // Limpiar mensajes de error y redirección de sesión antes de la redirección final
    unset($SESSION->loginerrormsg);
    unset($SESSION->logininfomsg);
    unset($SESSION->loginredirect); // Descartar loginredirect si estamos redirigiendo

    // Configurar la URL de destino
    $urltogo = $CFG->wwwroot . '/my'; // Página "Mi Moodle"

    // Verificar que la sesión se haya iniciado correctamente antes de redirigir
    if (isloggedin() && !isguestuser()) {
        // Redirección HTTP para una vista web exitosa
        redirect($urltogo);
    } else {
        // Falló la autenticación final en Moodle, a pesar de las validaciones previas.
        output_json_error('LOGIN_FAILED_MOODLE');
    }
} else {
    // Esto debería ser capturado por output_json_error('ERROR_MOODLE1') anteriormente,
    // pero es un fallback.
    output_json_error('USER_NOT_FOUND');
}

// El script debería terminar con una redirección o una respuesta JSON de error.
// El 'exit;' final es redundante si se usa redirect() o output_json_error() que ya contienen exit().
exit;