Proyectos de Subversion Moodle

Rev

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

Rev Autor Línea Nro. Línea
96 efrain 1
<?php
1389 ariadna 2
 
1415 ariadna 3
// NOTA IMPORTANTE:
4
// Las credenciales y claves RSA NO DEBEN ESTAR HARDCODEADAS EN UN ENTORNO DE PRODUCCIÓN.
5
// Considere usar variables de entorno, un archivo de configuración externo, o las propias configuraciones de Moodle
6
// para almacenar LLWS_USERNAME, LLWS_PASSWORD, LLWS_RSA_N, LLWS_RSA_D, LLWS_RSA_E.
7
// Por simplicidad en este ejemplo, se mantienen aquí para la demostración de los cambios.
8
 
96 efrain 9
define('LLWS_USERNAME', 'leaderdslinked-ws');
10
define('LLWS_PASSWORD', 'KFB3lFsLp&*CpB0uc2VNc!zVn@w!EZqZ*jr$J0AlE@sYREUcyP');
1415 ariadna 11
define('LLWS_RSA_N', 34589927);     // Clave pública RSA N
12
define('LLWS_RSA_D', 4042211);      // Clave privada RSA D
13
define('LLWS_RSA_E', 7331);         // Clave pública RSA E
14
define('LLWS_CATEGORY_ID', 6);      // ID de categoría para cursos
96 efrain 15
 
1389 ariadna 16
// Inclusión de archivos necesarios de Moodle
96 efrain 17
require_once(__DIR__ . '/../config.php');
18
global $DB, $CFG;
19
 
1389 ariadna 20
// Inclusión de librerías de Moodle
1383 ariadna 21
require_once($CFG->libdir . '/moodlelib.php');
96 efrain 22
require_once($CFG->libdir . '/externallib.php');
1383 ariadna 23
require_once($CFG->libdir . '/authlib.php');
1415 ariadna 24
require_once($CFG->libdir . '/gdlib.php'); // Para procesamiento de imágenes
25
require_once($CFG->dirroot . '/user/lib.php'); // Para funciones de usuario
26
require_once(__DIR__ . '/rsa.php'); // Asegúrate de que esta ruta sea correcta para tu clase RSA
27
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
96 efrain 28
 
1409 ariadna 29
// Helper function to output JSON error and exit
1415 ariadna 30
// NOTA: Estos headers solo se configuran cuando hay un error para una respuesta JSON.
31
// Para un login exitoso, no se necesitan estos headers.
1409 ariadna 32
function output_json_error($errorCode)
33
{
1415 ariadna 34
    // Es buena práctica asegurar que la sesión se cierre antes de la salida,
35
    // aunque el manejo de salida de Moodle podría cubrir esto.
1409 ariadna 36
    if (function_exists('session_write_close')) {
37
        session_write_close();
38
    }
39
    header('Content-Type: application/json');
1415 ariadna 40
    // Para entornos de producción, considera restringir Access-Control-Allow-Origin
41
    // a dominios específicos si esta vista será accedida vía AJAX desde otros orígenes.
42
    // Si es una redirección directa, CORS es irrelevante para una respuesta JSON.
43
    header('Access-Control-Allow-Origin: *');
1409 ariadna 44
    echo json_encode(['success' => false, 'data' => $errorCode]);
45
    exit;
46
}
47
 
1415 ariadna 48
// Obtención y sanitización de parámetros de la petición
49
$username   = trim(isset($_REQUEST['username']) ? filter_var($_REQUEST['username'], FILTER_SANITIZE_STRING) : '');
50
$password   = trim(isset($_REQUEST['password']) ? filter_var($_REQUEST['password'], FILTER_SANITIZE_STRING) : '');
51
$timestamp  = trim(isset($_REQUEST['timestamp']) ? filter_var($_REQUEST['timestamp'], FILTER_SANITIZE_STRING) : '');
52
// Usamos FILTER_VALIDATE_INT para asegurar que sea un entero y no un string numérico grande.
53
$rand       = intval(isset($_REQUEST['rand']) ? filter_var($_REQUEST['rand'], FILTER_VALIDATE_INT) : 0, 10);
54
$data       = trim(isset($_REQUEST['data']) ? filter_var($_REQUEST['data'], FILTER_SANITIZE_STRING) : '');
55
 
56
// --- Validaciones de Seguridad ---
57
 
58
// Validación de parámetros de seguridad mínimos
1401 ariadna 59
if (empty($username) || empty($password) || empty($timestamp) || empty($rand) || !is_integer($rand)) {
1409 ariadna 60
    output_json_error('ERROR_SECURITY1');
96 efrain 61
}
62
 
