tmgmt_file.test 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <?php
  2. /**
  3. * @file
  4. * Test cases for the file translator module.
  5. */
  6. /**
  7. * Basic tests for the file translator.
  8. */
  9. class TMGMTFileTestCase extends TMGMTBaseTestCase {
  10. static function getInfo() {
  11. return array(
  12. 'name' => 'File Translator tests',
  13. 'description' => 'Tests the file translator plugin integration.',
  14. 'group' => 'Translation Management',
  15. );
  16. }
  17. function setUp() {
  18. parent::setUp(array('tmgmt_file', 'tmgmt_ui'));
  19. $this->loginAsAdmin();
  20. $this->setEnvironment('de');
  21. }
  22. /**
  23. * Test the content processing for XLIFF export and import.
  24. */
  25. function testXLIFFTextProcessing() {
  26. $translator = $this->createTranslator();
  27. $translator->plugin = 'file';
  28. $translator->settings = array(
  29. 'export_format' => 'xlf',
  30. 'xliff_processing' => TRUE,
  31. );
  32. $translator->save();
  33. // Get the source text.
  34. $source_text = trim(file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'));
  35. // Create the reader instance, it will be used through the tests.
  36. $reader = new XMLReader();
  37. $xliff_elements = array('bpt', 'ept', 'ph', 'x', '#text', '#cdata-section', 'content');
  38. // ==== First test the whole cycle ==== //
  39. $job = $this->createJob();
  40. $job->translator = $translator->name;
  41. $job->addItem('test_html_source', 'test', '1');
  42. // Requesting translation will mask the html.
  43. $job->requestTranslation();
  44. $content = $this->getTransUnitsContent($job);
  45. // Test that the exported trans unit contains only xliff elements.
  46. $reader->XML('<content>' . $content[0]['source'] . '</content>');
  47. while ($reader->read()) {
  48. if (!in_array($reader->name, $xliff_elements)) {
  49. $this->fail(t('The source contains unexpected element %element', array('%element' => $reader->name)));
  50. }
  51. }
  52. $reader->XML('<content>' . $content[0]['target'] . '</content>');
  53. while ($reader->read()) {
  54. if (!in_array($reader->name, $xliff_elements)) {
  55. $this->fail(t('The target contains unexpected element %element', array('%element' => $reader->name)));
  56. }
  57. }
  58. // Import the file, make sure all the html has been revealed and no xliff
  59. // elements are present in the job translation.
  60. $messages = $job->getMessages();
  61. $message = reset($messages);
  62. $translated_file = 'public://tmgmt_file/translated.xlf';
  63. $this->createTranslationFile($message->variables['!link'], 'one paragraph', 'one translated paragraph', $translated_file);
  64. $uri = $job->uri();
  65. $edit = array(
  66. 'files[file]' => $translated_file,
  67. );
  68. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  69. // Reset caches and reload job.
  70. entity_get_controller('tmgmt_job')->resetCache();
  71. entity_get_controller('tmgmt_job_item')->resetCache();
  72. $job = tmgmt_job_load($job->tjid);
  73. // Do the comparison of the translation text and the source. It must be the
  74. // same as there was no change done to the translation.
  75. $item_data = $job->getData(array(1, 'dummy', 'deep_nesting'));
  76. $this->assertEqual(trim($item_data[1]['#translation']['#text']), str_replace('one paragraph', 'one translated paragraph', $source_text));
  77. $job_items = $job->getItems();
  78. /** @var TMGMTJobItem $job_item */
  79. $job_item = array_shift($job_items);
  80. // Job item must be in review.
  81. $this->assertTrue($job_item->isNeedsReview());
  82. $this->assertIntegrityCheck($job, FALSE);
  83. // ==== Test integrity check ==== //
  84. $job = $this->createJob();
  85. $job->translator = $translator->name;
  86. $job->addItem('test_html_source', 'test', '1');
  87. $job->requestTranslation();
  88. $messages = $job->getMessages();
  89. $message = reset($messages);
  90. // Get the xml content and remove the element representing <br />. This will
  91. // result in different element counts in the source and target and should
  92. // trigger an error and not import the translation.
  93. $translated_file = 'public://tmgmt_file/translated.xlf';
  94. $this->createTranslationFile($message->variables['!link'], '<x id="tjiid2-4" ctype="lb"/>', '', $translated_file);
  95. $uri = $job->uri();
  96. $edit = array(
  97. 'files[file]' => $translated_file,
  98. );
  99. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  100. entity_get_controller('tmgmt_job')->resetCache();
  101. entity_get_controller('tmgmt_job_item')->resetCache();
  102. $job = tmgmt_job_load($job->tjid);
  103. $this->assertIntegrityCheck($job);
  104. // Set the XLIFF processing to FALSE and test it results in the source
  105. // text not being XLIFF processed.
  106. $translator->settings['xliff_processing'] = FALSE;
  107. $translator->save();
  108. $job = $this->createJob();
  109. $job->translator = $translator->name;
  110. $job->addItem('test_html_source', 'test', '1');
  111. $job->requestTranslation();
  112. $targets = $this->getTransUnitsContent($job);
  113. $this->assertEqual(trim(html_entity_decode($targets['0']['source'])), $source_text);
  114. }
  115. /**
  116. * Test the CDATA option for XLIFF export and import.
  117. */
  118. function testXLIFFCDATA() {
  119. $translator = $this->createTranslator();
  120. $translator->plugin = 'file';
  121. $translator->settings = array(
  122. 'export_format' => 'xlf',
  123. 'xliff_cdata' => TRUE,
  124. );
  125. $translator->save();
  126. // Get the source text.
  127. $source_text = trim(file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'));
  128. // Create a new job.
  129. $job = $this->createJob();
  130. $job->translator = $translator->name;
  131. $job->addItem('test_html_source', 'test', '1');
  132. $job->requestTranslation();
  133. $messages = $job->getMessages();
  134. $message = reset($messages);
  135. $download_url = $message->variables['!link'];
  136. // Get XLIFF content.
  137. $xliff = file_get_contents($download_url);
  138. $dom = new \DOMDocument();
  139. $dom->loadXML($xliff);
  140. $this->assertTrue($dom->schemaValidate(drupal_get_path('module', 'tmgmt_file') . '/xliff-core-1.2-strict.xsd'));
  141. // "Translate" items.
  142. $xml = simplexml_import_dom($dom);
  143. $translated_text = array();
  144. foreach ($xml->file->body->children() as $group) {
  145. foreach ($group->children() as $transunit) {
  146. if ($transunit->getName() == 'trans-unit') {
  147. // The target should be empty.
  148. $this->assertEqual($transunit->target, '');
  149. // Update translations using CDATA.
  150. $node = dom_import_simplexml($transunit->target);
  151. $owner = $node->ownerDocument;
  152. $node->appendChild($owner->createCDATASection($xml->file['target-language'] . '_' . (string) $transunit->source));
  153. // Store the text to allow assertions later on.
  154. $translated_text[(string) $group['id']][(string) $transunit['id']] = (string) $transunit->target;
  155. }
  156. }
  157. }
  158. $translated_file = 'public://tmgmt_file/translated file.xlf';
  159. $xml->asXML($translated_file);
  160. // Import the file and check translation for the "dummy" item.
  161. $uri = $job->uri();
  162. $edit = array(
  163. 'files[file]' => $translated_file,
  164. );
  165. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  166. $this->clickLink(t('review'));
  167. foreach ($translated_text[1] as $key => $value) {
  168. $this->assertText(htmlspecialchars($value));
  169. }
  170. }
  171. /**
  172. * Gets trans-unit content from the XLIFF file that has been exported for the
  173. * given job as last.
  174. */
  175. protected function getTransUnitsContent(TMGMTJob $job) {
  176. $messages = $job->getMessages();
  177. $message = reset($messages);
  178. $download_url = $message->variables['!link'];
  179. $xml_string = file_get_contents($download_url);
  180. $xml = simplexml_load_string($xml_string);
  181. // Register the xliff namespace, required for xpath.
  182. $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
  183. $reader = new XMLReader();
  184. $data = array();
  185. $i = 0;
  186. foreach ($xml->xpath('//xliff:trans-unit') as $unit) {
  187. $reader->XML($unit->source->asXML());
  188. $reader->read();
  189. $data[$i]['source'] = $reader->readInnerXML();
  190. $reader->XML($unit->target->asXML());
  191. $reader->read();
  192. $data[$i]['target'] = $reader->readInnerXML();
  193. $i++;
  194. }
  195. return $data;
  196. }
  197. /**
  198. * Tests export and import for the HTML format.
  199. */
  200. function testHTML() {
  201. $translator = $this->createTranslator();
  202. $translator->plugin = 'file';
  203. $translator->settings = array(
  204. 'export_format' => 'html',
  205. );
  206. $translator->save();
  207. $job = $this->createJob();
  208. $job->translator = $translator->name;
  209. $job->addItem('test_source', 'test', '1');
  210. $job->addItem('test_source', 'test', '2');
  211. $job->requestTranslation();
  212. $messages = $job->getMessages();
  213. $message = reset($messages);
  214. $download_url = $message->variables['!link'];
  215. // "Translate" items.
  216. $xml = simplexml_load_file($download_url);
  217. $translated_text = array();
  218. foreach ($xml->body->children() as $group) {
  219. for ($i = 0; $i < $group->count(); $i++) {
  220. // This does not actually override the whole object, just the content.
  221. $group->div[$i] = (string) $xml->head->meta[3]['content'] . '_' . (string) $group->div[$i];
  222. // Store the text to allow assertions later on.
  223. $translated_text[(string) $group['id']][(string) $group->div[$i]['id']] = (string) $group->div[$i];
  224. }
  225. }
  226. $translated_file = 'public://tmgmt_file/translated.html';
  227. $xml->asXML($translated_file);
  228. $this->importFile($translated_file, $translated_text, $job);
  229. }
  230. /**
  231. * Tests import and export for the XLIFF format.
  232. */
  233. function testXLIFF() {
  234. $translator = $this->createTranslator();
  235. $translator->plugin = 'file';
  236. $translator->settings = array(
  237. 'export_format' => 'xlf',
  238. );
  239. $translator->save();
  240. // Set multiple data items for the source.
  241. variable_set('tmgmt_test_source_data', array(
  242. 'dummy' => array(
  243. 'deep_nesting' => array(
  244. '#text' => file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html') . ' @id.',
  245. '#label' => 'Label of deep nested item @id',
  246. ),
  247. ),
  248. 'another_item' => array(
  249. '#text' => 'Text of another item @id.',
  250. '#label' => 'Label of another item @id.',
  251. ),
  252. ));
  253. $job = $this->createJob();
  254. $job->translator = $translator->name;
  255. $first_item = $job->addItem('test_source', 'test', '1');
  256. // Keep the first item data for later use.
  257. $first_item_data = tmgmt_flatten_data($first_item->getData());
  258. $job->addItem('test_source', 'test', '2');
  259. $job->requestTranslation();
  260. $messages = $job->getMessages();
  261. $message = reset($messages);
  262. $download_url = $message->variables['!link'];
  263. $xliff = file_get_contents($download_url);
  264. $dom = new DOMDocument();
  265. $dom->loadXML($xliff);
  266. $this->assertTrue($dom->schemaValidate(drupal_get_path('module', 'tmgmt_file') . '/xliff-core-1.2-strict.xsd'));
  267. // "Translate" items.
  268. $xml = simplexml_import_dom($dom);
  269. $translated_text = array();
  270. foreach ($xml->file->body->children() as $group) {
  271. foreach ($group->children() as $transunit) {
  272. if ($transunit->getName() == 'trans-unit') {
  273. // The target should be empty.
  274. $this->assertEqual($transunit->target, '');
  275. $transunit->target = $xml->file['target-language'] . '_' . (string) $transunit->source;
  276. // Store the text to allow assertions later on.
  277. $translated_text[(string) $group['id']][(string) $transunit['id']] = (string) $transunit->target;
  278. }
  279. }
  280. }
  281. // Change the job id to a non-existing one and try to import it.
  282. $wrong_xml = clone $xml;
  283. $wrong_xml->file->header->{'phase-group'}->phase['job-id'] = 500;
  284. $wrong_file = 'public://tmgmt_file/wrong_file.xlf';
  285. $wrong_xml->asXML($wrong_file);
  286. $uri = $job->uri();
  287. $edit = array(
  288. 'files[file]' => $wrong_file,
  289. );
  290. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  291. $this->assertText(t('Failed to validate file, import aborted.'));
  292. // Change the job id to a wrong one and try to import it.
  293. $wrong_xml = clone $xml;
  294. $second_job = $this->createJob();
  295. $second_job->translator = $translator->name;
  296. // We need to add the elements count value into settings, otherwise the
  297. // validation will fail on integrity check.
  298. $second_job->settings['xliff_validation'][1] = 0;
  299. $second_job->settings['xliff_validation'][2] = 0;
  300. $second_job->save();
  301. $wrong_xml->file->header->{'phase-group'}->phase['job-id'] = $second_job->tjid;
  302. $wrong_file = 'public://tmgmt_file/wrong_file.xlf';
  303. $wrong_xml->asXML($wrong_file);
  304. $uri = $job->uri();
  305. $edit = array(
  306. 'files[file]' => $wrong_file,
  307. );
  308. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  309. $this->assertRaw(t('The imported file job id @file_tjid does not match the job id @job_tjid.', array(
  310. '@file_tjid' => $second_job->tjid,
  311. '@job_tjid' => $job->tjid,
  312. )));
  313. $translated_file = 'public://tmgmt_file/translated file.xlf';
  314. $xml->asXML($translated_file);
  315. // Import the file and accept translation for the "dummy" item.
  316. $uri = $job->uri();
  317. $edit = array(
  318. 'files[file]' => $translated_file,
  319. );
  320. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  321. $this->clickLink(t('review'));
  322. $this->drupalPostAJAX(NULL, NULL, array('reviewed-dummy|deep_nesting' => '✓'));
  323. // Update the translation for "another" item and import.
  324. $xml->file->body->group[0]->{'trans-unit'}[1]->target = $xml->file->body->group[0]->{'trans-unit'}[1]->target . ' updated';
  325. $xml->asXML($translated_file);
  326. $uri = $job->uri();
  327. $edit = array(
  328. 'files[file]' => $translated_file,
  329. );
  330. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  331. // At this point we must have the "dummy" item accepted and intact. The
  332. // "another" item must have updated translation.
  333. $this->clickLink(t('review'));
  334. $this->assertFieldByName('dummy|deep_nesting[translation]', 'de_' . $first_item_data['dummy][deep_nesting']['#text']);
  335. $this->assertFieldByName('another_item[translation]', 'de_' . $first_item_data['another_item']['#text'] . ' updated');
  336. // Now finish the import/save as completed process doing another extra
  337. // import. The extra import will test that a duplicate import of the same
  338. // file does not break the process.
  339. $this->importFile($translated_file, $translated_text, $job);
  340. $this->assertNoText(t('Import translated file'));
  341. // Create a job, assign to the file translator and delete before attaching
  342. // a file.
  343. $other_job = $this->createJob();
  344. $other_job->translator = $translator->name;
  345. $other_job->save();
  346. $other_job->delete();
  347. // Make sure the file of the other job still exists.
  348. $response = drupal_http_request($download_url);
  349. $this->assertEqual(200, $response->code);
  350. // Delete the job and then make sure that the file has been deleted.
  351. $job->delete();
  352. $response = drupal_http_request($download_url);
  353. $this->assertEqual(404, $response->code);
  354. }
  355. /**
  356. * Tests storing files in the private file system.
  357. */
  358. function testPrivate() {
  359. // Enable the private file system.
  360. variable_set('file_private_path', variable_get('file_public_path') . '/private');
  361. // Create a translator using the private file system.
  362. // @todo: Test the configuration UI.
  363. $translator = $this->createTranslator();
  364. $translator->plugin = 'file';
  365. $translator->settings = array(
  366. 'export_format' => 'xlf',
  367. 'scheme' => 'private',
  368. );
  369. $translator->save();
  370. $job = $this->createJob();
  371. $job->translator = $translator->name;
  372. $job->addItem('test_source', 'test', '1');
  373. $job->addItem('test_source', 'test', '2');
  374. $job->requestTranslation();
  375. $messages = $job->getMessages();
  376. $message = reset($messages);
  377. $download_url = $message->variables['!link'];
  378. $this->drupalGet($download_url);
  379. // Verify that the URL is served using the private file system and the
  380. // access checks work.
  381. $this->assertTrue(preg_match('|system/files|', $download_url));
  382. $this->assertResponse(200);
  383. $this->drupalLogout();
  384. // Verify that access is now protected.
  385. $this->drupalGet($download_url);
  386. $this->assertResponse(403);
  387. }
  388. protected function importFile($translated_file, $translated_text, TMGMTJob $job) {
  389. // To test the upload form functionality, navigate to the edit form.
  390. $uri = $job->uri();
  391. $edit = array(
  392. 'files[file]' => $translated_file,
  393. );
  394. $this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
  395. // Make sure the translations have been imported correctly.
  396. $this->assertNoText(t('In progress'));
  397. // @todo: Enable this assertion once new releases for views and entity
  398. // module are out.
  399. //$this->assertText(t('Needs review'));
  400. // Review both items.
  401. $this->clickLink(t('review'));
  402. foreach ($translated_text[1] as $key => $value) {
  403. $this->assertText(check_plain($value));
  404. }
  405. foreach ($translated_text[2] as $key => $value) {
  406. $this->assertNoText(check_plain($value));
  407. }
  408. $this->drupalPost(NULL, array(), t('Save as completed'));
  409. // Review both items.
  410. $this->clickLink(t('review'));
  411. foreach ($translated_text[1] as $key => $value) {
  412. $this->assertNoText(check_plain($value));
  413. }
  414. foreach ($translated_text[2] as $key => $value) {
  415. $this->assertText(check_plain($value));
  416. }
  417. $this->drupalPost(NULL, array(), t('Save as completed'));
  418. // @todo: Enable this assertion once new releases for views and entity
  419. // module are out.
  420. //$this->assertText(t('Accepted'));
  421. $this->assertText(t('Finished'));
  422. $this->assertNoText(t('Needs review'));
  423. }
  424. /**
  425. * Creates a translated XLIFF file based on the replacement definition.
  426. *
  427. * @param string $source_file
  428. * Source file name.
  429. * @param $search
  430. * String to search in the source.
  431. * @param $replace
  432. * String to replace it with in the target.
  433. * @param $translated_file
  434. * Name of the file to write.
  435. */
  436. protected function createTranslationFile($source_file, $search, $replace, $translated_file) {
  437. $xml_string = file_get_contents($source_file);
  438. preg_match('/<source xml:lang="en">(.+)<\/source>/s', $xml_string, $matches);
  439. $target = str_replace($search, $replace, $matches[1]);
  440. if ($replace) {
  441. $this->assertTrue(strpos($target, $replace) !== FALSE, 'String replaced in translation');
  442. }
  443. $translated_xml_string = str_replace('<target xml:lang="de"/>', '<target xml:lang="de">' . $target . '</target>', $xml_string);
  444. file_put_contents($translated_file, $translated_xml_string);
  445. }
  446. /**
  447. * Asserts import integrity for a job.
  448. *
  449. * @param TMGMTJob $job
  450. * The job to check.
  451. * @param bool $expected
  452. * (optional) If an integrity failed message is expected or not, defaults
  453. * to FALSE.
  454. */
  455. protected function assertIntegrityCheck(TMGMTJob $job, $expected = TRUE) {
  456. $integrity_check_failed = FALSE;
  457. /** @var TMGMTMessage $message */
  458. foreach ($job->getMessages() as $message) {
  459. if ($message->getMessage() == t('Failed to validate semantic integrity of %key element. Please check also the HTML code of the element in the review process.', array('%key' => 'dummy][deep_nesting'))) {
  460. $integrity_check_failed = TRUE;
  461. break;
  462. }
  463. }
  464. // Check if the message was found or not, based on the expected argument.
  465. if ($expected) {
  466. $this->assertTrue($integrity_check_failed, 'The validation of semantic integrity must fail.');
  467. }
  468. else {
  469. $this->assertFalse($integrity_check_failed, 'The validation of semantic integrity must not fail.');
  470. }
  471. }
  472. }