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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: