ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
ilPluginLanguageTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
26{
27 private ?string $plugin_root_directory = null;
28
29 protected function setUp(): void
30 {
32
33 if (!defined('ILIAS_ABSOLUTE_PATH')) {
34 define('ILIAS_ABSOLUTE_PATH', realpath(__DIR__ . '/../../../../'));
35 }
36 }
37
38 protected function tearDown(): void
39 {
40 if (isset($this->plugin_root_directory) && is_dir($this->plugin_root_directory)) {
41 MigratedPoFixture::removeDirectory($this->plugin_root_directory);
42 }
43
45 }
46
47 private function createPluginInfo(string $module_prefix): ilPluginInfo
48 {
49 $this->plugin_root_directory = sys_get_temp_dir() . '/ilias_plugin_lang_test_' . bin2hex(random_bytes(4));
50 mkdir($this->plugin_root_directory . '/lang', 0775, true);
51
52 $component = $this->createStub(ilComponentInfo::class);
53 $component->method('getId')->willReturn('comp');
54 $slot = $this->createStub(ilPluginSlotInfo::class);
55 $slot->method('getId')->willReturn('slot');
56
57 $plugin_info = $this->createStub(ilPluginInfo::class);
58 $plugin_info->method('getComponent')->willReturn($component);
59 $plugin_info->method('getPluginSlot')->willReturn($slot);
60 $plugin_info->method('getId')->willReturn($module_prefix);
61 $plugin_info->method('getPath')->willReturn($this->plugin_root_directory);
62
63 return $plugin_info;
64 }
65
67 private function writeShippedPluginPo(string $lang_key, array $entries): void
68 {
69 $catalog = new \ILIAS\Language\ComponentTranslation\Catalog\TranslationCatalog();
70 foreach ($entries as $identifier => $value) {
71 $catalog->add(\MigratedPoFixture::entry(null, $identifier, $value));
72 }
73 \MigratedPoFixture::writePo($this->plugin_root_directory . '/lang/ilias_' . $lang_key . '.po', $catalog);
74 }
75
77 {
78 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
79 touch($this->plugin_root_directory . '/lang/ilias_de.lang');
80 touch($this->plugin_root_directory . '/lang/ilias_en.po');
81
82 $files = $plugin_language->getAvailableLangFiles();
83 $by_key = array_combine(array_column($files, 'key'), array_column($files, 'file'));
84
85 $this->assertSame('ilias_de.lang', $by_key['de']);
86 $this->assertSame('ilias_en.po', $by_key['en']);
87 }
88
90 {
91 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
92 touch($this->plugin_root_directory . '/lang/ilias_de.lang');
93 touch($this->plugin_root_directory . '/lang/ilias_de.po');
94
95 $files = $plugin_language->getAvailableLangFiles();
96
97 $this->assertCount(1, $files);
98 $this->assertSame('ilias_de.po', $files[0]['file']);
99 }
100
107 private function stubDatabase(array &$captured_replace_entries): ilDBInterface
108 {
109 // replaceLangModule()'s self-check SELECT must unserialize() to an array (mantis #20046/#19140), while
110 // _getLocalChangesByModule()'s SELECT must return no rows: told apart by SQL text
111 $self_check_statement = $this->createStub(ilDBStatement::class);
112 $no_rows_statement = $this->createStub(ilDBStatement::class);
113 $db = $this->createStub(ilDBInterface::class);
114 $db->method('quote')->willReturnCallback(static fn($value, string $type = ''): string => "'" . (string) $value . "'");
115 $db->method('queryF')->willReturnCallback(
116 static fn(string $sql) => str_contains($sql, 'lng_modules') ? $self_check_statement : $no_rows_statement
117 );
118 $db->method('query')->willReturn($self_check_statement);
119 $db->method('fetchAssoc')->willReturnCallback(
120 static fn(ilDBStatement $statement): ?array => $statement === $self_check_statement
121 ? ['lang_array' => serialize([])]
122 : null
123 );
124 $db->method('manipulate')->willReturn(0);
125 $db->method('insert')->willReturn(1);
126 $db->method('replace')->willReturnCallback(
127 function (string $table, array $keys, array $values) use (&$captured_replace_entries): int {
128 $captured_replace_entries[$keys['module'][1] . '#:#' . $keys['identifier'][1]] = $values['value'][1];
129 return 1;
130 }
131 );
132 $this->setGlobalVariable('ilDB', $db);
133
134 return $db;
135 }
136
138 private function stubLoggerCapturingWarnings(array &$warnings): void
139 {
140 $logger = $this->createStub(ilLogger::class);
141 $logger->method('warning')->willReturnCallback(static function (string $message) use (&$warnings): void {
142 $warnings[] = $message;
143 });
144 $logger_factory = $this->createStub(ilLoggerFactory::class);
145 $logger_factory->method('getComponentLogger')->willReturn($logger);
146 $this->setGlobalVariable('ilLoggerFactory', $logger_factory);
147 }
148
150 {
151 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
152 $this->writeShippedPluginPo('de', ['greeting' => 'Hallo']);
153 $captured = [];
154 $this->stubDatabase($captured);
155
156 $plugin_language->updateLanguages(['de']);
157
158 // no LanguageFileDirectoryManager is registered (a plugin cannot contribute one), so the overlay sync is a no-op
159 $this->assertSame('Hallo', $captured['comp_slot_ptest#:#comp_slot_ptest_greeting']);
160 }
161
164 {
165 $context = 'some_module';
166 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
167 $catalog = new \ILIAS\Language\ComponentTranslation\Catalog\TranslationCatalog();
168 $catalog->add(\MigratedPoFixture::entry($context, 'foreign', 'Fremd'));
169 $catalog->add(\MigratedPoFixture::entry(null, 'greeting', 'Hallo'));
170 \MigratedPoFixture::writePo($this->plugin_root_directory . '/lang/ilias_de.po', $catalog);
171 $captured = [];
172 $this->stubDatabase($captured);
173 $warnings = [];
174 $this->stubLoggerCapturingWarnings($warnings);
175
176 $plugin_language->updateLanguages(['de']);
177
178 $this->assertArrayNotHasKey('comp_slot_ptest#:#comp_slot_ptest_foreign', $captured);
179 $this->assertSame('Hallo', $captured['comp_slot_ptest#:#comp_slot_ptest_greeting']);
180 $this->assertNotEmpty(array_filter($warnings, static fn(string $w): bool => str_contains($w, 'msgctxt') && str_contains($w, 'foreign')));
181 }
182
185 {
186 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
187 $this->writeShippedPluginPo('de', ['greeting' => '<script>alert(1)</script>Hallo']);
188 $captured = [];
189 $this->stubDatabase($captured);
190 $warnings = [];
191 $this->stubLoggerCapturingWarnings($warnings);
192
193 $plugin_language->updateLanguages(['de']);
194
195 $value = $captured['comp_slot_ptest#:#comp_slot_ptest_greeting'];
196 $this->assertStringNotContainsString('<script', $value);
197 $this->assertStringContainsString('Hallo', $value);
198 $this->assertNotEmpty(array_filter($warnings, static fn(string $w): bool => str_contains($w, 'Markup not allowed')));
199 }
200
203 {
204 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
205 file_put_contents(
206 $this->plugin_root_directory . '/lang/ilias_de.lang',
207 "greeting#:#<script>alert(1)</script>Hallo\n"
208 );
209 $captured = [];
210 $this->stubDatabase($captured);
211 $warnings = [];
212 $this->stubLoggerCapturingWarnings($warnings);
213
214 $plugin_language->updateLanguages(['de']);
215
216 $value = $captured['comp_slot_ptest#:#comp_slot_ptest_greeting'];
217 $this->assertSame('<script>alert(1)</script>Hallo', $value);
218 $this->assertNotEmpty(array_filter($warnings, static fn(string $w): bool => str_contains($w, 'Markup not allowed')));
219 }
220
221 public function testEmptyTranslationsAreSkipped(): void
222 {
223 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
224 $this->writeShippedPluginPo('de', ['greeting' => 'Hallo', 'empty_one' => '']);
225 $captured = [];
226 $this->stubDatabase($captured);
227
228 $plugin_language->updateLanguages(['de']);
229
230 $this->assertArrayHasKey('comp_slot_ptest#:#comp_slot_ptest_greeting', $captured);
231 $this->assertArrayNotHasKey('comp_slot_ptest#:#comp_slot_ptest_empty_one', $captured);
232 }
233
236 {
237 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
238 file_put_contents($this->plugin_root_directory . '/lang/ilias_de.po', "msgid \"kaputt\n");
239 $this->writeShippedPluginPo('en', ['greeting' => 'Hello']);
240 $captured = [];
241 $this->stubDatabase($captured);
242 $warnings = [];
243 $this->stubLoggerCapturingWarnings($warnings);
244
245 $plugin_language->updateLanguages(['de', 'en']);
246
247 $this->assertSame('Hello', $captured['comp_slot_ptest#:#comp_slot_ptest_greeting'], 'the valid "en" .po is still applied');
248 $this->assertNotEmpty(array_filter($warnings, static fn(string $w): bool => str_contains($w, 'cannot be read')));
249 }
250
252 {
253 $plugin_language = new ilPluginLanguage($this->createPluginInfo('ptest'));
254 $lng = $this->createStub(ilLanguage::class);
255 $lng->method('txt')->willReturnCallback(static fn(string $key): string => $key === 'comp_slot_ptest_known' ? 'Bekannt' : '-' . $key . '-');
256 $lng->method('getDefaultLanguage')->willReturn('en');
257 $this->setGlobalVariable('lng', $lng);
258 $this->setGlobalVariable('ilClientIniFile', $this->createStub(ilIniFile::class));
259 $statement = $this->createStub(ilDBStatement::class);
260 $db = $this->createStub(ilDBInterface::class);
261 $db->method('quote')->willReturnCallback(static fn($value): string => "'" . (string) $value . "'");
262 $queries = [];
263 $db->method('query')->willReturnCallback(static function (string $sql) use (&$queries, $statement) {
264 $queries[] = $sql;
265
266 return $statement;
267 });
268 // answers only the lookup of the default language for exactly this module and key
269 $db->method('fetchAssoc')->willReturnCallback(static function () use (&$queries): ?array {
270 return str_contains(end($queries), "module = 'comp_slot_ptest' AND lang_key = 'en' AND identifier = 'comp_slot_ptest_missing'")
271 ? ['value' => 'Missing en']
272 : null;
273 });
274 $this->setGlobalVariable('ilDB', $db);
275
276 $this->assertSame('Bekannt', $plugin_language->txt('known'));
277 $this->assertSame([], $queries, 'a key present in the user language is not looked up again');
278 $this->assertSame('Missing en', $plugin_language->txt('missing'));
279 $this->assertCount(1, $queries);
280 $this->assertSame('-comp_slot_ptest_none-', $plugin_language->txt('none'));
281 }
282}
static writePo(string $file, TranslationCatalog $catalog)
static removeDirectory(string $directory)
static entry(?string $context, string $id, string $value)
Class ilLanguageBaseTest.
setGlobalVariable(string $name, $value)
Simple value class for information about a plugin.
ilPluginLanguage: .po takes precedence over .lang, and updateLanguages() reads a plugin's ilias_<lang...
testUpdateLanguagesWritesPoValuesWithThePrefixThroughTheDatabaseOnly()
createPluginInfo(string $module_prefix)
writeShippedPluginPo(string $lang_key, array $entries)
testABrokenPoSkipsOnlyThatLanguageAndOthersAreStillUpdated()
A broken .po skips only that language.
stubLoggerCapturingWarnings(array &$warnings)
testEntriesWithAMsgctxtAreIgnoredAndWarnedAbout()
An entry with a msgctxt (even empty) is ignored with a warning: a plugin .po has exactly one implicit...
testTxtFallsBackToTheDefaultLanguageForAKeyMissingInTheUserLanguage()
testDisallowedMarkupIsSanitisedAndWarnedAboutNotRejected()
Disallowed markup in a plugin .po value is cleaned and warned about.
testDisallowedMarkupInALangFileIsTakenOverUnchangedAndWarnedAbout()
Unlike a plugin .po, a .lang value is taken over unchanged: disallowed markup is only logged.
stubDatabase(array &$captured_replace_entries)
ilDB double for updateLanguages(): no local changes, replaceLangEntry()'s replace() captured,...
Interface ilDBInterface.
Interface ilDBStatement.
global $lng
Definition: privfeed.php:26