19declare(strict_types=1);
24use PHPUnit\Framework\Attributes\DataProvider;
25use PHPUnit\Framework\Attributes\PreserveGlobalState;
26use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
27use PHPUnit\Framework\MockObject\Stub;
37#[RunTestsInSeparateProcesses]
38#[PreserveGlobalState(false)]
45 protected function setUp(): void
49 if (!defined(
'ILIAS_ABSOLUTE_PATH')) {
50 define(
'ILIAS_ABSOLUTE_PATH', realpath(__DIR__ .
'/../../../../'));
55 (
new ReflectionClass(ilCachedLanguage::class))->getProperty(
'instances')->setValue(
null, []);
60 if ($this->fixture_directory !==
null) {
65 if ($this->created_client_data_dir_root && defined(
'CLIENT_DATA_DIR') && is_dir(
CLIENT_DATA_DIR)) {
78 array $shipped_entries,
79 ?array $overlay_entries
81 $this->fixture_directory ??=
'tmp-savevalues-fixtures-' . bin2hex(random_bytes(4));
82 $relative_path =
'components/ILIAS/Language/tests/' . $this->fixture_directory .
'/';
91 if ($overlay_entries !==
null) {
104 LanguageFileDirectoryManager::class,
111 return (
new ReflectionClass(ilObjLanguageExt::class))
112 ->getMethod(
'currentModuleContent')
113 ->invoke(
null, $lang_key,
$module);
119 $lng = (
new ReflectionClass(ilLanguage::class))->newInstanceWithoutConstructor();
134 $lng_modules_stmt = $this->createStub(ilDBStatement::class);
135 $remarks_stmt = $this->createStub(ilDBStatement::class);
136 $lng_data_stmt = $this->createStub(ilDBStatement::class);
137 $query_f_stmt = $this->createStub(ilDBStatement::class);
139 $db = $this->createStub(ilDBInterface::class);
140 $db->method(
'quote')->willReturnCallback(
static fn($value,
string $type =
''):
string =>
"'" . (
string) $value .
"'");
141 $db->method(
'in')->willReturn(
"module IN ('placeholder')");
142 $db->method(
'query')->willReturnCallback(
143 static function (
string $sql) use ($lng_modules_stmt, $remarks_stmt, $lng_data_stmt):
ilDBStatement {
144 if (str_contains($sql,
'lng_modules')) {
145 return $lng_modules_stmt;
147 if (str_contains($sql,
'remarks')) {
148 return $remarks_stmt;
150 return $lng_data_stmt;
153 $db->method(
'queryF')->willReturn($query_f_stmt);
155 $rows = $lng_data_rows;
156 $db->method(
'fetchAssoc')->willReturnCallback(
165 if ($statement === $query_f_stmt) {
166 return [
'lang_array' => serialize([])];
168 if ($statement === $lng_modules_stmt) {
169 return $lng_modules_row ===
null ? null : [
'lang_array' => serialize($lng_modules_row)];
171 if ($statement === $remarks_stmt) {
174 if ($statement === $lng_data_stmt) {
175 $row = array_shift(
$rows);
181 $db->method(
'manipulate')->willReturn(0);
182 $db->method(
'insert')->willReturn(1);
191 $fake_global_file = (
new ReflectionClass(ilLanguageFile::class))->newInstanceWithoutConstructor();
192 $fake_global_file->setAllValues([]);
193 $fake_global_file->setAllComments([]);
195 $cache = (
new ReflectionClass(ilLanguageFile::class))->getProperty(
'global_file_objects');
196 $existing = $cache->isInitialized() ? $cache->getValue() : [];
197 $cache->setValue(
null, $existing + [$lang_key => $fake_global_file]);
205 bool $merge_onto_current_content
208 ->getMethod(
'saveValues')
209 ->invoke(null, $lang_key, $values, $remarks, $merge_onto_current_content)[
'unwritten'];
215 $directory = $this->seedShippedAndOverlay(
217 [
'greeting' =>
'Shipped'],
218 [
'greeting' =>
'Aus dem Overlay']
220 $this->registerDirectoryManager($directory);
222 $db = $this->createMock(ilDBInterface::class);
223 $db->expects($this->never())->method(
'query');
227 [
'greeting' =>
'Aus dem Overlay'],
228 $this->callCurrentModuleContent(
'de',
'ctest')
237 #[DataProvider('writingOperationsOnUnreadableContent')]
240 $directory = $this->seedShippedAndOverlay(
'ctest4', [
'greeting' =>
'Shipped'], [
'greeting' =>
'Lokal']);
241 $this->seedShippedAndOverlay(
'ctest5', [
'greeting' =>
'Shipped',
'farewell' =>
'Tschüss'], [
'greeting' =>
'Lokal']);
242 $overlay_po = rtrim(
CLIENT_DATA_DIR,
'/') .
'/lang/ctest4/de/ctest4_de.po';
243 if ($broken ===
'overlay') {
244 file_put_contents($overlay_po,
"msgid \"kaputt\n");
247 constant(
'ILIAS_ABSOLUTE_PATH') .
'/components/ILIAS/Language/tests/' . $this->fixture_directory .
'/ctest4_de.po',
251 $overlay_before = (string) file_get_contents($overlay_po);
252 $this->registerDirectoryManager($directory,
MigratedPoFixture::directory(
'ctest5',
'components/ILIAS/Language/tests/' . $this->fixture_directory .
'/'));
253 $lng = $this->stubLng();
254 $this->seedEmptyGlobalLanguageFile(
'de');
255 $db = $this->mockDatabaseForWrites([
'greeting' =>
'Aus lng_modules']);
256 $inserted_modules = [];
257 $db->method(
'insert')->willReturnCallback(
static function (
string $table, array $values) use (&$inserted_modules):
int {
258 $inserted_modules[] = $table .
':' . ($values[
'module'][1] ??
'');
261 $lng_data_writes = [];
262 $db->method(
'replace')->willReturnCallback(
static function (
string $table, array $keys) use (&$lng_data_writes):
int {
263 $lng_data_writes[] = $table .
':' . ($keys[
'module'][1] ??
'');
267 $db->method(
'manipulate')->willReturnCallback(
static function (
string $sql) use (&$deleted):
int {
268 if (str_starts_with($sql,
'DELETE FROM lng_data')) {
274 $logger = $this->createStub(ilLogger::class);
275 $logger->method(
'warning')->willReturnCallback(
static function (
string $message) use (&$warnings):
void {
278 $logger_factory = $this->createStub(ilLoggerFactory::class);
279 $logger_factory->method(
'getComponentLogger')->willReturn(
$logger);
282 $values = [
'ctest4' .
$lng->separator .
'greeting' =>
'Neu',
'ctest5' .
$lng->separator .
'greeting' =>
'Neu'];
283 $unwritten = $operation ===
'save'
287 $this->assertSame([
'ctest4'], $unwritten);
288 $this->assertMatchesRegularExpression(
'/module "ctest4" for "de" cannot be read/', implode(
"\n", $warnings));
289 $this->assertSame([
'lng_modules:ctest5'], $inserted_modules,
'only the readable module is written');
290 $this->assertSame($operation ===
'save' ? [
'lng_data:ctest5'] : [], $lng_data_writes);
291 $this->assertCount($operation ===
'delete' ? 1 : 0, $deleted);
292 $this->assertSame($overlay_before, (
string) file_get_contents($overlay_po));
301 'save, shipped po' => [
'save',
'shipped'],
302 'save, overlay' => [
'save',
'overlay'],
303 'delete, shipped po' => [
'delete',
'shipped'],
304 'delete, overlay' => [
'delete',
'overlay'],
311 $directory = $this->seedShippedAndOverlay(
'ctest6', [
'greeting' =>
'Shipped'], [
'greeting' =>
'Lokal']);
312 file_put_contents(rtrim(
CLIENT_DATA_DIR,
'/') .
'/lang/ctest6/de/ctest6_de.po',
"msgid \"kaputt\n");
313 $this->registerDirectoryManager($directory);
314 $lng = $this->stubLng();
315 $this->seedEmptyGlobalLanguageFile(
'de');
316 $db = $this->mockDatabaseForWrites(
null);
317 $logger_factory = $this->createStub(ilLoggerFactory::class);
318 $logger_factory->method(
'getComponentLogger')->willReturn($this->createStub(ilLogger::class));
321 $db->method(
'insert')->willReturnCallback(
static function (
string $table, array $values) use (&$captured):
int {
322 if ($table ===
'lng_modules') {
323 $captured = unserialize($values[
'lang_array'][1] ??
'', [
'allowed_classes' =>
false]);
328 $this->callSaveValues(
'de', [
'ctest6' .
$lng->separator .
'greeting' =>
'Neu'], [],
false);
330 $this->assertSame([
'greeting' =>
'Neu'], $captured);
336 $directory = $this->seedShippedAndOverlay(
'ctest7', [
'greeting' =>
'Shipped',
'farewell' =>
'Tschüss'], [
'farewell' =>
'Pfiat di']);
337 $this->registerDirectoryManager($directory);
338 $lng = $this->stubLng();
339 $this->seedEmptyGlobalLanguageFile(
'de');
340 $db = $this->mockDatabaseForWrites(
null);
341 $logger_factory = $this->createStub(ilLoggerFactory::class);
342 $logger_factory->method(
'getComponentLogger')->willReturn($this->createStub(ilLogger::class));
345 $db->method(
'insert')->willReturnCallback(
static function (
string $table, array $values) use (&$captured):
int {
346 if ($table ===
'lng_modules') {
347 $captured = unserialize($values[
'lang_array'][1] ??
'', [
'allowed_classes' =>
false]);
354 $this->assertSame([], $unwritten);
355 $this->assertSame([
'greeting' =>
'Neu',
'farewell' =>
'Pfiat di'], $captured);
357 $this->assertSame(
'Pfiat di', $overlay->find(
null,
'farewell')?->getTranslation());
358 $this->assertSame(
'Neu', $overlay->find(
null,
'greeting')?->getTranslation());
367 if (function_exists(
'posix_geteuid') && posix_geteuid() === 0) {
368 $this->markTestSkipped(
'A non-writable directory cannot be forced for root (no root-proof variant here).');
371 $directory = $this->seedShippedAndOverlay(
373 [
'greeting' =>
'Shipped',
'farewell' =>
'Tschüss'],
374 [
'greeting' =>
'Lokal']
376 $this->registerDirectoryManager($directory);
377 $lng = $this->stubLng();
378 $logger = $this->createMock(ilLogger::class);
379 $logger->expects($this->atLeastOnce())->method(
'warning');
380 $logger_factory = $this->createStub(ilLoggerFactory::class);
381 $logger_factory->method(
'getComponentLogger')->willReturn(
$logger);
383 $this->mockDatabaseForWrites([
'greeting' =>
'Lokal',
'farewell' =>
'Tschüss']);
387 chmod($overlay_directory, 0555);
389 set_error_handler(
static fn():
bool =>
true, E_WARNING);
393 restore_error_handler();
394 chmod($overlay_directory, 0775);
397 $this->assertSame([
'ctest3'], $unwritten);
403 $GLOBALS[
'DIC'] = new \ILIAS\DI\Container();
406 'common#:#greeting' =>
'Hallo',
407 'common#:#formatting' =>
'<b>Hallo</b>',
410 $this->assertSame([], $result);
416 $lang_key =
'zz_changed_markup_test';
417 $bad =
'<script>alert(1)</script>';
418 $this->seedEmptyGlobalLanguageFile($lang_key);
422 [
'module' =>
'common',
'identifier' =>
'unmodified',
'value' => $bad],
423 [
'module' =>
'common',
'identifier' =>
'changed',
'value' =>
'Hallo'],
425 $statement = $this->createStub(ilDBStatement::class);
426 $statement->method(
'fetchRow')->willReturnCallback(
static function () use (&
$rows) {
427 return array_shift(
$rows);
429 $db = $this->createStub(ilDBInterface::class);
430 $db->method(
'quote')->willReturnCallback(
static fn($value,
string $type =
''):
string =>
"'" . (
string) $value .
"'");
431 $db->method(
'in')->willReturn(
"module IN ('placeholder')");
432 $db->method(
'like')->willReturn(
'1=1');
433 $db->method(
'query')->willReturn($statement);
437 'common#:#unmodified' => $bad,
438 'common#:#changed' => $bad,
441 $this->assertArrayNotHasKey(
'common#:#unmodified', $result,
'unchanged from the current lng_data value - not rejected');
442 $this->assertArrayHasKey(
'common#:#changed', $result,
'genuinely changed to a disallowed value - rejected');
448 $lang_key =
'zz_merge_false_test';
449 $this->seedEmptyGlobalLanguageFile($lang_key);
450 $lng = $this->stubLng();
451 $db = $this->mockDatabaseForWrites([
'greeting' =>
'Alt',
'farewell' =>
'Tschüss']);
454 $db->method(
'insert')->willReturnCallback(
455 static function (
string $table, array $values) use (&$captured):
int {
456 if ($table ===
'lng_modules') {
457 $captured = unserialize($values[
'lang_array'][1] ??
'', [
'allowed_classes' =>
false]);
463 $this->callSaveValues(
465 [
'wtest' .
$lng->separator .
'greeting' =>
'Neu'],
470 $this->assertSame([
'greeting' =>
'Neu'], $captured);
476 $lang_key =
'zz_merge_true_test';
477 $this->seedEmptyGlobalLanguageFile($lang_key);
478 $lng = $this->stubLng();
479 $db = $this->mockDatabaseForWrites([
'greeting' =>
'Alt',
'farewell' =>
'Tschüss']);
482 $db->method(
'insert')->willReturnCallback(
483 static function (
string $table, array $values) use (&$captured):
int {
484 if ($table ===
'lng_modules') {
485 $captured = unserialize($values[
'lang_array'][1] ??
'', [
'allowed_classes' =>
false]);
491 $this->callSaveValues(
493 [
'wtest' .
$lng->separator .
'greeting' =>
'Neu'],
498 $this->assertSame([
'greeting' =>
'Neu',
'farewell' =>
'Tschüss'], $captured);
static removeShippedDirectory(string $relative_path)
static ensureClientDataDirDefinedOrSkip(\PHPUnit\Framework\TestCase $test)
Guards every fixture that writes under CLIENT_DATA_DIR: a full-suite run may have defined it as the r...
static writePair(string $base_path, TranslationCatalog $catalog)
Writes "<base>.po" and, like MigratedLanguageFileSync::sync(), its compiled revision named by current...
static directory(string $prefix, string $path, bool $local=false)
static catalog(string $module, array $entries)
static removeDirectory(string $directory)
static readPo(string $file)
static writeShippedPo(string $relative_path, string $module, string $lang_key, TranslationCatalog $catalog)
Writes $catalog as the shipped .po: a module only counts as migrated while that exists.
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...
ilObjLanguageExt::currentModuleContent() (what a partial save/delete is applied to) and the $merge_on...
testMergeOntoCurrentContentTrueKeepsAKeyThatIsNotInTheNewEntries()
The default (true) contrasted with the test above: the untouched "farewell" survives.
string $fixture_directory
seedEmptyGlobalLanguageFile(string $lang_key)
mockDatabaseForWrites(?array $lng_modules_row, array $lng_data_rows=[])
$ilDB mock with a controllable lng_modules row and lng_data rows; the three SELECTs (lng_modules row,...
testFindInvalidMarkupOfChangedValuesNeverReadsCurrentOrShippedValuesWhenNothingIsSuspicious()
The expensive reads must not run when every value passes the plain markup check (an empty $GLOBALS['D...
testAnUnreadableModuleIsLeftOutCompletelyAndAReadableOneIsWrittenNormally(string $operation, string $broken)
A module whose shipped .po or overlay cannot be read: nothing of its change is written - no lng_data,...
static writingOperationsOnUnreadableContent()
callCurrentModuleContent(string $lang_key, string $module)
testOverlayValueIsUsedEvenWhenItDiffersFromTheLngModulesRowAndTheDatabaseIsNeverConsulted()
->never() on query(): once a readable overlay is found the database must not be queried at all.
testSavingOneKeyKeepsTheOverlaysLocalChangeOfAnotherKey()
A readable overlay: the local change of another key stays when one key is saved.
testAnImportWithoutMergingNeverChecksTheCurrentContent()
"delete"-mode import: the entries are the complete new content, nothing is read first - written as be...
testMergeOntoCurrentContentFalseDropsAKeyThatIsNotInTheNewEntries()
With $merge_onto_current_content=false ("delete"-mode import) $to_save is the complete new content: a...
bool $created_client_data_dir_root
True if this test defined CLIENT_DATA_DIR itself; tearDown() removes only such a root.
seedShippedAndOverlay(string $module, array $shipped_entries, ?array $overlay_entries)
callSaveValues(string $lang_key, array $values, array $remarks, bool $merge_onto_current_content)
registerDirectoryManager(LanguageFileDirectory ... $contributed)
testDeleteValuesReportsTheModuleWhoseOverlayCouldNotBeWritten()
An overlay write failure never undoes the database write: the module is reported back in the returned...
testFindInvalidMarkupOfChangedValuesAppliesTheCurrentAndShippedComparison()
A value failing the plain check is rejected only if it also differs from both the current and the shi...
Class ilLanguageBaseTest.
static _deleteValues(string $a_lang_key, array $a_values=array())
Delete a set of translation in the database.
static findInvalidMarkupOfChangedValues(string $a_lang_key, array $values)
The values a save into $a_lang_key would change and that contain markup TranslationMarkupPolicy does ...
static _saveValues(string $a_lang_key, array $a_values=array(), array $a_remarks=array())
Save a set of translation in the database.
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...
setGlobalVariable(string $name, mixed $value)
A directory of language files with a path and a prefix for them.