ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
LanguageInstallationManagerTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
28use PHPUnit\Framework\MockObject\MockObject;
29use PHPUnit\Framework\MockObject\Stub;
30use PHPUnit\Framework\TestCase;
31
37class LanguageInstallationManagerTest extends TestCase
38{
39 private function createDatabaseMock(): MockObject&ilDBInterface
40 {
41 $db = $this->createMock(ilDBInterface::class);
42 $db->method('nextId')->willReturn(42);
43 $db->method('quote')->willReturnCallback(
44 static fn(mixed $value): string => "'" . (string) $value . "'"
45 );
46 $db->method('now')->willReturn('NOW()');
47 $db->method('manipulate')->willReturn(1);
48
49 return $db;
50 }
51
56 private function createDatabaseStub(): Stub&ilDBInterface
57 {
58 $db = $this->createStub(ilDBInterface::class);
59 $db->method('nextId')->willReturn(42);
60 $db->method('quote')->willReturnCallback(
61 static fn(mixed $value): string => "'" . (string) $value . "'"
62 );
63 $db->method('now')->willReturn('NOW()');
64 $db->method('manipulate')->willReturn(1);
65
66 return $db;
67 }
68
69 private function createManager(
70 ilDBInterface $db,
72 ?\DateTimeImmutable $now = null
75 $db,
79 ),
80 (string) realpath(__DIR__ . '/../../../../../'),
81 $repository,
82 $now !== null ? static fn(): \DateTimeImmutable => $now : null
83 );
84 }
85
87 {
88 $db = $this->createDatabaseMock();
89 $db->expects($this->once())
90 ->method('manipulate')
91 ->with($this->logicalAnd(
92 $this->stringContains( 'UPDATE object_data'),
93 $this->stringContains("last_update = '2026-01-02 03:04:05'")
94 ));
95
96 $manager = $this->createManager(
97 $db,
98 $this->createStub(InstalledLanguageRepository::class),
99 new \DateTimeImmutable('2026-01-02 03:04:05', new \DateTimeZone('UTC'))
100 );
101
102 $manager->registerInstalledLanguage(
103 'de',
104 ['de' => ['obj_id' => 7, 'status' => 'not_installed']],
105 []
106 );
107 }
108
110 {
111 $db = $this->createDatabaseMock();
112 $db->expects($this->once())
113 ->method('manipulate')
114 ->with($this->logicalAnd(
115 $this->stringContains( 'INSERT INTO object_data'),
116 $this->stringContains('NOW(),NOW()')
117 ));
118
119 $manager = $this->createManager(
120 $db,
121 $this->createStub(InstalledLanguageRepository::class),
122 new \DateTimeImmutable('2026-01-02 03:04:05', new \DateTimeZone('UTC'))
123 );
124
125 $manager->registerInstalledLanguage('de', [], []);
126 }
127
129 {
130 $db = $this->createDatabaseStub();
131 $calls = [];
132 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
133 $calls[] = $query;
134 return 1;
135 });
136
137 $repository = $this->createStub(InstalledLanguageRepository::class);
138 $repository->method('getAvailableLanguages')->willReturn([
139 'fr' => ['obj_id' => 3, 'status' => 'installed'],
140 ]);
141 $repository->method('getLocalLanguages')->willReturn([]);
142
143 $manager = $this->createManager(
144 $db,
145 $repository,
146 new \DateTimeImmutable('2026-01-02 03:04:05', new \DateTimeZone('UTC'))
147 );
148
149 // 'fr' is no longer requested: flushed, bookkeeping row flipped to "not_installed" with a fresh last_update
150 $result = $manager->installLanguages([]);
151
152 $this->assertTrue($result);
153 $bookkeeping_update = array_values(array_filter(
154 $calls,
155 static fn(string $query): bool => str_contains($query, 'UPDATE object_data')
156 ));
157 $this->assertCount(1, $bookkeeping_update);
158 $this->assertStringContainsString("description = 'not_installed'", $bookkeeping_update[0]);
159 $this->assertStringContainsString("last_update = '2026-01-02 03:04:05'", $bookkeeping_update[0]);
160 }
161
163 {
164 $db = $this->createDatabaseMock();
165 $db->expects($this->once())
166 ->method('manipulate')
167 ->with($this->logicalAnd(
168 $this->stringContains( 'DELETE FROM lng_data'),
169 $this->stringContains('local_change IS NULL')
170 ));
171
172 $manager = $this->createManager($db, $this->createStub(InstalledLanguageRepository::class));
173
174 $manager->flushLanguageForInstallation('de');
175 }
176
178 {
179 $db = $this->createDatabaseStub();
180 $calls = [];
181 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
182 $calls[] = $query;
183 return 1;
184 });
185
186 $manager = $this->createManager($db, $this->createStub(InstalledLanguageRepository::class));
187
188 $manager->flushLanguageForUninstallation('de');
189
190 $this->assertCount(2, $calls);
191 $this->assertStringContainsString( 'DELETE FROM lng_data', $calls[0]);
192 $this->assertStringNotContainsString('local_change IS NULL', $calls[0]);
193 $this->assertStringContainsString( 'DELETE FROM lng_modules', $calls[1]);
194 }
195
201 {
202 $root = $this->createTempInstallationRoot();
203 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'test', 'Global Value']]);
204 $this->writeLangFile($root . '/lang/customizing/ilias_de.lang.local', [['common', 'test', 'Custom Value']]);
205
206 try {
207 $calls = [];
208 $db = $this->createDatabaseStub();
209 $db->method('in')->willReturn("module IN ('common')");
210 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
211 $calls[] = $query;
212 return 1;
213 });
214
215 $repository = $this->createMock(InstalledLanguageRepository::class);
216 // no DB local changes are consulted: a clean re-seed has nothing to merge
217 $repository->expects($this->never())->method('getLocalChanges');
218
219 $manager = new LanguageInstallationManager(
220 $db,
222 $root,
223 $repository
224 );
225
226 $manager->insertLanguageForRemovingLocalChanges('de');
227
228 $insert = array_values(array_filter(
229 $calls,
230 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_data')
231 ));
232 $this->assertCount(1, $insert);
233 $this->assertStringContainsString("'Global Value'", $insert[0]);
234 $this->assertStringNotContainsString('Custom Value', $insert[0]);
235 } finally {
236 $this->removeDirectory($root);
237 }
238 }
239
244 {
245 $root = $this->createTempInstallationRoot();
246 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'test', 'Global Value']]);
247 $this->writeLangFile($root . '/lang/customizing/ilias_de.lang.local', [['common', 'test', 'Custom Value']]);
248
249 try {
250 $calls = [];
251 $db = $this->createDatabaseStub();
252 $db->method('in')->willReturn("module IN ('common')");
253 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
254 $calls[] = $query;
255 return 1;
256 });
257
258 $repository = $this->createStub(InstalledLanguageRepository::class);
259 $repository->method('getLocalChanges')->willReturn([]);
260
261 $manager = new LanguageInstallationManager(
262 $db,
264 $root,
265 $repository,
266 static fn(): \DateTimeImmutable => new \DateTimeImmutable('2026-01-02 03:04:05', new \DateTimeZone('UTC'))
267 );
268
269 $manager->insertLanguageForInstallation('de');
270
271 $insert = array_values(array_filter(
272 $calls,
273 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_data')
274 ));
275 $this->assertCount(1, $insert);
276 $this->assertStringContainsString("'Global Value'", $insert[0]);
277 $this->assertStringContainsString("'Custom Value'", $insert[0]);
278 } finally {
279 $this->removeDirectory($root);
280 }
281 }
282
289 {
290 $root = $this->createTempInstallationRoot();
291 $this->writeLangFile($root . '/lang/ilias_de.lang', [
292 ['common', 'locally_changed', 'Global Value'],
293 ['common', 'plain', 'Plain Global'],
294 ]);
295
296 $local_entries = [['common', 'newer_in_db', 'From Local File']];
297 for ($i = 0; $i < 200; $i++) {
298 $local_entries[] = ['common', 'override_' . $i, 'Local ' . $i];
299 }
300 $this->writeLangFile($root . '/lang/customizing/ilias_de.lang.local', $local_entries);
301
302 try {
303 $calls = 0;
304 $inserts = [];
305 $db = $this->createDatabaseStub();
306 $db->method('in')->willReturn("module IN ('common')");
307 $db->method('manipulate')->willReturnCallback(
308 static function (string $query) use (&$inserts): int {
309 if (str_starts_with($query, 'INSERT INTO lng_data')) {
310 $inserts[] = $query;
311 }
312 return 1;
313 }
314 );
315
316 $repository = $this->createStub(InstalledLanguageRepository::class);
317 $repository->method('getLocalChanges')->willReturnCallback(
318 static function (string $lang_key, string $min_date = '') use (&$calls): array {
319 $calls++;
320 // no date: the seed of changes already in the database; with a date: changes newer than the customizing file
321 return $min_date === ''
322 ? ['common' => ['locally_changed' => 'DB Local Change']]
323 : ['common' => ['newer_in_db' => 'DB Is Newer']];
324 }
325 );
326
327 $manager = new LanguageInstallationManager(
328 $db,
330 $root,
331 $repository,
332 static fn(): \DateTimeImmutable => new \DateTimeImmutable('2026-01-02 03:04:05', new \DateTimeZone('UTC'))
333 );
334
335 $manager->insertLanguageForInstallation('de');
336
337 // one seed lookup plus one for the single customizing directory, independent of the 201 entries
338 $this->assertSame(2, $calls);
339
340 $this->assertCount(1, $inserts);
341 $insert = $inserts[0];
342 // a global entry with a local change in the database keeps the database value
343 $this->assertStringNotContainsString("'Global Value'", $insert);
344 $this->assertStringContainsString("'Plain Global'", $insert);
345 // a customizing entry with an even newer database change is not overwritten by the file either
346 $this->assertStringNotContainsString("'From Local File'", $insert);
347 $this->assertStringContainsString("'Local 0'", $insert);
348 $this->assertStringContainsString("'Local 199'", $insert);
349 } finally {
350 $this->removeDirectory($root);
351 }
352 }
353
359 {
360 $root = $this->createTempInstallationRoot();
361 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'test', 'Global Value']]);
362 $this->writeLangFile($root . '/lang/customizing/ilias_de.lang.local', [['common', 'test', 'Custom Value']]);
363
364 try {
365 $calls = [];
366 $db = $this->createDatabaseStub();
367 $db->method('in')->willReturn("module IN ('common')");
368 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
369 $calls[] = $query;
370 return 1;
371 });
372
373 $repository = $this->createMock(InstalledLanguageRepository::class);
374 $repository->expects($this->once())
375 ->method('getLanguageEntries')
376 ->with('de')
377 ->willReturn([]);
378 // insertLanguage()'s "newer change in the DB?" check still calls getLocalChanges() once per local directory;
379 // the seed itself comes from getLanguageEntries() alone
380 $repository->method('getLocalChanges')->willReturn([]);
381
382 $manager = new LanguageInstallationManager(
383 $db,
385 $root,
386 $repository
387 );
388
389 $manager->insertLanguageForApplyingLocalChanges('de');
390
391 $insert = array_values(array_filter(
392 $calls,
393 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_data')
394 ));
395 $this->assertCount(1, $insert);
396 $this->assertStringContainsString("'Custom Value'", $insert[0]);
397 $this->assertStringNotContainsString('Global Value', $insert[0]);
398
399 $flush = array_values(array_filter(
400 $calls,
401 static fn(string $query): bool => str_starts_with($query, 'DELETE FROM lng_data')
402 ));
403 $this->assertCount(0, $flush);
404 } finally {
405 $this->removeDirectory($root);
406 }
407 }
408
415 {
416 $root = $this->createTempInstallationRoot();
417 // only 'overridden' is touched by the customizing file; 'untouched_by_customizing' must survive via the seed
418 $this->writeLangFile(
419 $root . '/lang/customizing/ilias_de.lang.local',
420 [['common', 'overridden', 'New Custom Value']]
421 );
422
423 try {
424 $calls = [];
425 $db = $this->createDatabaseStub();
426 $db->method('in')->willReturn("module IN ('common')");
427 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
428 $calls[] = $query;
429 return 1;
430 });
431
432 $repository = $this->createStub(InstalledLanguageRepository::class);
433 $repository->method('getLanguageEntries')->willReturnMap([
434 ['de', [
435 'common' => [
436 'overridden' => 'Old Value',
437 'untouched_by_customizing' => 'Still Here',
438 ],
439 ]],
440 ]);
441
442 $manager = new LanguageInstallationManager(
443 $db,
445 $root,
446 $repository
447 );
448
449 $manager->insertLanguageForApplyingLocalChanges('de');
450
451 $modules_insert = array_values(array_filter(
452 $calls,
453 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_modules')
454 ));
455 $this->assertCount(1, $modules_insert);
456 $serialized = serialize([
457 'overridden' => 'New Custom Value',
458 'untouched_by_customizing' => 'Still Here',
459 ]);
460 $this->assertStringContainsString($serialized, $modules_insert[0]);
461 } finally {
462 $this->removeDirectory($root);
463 }
464 }
465
471 {
472 $root = $this->createTempInstallationRoot();
473 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'greeting', 'Hallo']]);
474
475 try {
476 $calls = [];
477 $db = $this->createDatabaseStub();
478 $db->method('in')->willReturn("module IN ('common')");
479 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
480 $calls[] = $query;
481 return 1;
482 });
483
484 $repository = $this->createStub(InstalledLanguageRepository::class);
485 $repository->method('checkLanguage')->willReturnMap([
486 ['de', true],
487 ]);
488 $repository->method('getAvailableLanguages')->willReturn([]);
489 $repository->method('getLocalLanguages')->willReturn([]);
490 $repository->method('getLocalChanges')->willReturn([]);
491
492 $manager = new LanguageInstallationManager(
493 $db,
495 $root,
496 $repository
497 );
498
499 $result = $manager->installLanguages(['de']);
500
501 $this->assertTrue($result);
502
503 $inserts_object_data = array_values(array_filter(
504 $calls,
505 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO object_data')
506 ));
507 $this->assertCount(1, $inserts_object_data);
508 $this->assertStringContainsString("'de'", $inserts_object_data[0]);
509 $this->assertStringContainsString("'installed'", $inserts_object_data[0]);
510 $this->assertStringNotContainsString("'installed_local'", $inserts_object_data[0]);
511
512 $keep_local_flush = array_values(array_filter(
513 $calls,
514 static fn(string $query): bool => str_contains($query, 'DELETE FROM lng_data')
515 && str_contains($query, 'local_change IS NULL')
516 ));
517 $this->assertCount(1, $keep_local_flush);
518 } finally {
519 $this->removeDirectory($root);
520 }
521 }
522
528 {
529 $root = $this->createTempInstallationRoot();
530 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'greeting', 'Hallo']]);
531
532 try {
533 $calls = [];
534 $db = $this->createDatabaseStub();
535 $db->method('in')->willReturn("module IN ('common')");
536 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
537 $calls[] = $query;
538 return 1;
539 });
540
541 $repository = $this->createStub(InstalledLanguageRepository::class);
542 $repository->method('checkLanguage')->willReturnMap([
543 ['de', true],
544 ['xx', false],
545 ]);
546 $repository->method('getAvailableLanguages')->willReturn([
547 // known from a previous installation, its language file has become invalid
548 'xx' => ['obj_id' => 5, 'status' => 'installed'],
549 ]);
550 $repository->method('getLocalLanguages')->willReturn([]);
551 $repository->method('getLocalChanges')->willReturn([]);
552
553 $manager = new LanguageInstallationManager(
554 $db,
556 $root,
557 $repository
558 );
559
560 $result = $manager->installLanguages(['de', 'xx']);
561
562 $this->assertSame(['xx'], $result);
563
564 $inserts_object_data = array_values(array_filter(
565 $calls,
566 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO object_data')
567 ));
568 $this->assertCount(1, $inserts_object_data);
569 $this->assertStringContainsString("'de'", $inserts_object_data[0]);
570
571 // 'xx' is known (obj_id 5) but failed validation: no flush, no status update, no resync
572 foreach ($calls as $query) {
573 $this->assertStringNotContainsString("obj_id = '5'", $query);
574 $this->assertStringNotContainsString("lang_key = 'xx'", $query);
575 }
576 } finally {
577 $this->removeDirectory($root);
578 }
579 }
580
586 {
587 $root = $this->createTempInstallationRoot();
588 $this->writeLangFile($root . '/lang/ilias_fr.lang', [['common', 'greeting', 'Bonjour']]);
589
590 try {
591 $calls = [];
592 $db = $this->createDatabaseStub();
593 $db->method('in')->willReturn("module IN ('common')");
594 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
595 $calls[] = $query;
596 return 1;
597 });
598
599 $repository = $this->createStub(InstalledLanguageRepository::class);
600 $repository->method('checkLanguage')->willReturn(true);
601 $repository->method('getAvailableLanguages')->willReturn([
602 'fr' => ['obj_id' => 9, 'status' => 'installed'],
603 ]);
604 $repository->method('getLocalLanguages')->willReturn([]);
605 $repository->method('getLocalChanges')->willReturn([]);
606
607 $manager = new LanguageInstallationManager(
608 $db,
610 $root,
611 $repository
612 );
613
614 $result = $manager->installLanguages(['fr']);
615 $this->assertTrue($result);
616
617 // no full flush: flushLanguage("all") deletes without a module filter, unlike insertLanguage()'s own
618 // per-module cleanup ("AND module IN (...)")
619 $full_flush_deletes = array_values(array_filter(
620 $calls,
621 static fn(string $query): bool => $query === "DELETE FROM lng_modules WHERE lang_key = 'fr'"
622 ));
623 $this->assertCount(0, $full_flush_deletes);
624
625 foreach ($calls as $query) {
626 $this->assertStringNotContainsString('not_installed', $query);
627 }
628
629 // ...but the bookkeeping row is refreshed via UPDATE, keeping "installed"
630 $object_data_updates = array_values(array_filter(
631 $calls,
632 static fn(string $query): bool => str_starts_with($query, 'UPDATE object_data')
633 ));
634 $this->assertCount(1, $object_data_updates);
635 $this->assertStringContainsString("obj_id = '9'", $object_data_updates[0]);
636 $this->assertStringContainsString("description = 'installed'", $object_data_updates[0]);
637 } finally {
638 $this->removeDirectory($root);
639 }
640 }
641
648 {
649 $db = $this->createDatabaseStub();
650 $calls = [];
651 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
652 $calls[] = $query;
653 return 1;
654 });
655
656 $repository = $this->createStub(InstalledLanguageRepository::class);
657 $repository->method('getAvailableLanguages')->willReturn([
658 'de' => ['obj_id' => 4, 'status' => 'not_installed'],
659 ]);
660 $repository->method('getLocalLanguages')->willReturn([]);
661
662 $manager = $this->createManager($db, $repository);
663
664 $result = $manager->installLanguages([]);
665
666 $this->assertTrue($result);
667
668 $this->assertStringContainsString(
669 "DELETE FROM lng_modules WHERE lang_key = 'de'",
670 implode("\n", $calls)
671 );
672
673 // ...but no UPDATE: it was already "not_installed"
674 $updates = array_values(array_filter(
675 $calls,
676 static fn(string $query): bool => str_starts_with($query, 'UPDATE object_data')
677 ));
678 $this->assertCount(0, $updates);
679 }
680
687 {
688 $root = $this->createTempInstallationRoot();
689 file_put_contents(
690 $root . '/lang/ilias_de.lang',
691 "<!-- language file start -->\n"
692 . "assessment#:#discard_answer#:#Delete Answer###fau: testNav\n"
693 );
694
695 try {
696 $calls = [];
697 $db = $this->createDatabaseStub();
698 $db->method('in')->willReturn("module IN ('assessment')");
699 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
700 $calls[] = $query;
701 return 1;
702 });
703
704 $repository = $this->createStub(InstalledLanguageRepository::class);
705 $repository->method('getLocalChanges')->willReturn([]);
706
707 $manager = new LanguageInstallationManager(
708 $db,
710 $root,
711 $repository
712 );
713
714 $manager->insertLanguageForInstallation('de');
715
716 $insert = array_values(array_filter(
717 $calls,
718 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_data')
719 ));
720 $this->assertCount(1, $insert);
721
722 // the value is truncated at "###": the comment must not leak into it
723 $this->assertStringContainsString("'Delete Answer'", $insert[0]);
724 $this->assertStringNotContainsString('Delete Answer###', $insert[0]);
725
726 // the comment is preserved in the remarks column, not discarded as NULL
727 $this->assertStringContainsString(
728 "'fau: testNav'",
729 $insert[0],
730 'The "###" comment suffix must be stored in the remarks column of lng_data, not discarded.'
731 );
732 } finally {
733 $this->removeDirectory($root);
734 }
735 }
736
741 {
742 $root = $this->createTempInstallationRoot();
743 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'test', 'Plain Value']]);
744
745 try {
746 $calls = [];
747 $db = $this->createDatabaseStub();
748 $db->method('in')->willReturn("module IN ('common')");
749 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
750 $calls[] = $query;
751 return 1;
752 });
753
754 $repository = $this->createStub(InstalledLanguageRepository::class);
755 $repository->method('getLocalChanges')->willReturn([]);
756
757 $manager = new LanguageInstallationManager(
758 $db,
760 $root,
761 $repository
762 );
763
764 $manager->insertLanguageForInstallation('de');
765
766 $insert = array_values(array_filter(
767 $calls,
768 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_data')
769 ));
770 $this->assertCount(1, $insert);
771 $this->assertStringContainsString("'Plain Value'", $insert[0]);
772 // the mock's quote() casts values to string, so NULL shows as an empty quoted string before the closing paren
773 $this->assertMatchesRegularExpression(
774 "/,'',''\\)/",
775 $insert[0],
776 'Expected the tuple to end in (...,local_change=\'\',remarks=\'\') for a line without a comment suffix.'
777 );
778 } finally {
779 $this->removeDirectory($root);
780 }
781 }
782
789 {
790 $root = $this->createTempInstallationRoot();
791 mkdir($root . '/lang/file', 0777, true);
792 file_put_contents(
793 $root . '/lang/file/ilias_de.lang',
794 "<!-- language file start -->\nadd_file#:#Add File\n"
795 );
796
797 try {
798 $calls = [];
799 $db = $this->createDatabaseStub();
800 $db->method('in')->willReturn("module IN ('file')");
801 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
802 $calls[] = $query;
803 return 1;
804 });
805
806 $repository = $this->createStub(InstalledLanguageRepository::class);
807 $repository->method('getLocalChanges')->willReturn([]);
808
809 $manager = new LanguageInstallationManager(
810 $db,
814 $this->createPrefixedComponentDirectory('lang/file/', 'file')
815 ),
816 $root,
817 $repository
818 );
819
820 $manager->insertLanguageForInstallation('de');
821
822 $insert = array_values(array_filter(
823 $calls,
824 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_data')
825 ));
826 $this->assertCount(1, $insert);
827 // module=prefix, identifier=first field, value=second field
828 $this->assertStringContainsString("('file','add_file','de','Add File'", $insert[0]);
829 } finally {
830 $this->removeDirectory($root);
831 }
832 }
833
839 {
840 $root = $this->createTempInstallationRoot();
841 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'hello', 'Welt']]);
842
843 try {
844 $calls = [];
845 $db = $this->createDatabaseStub();
846 $db->method('in')->willReturn("module IN ('common')");
847 $db->method('manipulate')->willReturnCallback(static function (string $query) use (&$calls): int {
848 $calls[] = $query;
849 return 1;
850 });
851
852 $repository = $this->createStub(InstalledLanguageRepository::class);
853 $repository->method('getLocalChanges')->willReturn([]);
854
855 $manager = new LanguageInstallationManager(
856 $db,
858 $root,
859 $repository
860 );
861
862 $manager->insertLanguageForInstallation('de');
863
864 // the DB write of the non-migrated module still happens
865 $inserts = array_values(array_filter(
866 $calls,
867 static fn(string $query): bool => str_starts_with($query, 'INSERT INTO lng_modules')
868 ));
869 $this->assertCount(1, $inserts);
870 $this->assertStringContainsString("'common'", $inserts[0]);
871 } finally {
872 $this->removeDirectory($root);
873 }
874 }
875
876 private function createPrefixedComponentDirectory(string $path, string $prefix): LanguageFileDirectory
877 {
878 return new class ($path, $prefix) implements LanguageFileDirectory {
879 public function __construct(private string $path, private string $prefix)
880 {
881 }
882
883 public function getPrefix(): string
884 {
885 return $this->prefix;
886 }
887
888 public function getPath(): string
889 {
890 return $this->path;
891 }
892
893 public function getSuffix(): string
894 {
895 return '';
896 }
897
898 public function isLocal(): bool
899 {
900 return false;
901 }
902 };
903 }
904
906 {
907 $root = $this->createTempInstallationRoot();
908 $this->writeLangFile($root . '/lang/ilias_de.lang', [['common', 'test', 'Global Value']]);
909
910 try {
911 $db = $this->createDatabaseMock();
912 $db->method('in')->willReturn("module IN ('common')");
913
914 $repository = $this->createMock(InstalledLanguageRepository::class);
915 $repository->method('getLocalChanges')->willReturn([]);
916
917 $cache = $this->createMock(TranslationCache::class);
918 $cache->expects($this->once())->method('invalidate')->with('de');
919
920 $manager = new LanguageInstallationManager(
921 $db,
923 $root,
924 $repository,
925 null,
926 $cache
927 );
928
929 $manager->insertLanguageForInstallation('de');
930 } finally {
931 $this->removeDirectory($root);
932 }
933 }
934
936 {
937 $cache = $this->createMock(TranslationCache::class);
938 $cache->expects($this->once())->method('invalidate')->with('de');
939
940 $manager = new LanguageInstallationManager(
941 $this->createDatabaseMock(),
943 (string) realpath(__DIR__ . '/../../../../../'),
944 $this->createMock(InstalledLanguageRepository::class),
945 null,
946 $cache
947 );
948
949 $manager->flushLanguageForUninstallation('de');
950 }
951
953 {
954 $cache = $this->createMock(TranslationCache::class);
955 $cache->expects($this->never())->method('invalidate');
956
957 $manager = new LanguageInstallationManager(
958 $this->createDatabaseMock(),
960 (string) realpath(__DIR__ . '/../../../../../'),
961 $this->createMock(InstalledLanguageRepository::class),
962 null,
963 $cache
964 );
965
966 $manager->flushLanguageForInstallation('de');
967 }
968
969 private function createTempInstallationRoot(): string
970 {
971 $dir = sys_get_temp_dir() . '/ilias_lang_test_' . bin2hex(random_bytes(8));
972 mkdir($dir . '/lang/customizing', 0777, true);
973 return $dir;
974 }
975
979 private function writeLangFile(string $path, array $entries): void
980 {
981 $lines = ["<!-- language file start -->"];
982 foreach ($entries as [$module, $identifier, $value]) {
983 $lines[] = "{$module}#:#{$identifier}#:#{$value}";
984 }
985 file_put_contents($path, implode("\n", $lines) . "\n");
986 }
987
988 private function removeDirectory(string $dir): void
989 {
990 if (!is_dir($dir)) {
991 return;
992 }
993 $items = scandir($dir);
994 foreach ($items as $item) {
995 if ($item === '.' || $item === '..') {
996 continue;
997 }
998 $path = $dir . '/' . $item;
999 if (is_dir($path)) {
1000 $this->removeDirectory($path);
1001 } else {
1002 unlink($path);
1003 }
1004 }
1005 rmdir($dir);
1006 }
1007}
Write access to the language installation: installing, flushing and registering languages in the data...
Pins last_update handling of the object_data bookkeeping rows: the UPDATE path sets it via the manage...
testInsertLanguageForRemovingLocalChangesIgnoresCustomizingDirectory()
Reinstalling a language while removing its local changes must not re-apply the customizing directory'...
createDatabaseStub()
Stub variant of createDatabaseMock() for tests without ->expects(): a plain createMock() there trigge...
testInstallLanguagesRegistersBrandNewLanguage()
installLanguages() registers a language that passes validation and is not yet in object_data via the ...
testInstallLanguagesDoesNotReUpdateAlreadyNotInstalledLanguage()
A language already marked "not_installed" is still flushed but gets no redundant "UPDATE ....
testLocalChangesAreLookedUpOncePerDirectoryNotPerEntry()
The local-changes lookup depends only on language and file mtime and nothing in the loop writes to th...
testInsertLanguageUsesDirectoryPrefixAsModuleForComponentFiles()
The two-field lines (identifier#:#value) of a component directory are written with the directory's pr...
createPrefixedComponentDirectory(string $path, string $prefix)
testInstallLanguagesResyncsAlreadyKnownRequestedLanguage()
A known, still requested language gets its bookkeeping row refreshed via registerInstalledLanguage(),...
testInsertLanguageForInstallationStillMergesCustomizingDirectory()
The ordinary installation path still merges the customizing directory's file on top of the global one...
testInsertLanguageForApplyingLocalChangesOnlyReadsCustomizingDirectory()
insertLanguageForApplyingLocalChanges() reads only the customizing directory: no base content in the ...
createManager(ilDBInterface $db, InstalledLanguageRepository $repository, ?\DateTimeImmutable $now=null)
testInsertLanguageLeavesRemarksNullWhenLineHasNoCommentSuffix()
A line without a "###" suffix leaves the remarks column NULL, not an empty string or the text "null".
testInsertLanguageStoresCommentSuffixInRemarksColumn()
A .lang line may carry a "###comment" suffix (e.g.
testInstallLanguagesReportsFailedLanguageAndLeavesItUntouched()
A language failing checkLanguage() is reported as failed (a list, not true) and skipped in the second...
testInsertLanguageForApplyingLocalChangesPreservesUnrelatedSeedEntriesInModulesCache()
insertLanguage() rebuilds the lng_modules row from the seed plus the given directories: if the seed h...
testInsertLanguageDoesNotErrorForModuleWithoutContributedDirectory()
A module read from the *.lang data but never contributed as a LanguageFileDirectory (most modules,...
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
return['3gp', '7z', 'ai', 'aif', 'aifc', 'aiff', 'au', 'arw', 'avi', 'backup', 'bak', 'bas', 'bpmn', 'bpmn2', 'bmp', 'bib', 'bibtex', 'bz', 'bz2', 'c', 'c++', 'cc', 'cct', 'cdf', 'cer', 'class', 'cls', 'conf', 'cpp', 'crt', 'crs', 'crw', 'cr2', 'css', 'cst', 'csv', 'cur', 'db', 'dcr', 'des', 'dng', 'doc', 'docx', 'dot', 'dotx', 'dtd', 'dvi', 'el', 'eps', 'epub', 'f', 'f77', 'f90', 'flv', 'for', 'g3', 'gif', 'gl', 'gan', 'ggb', 'gsd', 'gsm', 'gtar', 'gz', 'gzip', 'h', 'hpp', 'htm', 'html', 'htmls', 'ibooks', 'ico', 'ics', 'ini', 'ipynb', 'java', 'jbf', 'jpeg', 'jpg', 'js', 'jsf', 'jso', 'json', 'latex', 'lang', 'less', 'log', 'lsp', 'ltx', 'm1v', 'm2a', 'm2v', 'm3u', 'm4a', 'm4v', 'markdown', 'm', 'mat', 'md', 'mdl', 'mdown', 'mid', 'min', 'midi', 'mobi', 'mod', 'mov', 'movie', 'mp2', 'mp3', 'mp4', 'mpa', 'mpeg', 'mpg', 'mph', 'mpga', 'mpp', 'mpt', 'mpv', 'mpx', 'mv', 'mw', 'mv4', 'nb', 'nbp', 'nef', 'nif', 'niff', 'obj', 'obm', 'odt', 'ods', 'odp', 'odg', 'odf', 'oga', 'ogg', 'ogv', 'old', 'p', 'pas', 'pbm', 'pcl', 'pct', 'pcx', 'pdf', 'pgm', 'pic', 'pict', 'png', 'por', 'pov', 'project', 'properties', 'ppa', 'ppm', 'pps', 'ppsx', 'ppt', 'pptx', 'ppz', 'ps', 'psd', 'pwz', 'qt', 'qtc', 'qti', 'qtif', 'r', 'ra', 'ram', 'rar', 'rast', 'rda', 'rev', 'rexx', 'ris', 'rf', 'rgb', 'rm', 'rmd', 'rmi', 'rmm', 'rmp', 'rt', 'rtf', 'rtx', 'rv', 's', 's3m', 'sav', 'sbs', 'sec', 'sdml', 'sgm', 'sgml', 'smi', 'smil', 'srt', 'sps', 'spv', 'stl', 'svg', 'swa', 'swf', 'swz', 'tar', 'tex', 'texi', 'texinfo', 'text', 'tgz', 'tif', 'tiff', 'ttf', 'txt', 'tmp', 'uvproj', 'vdf', 'vimeo', 'viv', 'vivo', 'vrml', 'vsdx', 'vtt', 'wav', 'webm', 'wmv', 'wmx', 'wmz', 'woff', 'wwd', 'xhtml', 'xif', 'xls', 'xlsx', 'xmind', 'xml', 'xsl', 'xsd', 'zip']
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
A directory of language files with a path and a prefix for them.
Read-only access to the installed languages (database) and the available or valid language files (fil...
Cache of the translations of a language, which must be invalidated whenever lng_modules changes.
Interface ilDBInterface.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc