19declare(strict_types=1);
24use PHPUnit\Framework\Attributes\DataProvider;
25use PHPUnit\Framework\Attributes\RunInSeparateProcess;
26use PHPUnit\Framework\TestCase;
40 protected function setUp(): void
44 $this->root = sys_get_temp_dir() .
'/ilias_mlfp_test_' . bin2hex(random_bytes(6));
45 mkdir($this->root, 0775,
true);
53 private function writeIni(
string $clients_section): void
55 file_put_contents($this->root .
'/ilias.ini.php',
"; <?php exit; ?>\n[server]\nabsolute_path = \"x\"\n[clients]\n" . $clients_section);
62 #[RunInSeparateProcess]
65 mkdir($this->root .
'/data/default', 0775,
true);
66 mkdir($this->root .
'/data/logs', 0775,
true);
67 $this->
writeIni(
"datadir = \"{$this->root}/data\"\ndefault = \"default\"\n");
69 $this->assertSame($this->root .
'/data/default', MigratedLanguageFilePaths::resolveClientDataDir($this->root));
75 #[RunInSeparateProcess]
78 mkdir($this->root .
'/data/myclient', 0775,
true);
79 $this->
writeIni(
"datadir = \"{$this->root}/data\"\n");
81 $this->assertNull(MigratedLanguageFilePaths::resolveClientDataDir($this->root));
84 #[RunInSeparateProcess]
87 mkdir($this->root .
'/public/data/myclient', 0775,
true);
88 $this->
writeIni(
"datadir = \"public/data\"\ndefault = \"myclient\"\n");
90 $this->assertSame($this->root .
'/public/data/myclient', MigratedLanguageFilePaths::resolveClientDataDir($this->root .
'/'));
93 #[RunInSeparateProcess]
96 $datadir = $this->root .
'/elsewhere';
97 mkdir($datadir .
'/myclient', 0775,
true);
98 mkdir($this->root .
'/sub', 0775,
true);
99 file_put_contents($this->root .
'/sub/ilias.ini.php',
"[clients]\ndatadir = \"$datadir/\"\ndefault = \"myclient\"\n");
101 $this->assertSame($datadir .
'/myclient', MigratedLanguageFilePaths::resolveClientDataDir($this->root .
'/sub'));
104 #[RunInSeparateProcess]
105 #[DataProvider('unresolvableIniContents')]
108 mkdir($this->root .
'/data/default', 0775,
true);
109 if ($clients_section !==
null) {
110 $this->
writeIni(str_replace(
'{root}', $this->root, $clients_section));
113 $this->assertNull(MigratedLanguageFilePaths::resolveClientDataDir($this->root));
119 'no ilias.ini.php' => [
null],
120 'no datadir' => [
"default = \"default\"\n"],
121 'empty datadir' => [
"datadir = \"\"\ndefault = \"default\"\n"],
122 'datadir is an array' => [
"datadir[] = \"{root}/data\"\ndefault = \"default\"\n"],
123 'no default client' => [
"datadir = \"{root}/data\"\n"],
124 'empty default client' => [
"datadir = \"{root}/data\"\ndefault = \"\"\n"],
125 'client directory does not exist (yet)' => [
"datadir = \"{root}/data\"\ndefault = \"other\"\n"],
126 'datadir does not exist' => [
"datadir = \"{root}/nothing\"\ndefault = \"default\"\n"],
127 'path traversal in client id' => [
"datadir = \"{root}/data/default\"\ndefault = \"..\"\n"],
131 #[RunInSeparateProcess]
134 file_put_contents($this->root .
'/ilias.ini.php',
"[clients\ndatadir = \"x\n");
136 $this->assertNull(MigratedLanguageFilePaths::resolveClientDataDir($this->root));
142 #[RunInSeparateProcess]
145 mkdir($this->root .
'/data/default', 0775,
true);
146 $this->
writeIni(
"datadir = \"{$this->root}/data\"\ndefault = \"default\"\n");
147 define(
'CLIENT_DATA_DIR',
'/from/the/constant');
149 $this->assertSame(
'/from/the/constant', MigratedLanguageFilePaths::resolveClientDataDir($this->root));
150 $this->assertSame(
'/from/the/constant', MigratedLanguageFilePaths::resolveClientDataDir());
154 #[RunInSeparateProcess]
157 mkdir($this->root .
'/data/default', 0775,
true);
158 mkdir($this->root .
'/data/other', 0775,
true);
159 $this->
writeIni(
"datadir = \"{$this->root}/data\"\ndefault = \"default\"\n");
160 define(
'CLIENT_ID',
'other');
162 $this->assertSame($this->root .
'/data/other', MigratedLanguageFilePaths::resolveClientDataDir($this->root));
167 mkdir($this->root .
'/client', 0775);
169 $this->assertSame($this->root .
'/client', MigratedLanguageFilePaths::fromDataDirAndClientId($this->root,
'client'));
170 $this->assertSame($this->root .
'/client', MigratedLanguageFilePaths::fromDataDirAndClientId($this->root .
'//',
'client'));
173 #[DataProvider('invalidParts')]
176 mkdir($this->root .
'/client', 0775);
179 $this->assertNull(MigratedLanguageFilePaths::fromDataDirAndClientId(
$data_dir,
$client_id));
185 'no data dir' => [
null,
'client'],
186 'empty data dir' => [
'',
'client'],
187 'no client id' => [
'{root}',
null],
188 'empty client id' => [
'{root}',
''],
189 'dot' => [
'{root}/client',
'.'],
190 'dot dot' => [
'{root}/client',
'..'],
191 'slash' => [
'{root}',
'client/'],
192 'nested' => [
'/', ltrim(
'{root}',
'/') .
'/client'],
193 'backslash' => [
'{root}',
'cli\\ent'],
194 'nul byte' => [
'{root}',
"client\0"],
195 'not existing' => [
'{root}',
'other'],
201 file_put_contents($this->root .
'/client',
'x');
203 $this->assertNull(MigratedLanguageFilePaths::fromDataDirAndClientId($this->root,
'client'));
211 '/srv/ilias/components/ILIAS/TermsOfService/lang/tos_de',
212 MigratedLanguageFilePaths::shippedBasePath(
'/srv/ilias/', $directory,
'de')
215 '/var/iliasdata/default/lang/tos/de/tos_de.po',
216 MigratedLanguageFilePaths::overlayPoFile(
'/var/iliasdata/default/', $directory,
'de')
225 '/srv/ilias/components/ILIAS/TermsOfService/lang/tos_en',
226 MigratedLanguageFilePaths::shippedBasePath(
'/srv/ilias', $directory,
'en')
229 '/data/client/lang/tos/en/tos_en.po',
230 MigratedLanguageFilePaths::overlayPoFile(
'/data/client', $directory,
'en')
238 #[DataProvider('invalidLangKeys')]
243 $this->expectException(\InvalidArgumentException::class);
245 MigratedLanguageFilePaths::shippedBasePath(
'/srv/ilias/', $directory, $lang_key);
254 'trailing newline' => [
"de\n"],
255 'trailing CRLF' => [
"de\r\n"],
256 'uppercase' => [
'DE'],
257 'one letter' => [
'd'],
258 'three letters' => [
'deu'],
267 '/srv/ilias/components/ILIAS/TermsOfService/lang/tos_de',
268 MigratedLanguageFilePaths::shippedBasePath(
'/srv/ilias/', $directory,
'de')
274 MigratedLanguageFilePaths::useArtifactDirectoryForTests(
null);
277 '/srv/ilias/artifacts/language',
278 MigratedLanguageFilePaths::shippedArtifactDirectory(
'/srv/ilias/')
281 '/srv/ilias/artifacts/language',
282 MigratedLanguageFilePaths::shippedArtifactDirectory(
'/srv/ilias')
288 $build = MigratedLanguageFilePaths::buildDirectory(
'/srv/ilias/artifacts/language',
'b-0123456789abcdef');
290 $this->assertSame(
'/srv/ilias/artifacts/language/b-0123456789abcdef', $build);
291 $this->assertSame($build .
'/messages/LC_MESSAGES/tos.de.mo', MigratedLanguageFilePaths::buildMoFile($build,
'tos',
'de'));
292 $this->assertSame($build .
'/keys/tos.php', MigratedLanguageFilePaths::buildKeysFile($build,
'tos'));
293 $this->assertSame(
'tos.de.overlay', MigratedLanguageFilePaths::overlayDomain(
'tos',
'de'));
300 #[DataProvider('unsafeModuleNames')]
303 $this->expectException(\InvalidArgumentException::class);
305 MigratedLanguageFilePaths::buildMoFile(
'/srv/ilias/artifacts/language/b-0123456789abcdef',
$module,
'de');
314 'path traversal' => [
'../../etc/passwd'],
315 'slash' => [
'tos/evil'],
316 'backslash' => [
'tos\\evil'],
317 'nul byte' => [
"tos\0evil"],
323 #[DataProvider('invalidLangKeys')]
326 $this->expectException(\InvalidArgumentException::class);
328 MigratedLanguageFilePaths::overlayDirectory(
'/data/client',
'tos', $lang_key);
338 private string $prefix,
339 private string $path,
344 public function getPrefix():
string
346 return $this->prefix;
349 public function getPath():
string
354 public function getSuffix():
string
359 public function isLocal():
bool
364 public function getShippedFileNamePattern():
string
371 #[DataProvider('invalidShippedFileNamePatterns')]
374 $this->expectException(\InvalidArgumentException::class);
376 MigratedLanguageFilePaths::assertValidShippedFileNamePattern(
$pattern);
385 'no %s at all' => [
'x'],
387 'path traversal' => [
'..%s'],
388 'two placeholders' => [
'%s%s'],
389 'another "%" besides the placeholder' => [
'ilias_%d%s'],
393 #[DataProvider('validShippedFileNamePatterns')]
396 MigratedLanguageFilePaths::assertValidShippedFileNamePattern(
$pattern);
397 $this->addToAssertionCount(1);
406 'prefix-based default shape' => [
'tos_%s'],
407 'stable, module-independent shape' => [
'ilias_%s'],
408 'dot and dash are allowed' => [
'ilias-plugin.v2_%s'],
409 'placeholder at the very start' => [
'%s_suffix'],
417 $this->assertSame(
'tos_%s', MigratedLanguageFilePaths::shippedFileNamePattern($directory));
422 $directory = $this->
directoryWithPattern(
'tos',
'components/ILIAS/TermsOfService/lang/',
'ilias_%s');
424 $this->assertSame(
'ilias_%s', MigratedLanguageFilePaths::shippedFileNamePattern($directory));
429 $directory = $this->
directoryWithPattern(
'tos',
'components/ILIAS/TermsOfService/lang/',
'ilias_%s');
432 '/srv/ilias/components/ILIAS/TermsOfService/lang/ilias_de',
433 MigratedLanguageFilePaths::shippedBasePath(
'/srv/ilias/', $directory,
'de')
443 $directory = $this->
directoryWithPattern(
'tos',
'components/ILIAS/TermsOfService/lang/',
'ilias_%s');
446 '/data/client/lang/tos/de/ilias_de.po',
447 MigratedLanguageFilePaths::overlayPoFile(
'/data/client', $directory,
'de')
453 mkdir($this->root .
'/lang', 0775,
true);
454 touch($this->root .
'/lang/ilias_de.po');
455 touch($this->root .
'/lang/ilias_en.po');
456 touch($this->root .
'/lang/ilias_fr.po');
458 touch($this->root .
'/lang/ilias_pt_BR.po');
460 touch($this->root .
'/lang/ilias_de.lang');
461 touch($this->root .
'/lang/other_de.po');
464 $files = MigratedLanguageFilePaths::findShippedPoFiles($this->root, $directory);
466 $this->assertSame([
'de',
'en',
'fr'], array_keys(
$files));
467 $this->assertSame($this->root .
'/lang/ilias_de.po',
$files[
'de']);
472 mkdir($this->root .
'/lang', 0775,
true);
473 touch($this->root .
'/lang/tos_de.po');
474 touch($this->root .
'/lang/tos_pt_BR.po');
477 $files = MigratedLanguageFilePaths::findShippedPoFiles($this->root, $directory);
479 $this->assertSame([
'de'], array_keys(
$files));
486 $this->expectException(\InvalidArgumentException::class);
488 MigratedLanguageFilePaths::findShippedPoFiles($this->root, $directory);
491 #[DataProvider('templateFileNamePatterns')]
494 $this->assertSame($expected, MigratedLanguageFilePaths::templateFileName(
$pattern,
$module));
503 'prefix-based default shape' => [
'tos_%s',
'tos',
'tos.pot'],
504 'stable, module-independent shape' => [
'ilias_%s',
'tos',
'ilias.pot'],
505 'placeholder alone falls back to the module' => [
'%s',
'tos',
'tos.pot'],
506 'dashes and dots around the placeholder are trimmed too' => [
'ilias-plugin.v2_%s',
'tos',
'ilias-plugin.v2.pot'],
507 'placeholder at the very start' => [
'%s_suffix',
'tos',
'suffix.pot'],
515 'components/Vendor/CompA/lang/alpha.pot',
516 'components/Vendor/CompB/lang/beta.pot',
517 'components/Vendor/CompB/lang/Bad Name.pot',
518 'components/Vendor/CompB/lang/gamma_de.po',
519 'components/Vendor/CompC/other/delta.pot',
520 'components/Vendor/CompD/lang/sub/epsilon.pot',
521 'components/CompE/lang/zeta.pot',
524 is_dir(dirname($this->root .
'/' . $file)) || mkdir(dirname($this->root .
'/' . $file), 0775,
true);
525 file_put_contents($this->root .
'/' . $file,
'');
527 mkdir($this->root .
'/components/Vendor/CompF/lang/theta.pot', 0775,
true);
529 $templates = MigratedLanguageFilePaths::findShippedTemplates($this->root .
'/');
533 [
'alpha' => $this->root .
'/components/Vendor/CompA/lang',
'beta' => $this->root .
'/components/Vendor/CompB/lang'],
536 $this->assertSame([], MigratedLanguageFilePaths::findShippedTemplates($this->root .
'/nowhere'));
546 'lang/<prefix>.pot of a component' => [
'components/Vendor/Comp/lang/',
null,
'zzs.pot',
true],
547 'no template' => [
'components/Vendor/Comp/lang/',
null,
'',
false],
548 'template in another directory' => [
'components/Vendor/Comp/other/',
null,
'zzs.pot',
false],
549 'component directly below components/' => [
'components/Comp/lang/',
null,
'zzs.pot',
false],
550 'template named like the pattern, not the module' => [
'components/Vendor/Comp/lang/',
'ilias_%s',
'ilias.pot',
false],
554 #[DataProvider('templateLocations')]
561 if ($template !==
'') {
562 mkdir($this->root .
'/' . $path, 0775,
true);
563 file_put_contents($this->root .
'/' . $path . $template,
'');
569 $this->assertSame($expected, MigratedLanguageFilePaths::isFoundWithoutGraph($this->root, $directory));
571 $this->assertArrayHasKey(
'zzs', MigratedLanguageFilePaths::findShippedTemplates($this->root));
trait RestoresProcessState
Whatever a test does to the process (locale, LANGUAGE/LC_*, LOCPATH, the static state of the runtime ...
The single place that knows where the files of a module migrated to PO/MO live:
MigratedLanguageFilePaths, the single resolver of shipped/overlay paths and of the client data direct...
static unresolvableIniContents()
testALeadingSlashOfTheDirectoryPathDoesNotEscapeTheRoot()
testOverlayDirectoryRejectsAMalformedLangKey(string $lang_key)
testUsesTheIniDefaultClientEvenWhenTheDataDirHoldsFurtherDirectories()
A data directory regularly holds more than the client directory (e.g.
testFindShippedTemplatesListsValidModulesOfComponentLangDirectoriesOnly()
testBuildPathsComposeModuleAndLanguageBelowTheBuild()
testTheClientDataDirConstantWins()
In a bootstrapped request CLIENT_DATA_DIR is authoritative - also over a (different) ini.
testShippedArtifactDirectoryIsBelowArtifactsLanguageOfTheRoot()
testReturnsNullForAnIncompleteIniOrAMissingClientDirectory(?string $clients_section)
testShippedFileNamePatternUsesTheDirectorysOwnPatternWhenItImplementsTheInterface()
static templateFileNamePatterns()
testFromDataDirAndClientIdRejectsAFileInsteadOfADirectory()
testIsFoundWithoutGraphIsTrueOnlyIfFindShippedTemplatesWouldFindTheModule(string $path, ?string $pattern, string $template, bool $expected)
testAssertValidShippedFileNamePatternAcceptsValidPatterns(string $pattern)
testTheOverlayDirectoryStaysModuleBasedEvenWithTheIliasSchema()
The overlay directory stays module-based (the prefix) even for a directory with the "ilias_%s" shippe...
testClientIdWinsOverTheIniDefaultClient()
A defined CLIENT_ID (the client of this request) wins over [clients] default.
static unsafeModuleNames()
testTheOverlayPoIsNamedLikeTheShippedPoBelowLangModuleLanguage()
testResolvesARelativeDatadirAgainstTheIliasRoot()
testFindShippedPoFilesWithThePlainPrefixPattern()
testFromDataDirAndClientIdComposesAnExistingDirectory()
testFromDataDirAndClientIdRejectsMissingOrUnsafeParts(?string $data_dir, ?string $client_id)
testAnUnparsableIniYieldsNullWithoutAWarning()
static invalidShippedFileNamePatterns()
testFindShippedPoFilesThrowsForAnInvalidPattern()
testTemplateFileName(string $pattern, string $module, string $expected)
testAssertValidShippedFileNamePatternRejectsInvalidPatterns(string $pattern)
testFindShippedPoFilesListsOnlyFilesMatchingThePatternKeyedByLangKeySorted()
static templateLocations()
directoryWithPattern(string $prefix, string $path, string $pattern)
A directory implementing NamesShippedLanguageFiles with a given pattern.
testShippedBasePathAcceptsAPlainTwoLetterLangKey()
testUsesAnAbsoluteDatadirAsIsAndToleratesATrailingSlash()
testBuildMoFileRejectsAModuleThatIsNotAPlainFileName(string $module)
The module becomes part of a file name and a gettext domain - a prefix that is not a plain file name ...
testShippedFileNamePatternFallsBackToPrefixUnderscoreForAPlainDirectory()
static validShippedFileNamePatterns()
testShippedBasePathUsesTheDirectorysOwnShippedFileNamePattern()
testDoesNotGuessTheClientFromTheOnlySubdirectory()
No guessing: without "[clients] default" a single existing subdirectory is not picked.
testShippedBasePathRejectsAMalformedLangKey(string $lang_key)
LANGUAGE_KEY_FORMAT ends in \z, not $ (which PCRE also matches before a trailing newline): a key with...
writeIni(string $clients_section)
static directory(string $prefix, string $path, bool $local=false)
static removeDirectory(string $directory)
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
A directory of language files with a path and a prefix for them.
Optional addition to LanguageFileDirectory for shipped .po files not named <prefix>_<lang>....
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
if(!file_exists('../ilias.ini.php'))