ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFilePathsTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
24use PHPUnit\Framework\Attributes\DataProvider;
25use PHPUnit\Framework\Attributes\RunInSeparateProcess;
26use PHPUnit\Framework\TestCase;
27
34class MigratedLanguageFilePathsTest extends TestCase
35{
37
38 private string $root;
39
40 protected function setUp(): void
41 {
42 // never the build of the installation, see MigratedPoFixture::resetRuntime()
44 $this->root = sys_get_temp_dir() . '/ilias_mlfp_test_' . bin2hex(random_bytes(6));
45 mkdir($this->root, 0775, true);
46 }
47
48 protected function tearDown(): void
49 {
51 }
52
53 private function writeIni(string $clients_section): void
54 {
55 file_put_contents($this->root . '/ilias.ini.php', "; <?php exit; ?>\n[server]\nabsolute_path = \"x\"\n[clients]\n" . $clients_section);
56 }
57
62 #[RunInSeparateProcess]
64 {
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");
68
69 $this->assertSame($this->root . '/data/default', MigratedLanguageFilePaths::resolveClientDataDir($this->root));
70 }
71
75 #[RunInSeparateProcess]
77 {
78 mkdir($this->root . '/data/myclient', 0775, true);
79 $this->writeIni("datadir = \"{$this->root}/data\"\n");
80
81 $this->assertNull(MigratedLanguageFilePaths::resolveClientDataDir($this->root));
82 }
83
84 #[RunInSeparateProcess]
86 {
87 mkdir($this->root . '/public/data/myclient', 0775, true);
88 $this->writeIni("datadir = \"public/data\"\ndefault = \"myclient\"\n");
89
90 $this->assertSame($this->root . '/public/data/myclient', MigratedLanguageFilePaths::resolveClientDataDir($this->root . '/'));
91 }
92
93 #[RunInSeparateProcess]
95 {
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");
100
101 $this->assertSame($datadir . '/myclient', MigratedLanguageFilePaths::resolveClientDataDir($this->root . '/sub'));
102 }
103
104 #[RunInSeparateProcess]
105 #[DataProvider('unresolvableIniContents')]
106 public function testReturnsNullForAnIncompleteIniOrAMissingClientDirectory(?string $clients_section): void
107 {
108 mkdir($this->root . '/data/default', 0775, true);
109 if ($clients_section !== null) {
110 $this->writeIni(str_replace('{root}', $this->root, $clients_section));
111 }
112
113 $this->assertNull(MigratedLanguageFilePaths::resolveClientDataDir($this->root));
114 }
115
116 public static function unresolvableIniContents(): array
117 {
118 return [
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"],
128 ];
129 }
130
131 #[RunInSeparateProcess]
133 {
134 file_put_contents($this->root . '/ilias.ini.php', "[clients\ndatadir = \"x\n");
135
136 $this->assertNull(MigratedLanguageFilePaths::resolveClientDataDir($this->root));
137 }
138
142 #[RunInSeparateProcess]
143 public function testTheClientDataDirConstantWins(): void
144 {
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');
148
149 $this->assertSame('/from/the/constant', MigratedLanguageFilePaths::resolveClientDataDir($this->root));
150 $this->assertSame('/from/the/constant', MigratedLanguageFilePaths::resolveClientDataDir());
151 }
152
154 #[RunInSeparateProcess]
156 {
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');
161
162 $this->assertSame($this->root . '/data/other', MigratedLanguageFilePaths::resolveClientDataDir($this->root));
163 }
164
166 {
167 mkdir($this->root . '/client', 0775);
168
169 $this->assertSame($this->root . '/client', MigratedLanguageFilePaths::fromDataDirAndClientId($this->root, 'client'));
170 $this->assertSame($this->root . '/client', MigratedLanguageFilePaths::fromDataDirAndClientId($this->root . '//', 'client'));
171 }
172
173 #[DataProvider('invalidParts')]
175 {
176 mkdir($this->root . '/client', 0775);
177 $data_dir = $data_dir === null ? null : str_replace('{root}', $this->root, $data_dir);
178
179 $this->assertNull(MigratedLanguageFilePaths::fromDataDirAndClientId($data_dir, $client_id));
180 }
181
182 public static function invalidParts(): array
183 {
184 return [
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'],
196 ];
197 }
198
200 {
201 file_put_contents($this->root . '/client', 'x');
202
203 $this->assertNull(MigratedLanguageFilePaths::fromDataDirAndClientId($this->root, 'client'));
204 }
205
207 {
208 $directory = MigratedPoFixture::directory('tos', 'components/ILIAS/TermsOfService/lang/');
209
210 $this->assertSame(
211 '/srv/ilias/components/ILIAS/TermsOfService/lang/tos_de',
212 MigratedLanguageFilePaths::shippedBasePath('/srv/ilias/', $directory, 'de')
213 );
214 $this->assertSame(
215 '/var/iliasdata/default/lang/tos/de/tos_de.po',
216 MigratedLanguageFilePaths::overlayPoFile('/var/iliasdata/default/', $directory, 'de')
217 );
218 }
219
221 {
222 $directory = MigratedPoFixture::directory('tos', '/components/ILIAS/TermsOfService/lang/');
223
224 $this->assertSame(
225 '/srv/ilias/components/ILIAS/TermsOfService/lang/tos_en',
226 MigratedLanguageFilePaths::shippedBasePath('/srv/ilias', $directory, 'en')
227 );
228 $this->assertSame(
229 '/data/client/lang/tos/en/tos_en.po',
230 MigratedLanguageFilePaths::overlayPoFile('/data/client', $directory, 'en')
231 );
232 }
233
238 #[DataProvider('invalidLangKeys')]
239 public function testShippedBasePathRejectsAMalformedLangKey(string $lang_key): void
240 {
241 $directory = MigratedPoFixture::directory('tos', 'components/ILIAS/TermsOfService/lang/');
242
243 $this->expectException(\InvalidArgumentException::class);
244
245 MigratedLanguageFilePaths::shippedBasePath('/srv/ilias/', $directory, $lang_key);
246 }
247
251 public static function invalidLangKeys(): array
252 {
253 return [
254 'trailing newline' => ["de\n"],
255 'trailing CRLF' => ["de\r\n"],
256 'uppercase' => ['DE'],
257 'one letter' => ['d'],
258 'three letters' => ['deu'],
259 ];
260 }
261
263 {
264 $directory = MigratedPoFixture::directory('tos', 'components/ILIAS/TermsOfService/lang/');
265
266 $this->assertSame(
267 '/srv/ilias/components/ILIAS/TermsOfService/lang/tos_de',
268 MigratedLanguageFilePaths::shippedBasePath('/srv/ilias/', $directory, 'de')
269 );
270 }
271
273 {
274 MigratedLanguageFilePaths::useArtifactDirectoryForTests(null);
275
276 $this->assertSame(
277 '/srv/ilias/artifacts/language',
278 MigratedLanguageFilePaths::shippedArtifactDirectory('/srv/ilias/')
279 );
280 $this->assertSame(
281 '/srv/ilias/artifacts/language',
282 MigratedLanguageFilePaths::shippedArtifactDirectory('/srv/ilias')
283 );
284 }
285
287 {
288 $build = MigratedLanguageFilePaths::buildDirectory('/srv/ilias/artifacts/language', 'b-0123456789abcdef');
289
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'));
294 }
295
300 #[DataProvider('unsafeModuleNames')]
302 {
303 $this->expectException(\InvalidArgumentException::class);
304
305 MigratedLanguageFilePaths::buildMoFile('/srv/ilias/artifacts/language/b-0123456789abcdef', $module, 'de');
306 }
307
311 public static function unsafeModuleNames(): array
312 {
313 return [
314 'path traversal' => ['../../etc/passwd'],
315 'slash' => ['tos/evil'],
316 'backslash' => ['tos\\evil'],
317 'nul byte' => ["tos\0evil"],
318 'empty' => [''],
319 'dot' => ['tos.de'],
320 ];
321 }
322
323 #[DataProvider('invalidLangKeys')]
324 public function testOverlayDirectoryRejectsAMalformedLangKey(string $lang_key): void
325 {
326 $this->expectException(\InvalidArgumentException::class);
327
328 MigratedLanguageFilePaths::overlayDirectory('/data/client', 'tos', $lang_key);
329 }
330
334 private function directoryWithPattern(string $prefix, string $path, string $pattern): LanguageFileDirectory
335 {
336 return new class ($prefix, $path, $pattern) implements LanguageFileDirectory, NamesShippedLanguageFiles {
337 public function __construct(
338 private string $prefix,
339 private string $path,
340 private string $pattern
341 ) {
342 }
343
344 public function getPrefix(): string
345 {
346 return $this->prefix;
347 }
348
349 public function getPath(): string
350 {
351 return $this->path;
352 }
353
354 public function getSuffix(): string
355 {
356 return '';
357 }
358
359 public function isLocal(): bool
360 {
361 return false;
362 }
363
364 public function getShippedFileNamePattern(): string
365 {
366 return $this->pattern;
367 }
368 };
369 }
370
371 #[DataProvider('invalidShippedFileNamePatterns')]
373 {
374 $this->expectException(\InvalidArgumentException::class);
375
376 MigratedLanguageFilePaths::assertValidShippedFileNamePattern($pattern);
377 }
378
382 public static function invalidShippedFileNamePatterns(): array
383 {
384 return [
385 'no %s at all' => ['x'],
386 'slash' => ['a/%s'],
387 'path traversal' => ['..%s'],
388 'two placeholders' => ['%s%s'],
389 'another "%" besides the placeholder' => ['ilias_%d%s'],
390 ];
391 }
392
393 #[DataProvider('validShippedFileNamePatterns')]
395 {
396 MigratedLanguageFilePaths::assertValidShippedFileNamePattern($pattern);
397 $this->addToAssertionCount(1);
398 }
399
403 public static function validShippedFileNamePatterns(): array
404 {
405 return [
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'],
410 ];
411 }
412
414 {
415 $directory = MigratedPoFixture::directory('tos', 'components/ILIAS/TermsOfService/lang/');
416
417 $this->assertSame('tos_%s', MigratedLanguageFilePaths::shippedFileNamePattern($directory));
418 }
419
421 {
422 $directory = $this->directoryWithPattern('tos', 'components/ILIAS/TermsOfService/lang/', 'ilias_%s');
423
424 $this->assertSame('ilias_%s', MigratedLanguageFilePaths::shippedFileNamePattern($directory));
425 }
426
428 {
429 $directory = $this->directoryWithPattern('tos', 'components/ILIAS/TermsOfService/lang/', 'ilias_%s');
430
431 $this->assertSame(
432 '/srv/ilias/components/ILIAS/TermsOfService/lang/ilias_de',
433 MigratedLanguageFilePaths::shippedBasePath('/srv/ilias/', $directory, 'de')
434 );
435 }
436
442 {
443 $directory = $this->directoryWithPattern('tos', 'components/ILIAS/TermsOfService/lang/', 'ilias_%s');
444
445 $this->assertSame(
446 '/data/client/lang/tos/de/ilias_de.po',
447 MigratedLanguageFilePaths::overlayPoFile('/data/client', $directory, 'de')
448 );
449 }
450
452 {
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');
457 // does not yield a plain two-letter language key - must not be listed
458 touch($this->root . '/lang/ilias_pt_BR.po');
459 // wrong extension / wrong prefix - must not be listed either
460 touch($this->root . '/lang/ilias_de.lang');
461 touch($this->root . '/lang/other_de.po');
462 $directory = $this->directoryWithPattern('tos', 'lang/', 'ilias_%s');
463
464 $files = MigratedLanguageFilePaths::findShippedPoFiles($this->root, $directory);
465
466 $this->assertSame(['de', 'en', 'fr'], array_keys($files));
467 $this->assertSame($this->root . '/lang/ilias_de.po', $files['de']);
468 }
469
471 {
472 mkdir($this->root . '/lang', 0775, true);
473 touch($this->root . '/lang/tos_de.po');
474 touch($this->root . '/lang/tos_pt_BR.po');
475 $directory = MigratedPoFixture::directory('tos', 'lang/');
476
477 $files = MigratedLanguageFilePaths::findShippedPoFiles($this->root, $directory);
478
479 $this->assertSame(['de'], array_keys($files));
480 }
481
483 {
484 $directory = $this->directoryWithPattern('tos', 'lang/', 'x');
485
486 $this->expectException(\InvalidArgumentException::class);
487
488 MigratedLanguageFilePaths::findShippedPoFiles($this->root, $directory);
489 }
490
491 #[DataProvider('templateFileNamePatterns')]
492 public function testTemplateFileName(string $pattern, string $module, string $expected): void
493 {
494 $this->assertSame($expected, MigratedLanguageFilePaths::templateFileName($pattern, $module));
495 }
496
500 public static function templateFileNamePatterns(): array
501 {
502 return [
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'],
508 ];
509 }
510
512 {
513 foreach (
514 [
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',
522 ] as $file
523 ) {
524 is_dir(dirname($this->root . '/' . $file)) || mkdir(dirname($this->root . '/' . $file), 0775, true);
525 file_put_contents($this->root . '/' . $file, '');
526 }
527 mkdir($this->root . '/components/Vendor/CompF/lang/theta.pot', 0775, true);
528
529 $templates = MigratedLanguageFilePaths::findShippedTemplates($this->root . '/');
530
531 ksort($templates);
532 $this->assertSame(
533 ['alpha' => $this->root . '/components/Vendor/CompA/lang', 'beta' => $this->root . '/components/Vendor/CompB/lang'],
534 $templates
535 );
536 $this->assertSame([], MigratedLanguageFilePaths::findShippedTemplates($this->root . '/nowhere'));
537 }
538
543 public static function templateLocations(): array
544 {
545 return [
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],
551 ];
552 }
553
554 #[DataProvider('templateLocations')]
556 string $path,
557 ?string $pattern,
558 string $template,
559 bool $expected
560 ): void {
561 if ($template !== '') {
562 mkdir($this->root . '/' . $path, 0775, true);
563 file_put_contents($this->root . '/' . $path . $template, '');
564 }
565 $directory = $pattern === null
566 ? MigratedPoFixture::directory('zzs', $path)
567 : $this->directoryWithPattern('zzs', $path, $pattern);
568
569 $this->assertSame($expected, MigratedLanguageFilePaths::isFoundWithoutGraph($this->root, $directory));
570 if ($expected) {
571 $this->assertArrayHasKey('zzs', MigratedLanguageFilePaths::findShippedTemplates($this->root));
572 }
573 }
574}
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...
testOverlayDirectoryRejectsAMalformedLangKey(string $lang_key)
testUsesTheIniDefaultClientEvenWhenTheDataDirHoldsFurtherDirectories()
A data directory regularly holds more than the client directory (e.g.
testTheClientDataDirConstantWins()
In a bootstrapped request CLIENT_DATA_DIR is authoritative - also over a (different) ini.
testReturnsNullForAnIncompleteIniOrAMissingClientDirectory(?string $clients_section)
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.
testFromDataDirAndClientIdRejectsMissingOrUnsafeParts(?string $data_dir, ?string $client_id)
testTemplateFileName(string $pattern, string $module, string $expected)
testAssertValidShippedFileNamePatternRejectsInvalidPatterns(string $pattern)
directoryWithPattern(string $prefix, string $path, string $pattern)
A directory implementing NamesShippedLanguageFiles with a given pattern.
testBuildMoFileRejectsAModuleThatIsNotAPlainFileName(string $module)
The module becomes part of a file name and a gettext domain - a prefix that is not a plain file name ...
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...
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>....
$client_id
Definition: ltiresult.php:32
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
if(!file_exists('../ilias.ini.php'))