Passed
Push — 1.11.x ( a6e793...dfe42b )
by Yannick
09:53
created

generateMailFromFirstAndLastNames()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 7
rs 10
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
/**
6
 * This script imports users from an XLSX file, compares them to existing users in the Chamilo database,
7
 * updates or inserts users based on specific rules, and generates XLSX files for accounts with missing
8
 * email, lastname, or username, and exports accounts with duplicate Mail or Nom Prénom fields.
9
 * Uses 'Nom' and 'Prénom' columns for name duplicates and includes 'Actif' in exports.
10
 * - Skips import of users with empty 'Actif' unless they exist in DB (then updates, including inactive status)
11
 * - Updates existing users by username (always generated via generateProposedLogin) instead of importing as new
12
 * - Stores 'Matricule' as extra field 'external_user_id' without trimming leading zeros
13
 * - Logs decisions (skipped, updated, inserted) with details in simulation or proceed mode
14
 * - Stops processing on two consecutive empty rows in XLSX
15
 * - Allows custom output directory for XLSX files via command line
16
 * - Always generates username using generateProposedLogin()
17
 * - Username format: lastname + first letter of each firstname word; for active duplicates, append next letter from last firstname part
18
 * - For 3+ occurrences of lastname + firstname, append increasing letters from last firstname part (e.g., jpii, jpiii)
19
 * - Generates unmatched_db_users.xlsx listing database users not found in the input XLSX based on username
20
 * - Exports terminal output to import-yyyymmddhhiiss.log in the output directory.
21
 */
