locale.batch.inc 12 KB

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