Upgraded drupal core with security updates
[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     // Check that the 'Anonymous' string is translated.
347     $config = \Drupal::languageManager()->getLanguageConfigOverride($langcode, 'user.settings');
348     $this->assertEqual($config->get('anonymous'), 'Anonymous German');
349   }
350
351   /**
352    * Test the translation are imported when a new language is created.
353    */
354   public function testCreatedLanguageTranslation() {
355     // Import a .po file to add de language.
356     $this->importPoFile($this->getPoFileWithConfigDe(), ['langcode' => 'de']);
357
358     // Get the language.entity.de label and check it's been translated.
359     $override = \Drupal::languageManager()->getLanguageConfigOverride('de', 'language.entity.de');
360     $this->assertEqual($override->get('label'), 'Deutsch');
361   }
362
363   /**
364    * Helper function: import a standalone .po file in a given language.
365    *
366    * @param string $contents
367    *   Contents of the .po file to import.
368    * @param array $options
369    *   (optional) Additional options to pass to the translation import form.
370    */
371   public function importPoFile($contents, array $options = []) {
372     $name = \Drupal::service('file_system')->tempnam('temporary://', "po_") . '.po';
373     file_put_contents($name, $contents);
374     $options['files[file]'] = $name;
375     $this->drupalPostForm('admin/config/regional/translate/import', $options, t('Import'));
376     drupal_unlink($name);
377   }
378
379   /**
380    * Helper function that returns a proper .po file.
381    */
382   public function getPoFile() {
383     return <<< EOF
384 msgid ""
385 msgstr ""
386 "Project-Id-Version: Drupal 8\\n"
387 "MIME-Version: 1.0\\n"
388 "Content-Type: text/plain; charset=UTF-8\\n"
389 "Content-Transfer-Encoding: 8bit\\n"
390 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
391
392 msgid "One sheep"
393 msgid_plural "@count sheep"
394 msgstr[0] "un mouton"
395 msgstr[1] "@count moutons"
396
397 msgid "Monday"
398 msgstr "lundi"
399
400 msgid "Tuesday"
401 msgstr "mardi"
402
403 msgid "Wednesday"
404 msgstr "mercredi"
405
406 msgid "Thursday"
407 msgstr "jeudi"
408
409 msgid "Friday"
410 msgstr "vendredi"
411
412 msgid "Saturday"
413 msgstr "samedi"
414
415 msgid "Sunday"
416 msgstr "dimanche"
417 EOF;
418   }
419
420   /**
421    * Helper function that returns a empty .po file.
422    */
423   public function getEmptyPoFile() {
424     return '';
425   }
426
427   /**
428    * Helper function that returns a bad .po file.
429    */
430   public function getBadPoFile() {
431     return <<< EOF
432 msgid ""
433 msgstr ""
434 "Project-Id-Version: Drupal 8\\n"
435 "MIME-Version: 1.0\\n"
436 "Content-Type: text/plain; charset=UTF-8\\n"
437 "Content-Transfer-Encoding: 8bit\\n"
438 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
439
440 msgid "Save configuration"
441 msgstr "Enregistrer la configuration"
442
443 msgid "edit"
444 msgstr "modifier<img SRC="javascript:alert(\'xss\');">"
445
446 msgid "delete"
447 msgstr "supprimer<script>alert('xss');</script>"
448
449 EOF;
450   }
451
452   /**
453    * Helper function that returns a proper .po file for testing.
454    */
455   public function getOverwritePoFile() {
456     return <<< EOF
457 msgid ""
458 msgstr ""
459 "Project-Id-Version: Drupal 8\\n"
460 "MIME-Version: 1.0\\n"
461 "Content-Type: text/plain; charset=UTF-8\\n"
462 "Content-Transfer-Encoding: 8bit\\n"
463 "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"
464
465 msgid "Monday"
466 msgstr "Montag"
467
468 msgid "Day"
469 msgstr "Jour"
470 EOF;
471   }
472
473   /**
474    * Helper function that returns a .po file which strings will be marked
475    * as customized.
476    */
477   public function getCustomPoFile() {
478     return <<< EOF
479 msgid ""
480 msgstr ""
481 "Project-Id-Version: Drupal 8\\n"
482 "MIME-Version: 1.0\\n"
483 "Content-Type: text/plain; charset=UTF-8\\n"
484 "Content-Transfer-Encoding: 8bit\\n"
485 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
486
487 msgid "One dog"
488 msgid_plural "@count dogs"
489 msgstr[0] "un chien"
490 msgstr[1] "@count chiens"
491
492 msgid "January"
493 msgstr "janvier"
494
495 msgid "February"
496 msgstr "février"
497
498 msgid "March"
499 msgstr "mars"
500
501 msgid "April"
502 msgstr "avril"
503
504 msgid "June"
505 msgstr "juin"
506 EOF;
507   }
508
509   /**
510    * Helper function that returns a .po file for testing customized strings.
511    */
512   public function getCustomOverwritePoFile() {
513     return <<< EOF
514 msgid ""
515 msgstr ""
516 "Project-Id-Version: Drupal 8\\n"
517 "MIME-Version: 1.0\\n"
518 "Content-Type: text/plain; charset=UTF-8\\n"
519 "Content-Transfer-Encoding: 8bit\\n"
520 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
521
522 msgid "January"
523 msgstr "januari"
524
525 msgid "February"
526 msgstr "februari"
527
528 msgid "July"
529 msgstr "juillet"
530 EOF;
531   }
532
533   /**
534    * Helper function that returns a .po file with context.
535    */
536   public function getPoFileWithContext() {
537     // Croatian (code hr) is one of the languages that have a different
538     // form for the full name and the abbreviated name for the month of May.
539     return <<< EOF
540 msgid ""
541 msgstr ""
542 "Project-Id-Version: Drupal 8\\n"
543 "MIME-Version: 1.0\\n"
544 "Content-Type: text/plain; charset=UTF-8\\n"
545 "Content-Transfer-Encoding: 8bit\\n"
546 "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"
547
548 msgctxt "Long month name"
549 msgid "May"
550 msgstr "Svibanj"
551
552 msgid "May"
553 msgstr "Svi."
554 EOF;
555   }
556
557   /**
558    * Helper function that returns a .po file with an empty last item.
559    */
560   public function getPoFileWithEmptyMsgstr() {
561     return <<< EOF
562 msgid ""
563 msgstr ""
564 "Project-Id-Version: Drupal 8\\n"
565 "MIME-Version: 1.0\\n"
566 "Content-Type: text/plain; charset=UTF-8\\n"
567 "Content-Transfer-Encoding: 8bit\\n"
568 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
569
570 msgid "Operations"
571 msgstr ""
572
573 EOF;
574   }
575
576   /**
577    * Helper function that returns a .po file with an empty last item.
578    */
579   public function getPoFileWithMsgstr() {
580     return <<< EOF
581 msgid ""
582 msgstr ""
583 "Project-Id-Version: Drupal 8\\n"
584 "MIME-Version: 1.0\\n"
585 "Content-Type: text/plain; charset=UTF-8\\n"
586 "Content-Transfer-Encoding: 8bit\\n"
587 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
588
589 msgid "Operations"
590 msgstr "Műveletek"
591
592 msgid "Will not appear in Drupal core, so we can ensure the test passes"
593 msgstr ""
594
595 EOF;
596   }
597
598   /**
599    * Helper function that returns a .po file with configuration translations.
600    */
601   public function getPoFileWithConfig() {
602     return <<< EOF
603 msgid ""
604 msgstr ""
605 "Project-Id-Version: Drupal 8\\n"
606 "MIME-Version: 1.0\\n"
607 "Content-Type: text/plain; charset=UTF-8\\n"
608 "Content-Transfer-Encoding: 8bit\\n"
609 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
610
611 msgid "@site is currently under maintenance. We should be back shortly. Thank you for your patience."
612 msgstr "@site karbantartás alatt áll. Rövidesen visszatérünk. Köszönjük a türelmet."
613
614 msgid "Anonymous user"
615 msgstr "Névtelen felhasználó"
616
617 EOF;
618   }
619
620   /**
621    * Helper function that returns a .po file with configuration translations.
622    */
623   public function getPoFileWithConfigDe() {
624     return <<< EOF
625 msgid ""
626 msgstr ""
627 "Project-Id-Version: Drupal 8\\n"
628 "MIME-Version: 1.0\\n"
629 "Content-Type: text/plain; charset=UTF-8\\n"
630 "Content-Transfer-Encoding: 8bit\\n"
631 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
632
633 msgid "Anonymous"
634 msgstr "Anonymous German"
635
636 msgid "German"
637 msgstr "Deutsch"
638
639 EOF;
640   }
641
642 }