summaryrefslogtreecommitdiffstats
path: root/core/modules/locale/locale.batch.inc
blob: b509c2542b55f77616c1b575d24cde03558986ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<?php

/**
 * @file
 * Batch process to check the availability of remote or local po files.
 */

use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;

/**
 * Load the common translation API.
 */
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue: https://www.drupal.org/node/1834298.
require_once __DIR__ . '/locale.translation.inc';

/**
 * Implements callback_batch_operation().
 *
 * Checks the presence and creation time po translation files in located at
 * remote server location and local file system.
 *
 * @param string $project
 *   Machine name of the project for which to check the translation status.
 * @param string $langcode
 *   Language code of the language for which to check the translation.
 * @param array $options
 *   An array with options that can have the following elements:
 *   - 'finish_feedback': Whether or not to give feedback to the user when the
 *     batch is finished. Optional, defaults to TRUE.
 *   - 'use_remote': Whether or not to check the remote translation file.
 *     Optional, defaults to TRUE.
 * @param array|\ArrayAccess $context
 *   The batch context.
 */
function locale_translation_batch_status_check($project, $langcode, array $options, &$context) {
  $failure = $checked = FALSE;
  $options += [
    'finish_feedback' => TRUE,
    'use_remote' => TRUE,
  ];
  $source = locale_translation_get_status([$project], [$langcode]);
  $source = $source[$project][$langcode];

  // Check the status of local translation files.
  if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
    if ($file = locale_translation_source_check_file($source)) {
      locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
    }
    $checked = TRUE;
  }

  // Check the status of remote translation files.
  if ($options['use_remote'] && isset($source->files[LOCALE_TRANSLATION_REMOTE])) {
    $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE];
    if ($result = locale_translation_http_check($remote_file->uri)) {
      // Update the file object with the result data. In case of a redirect we
      // store the resulting uri.
      if (isset($result['last_modified'])) {
        $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
        $remote_file->timestamp = $result['last_modified'];
        locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file);
      }
      // @todo What to do with when the file is not found (404)? To prevent
      //   re-checking within the TTL (1day, 1week) we can set a last_checked
      //   timestamp or cache the result.
      $checked = TRUE;
    }
    else {
      $failure = TRUE;
    }
  }

  // Provide user feedback and record success or failure for reporting at the
  // end of the batch.
  if ($options['finish_feedback'] && $checked) {
    $context['results']['files'][] = $source->name;
  }
  if ($failure && !$checked) {
    $context['results']['failed_files'][] = $source->name;
  }
  $context['message'] = t('Checked translation for %project.', ['%project' => $source->project]);
}

/**
 * Implements callback_batch_finished().
 *
 * Set result message.
 *
 * @param bool $success
 *   TRUE if batch successfully completed.
 * @param array $results
 *   Batch results.
 */
function locale_translation_batch_status_finished($success, $results) {
  if ($success) {
    if (isset($results['failed_files'])) {
      if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
        $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')]);
      }
      else {
        $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.');
      }
      drupal_set_message($message, 'error');
    }
    if (isset($results['files'])) {
      drupal_set_message(\Drupal::translation()->formatPlural(
        count($results['files']),
        'Checked available interface translation updates for one project.',
        'Checked available interface translation updates for @count projects.'
      ));
    }
    if (!isset($results['failed_files']) && !isset($results['files'])) {
      drupal_set_message(t('Nothing to check.'));
    }
    \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
  }
  else {
    drupal_set_message(t('An error occurred trying to check available interface translation updates.'), 'error');
  }
}

/**
 * Implements callback_batch_operation().
 *
 * Downloads a remote gettext file into the translations directory. When
 * successfully the translation status is updated.
 *
 * @param object $project
 *   Source object of the translatable project.
 * @param string $langcode
 *   Language code.
 * @param array $context
 *   The batch context.
 *
 * @see locale_translation_batch_fetch_import()
 */
function locale_translation_batch_fetch_download($project, $langcode, &$context) {
  $sources = locale_translation_get_status([$project], [$langcode]);
  if (isset($sources[$project][$langcode])) {
    $source = $sources[$project][$langcode];
    if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) {
      if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) {
        $context['message'] = t('Downloaded translation for %project.', ['%project' => $source->project]);
        locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
      }
      else {
        $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
      }
    }
  }
}

/**
 * Implements callback_batch_operation().
 *
 * Imports a gettext file from the translation directory. When successfully the
 * translation status is updated.
 *
 * @param object $project
 *   Source object of the translatable project.
 * @param string $langcode
 *   Language code.
 * @param array $options
 *   Array of import options.
 * @param array $context
 *   The batch context.
 *
 * @see locale_translate_batch_import_files()
 * @see locale_translation_batch_fetch_download()
 */
