Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / locale / tests / src / Functional / LocaleImportFunctionalTest.php
1 <?php
2
3 namespace Drupal\Tests\locale\Functional;
4
5 use Drupal\Tests\BrowserTestBase;
6 use Drupal\Core\Language\LanguageInterface;
7
8 /**
9  * Tests the import of locale files.
10  *
11  * @group locale
12  */
13 class LocaleImportFunctionalTest extends BrowserTestBase {
14
15   /**
16    * Modules to enable.
17    *
18    * @var array
19    */
20   public static $modules = ['locale', 'dblog'];
21
22   /**
23    * A user able to create languages and import translations.
24    *
25    * @var \Drupal\user\Entity\User
26    */
27   protected $adminUser;
28
29   /**
30    * A user able to create languages, import translations and access site
31    * reports.
32    *
33    * @var \Drupal\user\Entity\User
34    */
35   protected $adminUserAccessSiteReports;
36
37   /**
38    * {@inheritdoc}
39    */
40   protected function setUp() {
41     parent::setUp();
42
43     // Copy test po files to the translations directory.
44     file_unmanaged_copy(__DIR__ . '/../../tests/test.de.po', 'translations://', FILE_EXISTS_REPLACE);
45     file_unmanaged_copy(__DIR__ . '/../../tests/test.xx.po', 'translations://', FILE_EXISTS_REPLACE);
46
47     $this->adminUser = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages']);
48     $this->adminUserAccessSiteReports = $this->drupalCreateUser(['administer languages', 'translate interface', 'access administration pages', 'access site reports']);
49     $this->drupalLogin($this->adminUser);
50
51     // Enable import of translations. By default this is disabled for automated
52     // tests.
53     $this->config('locale.settings')
54       ->set('translation.import_enabled', TRUE)
55       ->save();
56   }
57
58   /**
59    * Test import of standalone .po files.
60    */
61   public function testStandalonePoFile() {
62     // Try importing a .po file.
63     $this->importPoFile($this->getPoFile(), [
64       'langcode' => 'fr',
65     ]);
66     $this->config('locale.settings');
67     // The import should automatically create the corresponding language.
68     $this->assertRaw(t('The language %language has been created.', ['%language' => 'French']), 'The language has been automatically created.');
69
70     // The import should have created 8 strings.
71     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 8, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
72
73     // This import should have saved plural forms to have 2 variants.
74     $locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
75     $this->assertEqual(2, $locale_plurals, 'Plural number initialized.');
76
77     // Ensure we were redirected correctly.
78     $this->assertUrl(\Drupal::url('locale.translate_page', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
79
80     // Try importing a .po file with invalid tags.
81     $this->importPoFile($this->getBadPoFile(), [
82       'langcode' => 'fr',
83     ]);
84
85     // The import should have created 1 string and rejected 2.
86     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
87
88     $skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. See the log for details.', [':url' => \Drupal::url('dblog.overview')]);
89     $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
90
91     // Repeat the process with a user that can access site reports, and this
92     // time the different warnings must contain links to the log.
93     $this->drupalLogin($this->adminUserAccessSiteReports);
94
95     // Try importing a .po file with invalid tags.
96     $this->importPoFile($this->getBadPoFile(), [
97       'langcode' => 'fr',
98     ]);
99
100     $skip_message = \Drupal::translation()->formatPlural(2, 'One translation string was skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', '@count translation strings were skipped because of disallowed or malformed HTML. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]);
101     $this->assertRaw($skip_message, 'Unsafe strings were skipped.');
102
103     // Check empty files import with a user that cannot access site reports..
104     $this->drupalLogin($this->adminUser);
105     // Try importing a zero byte sized .po file.
106     $this->importPoFile($this->getEmptyPoFile(), [
107       'langcode' => 'fr',
108     ]);
109     // The import should have created 0 string and rejected 0.
110     $this->assertRaw(t('One translation file could not be imported. See the log for details.'), 'The empty translation file import reported no translations imported.');
111
112     // Repeat the process with a user that can access site reports, and this
113     // time the different warnings must contain links to the log.
114     $this->drupalLogin($this->adminUserAccessSiteReports);
115     // Try importing a zero byte sized .po file.
116     $this->importPoFile($this->getEmptyPoFile(), [
117       'langcode' => 'fr',
118     ]);
119     // The import should have created 0 string and rejected 0.
120     $this->assertRaw(t('One translation file could not be imported. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]), 'The empty translation file import reported no translations imported.');
121
122     // Try importing a .po file which doesn't exist.
123     $name = $this->randomMachineName(16);
124     $this->drupalPostForm('admin/config/regional/translate/import', [
125       'langcode' => 'fr',
126       'files[file]' => $name,
127     ], t('Import'));
128     $this->assertUrl(\Drupal::url('locale.translate_import', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
129     $this->assertText(t('File to import not found.'), 'File to import not found message.');
130
131     // Try importing a .po file with overriding strings, and ensure existing
132     // strings are kept.
133     $this->importPoFile($this->getOverwritePoFile(), [
134       'langcode' => 'fr',
135     ]);
136
137     // The import should have created 1 string.
138     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
139     // Ensure string wasn't overwritten.
140     $search = [
141       'string' => 'Montag',
142       'langcode' => 'fr',
143       'translation' => 'translated',
144     ];
145     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
146     $this->assertText(t('No strings available.'), 'String not overwritten by imported string.');
147
148     // This import should not have changed number of plural forms.
149     $locale_plurals = \Drupal::service('locale.plural.formula')->getNumberOfPlurals('fr');
150     $this->assertEqual(2, $locale_plurals, 'Plural numbers untouched.');
151
152     // Try importing a .po file with overriding strings, and ensure existing
153     // strings are overwritten.
154     $this->importPoFile($this->getOverwritePoFile(), [
155       'langcode' => 'fr',
156       'overwrite_options[not_customized]' => TRUE,
157     ]);
158
159     // The import should have updated 2 strings.
160     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 2, '%delete' => 0]), 'The translation file was successfully imported.');
161     // Ensure string was overwritten.
162     $search = [
163       'string' => 'Montag',
164       'langcode' => 'fr',
165       'translation' => 'translated',
166     ];
167     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
168     $this->assertNoText(t('No strings available.'), 'String overwritten by imported string.');
169     // This import should have changed number of plural forms.
170     $locale_plurals = \Drupal::service('locale.plural.formula')->reset()->getNumberOfPlurals('fr');
171     $this->assertEqual(3, $locale_plurals, 'Plural numbers changed.');
172
173     // Importing a .po file and mark its strings as customized strings.
174     $this->importPoFile($this->getCustomPoFile(), [
175       'langcode' => 'fr',
176       'customized' => TRUE,
177     ]);
178
179     // The import should have created 6 strings.
180     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 6, '%update' => 0, '%delete' => 0]), 'The customized translation file was successfully imported.');
181
182     // The database should now contain 6 customized strings (two imported
183     // strings are not translated).
184     $count = db_query('SELECT COUNT(*) FROM {locales_target} WHERE customized = :custom', [':custom' => 1])->fetchField();
185     $this->assertEqual($count, 6, 'Customized translations successfully imported.');
186
187     // Try importing a .po file with overriding strings, and ensure existing
188     // customized strings are kept.
189     $this->importPoFile($this->getCustomOverwritePoFile(), [
190       'langcode' => 'fr',
191       'overwrite_options[not_customized]' => TRUE,
192       'overwrite_options[customized]' => FALSE,
193     ]);
194
195     // The import should have created 1 string.
196     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The customized translation file was successfully imported.');
197     // Ensure string wasn't overwritten.
198     $search = [
199       'string' => 'januari',
200       'langcode' => 'fr',
201       'translation' => 'translated',
202     ];
203     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
204     $this->assertText(t('No strings available.'), 'Customized string not overwritten by imported string.');
205
206     // Try importing a .po file with overriding strings, and ensure existing
207     // customized strings are overwritten.
208     $this->importPoFile($this->getCustomOverwritePoFile(), [
209       'langcode' => 'fr',
210       'overwrite_options[not_customized]' => FALSE,
211       'overwrite_options[customized]' => TRUE,
212     ]);
213
214     // The import should have updated 2 strings.
215     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 2, '%delete' => 0]), 'The customized translation file was successfully imported.');
216     // Ensure string was overwritten.
217     $search = [
218       'string' => 'januari',
219       'langcode' => 'fr',
220       'translation' => 'translated',
221     ];
222     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
223     $this->assertNoText(t('No strings available.'), 'Customized string overwritten by imported string.');
224
225   }
226
227   /**
228    * Test msgctxt context support.
229    */
230   public function testLanguageContext() {
231     // Try importing a .po file.
232     $this->importPoFile($this->getPoFileWithContext(), [
233       'langcode' => 'hr',
234     ]);
235
236     // We cast the return value of t() to string so as to retrieve the
237     // translated value, rendered as a string.
238     $this->assertIdentical((string) t('May', [], ['langcode' => 'hr', 'context' => 'Long month name']), 'Svibanj', 'Long month name context is working.');
239     $this->assertIdentical((string) t('May', [], ['langcode' => 'hr']), 'Svi.', 'Default context is working.');
240   }
241
242   /**
243    * Test empty msgstr at end of .po file see #611786.
244    */
245   public function testEmptyMsgstr() {
246     $langcode = 'hu';
247
248     // Try importing a .po file.
249     $this->importPoFile($this->getPoFileWithMsgstr(), [
250       'langcode' => $langcode,
251     ]);
252
253     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 1, '%update' => 0, '%delete' => 0]), 'The translation file was successfully imported.');
254     $this->assertIdentical((string) t('Operations', [], ['langcode' => $langcode]), 'Műveletek', 'String imported and translated.');
255
256     // Try importing a .po file.
257     $this->importPoFile($this->getPoFileWithEmptyMsgstr(), [
258       'langcode' => $langcode,
259       'overwrite_options[not_customized]' => TRUE,
260     ]);
261     $this->assertRaw(t('One translation file imported. %number translations were added, %update translations were updated and %delete translations were removed.', ['%number' => 0, '%update' => 0, '%delete' => 1]), 'The translation file was successfully imported.');
262
263     $str = "Operations";
264     $search = [
265       'string' => $str,
266       'langcode' => $langcode,
267       'translation' => 'untranslated',
268     ];
269     $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
270     $this->assertText($str, 'Search found the string as untranslated.');
271   }
272
273   /**
274    * Tests .po file import with configuration translation.
275    */
276   public function testConfigPoFile() {
277     // Values for translations to assert. Config key, original string,
278     // translation and config property name.
279     $config_strings = [
280       'system.maintenance' => [
281         '@site is currently under maintenance. We should be back shortly. Thank you for your patience.',
282         '@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet.',
283         'message',
284       ],
285       'user.role.anonymous' => [
286         'Anonymous user',
287         'Névtelen felhasználó',
288         'label',
289       ],
290     ];
291
292     // Add custom language for testing.
293     $langcode = 'xx';
294     $edit = [
295       'predefined_langcode' => 'custom',
296       'langcode' => $langcode,
297       'label' => $this->randomMachineName(16),
298       'direction' => LanguageInterface::DIRECTION_LTR,
299     ];
300     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
301
302     // Check for the source strings we are going to translate. Adding the
303     // custom language should have made the process to export configuration
304     // strings to interface translation executed.
305     $locale_storage = $this->container->get('locale.storage');
306     foreach ($config_strings as $config_string) {
307       $string = $locale_storage->findString(['source' => $config_string[0], 'context' => '', 'type' => 'configuration']);
308       $this->assertTrue($string, 'Configuration strings have been created upon installation.');
309     }
310
311     // Import a .po file to translate.
312     $this->importPoFile($this->getPoFileWithConfig(), [
313       'langcode' => $langcode,
314     ]);
315
316     // Translations got recorded in the interface translation system.
317     foreach ($config_strings as $config_string) {
318       $search = [
319         'string' => $config_string[0],
320         'langcode' => $langcode,
321         'translation' => 'all',
322       ];
323       $this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
324       $this->assertText($config_string[1], format_string('Translation of @string found.', ['@string' => $config_string[0]]));
325     }
326
327     // Test that translations got recorded in the config system.
328     $overrides = \Drupal::service('language.config_factory_override');
329     foreach ($config_strings as $config_key => $config_string) {
330       $override = $overrides->getOverride($langcode, $config_key);
331       $this->assertEqual($override->get($config_string[2]), $config_string[1]);
332     }
333   }
334
335   /**
336    * Tests .po file import with user.settings configuration.
337    */
338   public function testConfigtranslationImportingPoFile() {
339     // Set the language code.
340     $langcode = 'de';
341
342     // Import a .po file to translate.
343     $this->importPoFile($this->getPoFileWithConfigDe(), [
344       'langcode' => $langcode,
345     ]);
346
347     // Check that the 'Anonymous' string is translated.
348     $config = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'user.settings');
349     $this->assertEqual($config->get('anonymous'), 'Anonymous German');
350   }
351
352   /**
353    * Test the translation are imported when a new language is created.
354    */
355   public function testCreatedLanguageTranslation() {
356     // Import a .po file to add de language.
357     $this->importPoFile($this->getPoFileWithConfigDe(), ['langcode' => 'de']);
358
359     // Get the language.entity.de label and check it's been translated.
360     $override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'language.entity.de');
361     $this->assertEqual($override->get('label'), 'Deutsch');
362   }
363
364   /**
365    * Helper function: import a standalone .po file in a given language.
366    *
367    * @param string $contents
368    *   Contents of the .po file to import.
369    * @param array $options
370    *   (optional) Additional options to pass to the translation import form.
371    */
372   public function importPoFile($contents, array $options = []) {
373     $name = \Drupal::service('file_system')->tempnam('temporary://', "po_") . '.po';
374     file_put_contents($name, $contents);
375     $options['files[file]'] = $name;
376     $this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
377     drupal_unlink($name);
378   }
379
380   /**
381    * Helper function that returns a proper .po file.
382    */
383   public function getPoFile() {
384     return <<< EOF
385 msgid ""
386 msgstr ""
387 "Project-Id-Version: Drupal 8\\n"
388 "MIME-Version: 1.0\\n"
389 "Content-Type: text/plain; charset=UTF-8\\n"
390 "Content-Transfer-Encoding: 8bit\\n"
391 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
392
393 msgid "One sheep"
394 msgid_plural "@count sheep"
395 msgstr[0] "un mouton"
396 msgstr[1] "@count moutons"
397
398 msgid "Monday"
399 msgstr "lundi"
400
401 msgid "Tuesday"
402 msgstr "mardi"
403
404 msgid "Wednesday"
405 msgstr "mercredi"
406
407 msgid "Thursday"
408 msgstr "jeudi"
409
410 msgid "Friday"
411 msgstr "vendredi"
412
413 msgid "Saturday"
414 msgstr "samedi"
415
416 msgid "Sunday"
417 msgstr "dimanche"
418 EOF;
419   }
420
421   /**
422    * Helper function that returns a empty .po file.
423    */
424   public function getEmptyPoFile() {
425     return '';
426   }
427
428   /**
429    * Helper function that returns a bad .po file.
430    */
431   public function getBadPoFile() {
432     return <<< EOF
433 msgid ""
434 msgstr ""
435 "Project-Id-Version: Drupal 8\\n"
436 "MIME-Version: 1.0\\n"
437 "Content-Type: text/plain; charset=UTF-8\\n"
438 "Content-Transfer-Encoding: 8bit\\n"
439 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
440
441 msgid "Save configuration"
442 msgstr "Enregistrer la configuration"
443
444 msgid "edit"
445 msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
446
447 msgid "delete"
448 msgstr "supprimer<script>alert('xss');</script>"
449
450 EOF;
451   }
452
453   /**
454    * Helper function that returns a proper .po file for testing.
455    */
456   public function getOverwritePoFile() {
457     return <<< EOF
458 msgid ""
459 msgstr ""
460 "Project-Id-Version: Drupal 8\\n"
461 "MIME-Version: 1.0\\n"
462 "Content-Type: text/plain; charset=UTF-8\\n"
463 "Content-Transfer-Encoding: 8bit\\n"
464 "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
465
466 msgid "Monday"
467 msgstr "Montag"
468
469 msgid "Day"
470 msgstr "Jour"
471 EOF;
472   }
473
474   /**
475    * Helper function that returns a .po file which strings will be marked
476    * as customized.
477    */
478   public function getCustomPoFile() {
479     return <<< EOF
480 msgid ""
481 msgstr ""
482 "Project-Id-Version: Drupal 8\\n"
483 "MIME-Version: 1.0\\n"
484 "Content-Type: text/plain; charset=UTF-8\\n"
485 "Content-Transfer-Encoding: 8bit\\n"
486 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
487
488 msgid "One dog"
489 msgid_plural "@count dogs"
490 msgstr[0] "un chien"
491 msgstr[1] "@count chiens"
492
493 msgid "January"
494 msgstr "janvier"
495
496 msgid "February"
497 msgstr "février"
498
499 msgid "March"
500 msgstr "mars"
501
502 msgid "April"
503 msgstr "avril"
504
505 msgid "June"
506 msgstr "juin"
507 EOF;
508   }
509
510   /**
511    * Helper function that returns a .po file for testing customized strings.
512    */
513   public function getCustomOverwritePoFile() {
514     return <<< EOF
515 msgid ""
516 msgstr ""
517 "Project-Id-Version: Drupal 8\\n"
518 "MIME-Version: 1.0\\n"
519 "Content-Type: text/plain; charset=UTF-8\\n"
520 "Content-Transfer-Encoding: 8bit\\n"
521 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
522
523 msgid "January"
524 msgstr "januari"
525
526 msgid "February"
527 msgstr "februari"
528
529 msgid "July"
530 msgstr "juillet"
531 EOF;
532   }
533
534   /**
535    * Helper function that returns a .po file with context.
536    */
537   public function getPoFileWithContext() {
538     // Croatian (code hr) is one of the languages that have a different
539     // form for the full name and the abbreviated name for the month of May.
540     return <<< EOF
541 msgid ""
542 msgstr ""
543 "Project-Id-Version: Drupal 8\\n"
544 "MIME-Version: 1.0\\n"
545 "Content-Type: text/plain; charset=UTF-8\\n"
546 "Content-Transfer-Encoding: 8bit\\n"
547 "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\\n"
548
549 msgctxt "Long month name"
550 msgid "May"
551 msgstr "Svibanj"
552
553 msgid "May"
554 msgstr "Svi."
555 EOF;
556   }
557
558   /**
559    * Helper function that returns a .po file with an empty last item.
560    */
561   public function getPoFileWithEmptyMsgstr() {
562     return <<< EOF
563 msgid ""
564 msgstr ""
565 "Project-Id-Version: Drupal 8\\n"
566 "MIME-Version: 1.0\\n"
567 "Content-Type: text/plain; charset=UTF-8\\n"
568 "Content-Transfer-Encoding: 8bit\\n"
569 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
570
571 msgid "Operations"
572 msgstr ""
573
574 EOF;
575   }
576
577   /**
578    * Helper function that returns a .po file with an empty last item.
579    */
580   public function getPoFileWithMsgstr() {
581     return <<< EOF
582 msgid ""
583 msgstr ""
584 "Project-Id-Version: Drupal 8\\n"
585 "MIME-Version: 1.0\\n"
586 "Content-Type: text/plain; charset=UTF-8\\n"
587 "Content-Transfer-Encoding: 8bit\\n"
588 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
589
590 msgid "Operations"
591 msgstr "Műveletek"
592
593 msgid "Will not appear in Drupal core, so we can ensure the test passes"
594 msgstr ""
595
596 EOF;
597   }
598
599   /**
600    * Helper function that returns a .po file with configuration translations.
601    */
602   public function getPoFileWithConfig() {
603     return <<< EOF
604 msgid ""
605 msgstr ""
606 "Project-Id-Version: Drupal 8\\n"
607 "MIME-Version: 1.0\\n"
608 "Content-Type: text/plain; charset=UTF-8\\n"
609 "Content-Transfer-Encoding: 8bit\\n"
610 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
611
612 msgid "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
613 msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
614
615 msgid "Anonymous user"
616 msgstr "Névtelen felhasználó"
617
618 EOF;
619   }
620
621   /**
622    * Helper function that returns a .po file with configuration translations.
623    */
624   public function getPoFileWithConfigDe() {
625     return <<< EOF
626 msgid ""
627 msgstr ""
628 "Project-Id-Version: Drupal 8\\n"
629 "MIME-Version: 1.0\\n"
630 "Content-Type: text/plain; charset=UTF-8\\n"
631 "Content-Transfer-Encoding: 8bit\\n"
632 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
633
634 msgid "Anonymous"
635 msgstr "Anonymous German"
636
637 msgid "German"
638 msgstr "Deutsch"
639
640 EOF;
641   }
642
643 }