19declare(strict_types=1);
26use PHPUnit\Framework\Attributes\DataProvider;
27use PHPUnit\Framework\MockObject\Stub;
28use PHPUnit\Framework\TestCase;
38 $db = $this->createStub(ilDBInterface::class);
39 $db->method(
'quote')->willReturnCallback(
40 static fn(mixed $value,
string $type):
string =>
"'" . (
string) $value .
"'"
42 $db->method(
'like')->willReturnCallback(
43 static fn(
string $column,
string $type,
string $value =
"?",
bool $ci =
true):
string =>
44 $column .
" LIKE " .
"'" . $value .
"'"
58 $objects = array_map(
static fn(array $row): \stdClass => (
object) $row,
$rows);
60 $db->method(
'fetchObject')->willReturn(...$objects);
68 $statement = $this->createStub(ilDBStatement::class);
70 $statement->method(
'fetchRow')->willReturn(...
$rows);
77 string $absolute_path =
'/does/not/matter/for/db-only/tests'
88 $db = $this->createReadDatabaseMock();
89 $db->method(
'query')->willReturn($this->createStub(ilDBStatement::class));
90 $this->configureDbToReturnObjectRows($db, [
91 [
'title' =>
'de',
'description' =>
'installed',
'obj_id' => 5],
92 [
'title' =>
'en',
'description' =>
'not_installed',
'obj_id' => 8],
95 $result = $this->createRepository($db)->getAvailableLanguages();
99 'de' => [
'obj_id' => 5,
'status' =>
'installed'],
100 'en' => [
'obj_id' => 8,
'status' =>
'not_installed'],
108 $db = $this->createReadDatabaseMock();
109 $db->method(
'query')->willReturn($this->createStub(ilDBStatement::class));
110 $this->configureDbToReturnObjectRows($db, []);
112 $this->assertSame([], $this->createRepository($db)->getAvailableLanguages());
117 $db = $this->createReadDatabaseMock();
118 $captured_query =
null;
119 $db->method(
'query')->willReturnCallback(
120 function (
string $query) use (&$captured_query):
ilDBStatement {
121 $captured_query = $query;
122 return $this->createStub(ilDBStatement::class);
125 $this->configureDbToReturnObjectRows($db, [
130 $result = $this->createRepository($db)->getInstalledLanguages();
132 $this->assertSame([
'de',
'en'], $result);
133 $this->assertStringContainsString(
"type = 'lng'", $captured_query);
134 $this->assertStringContainsString(
"description LIKE 'installed%'", $captured_query);
139 $db = $this->createReadDatabaseMock();
140 $captured_query =
null;
141 $db->method(
'query')->willReturnCallback(
142 function (
string $query) use (&$captured_query):
ilDBStatement {
143 $captured_query = $query;
144 return $this->createStub(ilDBStatement::class);
147 $this->configureDbToReturnObjectRows($db, [
151 $result = $this->createRepository($db)->getInstalledLocalLanguages();
153 $this->assertSame([
'de'], $result);
154 $this->assertStringContainsString(
"description = 'installed_local'", $captured_query);
159 $db = $this->createReadDatabaseMock();
160 $captured_query =
null;
161 $db->method(
'query')->willReturnCallback(
162 function (
string $query) use (&$captured_query):
ilDBStatement {
163 $captured_query = $query;
164 return $this->createStatementReturningAssocRows([]);
168 $this->createRepository($db)->getLocalChanges(
'de',
'',
'');
170 $this->assertStringContainsString(
"'1980-01-01 00:00:00'", $captured_query);
171 $this->assertStringContainsString(
"'2200-01-01 00:00:00'", $captured_query);
176 $db = $this->createReadDatabaseMock();
177 $captured_query =
null;
178 $db->method(
'query')->willReturnCallback(
179 function (
string $query) use (&$captured_query):
ilDBStatement {
180 $captured_query = $query;
181 return $this->createStatementReturningAssocRows([]);
185 $this->createRepository($db)->getLocalChanges(
'de',
'2020-01-01 00:00:00',
'2021-01-01 00:00:00');
187 $this->assertStringContainsString(
"'2020-01-01 00:00:00'", $captured_query);
188 $this->assertStringContainsString(
"'2021-01-01 00:00:00'", $captured_query);
189 $this->assertStringNotContainsString(
'1980-01-01', $captured_query);
190 $this->assertStringNotContainsString(
'2200-01-01', $captured_query);
195 $db = $this->createReadDatabaseMock();
196 $db->method(
'query')->willReturn($this->createStatementReturningAssocRows([
197 [
'module' =>
'common',
'identifier' =>
'foo',
'value' =>
'Foo Value'],
198 [
'module' =>
'common',
'identifier' =>
'bar',
'value' =>
'Bar Value'],
199 [
'module' =>
'other',
'identifier' =>
'baz',
'value' =>
'Baz Value'],
202 $result = $this->createRepository($db)->getLocalChanges(
'de');
206 'common' => [
'foo' =>
'Foo Value',
'bar' =>
'Bar Value'],
207 'other' => [
'baz' =>
'Baz Value'],
215 $db = $this->createReadDatabaseMock();
216 $db->method(
'query')->willReturn($this->createStatementReturningAssocRows([]));
218 $this->assertSame([], $this->createRepository($db)->getLocalChanges(
'de'));
223 $db = $this->createReadDatabaseMock();
224 $db->method(
'query')->willReturn($this->createStatementReturningAssocRows([
225 [
'module' =>
'common',
'identifier' =>
'foo',
'value' =>
'Foo Value'],
226 [
'module' =>
'common',
'identifier' =>
'bar',
'value' =>
'Bar Value'],
227 [
'module' =>
'other',
'identifier' =>
'baz',
'value' =>
'Baz Value'],
230 $result = $this->createRepository($db)->getLanguageEntries(
'de');
234 'common' => [
'foo' =>
'Foo Value',
'bar' =>
'Bar Value'],
235 'other' => [
'baz' =>
'Baz Value'],
243 $db = $this->createReadDatabaseMock();
244 $db->method(
'query')->willReturn($this->createStatementReturningAssocRows([]));
246 $this->assertSame([], $this->createRepository($db)->getLanguageEntries(
'de'));
255 $db = $this->createReadDatabaseMock();
256 $captured_query =
null;
257 $db->method(
'query')->willReturnCallback(
258 function (
string $query) use (&$captured_query):
ilDBStatement {
259 $captured_query = $query;
260 return $this->createStatementReturningAssocRows([]);
264 $this->createRepository($db)->getLanguageEntries(
'de');
266 $this->assertStringContainsString(
"lang_key = 'de'", $captured_query);
267 $this->assertStringNotContainsString(
'local_change', $captured_query);
272 $root = $this->createTempInstallationRoot();
273 file_put_contents($root .
'/lang/customizing/ilias_de.lang.local',
'irrelevant content');
274 file_put_contents($root .
'/lang/customizing/ilias_en.lang.local',
'irrelevant content');
276 file_put_contents($root .
'/lang/customizing/ilias_fr.lang',
'irrelevant content');
277 file_put_contents($root .
'/lang/customizing/README.md',
'irrelevant content');
280 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
282 $result = $repository->getLocalLanguages();
285 $this->assertSame([
'de',
'en'], $result);
286 $this->assertSame(array_values(array_unique($result)), array_values($result));
288 $this->removeDirectory($root);
298 $root = $this->createTempInstallationRoot();
299 file_put_contents($root .
'/lang/customizing/ilias_de.lang.local',
'irrelevant content');
300 file_put_contents($root .
'/lang/customizing/ilias_DE.lang.local',
'irrelevant content');
301 file_put_contents($root .
'/lang/customizing/ilias_12.lang.local',
'irrelevant content');
302 file_put_contents($root .
'/lang/customizing/ilias_Fr.lang.local',
'irrelevant content');
303 file_put_contents($root .
'/lang/customizing/ilias_deu.lang.local',
'irrelevant content');
306 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
308 $result = $repository->getLocalLanguages();
310 $this->assertSame([
'de'], $result);
312 $this->removeDirectory($root);
321 $root = $this->createTempInstallationRoot();
322 file_put_contents($root .
'/lang/ilias_de.lang',
'irrelevant content');
323 file_put_contents($root .
'/lang/ilias_DE.lang',
'irrelevant content');
324 file_put_contents($root .
'/lang/ilias_12.lang',
'irrelevant content');
327 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
329 $result = $repository->getInstallableLanguages();
331 $this->assertSame([
'de'], $result);
333 $this->removeDirectory($root);
343 $root = $this->createTempInstallationRoot();
344 file_put_contents($root .
'/lang/ilias_de.lang',
'irrelevant content');
346 file_put_contents($root .
"/lang/ilias_zz.lang\n",
'irrelevant content');
349 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
351 $result = $repository->getInstallableLanguages();
353 $this->assertSame([
'de'], $result);
355 $this->removeDirectory($root);
361 $root = $this->createTempInstallationRoot();
364 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
365 $this->assertSame([], $repository->getLocalLanguages());
367 $this->removeDirectory($root);
373 $root = $this->createTempInstallationRoot();
374 file_put_contents($root .
'/lang/ilias_de.lang',
'irrelevant content');
375 file_put_contents($root .
'/lang/ilias_en.lang',
'irrelevant content');
377 file_put_contents($root .
'/lang/ilias_fr.lang.local',
'irrelevant content');
378 file_put_contents($root .
'/lang/notes.txt',
'irrelevant content');
381 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
383 $result = $repository->getInstallableLanguages();
386 $this->assertSame([
'de',
'en'], $result);
388 $this->removeDirectory($root);
394 $root = $this->createTempInstallationRoot();
397 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
399 $this->assertFalse($repository->checkLocalLanguageFile(
'de'));
401 $this->removeDirectory($root);
407 $root = $this->createTempInstallationRoot();
409 $root .
'/lang/customizing/ilias_de.lang.local',
410 "<!-- language file start -->\ncommon#:#test#:#Custom Value\n"
414 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
415 $this->assertTrue($repository->checkLocalLanguageFile(
'de'));
417 $this->removeDirectory($root);
423 $root = $this->createTempInstallationRoot();
425 $root .
'/lang/customizing/ilias_de.lang.local',
426 "common#:#test#:#Custom Value\n"
430 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
431 $this->assertFalse($repository->checkLocalLanguageFile(
'de'));
433 $this->removeDirectory($root);
439 $root = $this->createTempInstallationRoot();
441 $root .
'/lang/customizing/ilias_de.lang.local',
442 "<!-- language file start -->\nonly_one_field\n"
446 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
447 $this->assertFalse($repository->checkLocalLanguageFile(
'de'));
449 $this->removeDirectory($root);
459 $manager = $this->createStub(LanguageFileDirectoryManager::class);
460 $manager->method(
'getCustomizingDirectories')->willReturnCallback(
461 static function (): \Generator {
468 $this->createReadDatabaseMock(),
473 $this->assertFalse($repository->checkLocalLanguageFile(
'de'));
483 $root = $this->createTempInstallationRoot();
484 file_put_contents($root .
'/lang/ilias_de.lang',
"<!-- language file start -->\ncommon#:#yes#:#Ja\n");
485 mkdir($root .
'/components/pilot/lang', 0777,
true);
486 if ($po_content !==
null) {
487 file_put_contents($root .
'/components/pilot/lang/pilot_de.po', $po_content);
493 $this->createReadDatabaseMock(),
499 return $repository->checkLanguage(
'de');
501 $this->removeDirectory($root);
507 $this->assertTrue($this->checkLanguageWithShippedPo(
"msgctxt \"pilot\"\nmsgid \"greeting\"\nmsgstr \"Hallo\"\n"));
514 #[DataProvider('unparsableShippedPo')]
517 $this->assertFalse($this->checkLanguageWithShippedPo($po_content));
522 $message =
"msgctxt \"pilot\"\nmsgid \"greeting\"\nmsgstr \"Hallo\"\n";
525 'unterminated string' => [
"msgctxt \"pilot\"\nmsgid \"abc\nmsgstr \"Hallo\"\n"],
527 'message without msgstr' => [
"msgctxt \"pilot\"\nmsgid \"greeting\"\n"],
528 'truncated inside a string' => [substr(
$message, 0, -4)],
529 'binary data' => [
"\xde\x12\x04\x95\x00\x00\x00\x00"],
530 'header value with a line break' => [
"msgid \"\"\nmsgstr \"\"\n\"X-A: a\\rb\\n\"\n\n" .
$message],
539 $this->assertTrue($this->checkLanguageWithShippedPo(
540 "\xEF\xBB\xBFmsgid \"\"\r\nmsgstr \"\"\r\n\"Language: de\\n\"\r\n\r\nmsgctxt \"pilot\"\r\nmsgid \"greeting\"\r\nmsgstr \"Hallo\"\r\n"
546 $this->assertTrue($this->checkLanguageWithShippedPo(
null));
551 $this->assertTrue($this->checkLanguageWithShippedPo(
"msgid \"abc\n",
true));
559 $this->assertTrue($this->checkLanguageWithShippedPo(
''));
568 $root = $this->createTempInstallationRoot();
569 file_put_contents($root .
'/lang/ilias_de.lang',
"<!-- language file start -->\ncommon#:#yes#:#Ja\n");
570 file_put_contents($root .
'/lang/customizing/ilias_de.lang.local',
"<!-- language file start -->\nonly_one_field\n");
573 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
575 $this->assertTrue($repository->checkShippedLanguage(
'de'),
'the invalid customizing file is never consulted');
576 $this->assertFalse($repository->checkLanguage(
'de'),
'precondition: the same file makes checkLanguage() fail');
578 $this->removeDirectory($root);
587 $root = $this->createTempInstallationRoot();
588 file_put_contents($root .
'/lang/ilias_de.lang',
"common#:#yes#:#Ja\n");
591 $repository = $this->createRepository($this->createReadDatabaseMock(), $root);
593 $this->assertFalse($repository->checkShippedLanguage(
'de'));
595 $this->removeDirectory($root);
605 $root = $this->createTempInstallationRoot();
606 file_put_contents($root .
'/lang/ilias_de.lang',
"<!-- language file start -->\ncommon#:#yes#:#Ja\n");
607 mkdir($root .
'/components/pilot/lang', 0777,
true);
608 file_put_contents($root .
'/components/pilot/lang/pilot_de.po',
"msgctxt \"pilot\"\nmsgid \"abc\nmsgstr \"Hallo\"\n");
613 $this->createReadDatabaseMock(),
618 $this->assertFalse($repository->checkShippedLanguage(
'de'));
620 $this->removeDirectory($root);
626 $dir = sys_get_temp_dir() .
'/ilias_lang_repo_test_' . bin2hex(random_bytes(8));
627 mkdir($dir .
'/lang/customizing', 0777,
true);
636 $items = scandir($dir);
637 foreach ($items as $item) {
638 if ($item ===
'.' || $item ===
'..') {
641 $path = $dir .
'/' . $item;
642 if (is_dir($path) && !is_link($path)) {
643 $this->removeDirectory($path);
A root determined by the caller, for the classes which are not wired by the component bootstrap.
Database-backed methods run against an ilDBInterface mock returning controlled rows; filesystem-backe...
testGetInstalledLanguagesQueriesForInstalledPrefixAndReturnsTitles()
testCheckShippedLanguageIgnoresAnInvalidCustomizingFileThatMakesCheckLanguageFail()
checkShippedLanguage() is checkLanguage() without the customizing directory: an invalid "ilias_de....
testGetLocalChangesStructuresResultAsModuleThenIdentifierThenValue()
testCheckLanguageAcceptsAModuleWithoutShippedPoForTheLanguage()
testGetLanguageEntriesReturnsEmptyArrayWhenNothingMatches()
testCheckLocalLanguageFileReturnsFalseWhenNoLocalFileExists()
testCheckLocalLanguageFileReturnsTrueForAValidLocalFile()
testCheckLanguageRejectsAnUnparsableShippedPo(string $po_content)
Everything gettext/gettext's StrictPoLoader rejects invalidates the language, including what the old ...
testGetLanguageEntriesQueryDoesNotFilterByLocalChange()
Unlike getLocalChanges(), getLanguageEntries() returns every stored entry of the language key,...
testGetAvailableLanguagesMapsObjIdAndStatusToTheCorrectLanguageNotSwapped()
testCheckLocalLanguageFileReturnsFalseForMalformedLine()
createStatementReturningAssocRows(array $rows)
createRepository(ilDBInterface $db, string $absolute_path='/does/not/matter/for/db-only/tests')
testGetLocalLanguagesIgnoresFilesWithAnInvalidLanguageKeyShapeInTheirName()
The discovery regex must accept exactly ParsesLanguageKeyList::LANGUAGE_KEY_FORMAT ('/^[a-z]{2}$/'): ...
testCheckShippedLanguageRejectsAnUnparsableShippedPo()
An unparsable shipped .po of a PO-maintained module is part of getDirectories() too,...
testGetLocalChangesDefaultsMinAndMaxDateWhenEmptyStringsArePassed()
testGetLanguageEntriesStructuresResultAsModuleThenIdentifierThenValue()
static unparsableShippedPo()
testGetInstallableLanguagesIgnoresFilesWithAnInvalidLanguageKeyShapeInTheirName()
Same for getInstallableLanguages() (lang/) instead of getLocalLanguages() (lang/customizing/).
testGetInstalledLocalLanguagesQueriesForExactlyInstalledLocalAndReturnsTitles()
testGetInstallableLanguagesIgnoresAFileNameWithATrailingNewline()
The regex ends in \z, not $, which in PCRE also matches before a trailing newline; file names may leg...
configureDbToReturnObjectRows(Stub &ilDBInterface $db, array $rows)
The read methods call fetchObject() on the database object, not on the statement, so the rows are stu...
testCheckShippedLanguageRejectsAnInvalidGlobalLanguageFile()
An invalid global "ilias_de.lang" is part of getDirectories(), so checkShippedLanguage() fails like c...
checkLanguageWithShippedPo(?string $po_content, bool $local_directory=false)
checkLanguage() for a migrated module: the shipped .po is the only source of its values,...
testGetLocalLanguagesReturnsEmptyArrayWhenCustomizingDirectoryIsEmpty()
removeDirectory(string $dir)
testCheckLanguageDoesNotParsePoFilesOfTheLocalDirectory()
testGetLocalChangesKeepsExplicitlyGivenDatesInsteadOfDefaulting()
testGetAvailableLanguagesReturnsEmptyArrayWhenNoLanguagesAreKnown()
testCheckLocalLanguageFileReturnsFalseForAnInvalidLocalFile()
createTempInstallationRoot()
testCheckLanguageAcceptsAParsableShippedPo()
testCheckLanguageAcceptsAShippedPoWithBomAndCrLf()
A BOM and CRLF line endings are no reason to refuse the language.
testGetLocalLanguagesExtractsKeysFromLocalFilesAndIgnoresUnrelatedFiles()
testGetInstallableLanguagesExtractsKeysFromMainFilesAndIgnoresUnrelatedFiles()
testCheckLocalLanguageFileReturnsFalseWhenNoCustomizingDirectoryIsConfiguredAtAll()
getCustomizingDirectories() always yields one directory in production, so the "zero directories" bran...
testGetLocalChangesReturnsEmptyArrayWhenNothingMatches()
testCheckLanguageAcceptsAnEmptyShippedPo()
An empty .po is a valid, empty catalog.
static directory(string $prefix, string $path, bool $local=false)
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...