function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
  $sources = locale_translation_get_status([$project], [$langcode]);
  if (isset($sources[$project][$langcode])) {
    $source = $sources[$project][$langcode];
    if (isset($source->type)) {
      if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {
        $file = $source->files[LOCALE_TRANSLATION_LOCAL];
        module_load_include('bulk.inc', 'locale');
        $options += [
          'message' => t('Importing translation for %project.', ['%project' => $source->project]),
        ];
        // Import the translation file. For large files the batch operations is
        // progressive and will be called repeatedly until finished.
        locale_translate_batch_import($file, $options, $context);

        // The import is finished.
        if (isset($context['finished']) && $context['finished'] == 1) {
          // The import is successful.
          if (isset($context['results']['files'][$file->uri])) {
            $context['message'] = t('Imported translation for %project.', ['%project' => $source->project]);

            // Save the data of imported source into the {locale_file} table and
            // update the current translation status.
            locale_translation_status_save($project, $langcode, LOCALE_TRANSLATION_CURRENT, $source->files[LOCALE_TRANSLATION_LOCAL]);
          }
        }
      }
    }
  }
}

/**
 * Implements callback_batch_finished().
 *
 * Set result message.
 *
 * @param bool $success
 *   TRUE if batch successfully completed.
 * @param array $results
 *   Batch results.
 */
function locale_translation_batch_fetch_finished($success, $results) {
  module_load_include('bulk.inc', 'locale');
  if ($success) {
    \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
  }
  return locale_translate_batch_finished($success, $results);
}

/**
 * Check if remote file exists and when it was last updated.
 *
 * @param string $uri
 *   URI of remote file.
 *
 * @return array|bool
 *   Associative array of file data with the following elements:
 *   - last_modified: Last modified timestamp of the translation file.
 *   - (optional) location: The location of the translation file. Is only set
 *     when a redirect (301) has occurred.
 *   TRUE if the file is not found. FALSE if a fault occurred.
 */
function locale_translation_http_check($uri) {
  $logger = \Drupal::logger('locale');
  try {
    $actual_uri = NULL;
    $response = \Drupal::service('http_client_factory')->fromOptions([
      'allow_redirects' => [
        'on_redirect' => function (RequestInterface $request, ResponseInterface $response, UriInterface $request_uri) use (&$actual_uri) {
          $actual_uri = (string) $request_uri;
        }
      ],
    ])->head($uri);
    $result = [];

    // Return the effective URL if it differs from the requested.
    if ($actual_uri && $actual_uri !== $uri) {
      $result['location'] = $actual_uri;
    }

    $result['last_modified'] = $response->hasHeader('Last-Modified') ? strtotime($response->getHeaderLine('Last-Modified')) : 0;
    return $result;
  }
  catch (RequestException $e) {
    // Handle 4xx and 5xx http responses.
    if ($response = $e->getResponse()) {
      if ($response->getStatusCode() == 404) {
        // File not found occurs when a translation file is not yet available
        // at the translation server. But also if a custom module or custom
        // theme does not define the location of a translation file. By default
        // the file is checked at the translation server, but it will not be
        // found there.
        $logger->notice('Translation file not found: @uri.', ['@uri' => $uri]);
        return TRUE;
      }
      $logger->notice('HTTP request to @url failed with error: @error.', ['@url' => $uri, '@error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()]);
    }
  }

  return FALSE;
}

/**
 * Downloads a translation file from a remote server.
 *
 * @param object $source_file
 *   Source file object with at least:
 *   - "uri": uri to download the file from.
 *   - "project": Project name.
 *   - "langcode": Translation language.
 *   - "version": Project version.
 *   - "filename": File name.
 * @param string $directory
 *   Directory where the downloaded file will be saved. Defaults to the
 *   temporary file path.
 *
 * @return object
 *   File object if download was successful. FALSE on failure.
 */
function locale_translation_download_source($source_file, $directory = 'temporary://') {
  if ($uri = system_retrieve_file($source_file->uri, $directory)) {
    $file = clone($source_file);
    $file->type = LOCALE_TRANSLATION_LOCAL;
    $file->uri = $uri;
    $file->directory = $directory;
    $file->timestamp = filemtime($uri);
    return $file;
  }
  \Drupal::logger('locale')->error('Unable to download translation file @uri.', ['@uri' => $source_file->uri]);
  return FALSE;
}