Passed
Push — 1.11.x ( 243677...85febd )
by Yannick
32:40 queued 20:25
created

removeAccents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 1
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
    die('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
        die("Error: Could not create output directory '$outputDir'\n");
61
    }
62
}
63
if (!is_writable($outputDir)) {
64
    die("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
    die("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
    die("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
        die("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
$xlsxUsernames = []; // Store usernames from XLSX
132
133
// Output columns for missing field and duplicate files
134
$outputColumns = ['Matricule', 'Nom', 'Prénom', 'Nom Prénom', 'Mail', 'N° de badge', 'Actif', 'Generated login'];
135
136
// Normalize string for duplicate detection
137
function normalizeName($name)
138
{
139
    $name = strtolower(trim($name));
140
    $name = preg_replace('/[\s-]+/', ' ', $name);
141
    return $name;
142
}
143
144
// Remove accents from strings
145
function removeAccents($str) {
146
    $str = str_replace(
147
        ['à','á','â','ã','ä','ç','è','é','ê','ë','ì','í','î','ï','ñ','ò','ó','ô','õ','ö','ù','ú','û','ü','ý','ÿ','À','Á','Â','Ã','Ä','Å','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ñ','Ò','Ó','Ô','Õ','Ö','Ù','Ú','Û','Ü','Ý'],
148
        ['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'],
149
        $str
150
    );
151
    return $str;
152
}
153
154
// Generate login based on lastname and firstname
155
function generateProposedLogin($xlsxLastname, $xlsxFirstname, $isActive, &$usedLogins) {
156
    $lastname = strtolower(trim(removeAccents($xlsxLastname)));
157
    $lastname = preg_replace('/[\s-]+/', '', $lastname);
158
159
    $firstname = trim(removeAccents($xlsxFirstname));
160
    $firstnameParts = preg_split('/[\s-]+/', $firstname, -1, PREG_SPLIT_NO_EMPTY);
161
    $firstLetters = '';
162
    foreach ($firstnameParts as $part) {
163
        if (!empty($part)) {
164
            $firstLetters .= strtolower(substr($part, 0, 1));
165
        }
166
    }
167
168
    // Base username: lastname + first letter of each firstname word
169
    $baseLogin = $lastname.$firstLetters;
170
    $login = $baseLogin;
171
172
    // Get last part of firstname for duplicate resolution
173
    $lastFirstnamePart = end($firstnameParts);
174
    $lastPartLetters = strtolower(preg_replace('/[\s-]+/', '', $lastFirstnamePart));
175
176
    // Increment occurrence count for this login
177
    $usedLogins['counts'][$login] = isset($usedLogins['counts'][$login]) ? $usedLogins['counts'][$login] + 1 : 1;
178
    $occurrence = $usedLogins['counts'][$login];
179
180
    // Handle duplicates
181
    if (isset($usedLogins['logins'][$login])) {
182
        // Only modify if both current and previous users are active
183
        if ($isActive && $usedLogins['logins'][$login]['active']) {
184
            if ($occurrence == 2) {
185
                // Second occurrence: append next letter from last firstname part
186
                if (strlen($lastPartLetters) > 1) {
187
                    $login = $baseLogin.substr($lastPartLetters, 1, 1); // e.g., 'i' from 'Pierre'
188
                } else {
189
                    $login = $baseLogin.'1'; // Fallback if no more letters
190
                }
191
            } elseif ($occurrence >= 3) {
192
                // Third+ occurrence: append increasing letters from last firstname part
193
                $extraLetters = min($occurrence - 1, strlen($lastPartLetters) - 1); // e.g., 2 letters for 3rd, 3 for 4th
194
                if ($extraLetters > 0) {
195
                    $login = $baseLogin.substr($lastPartLetters, 1, $extraLetters); // e.g., 'ii', 'iii'
196
                } else {
197
                    $login = $baseLogin.($occurrence - 1); // Fallback to number
198
                }
199
            }
200
        }
201
    }
202
203
    // Ensure uniqueness by appending a number if still conflicting
204
    $suffix = 1;
205
    $originalLogin = $login;
206
    while (isset($usedLogins['logins'][$login])) {
207
        $login = $originalLogin.$suffix;
208
        $suffix++;
209
    }
210
211
    // Store login with active status
212
    $usedLogins['logins'][$login] = ['active' => $isActive];
213
    return $login;
214
}
215
216
// Generate XLSX files for missing fields and duplicates
217
function createMissingFieldFile($filename, $rows, $columns) {
218
    if (empty($rows)) {
219
        echo "No rows to write for $filename\n";
220
        return;
221
    }
222
223
    $phpExcel = new PHPExcel();
224
    $worksheet = $phpExcel->getActiveSheet();
225
226
    foreach ($columns as $colIndex => $column) {
227
        $worksheet->setCellValueByColumnAndRow($colIndex, 1, $column);
228
    }
229
230
    foreach ($rows as $rowIndex => $rowData) {
231
        foreach ($columns as $colIndex => $column) {
232
            $worksheet->setCellValueByColumnAndRow($colIndex, $rowIndex + 2, $rowData[$column]);
233
        }
234
    }
235
236
    try {
237
        $writer = PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007');
238
        $writer->save($filename);
239
        echo "Generated $filename with ".count($rows)." rows\n";
240
    } catch (Exception $e) {
241
        echo "Error saving $filename: {$e->getMessage()}\n";
242
    }
243
}
244
245
// Detect potential issues in XLSX file
246
$usedLogins = ['logins' => [], 'counts' => []];
247
$generatedEmailCounts = [];
248
$emptyRowCount = 0;
249
foreach ($xlsxRows as $rowIndex => $xlsxRow) {
250
    // Check for empty row
251
    $isEmpty = true;
252
    foreach ($xlsxRow as $cell) {
253
        if (!empty(trim($cell))) {
254
            $isEmpty = false;
255
            break;
256
        }
257
    }
258
    if ($isEmpty) {
259
        $emptyRowCount++;
260
        if ($emptyRowCount >= 2) {
261
            echo "Stopping processing: Found two consecutive empty rows at row ".($rowIndex + 2)."\n";
262
            break;
263
        }
264
        continue;
265
    } else {
266
        $emptyRowCount = 0; // Reset counter if row is not empty
267
    }
268
269
    $xlsxUserData = [];
270
    foreach ($xlsxColumnMap as $dbField) {
271
        $xlsxUserData[$dbField] = $xlsxRow[$xlsxColumnIndices[$dbField]] ?? '';
272
    }
273
274
    // Generate username and store it
275
    $isActive = !empty($xlsxUserData['active']);
276
    $xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
277
    $xlsxUsernames[] = $xlsxUserData['username'];
278
279
    $rowData = [
280
        'Matricule' => $xlsxUserData['official_code'],
281
        'Nom' => $xlsxUserData['lastname'],
282
        'Prénom' => $xlsxUserData['firstname'],
283
        'Nom Prénom' => $xlsxUserData['fullname'],
284
        'Mail' => $xlsxUserData['email'],
285
        'N° de badge' => $xlsxUserData['password'],
286
        'Actif' => $xlsxUserData['active'],
287
        'Generated login' => $xlsxUserData['username'],
288
    ];
289
290
    if ($isActive) {
291
        if (empty($xlsxUserData['email']) && strpos($xlsxUserData['official_code'], '0009') !== false) {
292
            $emailLastnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['lastname'])), -1, PREG_SPLIT_NO_EMPTY);
293
            $emailLastname = !empty($emailLastnameParts[0]) ? strtolower($emailLastnameParts[0]) : '';
294
            $emailFirstnameParts = preg_split('/[\s-]+/', trim(removeAccents($xlsxUserData['firstname'])), -1, PREG_SPLIT_NO_EMPTY);
295
            $emailFirstname = !empty($emailFirstnameParts[0]) ? strtolower($emailFirstnameParts[0]) : '';
296
297
            $baseEmail = "{$emailLastname}.{$emailFirstname}@{$domain}";
298
            $generatedEmail = $baseEmail;
299
            $suffix = isset($generatedEmailCounts[$baseEmail]) ? count($generatedEmailCounts[$baseEmail]) + 1 : 1;
300
            if ($suffix > 1) {
301
                $generatedEmail = "{$emailLastname}.{$emailFirstname}{$suffix}@{$domain}";
302
            }
303
            $generatedEmail = strtoupper($generatedEmail);
304
            $generatedEmailCounts[$baseEmail][] = $rowData;
305
306
            $rowData['Mail'] = $generatedEmail;
307
            $xlsxUserData['email'] = $generatedEmail;
308
            $emailMissing[] = $rowData;
309
            $xlsxEmailCounts[$generatedEmail][] = $rowData;
310
        } elseif (empty($xlsxUserData['email'])) {
311
            $emailMissing[] = $rowData;
312
        }
313
314
        if (empty($xlsxUserData['lastname'])) {
315
            $lastnameMissing[] = $rowData;
316
        }
317
        // All usernames are generated
318
        $usernameMissing[] = $rowData;
319
320
        $email = strtolower(trim($xlsxUserData['email']));
321
        $name = normalizeName($xlsxUserData['fullname']);
322
        if (!empty($email)) {
323
            $xlsxEmailCounts[$email][] = $rowData;
324
        }
325
        if (!empty($xlsxUserData['fullname'])) {
326
            $xlsxNameCounts[$name][] = $rowData;
327
        }
328
    }
329
}
330
331
foreach ($xlsxEmailCounts as $email => $rows) {
332
    if (count($rows) > 1) {
333
        $duplicateEmails = array_merge($duplicateEmails, $rows);
334
    }
335
}
336
foreach ($xlsxNameCounts as $name => $rowData) {
337
    if (count($rowData) > 1) {
338
        $duplicateNames = array_merge($duplicateNames, $rowData);
339
    }
340
}
341
342
usort($duplicateEmails, function ($a, $b) {
343
    return strcmp(strtolower($a['Mail'] ?? ''), strtolower($b['Mail'] ?? ''));
344
});
345
346
usort($duplicateNames, function ($a, $b) {
347
    return strcmp(normalizeName($a['Nom Prénom'] ?? ''), normalizeName($b['Nom Prénom'] ?? ''));
348
});
349
350
createMissingFieldFile($outputDir.'email_missing.xlsx', $emailMissing, $outputColumns);
351
createMissingFieldFile($outputDir.'lastname_missing.xlsx', $lastnameMissing, $outputColumns);
352
createMissingFieldFile($outputDir.'username_missing.xlsx', $usernameMissing, $outputColumns);
353
createMissingFieldFile($outputDir.'duplicate_email.xlsx', $duplicateEmails, $outputColumns);
354
createMissingFieldFile($outputDir.'duplicate_name.xlsx', $duplicateNames, $outputColumns);
355
356
// Generate unmatched_db_users.xlsx
357
$unmatchedUsers = [];
358
$sql = "SELECT id, username, official_code, email FROM user";
359
$stmt = $database->query($sql);
360
while ($dbUser = $stmt->fetch()) {
361
    if (!in_array($dbUser['username'], $xlsxUsernames) && !empty($dbUser['username'])) {
362
        $unmatchedUsers[] = [
363
            'Matricule' => $dbUser['official_code'],
364
            'Username' => $dbUser['username'],
365
            'User ID' => $dbUser['id'],
366
            'E-mail' => $dbUser['email'],
367
        ];
368
    }
369
}
370
$unmatchedColumns = ['Matricule', 'Username', 'User ID', 'E-mail'];
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
foreach ($xlsxRows as $rowIndex => $rowData) {
380
    // Check for empty row
381
    $isEmpty = true;
382
    foreach ($rowData as $cell) {
383
        if (!empty(trim($cell))) {
384
            $isEmpty = false;
385
            break;
386
        }
387
    }
388
    if ($isEmpty) {
389
        $emptyRowCount++;
390
        if ($emptyRowCount >= 2) {
391
            echo "Stopping processing: Found two consecutive empty rows at row ".($rowIndex + 2)."\n";
392
            break;
393
        }
394
        continue;
395
    } else {
396
        $emptyRowCount = 0; // Reset counter if row is not empty
397
    }
398
399
    $xlsxUserData = [];
400
    foreach ($xlsxColumnMap as $dbField) {
401
        $xlsxUserData[$dbField] = $rowData[$xlsxColumnIndices[$dbField]] ?? '';
402
    }
403
404
    // Generate username
405
    $isActive = !empty($xlsxUserData['active']);
406
    $xlsxUserData['username'] = generateProposedLogin($xlsxUserData['lastname'], $xlsxUserData['firstname'], $isActive, $usedLogins);
407
    $dbUsername = Database::escape_string($xlsxUserData['username']);
408
409
    // Get current time for row logging
410
    $rowTime = new DateTime();
411
412
    // Skip users with Matricule starting with 0009
413
    if (strpos($xlsxUserData['official_code'], '0009') === 0) {
414
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - Matricule starts with 0009 (username: $dbUsername)\n";
415
        $userActions[] = [
416
            'Action Type' => 'skipped',
417
            'User ID' => '',
418
            'Username' => $dbUsername,
419
            'Official Code' => $xlsxUserData['official_code'],
420
            'E-mail' => $xlsxUserData['email'],
421
            'External User ID' => $xlsxUserData['official_code'],
422
            'Updated Fields' => 'Matricule starts with 0009',
423
        ];
424
        continue;
425
    }
426
427
    // Check for existing user by username
428
    $sql = "SELECT id, firstname, lastname, email, official_code, phone, active
429
            FROM user
430
            WHERE username = '$dbUsername'";
431
    $stmt = $database->query($sql);
432
    $dbUser = $stmt->fetch();
433
434
    // Prepare data for logging and potential import/update
435
    $xlsxMatricule = $xlsxUserData['official_code'] ?? ''; // Keep leading zeros
436
    $xlsxActive = !empty($xlsxUserData['active']) ? 1 : 0;
437
438
    // Decision logic
439
    if (empty($dbUser) && empty($xlsxUserData['active'])) {
440
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Skipped - 'Actif' is empty and no matching user in database (username: $dbUsername)\n";
441
        $userActions[] = [
442
            'Action Type' => 'skipped',
443
            'User ID' => '',
444
            'Username' => $dbUsername,
445
            'Official Code' => $xlsxUserData['official_code'],
446
            'E-mail' => $xlsxUserData['email'],
447
            'External User ID' => $xlsxMatricule,
448
            'Updated Fields' => 'Actif is empty and no matching user in database',
449
        ];
450
        continue;
451
    }
452
453
    // Validate required fields
454
    $requiredFields = ['lastname', 'firstname', 'email'];
455
    $missingFields = [];
456
    foreach ($requiredFields as $field) {
457
        if (empty($xlsxUserData[$field])) {
458
            $missingFields[] .= $field;
459
        }
460
    }
461
462
    if (!empty($missingFields)) {
463
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).': Skipped - missing fields: '.implode(', ', $missingFields)." (username: $dbUsername)\n";
464
        $userActions[] = [
465
            'Action Type' => 'skipped',
466
            'User ID' => '',
467
            'Username' => $dbUsername,
468
            'Official Code' => $xlsxUserData['official_code'],
469
            'E-mail' => $xlsxUserData['email'],
470
            'External User ID' => $xlsxMatricule,
471
            'Updated Fields' => 'Missing fields: '.implode(', ', $missingFields),
472
        ];
473
        continue;
474
    }
475
476
    if ($dbUser) {
477
        // Check for updates
478
        $updates = [];
479
        if ($dbUser['firstname'] !== $xlsxUserData['firstname']) {
480
            $updates[] .= "firstname: '".$dbUser['firstname']."' -> '".$xlsxUserData['firstname']."' ";
481
        }
482
        if ($dbUser['lastname'] !== $xlsxUserData['lastname']) {
483
            $updates[] .= "lastname: '".$dbUser['lastname']."' -> '".$xlsxUserData['lastname']."' ";
484
        }
485
        if ($dbUser['email'] !== $xlsxUserData['email']) {
486
            $updates[] .= "email: '".$dbUser['email']."' -> '".$xlsxUserData['email']."' ";
487
        }
488
        if ($dbUser['official_code'] !== $xlsxUserData['official_code']) {
489
            $updates[] .= "official_code: '".$dbUser['official_code']."' -> '".$xlsxUserData['official_code']."' ";
490
        }
491
        if ($dbUser['phone'] !== ($xlsxUserData['phone'] ?? '')) {
492
            $updates[] .= "phone: '".$dbUser['phone']."' -> '".($xlsxUserData['phone'] ?? '')."' ";
493
        }
494
        if ($dbUser['active'] != $xlsxActive) {
495
            $updates[] .= "active: ".$dbUser['active']." -> '".$xlsxActive."' ";
496
        }
497
498
        if (!empty($updates)) {
499
            echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Update - Existing user found, updates needed (username: $dbUsername)\n";
500
            echo "  Updates: ".implode(', ', $updates)."\n";
501
            if ($proceed) {
502
                try {
503
                    $user = UserManager::update_user(
504
                        $dbUser['id'],
505
                        $xlsxUserData['firstname'],
506
                        $xlsxUserData['lastname'],
507
                        $xlsxUserData['username'], // username generated from lastname + firstname's first letter (although it should not change, it is required by the update_user method)
508
                        null, // password not updated
509
                        null, // auth_source
510
                        $xlsxUserData['email'],
511
                        null, // status
512
                        $xlsxUserData['official_code'],
513
                        $xlsxUserData['phone'],
514
                        null, // picture_uri
515
                        null, // expiration_date
516
                        $xlsxActive
517
                    );
518
                    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...
519
                        // Update extra field 'external_user_id'
520
                        UserManager::update_extra_field_value($dbUser['id'], 'external_user_id', $xlsxMatricule);
521
                        echo "  Success: Updated user and external_user_id (username: $dbUsername)\n";
522
                        $userActions[] = [
523
                            'Action Type' => 'updated',
524
                            'User ID' => $dbUser['id'],
525
                            'Username' => $dbUsername,
526
                            'Official Code' => $xlsxUserData['official_code'],
527
                            'E-mail' => $xlsxUserData['email'],
528
                            'External User ID' => $xlsxMatricule,
529
                            'Updated Fields' => implode(', ', array_map(function($update) { return trim(explode(':', $update)[0]); }, $updates)),
530
                        ];
531
                    } else {
532
                        echo "  Error: Could not update user (username: $dbUsername)\n";
533
                        $userActions[] = [
534
                            'Action Type' => 'skipped',
535
                            'User ID' => $dbUser['id'],
536
                            'Username' => $dbUsername,
537
                            'Official Code' => $xlsxUserData['official_code'],
538
                            'E-mail' => $xlsxUserData['email'],
539
                            'External User ID' => $xlsxMatricule,
540
                            'Updated Fields' => 'Could not update user',
541
                        ];
542
                    }
543
                } catch (Exception $e) {
544
                    echo "  Error: Failed to update user (username: $dbUsername): {$e->getMessage()}\n";
545
                    $userActions[] = [
546
                        'Action Type' => 'skipped',
547
                        'User ID' => $dbUser['id'],
548
                        'Username' => $dbUsername,
549
                        'Official Code' => $xlsxUserData['official_code'],
550
                        'E-mail' => $xlsxUserData['email'],
551
                        'External User ID' => $xlsxMatricule,
552
                        'Updated Fields' => 'Failed to update user: '.$e->getMessage(),
553
                    ];
554
                }
555
            } else {
556
                echo "   Sim mode: Updated user and external_user_id (username: $dbUsername)\n";
557
                $userActions[] = [
558
                    'Action Type' => 'updated',
559
                    'User ID' => $dbUser['id'],
560
                    'Username' => $dbUsername,
561
                    'Official Code' => $xlsxUserData['official_code'],
562
                    'E-mail' => $xlsxUserData['email'],
563
                    'External User ID' => $xlsxMatricule,
564
                    'Updated Fields' => implode(', ', array_map(function($update) { return trim(explode(':', $update)[0]); }, $updates)),
565
                ];
566
            }
567
        } else {
568
            echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": No action - no changes needed (username: $dbUsername)\n";
569
            $userActions[] = [
570
                'Action Type' => 'skipped',
571
                'User ID' => $dbUser['id'],
572
                'Username' => $dbUsername,
573
                'Official Code' => $xlsxUserData['official_code'],
574
                'E-mail' => $xlsxUserData['email'],
575
                'External User ID' => $xlsxMatricule,
576
                'Updated Fields' => 'No changes needed',
577
            ];
578
        }
579
    } else {
580
        echo '['.$rowTime->format('H:i:s').'] Row '.($rowIndex + 2).": Insert new user - No existing user found (username: $dbUsername)\n";
581
        if ($proceed) {
582
            try {
583
                $password = !empty($xlsxUserData['password']) ? $xlsxUserData['password'] : 'temporary_password';
584
                $userId = $userManager->create_user(
585
                    $xlsxUserData['firstname'],
586
                    $xlsxUserData['lastname'],
587
                    5, // status (5 = student, adjust as needed)
588
                    $xlsxUserData['email'],
589
                    $dbUsername,
590
                    $password,
591
                    $xlsxUserData['official_code'],
592
                    '', // language
593
                    $xlsxUserData['phone'],
594
                    '',
595
                    null,
596
                    null,
597
                    $xlsxActive,
598
                    null,
599
                    null  // creator_id
600
                );
601
                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...
602
                    // Add extra field 'external_user_id'
603
                    $userManager->update_extra_field_value($userId, 'external_user_id', $xlsxMatricule);
604
                    echo "  Success: Created user and set external_user_id (username: $dbUsername)\n";
605
                    $userActions[] = [
606
                        'Action Type' => 'created',
607
                        'User ID' => $userId,
608
                        'Username' => $dbUsername,
609
                        'Official Code' => $xlsxUserData['official_code'],
610
                        'E-mail' => $xlsxUserData['email'],
611
                        'External User ID' => $xlsxMatricule,
612
                        'Updated Fields' => '',
613
                    ];
614
                } else {
615
                    echo "  Error: Could not create user (username: $dbUsername)\n";
616
                    $userActions[] = [
617
                        'Action Type' => 'skipped',
618
                        'User ID' => '',
619
                        'Username' => $dbUsername,
620
                        'Official Code' => $xlsxUserData['official_code'],
621
                        'E-mail' => $xlsxUserData['email'],
622
                        'External User ID' => $xlsxMatricule,
623
                        'Updated Fields' => 'Could not create user',
624
                    ];
625
                }
626
            } catch (Exception $e) {
627
                echo "  Error: Failed to insert user (username: $dbUsername): {$e->getMessage()}\n";
628
                $userActions[] = [
629
                    'Action Type' => 'skipped',
630
                    'User ID' => '',
631
                    'Username' => $dbUsername,
632
                    'Official Code' => $xlsxUserData['official_code'],
633
                    'E-mail' => $xlsxUserData['email'],
634
                    'External User ID' => $xlsxMatricule,
635
                    'Updated Fields' => 'Failed to insert user: '.$e->getMessage(),
636
                ];
637
            }
638
        } else {
639
            echo "   Sim mode: Inserted user and external_user_id (username: $dbUsername)\n";
640
            $userActions[] = [
641
                'Action Type' => 'created',
642
                'User ID' => '',
643
                'Username' => $dbUsername,
644
                'Official Code' => $xlsxUserData['official_code'],
645
                'E-mail' => $xlsxUserData['email'],
646
                'External User ID' => $xlsxMatricule,
647
                'Updated Fields' => '',
648
            ];
649
        }
650
    }
651
}
652
653
// Generate user actions XLSX file
654
$actionColumns = ['Action Type', 'User ID', 'Username', 'Official Code', 'E-mail', 'External User ID', 'Updated Fields'];
655
createMissingFieldFile($outputDir.'user_actions.xlsx', $userActions, $actionColumns);
656
657
if (!$proceed) {
658
    echo "\nUse --proceed to apply changes to the database.\n";
659
} else {
660
    echo "\nImport completed successfully.\n";
661
}
662
663
// Save terminal output to log file
664
$output = ob_get_clean();
665
echo $output; // Output to terminal
666
$endTime = new DateTime();
667
$output .= '['.$endTime->format('Y-m-d H:i:s')."] Script completed\n";
668
$logTimestamp = $startTime->format('YmdHis');
669
$logFile = $outputDir.'import-'.$logTimestamp.'.log';
670
if (!file_put_contents($logFile, $output)) {
671
    echo "Error: Could not write to log file $logFile\n";
672
} else {
673
    echo "Generated log file: $logFile\n";
674
}
675