22
23
// Ensure the script is run from the command line
24
if (php_sapi_name() !== 'cli') {
25
    exit('This script must be run from the command line.');
26
}
27
28
// Configuration
29
$domain = 'example.com'; // Manually configured domain for generated emails
30
31
// Include Chamilo bootstrap and necessary classes
32
require_once __DIR__.'/../../main/inc/global.inc.php';
33
// Include PHPExcel classes (assuming installed via Composer)
34
require_once __DIR__.'/../../vendor/autoload.php';
35
require_once __DIR__.'/../../vendor/phpoffice/phpexcel/Classes/PHPExcel.php';
36
require_once __DIR__.'/../../vendor/phpoffice/phpexcel/Classes/PHPExcel/IOFactory.php';
37
38
// Command-line arguments parsing (without getopt)
39
$proceed = false;
40
$outputDir = '/tmp'; // Default output directory
41
$xlsxFile = $argv[1] ?? ''; // Expect XLSX file path as first argument
42
43
// Parse arguments manually
44
for ($i = 2; $i < count($argv); $i++) {
45
    $arg = $argv[$i];
46
    if ($arg === '--proceed' || $arg === '-p') {
47
        $proceed = true;
48
    } elseif (preg_match('/^--output-dir=(.+)$/', $arg, $matches)) {
49
        $outputDir = $matches[1];
50
    } elseif ($arg === '-o' && $i + 1 < count($argv)) {
51
        $outputDir = $argv[++$i];
52
    } elseif (preg_match('/^--mail-domain=(.+)$/', $arg, $matches)) {
53
        $domain = $matches[1];
54
    }
55
}
56
57
// Validate and prepare output directory
58
if (!is_dir($outputDir)) {
59
    if (!mkdir($outputDir, 0755, true)) {
60
        exit("Error: Could not create output directory '$outputDir'\n");
61
    }
62
}
63
if (!is_writable($outputDir)) {
64
    exit("Error: Output directory '$outputDir' is not writable\n");
65
}
66
// Ensure trailing slash for consistency
67
$outputDir = rtrim($outputDir, '/').'/';
68
69
// Start output buffering to capture terminal output
70
ob_start();
71
72
// Add start timestamp
73
$startTime = new DateTime();
74
echo '['.$startTime->format('Y-m-d H:i:s')."] Script started\n";
75
76
// Debug: Log parsed arguments
77
echo "Parsed arguments:\n";
78
echo "  XLSX file: $xlsxFile\n";
79
echo "  Proceed: ".($proceed ? 'true' : 'false')."\n";
80
echo "  Output directory: $outputDir\n";
81
82
if (empty($xlsxFile) || !file_exists($xlsxFile)) {
83
    exit("Usage: php import_users_from_xlsx.php <path_to_xlsx_file> [-p|--proceed] [-o <directory>|--output-dir=<directory>]\n");
84
}
85
86
// Initialize database connection
87
global $database;
88
89
// Load XLSX file
90
try {
91
    $inputFileType = PHPExcel_IOFactory::identify($xlsxFile);
92
    $reader = PHPExcel_IOFactory::createReader($inputFileType);
93
    $phpExcel = $reader->load($xlsxFile);
94
    $worksheet = $phpExcel->getActiveSheet();
95
    $xlsxRows = $worksheet->toArray();
96
} catch (Exception $e) {
97
    exit("Error loading XLSX file: {$e->getMessage()}\n");
98
}
99
100
// Map XLSX columns to Chamilo database user table fields
101
$xlsxColumnMap = [
102
    'Nom' => 'lastname',
103
    'Prénom' => 'firstname',
104
    'Nom Prénom' => 'fullname',
105
    'Mail' => 'email',
106
    'Matricule' => 'official_code',
107
    'N° de badge' => 'password',
108
    'Tel mobile' => 'phone',
109
    'Actif' => 'active',
110
];
111
112
// Extract headers and validate
113
$xlsxHeaders = array_shift($xlsxRows);
114
$xlsxColumnIndices = [];
115
foreach ($xlsxColumnMap as $xlsxHeader => $dbField) {
116
    $index = array_search($xlsxHeader, $xlsxHeaders);
117
    if ($index === false) {
118
        exit("Missing required column: {$xlsxHeader}\n");
119
    }
120
    $xlsxColumnIndices[$dbField] = $index;
121
}
122
123
// Initialize arrays to store rows with missing fields, duplicates, and XLSX usernames
124
$emailMissing = [];
125
$lastnameMissing = [];
126
$usernameMissing = [];
127
$xlsxEmailCounts = [];
128
$xlsxNameCounts = [];
129
$duplicateEmails = [];
130
$duplicateNames = [];
131
$generatedEmails = []; // username -> generatedEmail
132
$xlsxUsernames = []; // Store usernames from XLSX
133
134
// Output columns for missing field and duplicate files
135
$outputColumns = ['Matricule', 'Nom', 'Prénom', 'Nom Prénom', 'Mail', 'N° de badge', 'Actif', 'Generated login'];
136
137
// Normalize string for duplicate detection
138
function normalizeName($name)
139
{
140
    $name = strtolower(trim($name));
141
    $name = preg_replace('/[\s-]+/', ' ', $name);
142
143
    return $name;
144
}
145
146
// Remove accents from strings
147
function removeAccents($str)
148
{
149
    $str = str_replace(
150
        ['à', 'á', 'â', 'ã', 'ä', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ù', 'Ú', 'Û', 'Ü', 'Ý',"'"],
151
        ['a', 'a', 'a', 'a', 'a', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'n', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y', 'y', 'A', 'A', 'A', 'A', 'A', 'A', 'C', 'E', 'E', 'E', 'E', 'I', 'I', 'I', 'I', 'N', 'O', 'O', 'O', 'O', 'O', 'U', 'U', 'U', 'U', 'Y',''],
152
        $str
153
    );
154
155
    return $str;
156
}
157
158
// Generate login based on lastname and firstname
159
function generateProposedLogin($xlsxLastname, $xlsxFirstname, $isActive, &$usedLogins)
160
{
161
    $lastname = strtolower(trim(removeAccents($xlsxLastname)));
162
    $lastname = preg_replace('/[\s-]+/', '', $lastname);
163
164
    $firstname = trim(removeAccents($xlsxFirstname));
165
    $firstnameParts = preg_split('/[\s-]+/', $firstname, -1, PREG_SPLIT_NO_EMPTY);
166
    $firstLetters = '';
167
    foreach ($firstnameParts as $part) {
168
        if (!empty($part)) {
169
            $firstLetters .= strtolower(substr($part, 0, 1));
170
        }
171
    }
172
173
    // Base username: lastname + first letter of each firstname word
174
    $baseLogin = $lastname.$firstLetters;
175
    $login = $baseLogin;
176
177
    // Get last part of firstname for duplicate resolution
178
    $lastFirstnamePart = end($firstnameParts);
179
    $lastPartLetters = strtolower(preg_replace('/[\s-]+/', '', $lastFirstnamePart));
180
181
    // Handle duplicates by incrementally adding letters from the last firstname part if active
182
    if ($isActive) {
183
        $letterCount = 0;
184
        while (isset($usedLogins['logins'][$login]) && $usedLogins['logins'][$login]['active']) {
185
            $letterCount++;
186
            if ($letterCount > strlen($lastPartLetters) - 1) {
187
                break; // No more letters available. Will append a number below
188
            }
189
            $login = $baseLogin . substr($lastPartLetters, 1, $letterCount);
190
        }
191
    }
192
193
    // Ensure uniqueness by appending a number if still conflicting
194
    $suffix = 1;
195
    $originalLogin = $login;
196
    while (isset($usedLogins['logins'][$login]) && $usedLogins['logins'][$login]['active']) {
197
        $login = $originalLogin . $suffix;
198
        $suffix++;
199
    }
200
201
    // Store login with active status
202
    $usedLogins['logins'][$login] = ['active' => $isActive];
203
204
    return $login;
205
}
206
207
// Generate XLSX files for missing fields and duplicates
208
function createMissingFieldFile($filename, $rows, $columns)
209
{
210
    if (empty($rows)) {
211
        echo "No rows to write for $filename\n";
212
213
        return;
214
    }
215
216
    $phpExcel = new PHPExcel();
217
    $worksheet = $phpExcel->getActiveSheet();
218
219
    foreach ($columns as $colIndex => $column) {
220
        $worksheet->setCellValueByColumnAndRow($colIndex, 1, $column);
221
    }
222
223
    foreach ($rows as $rowIndex => $rowData) {
224
        foreach ($columns as $colIndex => $column) {
225
            $worksheet->setCellValueByColumnAndRow($colIndex, $rowIndex + 2, $rowData[$column]);
226
        }
227
    }
228
229
    try {
230
        $writer = PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007');
231
        $writer->save($filename);
232
        echo "Generated $filename with ".count($rows)." rows\n";
233
    } catch (Exception $e) {
234
        echo "Error saving $filename: {$e->getMessage()}\n";
235
    }
236
}
237
238
/**
239
 * Generate a tentative e-mail address from firstname and lastname
240
 */
241
function generateMailFromFirstAndLastNames(string $firstname, string $lastname, string $domain): string
242
{
243
    $emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($lastname)), -1, PREG_SPLIT_NO_EMPTY);
244
    $emailLastname = !empty($emailLastnameParts[0]) ? strtolower($emailLastnameParts[0]) : '';
245
    $emailFirstnameParts = preg_split('/[\s-]+/', trim(removeAccents($firstname)), -1, PREG_SPLIT_NO_EMPTY);
246
    $emailFirstname = !empty($emailFirstnameParts[0]) ? strtolower($emailFirstnameParts[0]) : '';
247
    return "$emailLastname.$emailFirstname@$domain";
248
}
249
250
// Detect potential issues in XLSX file
251
$usedLogins = ['logins' => [], 'counts' => []];
252
$generatedEmailCounts = [];
253
$emptyRowCount = 0;
254
foreach ($xlsxRows as $rowIndex => $xlsxRow) {
255
    // Check for empty row
256
    $isEmpty = true;
257
    foreach ($xlsxRow as $cell) {
258
        if (!empty(trim($cell))) {
259
            $isEmpty = false;
260
            break;
261
        }
262
    }
263
    if ($isEmpty) {
264
        $emptyRowCount++;
265
        if ($emptyRowCount >= 2) {
266
            echo "Stopping processing: Found two consecutive empty rows at row ".($rowIndex + 2)."\n";
267
            break;
268
        }
269
        continue;
270
    } else {
271
        $emptyRowCount = 0; // Reset counter if row is not empty
272
    }
273
274
    $xlsxUserData = [];
275
    foreach ($xlsxColumnMap as $dbField) {
276
        $xlsxUserData[$dbField] = $xlsxRow[$xlsxColumnIndices[$dbField]] ?? '';
277
    }
278
279
    // Generate username and store it
280
    $isActive = !empty($xlsxUserData['active']);
281
    $xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
282
    $xlsxUsernames[] = $xlsxUserData['username'];
283
284
    $rowData = [
285
        'Matricule' => $xlsxUserData['official_code'],
286
        'Nom' => $xlsxUserData['lastname'],
287
        'Prénom' => $xlsxUserData['firstname'],
288
        'Nom Prénom' => $xlsxUserData['fullname'],
289
        'Mail' => $xlsxUserData['email'],
290
        'N° de badge' => $xlsxUserData['password'],
291
        'Actif' => $xlsxUserData['active'],
292
        'Generated login' => $xlsxUserData['username'],
293
    ];
294
295
    if ($isActive) {
296
        if (empty($xlsxUserData['email'])) {
297
            $generatedEmail = $baseEmail = generateMailFromFirstAndLastNames($xlsxUserData['firstname'], $xlsxUserData['lastname'], $domain);
298
            $suffix = isset($generatedEmailCounts[$baseEmail]) ? count($generatedEmailCounts[$baseEmail]) + 1 : 1;
299
            if ($suffix > 1) {
300
                $generatedEmail = preg_replace('/^([^@]+)@(.+)/', '${1}'.$suffix.'@${2}', $baseEmail);
301
            }
302
            $generatedEmail = strtoupper($generatedEmail);
303
            $generatedEmailCounts[$baseEmail][] = $rowData;
304
305
            $rowData['Mail'] = $generatedEmail;
306
            $xlsxUserData['email'] = $generatedEmail;
307
            $xlsxUserData['emailSource'] = 'Generated during import';
308
            $emailMissing[] = $rowData;
309
            $xlsxEmailCounts[$generatedEmail][] = $rowData;
310
            $generatedEmails[$xlsxUserData['official_code']] = [$generatedEmail];
311
        }
312
313
        if (empty($xlsxUserData['lastname'])) {
314
            $lastnameMissing[] = $rowData;
315
        }
316
        // All usernames are generated
317
        $usernameMissing[] = $rowData;
318
319
        $email = strtolower(trim($xlsxUserData['email']));
320
        $name = normalizeName($xlsxUserData['fullname']);
321
        if (!empty($email)) {
322
            $xlsxEmailCounts[$email][] = $rowData;
323
        }
324
        if (!empty($xlsxUserData['fullname'])) {
325
            $xlsxNameCounts[$name][] = $rowData;
326
        }
327
    }
328
}
329
330
foreach ($xlsxEmailCounts as $email => $rows) {
331
    if (count($rows) > 1) {
332
        $duplicateEmails = array_merge($duplicateEmails, $rows);
333
    }
334
}
335
foreach ($xlsxNameCounts as $name => $rowData) {
336
    if (count($rowData) > 1) {
337
        $duplicateNames = array_merge($duplicateNames, $rowData);
338
    }
339
}
340
341
usort($duplicateEmails, function ($a, $b) {
342
    return strcmp(strtolower($a['Mail'] ?? ''), strtolower($b['Mail'] ?? ''));
343
});
344
345
usort($duplicateNames, function ($a, $b) {
346
    return strcmp(normalizeName($a['Nom Prénom'] ?? ''), normalizeName($b['Nom Prénom'] ?? ''));
347
});
348
349
createMissingFieldFile($outputDir.'email_missing.xlsx', $emailMissing, $outputColumns);
350
createMissingFieldFile($outputDir.'lastname_missing.xlsx', $lastnameMissing, $outputColumns);
351
createMissingFieldFile($outputDir.'username_missing.xlsx', $usernameMissing, $outputColumns);
352
createMissingFieldFile($outputDir.'duplicate_email.xlsx', $duplicateEmails, $outputColumns);
353
createMissingFieldFile($outputDir.'duplicate_name.xlsx', $duplicateNames, $outputColumns);
354
355
// Generate unmatched_db_users.xlsx
356
$unmatchedUsers = [];
357
$sql = "SELECT id, username, official_code, email, active FROM user";
358
$stmt = $database->query($sql);
359
while ($dbUser = $stmt->fetch()) {
360
    if (!in_array($dbUser['username'], $xlsxUsernames) && !empty($dbUser['username'])) {
361
        $unmatchedUsers[] = [
362
            'Matricule' => $dbUser['official_code'],
363
            'Username' => $dbUser['username'],
364
            'User ID' => $dbUser['id'],
365
            'E-mail' => $dbUser['email'],
366
            'Active' => $dbUser['active']?'Yes':'No',
367
        ];
368
    }
369
}
370
$unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail', 'Active'];
371
createMissingFieldFile($outputDir.'unmatched_db_users.xlsx', $unmatchedUsers, $unmatchedColumns);
372
373
// Process users: compare with database, log decisions, and update/insert if --proceed
374
echo "\n=== Processing Users ===\n";
375
$userManager = new UserManager();
376
$usedLogins = ['logins' => [], 'counts' => []]; // Reset usedLogins to avoid false duplicates
377
$emptyRowCount = 0;
378
$userActions = []; // Initialize array to store user actions
379
$userSkippedWhileActive = []; // Initialize array to store special cases
380
foreach ($xlsxRows as $rowIndex => $rowData) {
381
    // Check for empty row
382
    $emailSource = 'SAP';
383
    $isEmpty = true;
384
    foreach ($rowData as $cell) {
385
        if (!empty(trim($cell))) {
386
            $isEmpty = false;
387
            break;
388
        }
389
    }
390
    if ($isEmpty) {
391
        $emptyRowCount++;
392
        if ($emptyRowCount >= 2) {
393
            echo "Stopping processing: Found two consecutive empty rows at row ".($rowIndex + 2)."\n";
394
            break;
395
        }
396
        continue;
397
    } else {
398
        $emptyRowCount = 0; // Reset counter if row is not empty
399
    }
400
401
    $xlsxUserData = [];
402
    foreach ($xlsxColumnMap as $dbField) {
403
        $xlsxUserData[$dbField] = $rowData[$xlsxColumnIndices[$dbField]] ?? '';
404
    }
405
406
    // Generate username
407
    $isActive = !empty($xlsxUserData['active']);
408
    $xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
409
    $dbUsername = Database::escape_string($xlsxUserData['username']);
410
411
    if (!empty($xlsxUserData['official_code']) && !empty($generatedEmails[$xlsxUserData['official_code']]))
412
    {
413
        $emailSource = 'E-mail generated during import';
414
        $xlsxUserData['email'] = $generatedEmails[$xlsxUserData['official_code']];
415
    } elseif (!empty($rowData['emailSource'])) {
416
        $emailSource = $rowData['emailSource'];
417
    }
418
419
    // Get current time for row logging
420
    $rowTime = new DateTime();
421
422
    // Skip users with Matricule starting with 0009
423
    if (strpos($xlsxUserData['official_code'], '0009') === 0) {
424
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - Matricule starts with 0009 (username: $dbUsername)\n";
425
        $logRow = [
426
            'Action Type' => 'skipped',
427
            'User ID' => '',
428
            'Username' => $dbUsername,
429
            'Official Code' => $xlsxUserData['official_code'],
430
            'E-mail' => $xlsxUserData['email'],
431
            'E-mail source' => $emailSource,
432
            'External User ID' => $xlsxUserData['official_code'],
433
            'Updated Fields' => 'Matricule starts with 0009',
434
        ];
435
        $userActions[] = $logRow;
436
        $userSkippedWhileActive[] = $logRow;
437
        continue;
438
    }
439
440
    // Check for existing user by username
441
    $sql = "SELECT id, firstname, lastname, email, official_code, phone, active, status, picture_uri, expiration_date, language, creator_id
442
            FROM user
443
            WHERE username = '$dbUsername'";
444
    $stmt = $database->query($sql);
445
    $dbUser = $stmt->fetch();
446
447
    // Prepare data for logging and potential import/update
448
    $xlsxMatricule = $xlsxUserData['official_code'] ?? ''; // Keep leading zeros
449
    $xlsxActive = !empty($xlsxUserData['active']) ? 1 : 0;
450
451
    // Decision logic
452
    if (empty($dbUser) && empty($xlsxUserData['active'])) {
453
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - 'Actif' is empty and no matching user in database (username: $dbUsername)\n";
454
        $emailSource = 'Not relevant (user ignored)';
455
        $userActions[] = [
456
            'Action Type' => 'skipped',
457
            'User ID' => '',
458
            'Username' => $dbUsername,
459
            'Official Code' => $xlsxUserData['official_code'],
460
            'E-mail' => $xlsxUserData['email'],
461
            'E-mail source' => $emailSource,
462
            'External User ID' => $xlsxMatricule,
463
            'Updated Fields' => '"Active" field is empty and no matching user in database',
464
        ];
465
        continue;
466
    }
467
468
    // Validate required fields
469
    $requiredFields = ['lastname', 'firstname', 'email'];
470
    $missingFields = [];
471
    foreach ($requiredFields as $field) {
472
        if (empty($xlsxUserData[$field])) {
473
            $missingFields[] .= $field;
474
            if ($field == 'email') {
475
                $emailSource = 'EMPTY IN SAP';
476
            }
477
        }
478
    }
479
480
    if (!empty($missingFields)) {
481
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).': Skipped - missing fields: '.implode(', ', $missingFields)." (username: $dbUsername)\n";
482
        $logRow = [
483
            'Action Type' => 'skipped',
484
            'User ID' => '',
485
            'Username' => $dbUsername,
486
            'Official Code' => $xlsxUserData['official_code'],
487
            'E-mail' => $xlsxUserData['email'],
488
            'E-mail source' => $emailSource,
489
            'External User ID' => $xlsxMatricule,
490
            'Updated Fields' => 'Missing fields: '.implode(', ', $missingFields),
491
        ];
492
        $userActions[] = $logRow;
493
        $userSkippedWhileActive[] = $logRow;
494
        continue;
495
    }
