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