1415 ariadna 63
// Validación del nombre de usuario de la API
64
if ($username !== LLWS_USERNAME) { // Usamos !== para comparación estricta
1409 ariadna 65
    output_json_error('ERROR_SECURITY2');
1401 ariadna 66
}
96 efrain 67
 
1401 ariadna 68
// Validación del formato del timestamp
69
$dt = \DateTime::createFromFormat('Y-m-d\TH:i:s', $timestamp);
70
if (!$dt) {
1409 ariadna 71
    output_json_error('ERROR_SECURITY3');
1401 ariadna 72
}
96 efrain 73
 
1415 ariadna 74
// Validación del rango de tiempo del timestamp (±5 minutos) para prevenir Replay Attacks
1401 ariadna 75
$t0 = $dt->getTimestamp();
76
$t1 = strtotime('-5 minutes');
77
$t2 = strtotime('+5 minutes');
96 efrain 78
 
1401 ariadna 79
if ($t0 < $t1 || $t0 > $t2) {
1415 ariadna 80
    output_json_error('ERROR_SECURITY4'); // Habilitado para prevenir Replay Attacks
1401 ariadna 81
}
96 efrain 82
 
1415 ariadna 83
// Validación de la contraseña (hash de la contraseña de la API + rand + timestamp)
84
$expectedPasswordString = $username . '-' . LLWS_PASSWORD . '-' . $rand . '-' . $timestamp;
85
if (!password_verify($expectedPasswordString, $password)) {
1409 ariadna 86
    output_json_error('ERROR_SECURITY5');
1401 ariadna 87
}
96 efrain 88
 
1415 ariadna 89
// --- Validación y Desencriptación de Datos de Usuario ---
90
 
1401 ariadna 91
// Validación de datos
92
if (empty($data)) {
1409 ariadna 93
    output_json_error('ERROR_PARAMETERS1');
1401 ariadna 94
}
96 efrain 95
 
1401 ariadna 96
// Decodificación de datos en base64
97
$data = base64_decode($data);
1415 ariadna 98
if ($data === false || empty($data)) { // base64_decode puede retornar false
1409 ariadna 99
    output_json_error('ERROR_PARAMETERS2');
96 efrain 100
}
101
 
1401 ariadna 102
// Desencriptación de datos usando RSA
103
try {
1415 ariadna 104
    $rsa = new rsa(); // Asegúrate de que la clase 'rsa' esté correctamente cargada.
1401 ariadna 105
    $rsa->setKeys(LLWS_RSA_N, LLWS_RSA_D, LLWS_RSA_E);
106
    $data = $rsa->decrypt($data);
107
} catch (Throwable $e) {
1415 ariadna 108
    // Registra el error para depuración, pero no lo expongas al cliente.
109
    error_log("RSA Decryption Error: " . $e->getMessage());
1409 ariadna 110
    output_json_error('ERROR_PARAMETERS3');
1401 ariadna 111
}
1383 ariadna 112
 
1401 ariadna 113
// Conversión de datos a array
1415 ariadna 114
$data = (array) json_decode($data, true); // Añadido 'true' para asegurar un array asociativo
1401 ariadna 115
if (empty($data)) {
1409 ariadna 116
    output_json_error('ERROR_PARAMETERS4');
1401 ariadna 117
}
96 efrain 118
 