496
497
    // If the user was found/existed in the local database
498
    if ($dbUser) {
499
        // Check for updates
500
        $updates = [];
501
        if ($dbUser['firstname'] !== $xlsxUserData['firstname']) {
502
            $updates[] .= "firstname: '".$dbUser['firstname']."' -> '".$xlsxUserData['firstname']."' ";
503
        }
504
        if ($dbUser['lastname'] !== $xlsxUserData['lastname']) {
505
            $updates[] .= "lastname: '".$dbUser['lastname']."' -> '".$xlsxUserData['lastname']."' ";
506
        }
507
        if ($dbUser['email'] !== $xlsxUserData['email']) {
508
            $updates[] .= "email: '".$dbUser['email']."' -> '".$xlsxUserData['email']."' ";
509
        }
510
        if ($dbUser['official_code'] !== $xlsxUserData['official_code']) {
511
            $updates[] .= "official_code: '".$dbUser['official_code']."' -> '".$xlsxUserData['official_code']."' ";
512
        }
513
        if ($dbUser['phone'] !== ($xlsxUserData['phone'] ?? '')) {
514
            $updates[] .= "phone: '".$dbUser['phone']."' -> '".($xlsxUserData['phone'] ?? '')."' ";
515
        }
516
        if ($dbUser['active'] != $xlsxActive) {
517
            $updates[] .= "active: ".$dbUser['active']." -> '".$xlsxActive."' ";
518
        }
519
520
        if (!empty($updates)) {
521
            echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Update - Existing user found, updates needed (username: $dbUsername)\n";
522
            echo "  Updates: ".implode(', ', $updates)."\n";
523
            if ($proceed) {
524
                try {
525
                    $user = UserManager::update_user(
526
                        $dbUser['id'],
527
                        $xlsxUserData['firstname'],
528
                        $xlsxUserData['lastname'],
529
                        $xlsxUserData['username'], // username generated from lastname + firstname's first letter (although it should not change, it is required by the update_user method)
530
                        null, // password not updated
531
                        null, // auth_source
532
                        $xlsxUserData['email'],
533
                        $dbUser['status'], // status
534
                        $xlsxUserData['official_code'],
535
                        $xlsxUserData['phone'],
536
                        $dbUser['picture_uri'], // picture_uri
537
                        $dbUser['expiration_date'], // expiration_date
538
                        $xlsxActive,
539
                        $dbUser['creator_id'],
540
                        0,
541
                        null,
542
                        $dbUser['language']
543
                    );
544
                    if ($user) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
545
                        // Update extra field 'external_user_id'
546
                        UserManager::update_extra_field_value($dbUser['id'], 'external_user_id', $xlsxMatricule);
547
                        echo "  Success: Updated user and external_user_id (username: $dbUsername)\n";
548
                        $userActions[] = [
549
                            'Action Type' => 'updated',
550
                            'User ID' => $dbUser['id'],
551
                            'Username' => $dbUsername,
552
                            'Official Code' => $xlsxUserData['official_code'],
553
                            'E-mail' => $xlsxUserData['email'],
554
                            'E-mail source' => $emailSource,
555
                            'External User ID' => $xlsxMatricule,
556
                            'Updated Fields' => implode(', ', array_map(function($update) { return trim(explode(':', $update)[0]); }, $updates)),
557
                        ];
558
                    } else {
559
                        echo "  Error: Could not update user (username: $dbUsername)\n";
560
                        $logRow = [
561
                            'Action Type' => 'skipped',
562
                            'User ID' => $dbUser['id'],
563
                            'Username' => $dbUsername,
564
                            'Official Code' => $xlsxUserData['official_code'],
565
                            'E-mail' => $xlsxUserData['email'],
566
                            'E-mail source' => $emailSource,
567
                            'External User ID' => $xlsxMatricule,
568
                            'Updated Fields' => 'Could not update user',
569
                        ];
570
                        $userActions[] = $logRow;
571
                        $userSkippedWhileActive[] = $logRow;
572
                    }
573
                } catch (Exception $e) {
574
                    echo "  Error: Failed to update user (username: $dbUsername): {$e->getMessage()}\n";
575
                    $logRow = [
576
                        'Action Type' => 'skipped',
577
                        'User ID' => $dbUser['id'],
578
                        'Username' => $dbUsername,
579
                        'Official Code' => $xlsxUserData['official_code'],
580
                        'E-mail' => $xlsxUserData['email'],
581
                        'E-mail source' => $emailSource,
582
                        'External User ID' => $xlsxMatricule,
583
                        'Updated Fields' => 'Failed to update user: '.$e->getMessage(),
584
                    ];
585
                    $userActions[] = $logRow;
586
                    $userSkippedWhileActive[] = $logRow;
587
                }
588
            } else {
589
                echo "   Sim mode: Updated user and external_user_id (username: $dbUsername)\n";
590
                $userActions[] = [
591
                    'Action Type' => 'updated',
592
                    'User ID' => $dbUser['id'],
593
                    'Username' => $dbUsername,
594
                    'Official Code' => $xlsxUserData['official_code'],
595
                    'E-mail' => $xlsxUserData['email'],
596
                    'E-mail source' => $emailSource,
597
                    'External User ID' => $xlsxMatricule,
598
                    'Updated Fields' => implode(', ', array_map(function($update) { return trim(explode(':', $update)[0]); }, $updates)),
599
                ];
600
            }
601
        } else {
602
            echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": No action - no changes needed (username: $dbUsername)\n";
603
            $logRow = [
604
                'Action Type' => 'skipped',
605
                'User ID' => $dbUser['id'],
606
                'Username' => $dbUsername,
607
                'Official Code' => $xlsxUserData['official_code'],
608
                'E-mail' => $xlsxUserData['email'],
609
                'E-mail source' => $emailSource,
610
                'External User ID' => $xlsxMatricule,
611
                'Updated Fields' => 'No changes needed',
612
            ];
613
            $userActions[] = $logRow;
614
            $userSkippedWhileActive[] = $logRow;
615
        }
