locale.batch.inc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <?php
  2. /**
  3. * @file
  4. * Batch process to check the availability of remote or local po files.
  5. */
  6. use GuzzleHttp\Exception\RequestException;
  7. use Psr\Http\Message\RequestInterface;
  8. use Psr\Http\Message\ResponseInterface;
  9. use Psr\Http\Message\UriInterface;
  10. /**
  11. * Load the common translation API.
  12. */
  13. // @todo Combine functions differently in files to avoid unnecessary includes.
  14. // Follow-up issue: https://www.drupal.org/node/1834298.
  15. require_once __DIR__ . '/locale.translation.inc';
  16. /**
  17. * Implements callback_batch_operation().
  18. *
  19. * Checks the presence and creation time po translation files in located at
  20. * remote server location and local file system.
  21. *
  22. * @param string $project
  23. * Machine name of the project for which to check the translation status.
  24. * @param string $langcode
  25. * Language code of the language for which to check the translation.
  26. * @param array $options
  27. * An array with options that can have the following elements:
  28. * - 'finish_feedback': Whether or not to give feedback to the user when the
  29. * batch is finished. Optional, defaults to TRUE.
  30. * - 'use_remote': Whether or not to check the remote translation file.
  31. * Optional, defaults to TRUE.
  32. * @param array|\ArrayAccess $context
  33. * The batch context.
  34. */
  35. function locale_translation_batch_status_check($project, $langcode, array $options, &$context) {
  36. $failure = $checked = FALSE;
  37. $options += [
  38. 'finish_feedback' => TRUE,
  39. 'use_remote' => TRUE,
  40. ];
  41. $source = locale_translation_get_status([$project], [$langcode]);
  42. $source = $source[$project][$langcode];
  43. // Check the status of local translation files.
  44. if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
  45. if ($file = locale_translation_source_check_file($source)) {
  46. locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
  47. }
  48. $checked = TRUE;
  49. }
  50. // Check the status of remote translation files.
  51. if ($options['use_remote'] && isset($source->files[LOCALE_TRANSLATION_REMOTE])) {
  52. $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE];
  53. if ($result = locale_translation_http_check($remote_file->uri)) {
  54. // Update the file object with the result data. In case of a redirect we
  55. // store the resulting uri.
  56. if (isset($result['last_modified'])) {
  57. $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
  58. $remote_file->timestamp = $result['last_modified'];
  59. locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file);
  60. }
  61. // @todo What to do with when the file is not found (404)? To prevent
  62. // re-checking within the TTL (1day, 1week) we can set a last_checked
  63. // timestamp or cache the result.
  64. $checked = TRUE;
  65. }
  66. else {
  67. $failure = TRUE;
  68. }
  69. }
  70. // Provide user feedback and record success or failure for reporting at the
  71. // end of the batch.
  72. if ($options['finish_feedback'] && $checked) {
  73. $context['results']['files'][] = $source->name;
  74. }
  75. if ($failure && !$checked) {
  76. $context['results']['failed_files'][] = $source->name;
  77. }
  78. $context['message'] = t('Checked translation for %project.', ['%project' => $source->project]);
  79. }
  80. /**
  81. * Implements callback_batch_finished().
  82. *
  83. * Set result message.
  84. *
  85. * @param bool $success
  86. * TRUE if batch successfully completed.
  87. * @param array $results
  88. * Batch results.
  89. */
  90. function locale_translation_batch_status_finished($success, $results) {
  91. if ($success) {
  92. if (isset($results['failed_files'])) {
  93. if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
  94. $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation file could not be checked. <a href=":url">See the log</a> for details.', '@count translation files could not be checked. <a href=":url">See the log</a> for details.', [':url' => \Drupal::url('dblog.overview')]);
  95. }
  96. else {
  97. $message = \Drupal::translation()->formatPlural(count($results['failed_files']), 'One translation files could not be checked. See the log for details.', '@count translation files could not be checked. See the log for details.');
  98. }
  99. \Drupal::messenger()->addError($message);
  100. }
  101. if (isset($results['files'])) {
  102. \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural(
  103. count($results['files']),
  104. 'Checked available interface translation updates for one project.',
  105. 'Checked available interface translation updates for @count projects.'
  106. ));
  107. }
  108. if (!isset($results['failed_files']) && !isset($results['files'])) {
  109. \Drupal::messenger()->addStatus(t('Nothing to check.'));
  110. }
  111. \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
  112. }
  113. else {
  114. \Drupal::messenger()->addError(t('An error occurred trying to check available interface translation updates.'));
  115. }
  116. }
  117. /**
  118. * Implements callback_batch_operation().
  119. *
  120. * Downloads a remote gettext file into the translations directory. When
  121. * successfully the translation status is updated.
  122. *
  123. * @param object $project
  124. * Source object of the translatable project.
  125. * @param string $langcode
  126. * Language code.
  127. * @param array $context
  128. * The batch context.
  129. *
  130. * @see locale_translation_batch_fetch_import()
  131. */
  132. function locale_translation_batch_fetch_download($project, $langcode, &$context) {
  133. $sources = locale_translation_get_status([$project], [$langcode]);
  134. if (isset($sources[$project][$langcode])) {
  135. $source = $sources[$project][$langcode];
  136. if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) {
  137. if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) {
  138. $context['message'] = t('Downloaded translation for %project.', ['%project' => $source->project]);
  139. locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
  140. }
  141. else {
  142. $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
  143. }
  144. }
  145. }
  146. }
  147. /**
  148. * Implements callback_batch_operation().
  149. *
  150. * Imports a gettext file from the translation directory. When successfully the
  151. * translation status is updated.
  152. *
  153. * @param object $project
  154. * Source object of the translatable project.
  155. * @param string $langcode
  156. * Language code.
  157. * @param array $options
  158. * Array of import options.
  159. * @param array $context
  160. * The batch context.
  161. *
  162. * @see locale_translate_batch_import_files()
  163. * @see locale_translation_batch_fetch_download()
  164. */
  165. function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
  166. $sources = locale_translation_get_status([$project], [$langcode]);
  167. if (isset($sources[$project][$langcode])) {
  168. $source = $sources[$project][$langcode];
  169. if (isset($source->type)) {
  170. if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {
  171. $file = $source->files[LOCALE_TRANSLATION_LOCAL];
  172. module_load_include('bulk.inc', 'locale');
  173. $options += [
  174. 'message' => t('Importing translation for %project.', ['%project' => $source->project]),
  175. ];
  176. // Import the translation file. For large files the batch operations is
  177. // progressive and will be called repeatedly until finished.
  178. locale_translate_batch_import($file, $options, $context);
  179. // The import is finished.
  180. if (isset($context['finished']) && $context['finished'] == 1) {
  181. // The import is successful.
  182. if (isset($context['results']['files'][$file->uri])) {
  183. $context['message'] = t('Imported translation for %project.', ['%project' => $source->project]);
  184. // Save the data of imported source into the {locale_file} table and
  185. // update the current translation status.
  186. locale_translation_status_save($project, $langcode, LOCALE_TRANSLATION_CURRENT, $source->files[LOCALE_TRANSLATION_LOCAL]);
  187. }
  188. }
  189. }
  190. }
  191. }
  192. }
  193. /**
  194. * Implements callback_batch_finished().
  195. *
  196. * Set result message.
  197. *
  198. * @param bool $success
  199. * TRUE if batch successfully completed.
  200. * @param array $results
  201. * Batch results.
  202. */
  203. function locale_translation_batch_fetch_finished($success, $results) {
  204. module_load_include('bulk.inc', 'locale');
  205. if ($success) {
  206. \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
  207. }
  208. return locale_translate_batch_finished($success, $results);
  209. }
  210. /**
  211. * Check if remote file exists and when it was last updated.
  212. *
  213. * @param string $uri
  214. * URI of remote file.
  215. *
  216. * @return array|bool
  217. * Associative array of file data with the following elements:
  218. * - last_modified: Last modified timestamp of the translation file.
  219. * - (optional) location: The location of the translation file. Is only set
  220. * when a redirect (301) has occurred.
  221. * TRUE if the file is not found. FALSE if a fault occurred.
  222. */
  223. function locale_translation_http_check($uri) {
  224. $logger = \Drupal::logger('locale');
  225. try {
  226. $actual_uri = NULL;
  227. $response = \Drupal::service('http_client_factory')->fromOptions([
  228. 'allow_redirects' => [
  229. 'on_redirect' => function (RequestInterface $request, ResponseInterface $response, UriInterface $request_uri) use (&$actual_uri) {
  230. $actual_uri = (string) $request_uri;
  231. },
  232. ],
  233. ])->head($uri);
  234. $result = [];
  235. // Return the effective URL if it differs from the requested.
  236. if ($actual_uri && $actual_uri !== $uri) {
  237. $result['location'] = $actual_uri;
  238. }
  239. $result['last_modified'] = $response->hasHeader('Last-Modified') ? strtotime($response->getHeaderLine('Last-Modified')) : 0;
  240. return $result;
  241. }
  242. catch (RequestException $e) {
  243. // Handle 4xx and 5xx http responses.
  244. if ($response = $e->getResponse()) {
  245. if ($response->getStatusCode() == 404) {
  246. // File not found occurs when a translation file is not yet available
  247. // at the translation server. But also if a custom module or custom
  248. // theme does not define the location of a translation file. By default
  249. // the file is checked at the translation server, but it will not be
  250. // found there.
  251. $logger->notice('Translation file not found: @uri.', ['@uri' => $uri]);
  252. return TRUE;
  253. }
  254. $logger->notice('HTTP request to @url failed with error: @error.', ['@url' => $uri, '@error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()]);
  255. }
  256. }
  257. return FALSE;
  258. }
  259. /**
  260. * Downloads a translation file from a remote server.
  261. *
  262. * @param object $source_file
  263. * Source file object with at least:
  264. * - "uri": uri to download the file from.
  265. * - "project": Project name.
  266. * - "langcode": Translation language.
  267. * - "version": Project version.
  268. * - "filename": File name.
  269. * @param string $directory
  270. * Directory where the downloaded file will be saved. Defaults to the
  271. * temporary file path.
  272. *
  273. * @return object
  274. * File object if download was successful. FALSE on failure.
  275. */
  276. function locale_translation_download_source($source_file, $directory = 'temporary://') {
  277. if ($uri = system_retrieve_file($source_file->uri, $directory, FALSE, FILE_EXISTS_REPLACE)) {
  278. $file = clone($source_file);
  279. $file->type = LOCALE_TRANSLATION_LOCAL;
  280. $file->uri = $uri;
  281. $file->directory = $directory;
  282. $file->timestamp = filemtime($uri);
  283. return $file;
  284. }
  285. \Drupal::logger('locale')->error('Unable to download translation file @uri.', ['@uri' => $source_file->uri]);
  286. return FALSE;
  287. }