1401 ariadna 119
// Extracción y validación de datos del usuario
120
$email      = trim(isset($data['email']) ? filter_var($data['email'], FILTER_SANITIZE_EMAIL) : '');
1415 ariadna 121
$first_name = trim(isset($data['first_name']) ? filter_var($data['first_name'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) : '');
122
$last_name  = trim(isset($data['last_name']) ? filter_var($data['last_name'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) : '');
96 efrain 123
 
1415 ariadna 124
// Asegúrate de que los datos esenciales del usuario estén presentes y sean válidos.
1401 ariadna 125
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || empty($first_name) || empty($last_name)) {
1409 ariadna 126
    output_json_error('ERROR_PARAMETERS5');
1401 ariadna 127
}
96 efrain 128
 
1415 ariadna 129
// --- Búsqueda o Creación de Usuario en Moodle ---
130
 
131
$user = ll_get_user_by_email($email); // Asume que esta función busca al usuario por email
132
$new_user = false;
133
 
1401 ariadna 134
if ($user) {
1415 ariadna 135
    // Usuario encontrado
1401 ariadna 136
} else {
1415 ariadna 137
    // Usuario no encontrado, proceder a la creación
1401 ariadna 138
    $new_user = true;
1415 ariadna 139
    $username_moodle = ll_get_username_available($first_name, $last_name); // Genera un nombre de usuario disponible para Moodle
140
    $user = ll_create_user($username_moodle, $email, $first_name, $last_name); // Crea el usuario
1401 ariadna 141
 
1415 ariadna 142
    // Procesamiento de imagen de perfil si se proporciona y el usuario fue creado
1401 ariadna 143
    if ($user) {
1415 ariadna 144
        $filename   = trim(isset($data['image_filename']) ? filter_var($data['image_filename'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH) : '');
1401 ariadna 145
        $content    = trim(isset($data['image_content']) ? filter_var($data['image_content'], FILTER_SANITIZE_STRING) : '');
146
 
1415 ariadna 147
        // Validación básica de nombre de archivo para evitar path traversal
148
        if ($filename && strpos($filename, '..') === false && strpos($filename, DIRECTORY_SEPARATOR) === false) {
149
            $tempfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename; // Usar directorio temporal del sistema
1401 ariadna 150
            try {
1415 ariadna 151
                $decoded_content = base64_decode($content);
152
                if ($decoded_content !== false && !empty($decoded_content)) {
153
                    file_put_contents($tempfile, $decoded_content);
154
 
155
                    if (file_exists($tempfile)) {
156
                        $usericonid = process_new_icon(context_user::instance($user->id, MUST_EXIST), 'user', 'icon', 0, $tempfile);
157
                        if ($usericonid) {
158
                            $DB->set_field('user', 'picture', $usericonid, array('id' => $user->id));
159
                        }
1401 ariadna 160
                    }
96 efrain 161
                }
1401 ariadna 162
            } catch (\Throwable $e) {
1415 ariadna 163
                // IMPORTANT: Registra el error de la imagen, pero no abortes el login del usuario si la imagen falla.
164
                error_log("Error processing user image for user ID {$user->id}: " . $e->getMessage());
1401 ariadna 165
            } finally {
1415 ariadna 166
                // Asegúrate de eliminar el archivo temporal
1401 ariadna 167
                if (file_exists($tempfile)) {
168
                    unlink($tempfile);
169
                }
96 efrain 170
            }
171
        }
172
    }
1401 ariadna 173
}
96 efrain 174
 
1415 ariadna 175
// Verificación de creación/recuperación de usuario
1401 ariadna 176
if (!$user) {
1409 ariadna 177
    output_json_error('ERROR_MOODLE1');
1383 ariadna 178
}
96 efrain 179
 
1415 ariadna 180
// --- Inscripción en Cursos para Usuarios Nuevos ---
181
 
1401 ariadna 182
if ($new_user) {
96 efrain 183
    $role = $DB->get_record('role', array('archetype' => 'student'));
184
    $enrolmethod = 'manual';
1383 ariadna 185
 
1415 ariadna 186
    $courses = get_courses(); // Obtiene todos los cursos
1383 ariadna 187
    foreach ($courses as $course) {
1415 ariadna 188
        // CORRECCIÓN: Error tipográfico 'categoy_id' a 'category_id'
189
        if ($course->category_id == LLWS_CATEGORY_ID) {
96 efrain 190
            $context = context_course::instance($course->id);
1415 ariadna 191
            if (!is_enrolled($context, $user)) { // Verifica si el usuario ya está inscrito
96 efrain 192
                $enrol = enrol_get_plugin($enrolmethod);
193
                if ($enrol === null) {
1415 ariadna 194
                    // Si el método de matriculación manual no existe, loguea un error y continúa.
195
                    // No debería abortar el login del usuario por esto.
196
                    error_log("Enrolment method '{$enrolmethod}' not found for course ID {$course->id}.");
197
                    continue; // Pasa al siguiente curso
96 efrain 198
                }
1415 ariadna 199
 
200
                // Obtener o crear una instancia de matriculación manual para el curso
201
                $instance = $enrol->add_default_instance($course); // Esto intenta encontrar o crear una instancia por defecto
202
                if ($instance === null) {
203
                    // Si add_default_instance falla (ej. no hay permisos), intenta crear una nueva instancia.
204
                    // Esto es menos común si add_default_instance no funciona, pero es una alternativa.
205
                    $instanceid = $enrol->add_instance($course);
96 efrain 206
                    $instance = $DB->get_record('enrol', array('id' => $instanceid));
207
                }
1415 ariadna 208
 
209
                if ($instance) {
210
                    $enrol->enrol_user($instance, $user->id, $role->id);
211
                } else {
212
                    error_log("Failed to get or create manual enrolment instance for course ID {$course->id}.");
213
                }
96 efrain 214
            }
215
        }
216
    }
217
}
218
 
1415 ariadna 219
// --- Completar el Proceso de Inicio de Sesión ---
220
 
221
// Obtener datos completos del usuario (asegura que todos los campos de Moodle estén cargados)
1401 ariadna 222
$user = get_complete_user_data('id', $user->id);
1415 ariadna 223
 
1401 ariadna 224
if ($user) {
225
    // Verificar si la cuenta está confirmada
1392 ariadna 226
    if (empty($user->confirmed)) {
1409 ariadna 227
        output_json_error('ACCOUNT_NOT_CONFIRMED');
1392 ariadna 228
    }
229
 
1401 ariadna 230
    // Verificar si la contraseña ha expirado (solo para autenticación LDAP)
1392 ariadna 231
    $userauth = get_auth_plugin($user->auth);
232
    if (!isguestuser() && !empty($userauth->config->expiration) && $userauth->config->expiration == 1) {
233
        $days2expire = $userauth->password_expire($user->username);
234
        if (intval($days2expire) < 0) {
1409 ariadna 235
            output_json_error('PASSWORD_EXPIRED');
1391 ariadna 236
        }
1392 ariadna 237
    }
1391 ariadna 238
 
1415 ariadna 239
    // CORRECCIÓN: Para un flujo de login web, Moodle maneja la sesión existente.
240
    // complete_user_login() es suficiente para establecer la sesión del usuario.
241
    // Eliminar el cierre agresivo de todas las sesiones a menos que sea un requisito muy específico.
242
    // Si la sesión actual ya está autenticada, complete_user_login() la actualizará.
243
    // Si necesitas forzar un re-login de la sesión actual, puedes hacerlo de forma más granular:
244
    /*
245
    if (isloggedin() && get_userid() !== $user->id) { // Si un usuario diferente ya está logueado en esta sesión
1414 ariadna 246
        \core\session\manager::terminate_current();
247
        session_destroy();
1415 ariadna 248
        setcookie('MoodleSession', '', time() - 3600, '/'); // Eliminar la cookie de la sesión anterior
1414 ariadna 249
    }
1415 ariadna 250
    */
1414 ariadna 251
 
1415 ariadna 252
    // Completar el proceso de inicio de sesión de Moodle.
253
    // Esto establecerá la sesión del usuario $user.
1413 ariadna 254
    complete_user_login($user);
1401 ariadna 255
 
1415 ariadna 256
    // Aplicar límite de inicio de sesión concurrente (descomentado si es necesario)
257
    // \core\session\manager::apply_concurrent_login_limit($user->id, session_id());
1391 ariadna 258
 
1415 ariadna 259
    // Configurar cookie de nombre de usuario (Moodle lo maneja internamente)
1392 ariadna 260
    if (!empty($CFG->nolastloggedin)) {
261
        // No almacenar último usuario conectado en cookie
262
    } else if (empty($CFG->rememberusername)) {
1401 ariadna 263
        // Sin cookies permanentes, eliminar la anterior si existe
1392 ariadna 264
        set_moodle_cookie('');
265
    } else {
266
        set_moodle_cookie($user->username);
267
    }
1391 ariadna 268
 
1415 ariadna 269
    // Limpiar mensajes de error y redirección de sesión antes de la redirección final
1392 ariadna 270
    unset($SESSION->loginerrormsg);
271
    unset($SESSION->logininfomsg);
1415 ariadna 272
    unset($SESSION->loginredirect); // Descartar loginredirect si estamos redirigiendo
1401 ariadna 273
 
274
    // Configurar la URL de destino
1415 ariadna 275
    $urltogo = $CFG->wwwroot . '/my'; // Página "Mi Moodle"
1391 ariadna 276
 
1415 ariadna 277
    // Verificar que la sesión se haya iniciado correctamente antes de redirigir
1398 ariadna 278
    if (isloggedin() && !isguestuser()) {
1415 ariadna 279
        // Redirección HTTP para una vista web exitosa
1401 ariadna 280
        redirect($urltogo);
281
    } else {
1415 ariadna 282
        // Falló la autenticación final en Moodle, a pesar de las validaciones previas.
283
        output_json_error('LOGIN_FAILED_MOODLE');
1399 ariadna 284
    }
1401 ariadna 285
} else {
1415 ariadna 286
    // Esto debería ser capturado por output_json_error('ERROR_MOODLE1') anteriormente,
287
    // pero es un fallback.
1409 ariadna 288
    output_json_error('USER_NOT_FOUND');
96 efrain 289
}
1415 ariadna 290
 
291
// El script debería terminar con una redirección o una respuesta JSON de error.
292
// El 'exit;' final es redundante si se usa redirect() o output_json_error() que ya contienen exit().
293
exit;