616
    } else {
617
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Insert new user - No existing user found (username: $dbUsername)\n";
618
        if ($proceed) {
619
            try {
620
                $password = !empty($xlsxUserData['password']) ? $xlsxUserData['password'] : 'temporary_password';
621
                $userId = $userManager->create_user(
622
                    $xlsxUserData['firstname'],
623
                    $xlsxUserData['lastname'],
624
                    5, // status (5 = student, adjust as needed)
625
                    $xlsxUserData['email'],
626
                    $dbUsername,
627
                    $password,
628
                    $xlsxUserData['official_code'],
629
                    '', // language
630
                    $xlsxUserData['phone'],
631
                    '',
632
                    null,
633
                    null,
634
                    $xlsxActive,
635
                    0,
636
                    null  // creator_id
637
                );
638
                if ($userId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
639
                    // Add extra field 'external_user_id'
640
                    $userManager->update_extra_field_value($userId, 'external_user_id', $xlsxMatricule);
641
                    echo "  Success: Created user and set external_user_id (username: $dbUsername)\n";
642
                    $userActions[] = [
643
                        'Action Type' => 'created',
644
                        'User ID' => $userId,
645
                        'Username' => $dbUsername,
646
                        'Official Code' => $xlsxUserData['official_code'],
647
                        'E-mail' => $xlsxUserData['email'],
648
                        'E-mail source' => $emailSource,
649
                        'External User ID' => $xlsxMatricule,
650
                        'Updated Fields' => '',
651
                    ];
652
                } else {
653
                    echo "  Error: Could not create user (username: $dbUsername)\n";
654
                    $logRow = [
655
                        'Action Type' => 'skipped',
656
                        'User ID' => '',
657
                        'Username' => $dbUsername,
658
                        'Official Code' => $xlsxUserData['official_code'],
659
                        'E-mail' => $xlsxUserData['email'],
660
                        'E-mail source' => $emailSource,
661
                        'External User ID' => $xlsxMatricule,
662
                        'Updated Fields' => 'Could not create user',
663
                    ];
664
                    $userActions[] = $logRow;
665
                    $userSkippedWhileActive[] = $logRow;
666
                }
667
            } catch (Exception $e) {
668
                echo "  Error: Failed to insert user (username: $dbUsername): {$e->getMessage()}\n";
669
                $logRow = [
670
                    'Action Type' => 'skipped',
671
                    'User ID' => '',
672
                    'Username' => $dbUsername,
673
                    'Official Code' => $xlsxUserData['official_code'],
674
                    'E-mail' => $xlsxUserData['email'],
675
                    'E-mail source' => $emailSource,
676
                    'External User ID' => $xlsxMatricule,
677
                    'Updated Fields' => 'Failed to insert user: '.$e->getMessage(),
678
                ];
679
                $userActions[] = $logRow;
680
                $userSkippedWhileActive[] = $logRow;
681
            }
682
        } else {
683
            echo "   Sim mode: Inserted user and external_user_id (username: $dbUsername)\n";
684
            $userActions[] = [
685
                'Action Type' => 'created',
686
                'User ID' => '',
687
                'Username' => $dbUsername,
688
                'Official Code' => $xlsxUserData['official_code'],
689
                'E-mail' => $xlsxUserData['email'],
690
                'E-mail source' => $emailSource,
691
                'External User ID' => $xlsxMatricule,
692
                'Updated Fields' => '',
693
            ];
694
        }
695
    }
696
}
697
698
// Generate user actions XLSX file
699
$actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'E-mail source', 'External User ID', 'Updated Fields'];
700
createMissingFieldFile($outputDir.'user_actions.xlsx', $userActions, $actionColumns);
701
createMissingFieldFile($outputDir.'skipped_user_actions.xlsx', $userSkippedWhileActive, $actionColumns);
702
703
if (!$proceed) {
704
    echo "\nUse --proceed to apply changes to the database.\n";
705
} else {
706
    echo "\nImport completed successfully.\n";
707
}
708
709
// Save terminal output to log file
710
$output = ob_get_clean();
711
echo $output; // Output to terminal
712
$endTime = new DateTime();
713
$output .= '['.$endTime->format('Y-m-d H:i:s')."] Script completed\n";
714
$logTimestamp = $startTime->format('YmdHis');
715
$logFile = $outputDir.'import-'.$logTimestamp.'.log';
716
if (!file_put_contents($logFile, $output)) {
717
    echo "Error: Could not write to log file $logFile\n";
718
} else {
719
    echo "Generated log file: $logFile\n";
720
}
721