locale.batch.inc 11.5 KB
Newer Older
1 2 3 4
<?php

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

8
use GuzzleHttp\Exception\RequestException;
9 10 11
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
12

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

/**
21
 * Implements callback_batch_operation().
22
 *
23 24
 * Checks the presence and creation time po translation files in located at
 * remote server location and local file system.
25
 *
26 27 28 29 30
 * @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
31
 *   An array with options that can have the following elements:
32 33 34 35
 *   - '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.
36
 * @param array|\ArrayAccess $context
37
 *   The batch context.
38
 */
39
function locale_translation_batch_status_check($project, $langcode, array $options, &$context) {
40
  $failure = $checked = FALSE;
41
  $options += [
42 43
    'finish_feedback' => TRUE,
    'use_remote' => TRUE,
44 45
  ];
  $source = locale_translation_get_status([$project], [$langcode]);
46 47 48 49 50 51 52 53 54
  $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;
  }
55

56 57 58 59
  // 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)) {
60
      // Update the file object with the result data. In case of a redirect we
61
      // store the resulting uri.
62 63 64
      if (isset($result['last_modified'])) {
        $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
        $remote_file->timestamp = $result['last_modified'];
65
        locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file);
66
      }
67 68 69 70
      // @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;
71 72
    }
    else {
73
      $failure = TRUE;
74 75 76
    }
  }

77 78 79 80
  // 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;
81
  }
82 83
  if ($failure && !$checked) {
    $context['results']['failed_files'][] = $source->name;
84
  }
85
  $context['message'] = t('Checked translation for %project.', ['%project' => $source->project]);
86 87 88
}

/**
89 90 91
 * Implements callback_batch_finished().
 *
 * Set result message.
92
 *
93
 * @param bool $success
94
 *   TRUE if batch successfully completed.
95 96 97 98
 * @param array $results
 *   Batch results.
 */
function locale_translation_batch_status_finished($success, $results) {
99 100
  if ($success) {
    if (isset($results['failed_files'])) {
101
      if (\Drupal::moduleHandler()->moduleExists('dblog') && \Drupal::currentUser()->hasPermission('access site reports')) {
102
        $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')]);
103 104
      }
      else {
105
        $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.');
106
      }
107
      \Drupal::messenger()->addError($message);
108 109
    }
    if (isset($results['files'])) {
110
      \Drupal::messenger()->addStatus(\Drupal::translation()->formatPlural(
111
        count($results['files']),
112 113 114 115
        'Checked available interface translation updates for one project.',
        'Checked available interface translation updates for @count projects.'
      ));
    }
116
    if (!isset($results['failed_files']) && !isset($results['files'])) {
117
      \Drupal::messenger()->addStatus(t('Nothing to check.'));
118
    }
119
    \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
120 121
  }
  else {
122
    \Drupal::messenger()->addError(t('An error occurred trying to check available interface translation updates.'));
123 124 125
  }
}

126
/**
127
 * Implements callback_batch_operation().
128
 *
129 130
 * Downloads a remote gettext file into the translations directory. When
 * successfully the translation status is updated.
131 132 133 134 135
 *
 * @param object $project
 *   Source object of the translatable project.
 * @param string $langcode
 *   Language code.
136 137
 * @param array $context
 *   The batch context.
138 139 140 141
 *
 * @see locale_translation_batch_fetch_import()
 */
function locale_translation_batch_fetch_download($project, $langcode, &$context) {
142
  $sources = locale_translation_get_status([$project], [$langcode]);
143 144
  if (isset($sources[$project][$langcode])) {
    $source = $sources[$project][$langcode];
145
    if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) {
146
      if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) {
147
        $context['message'] = t('Downloaded translation for %project.', ['%project' => $source->project]);
148
        locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file);
149 150 151 152 153 154 155 156 157
      }
      else {
        $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
      }
    }
  }
}

/**
158
 * Implements callback_batch_operation().
159
 *
160 161
 * Imports a gettext file from the translation directory. When successfully the
 * translation status is updated.
162 163 164 165 166 167 168
 *
 * @param object $project
 *   Source object of the translatable project.
 * @param string $langcode
 *   Language code.
 * @param array $options
 *   Array of import options.
169 170
 * @param array $context
 *   The batch context.
171 172 173 174 175
 *
 * @see locale_translate_batch_import_files()
 * @see locale_translation_batch_fetch_download()
 */
function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
176
  $sources = locale_translation_get_status([$project], [$langcode]);
177 178
  if (isset($sources[$project][$langcode])) {
    $source = $sources[$project][$langcode];
179 180
    if (isset($source->type)) {
      if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {
181
        $file = $source->files[LOCALE_TRANSLATION_LOCAL];
182
        module_load_include('bulk.inc', 'locale');
183 184 185
        $options += [
          'message' => t('Importing translation for %project.', ['%project' => $source->project]),
        ];
186
        // Import the translation file. For large files the batch operations is
187
        // progressive and will be called repeatedly until finished.
188 189 190 191
        locale_translate_batch_import($file, $options, $context);

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

196 197 198
            // 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]);
199 200 201 202 203 204 205 206
          }
        }
      }
    }
  }
}

/**
207 208 209
 * Implements callback_batch_finished().
 *
 * Set result message.
210
 *
211
 * @param bool $success
212
 *   TRUE if batch successfully completed.
213
 * @param array $results
214 215 216 217
 *   Batch results.
 */
function locale_translation_batch_fetch_finished($success, $results) {
  module_load_include('bulk.inc', 'locale');
218
  if ($success) {
219
    \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME);
220
  }
221 222 223
  return locale_translate_batch_finished($success, $results);
}

224 225 226
/**
 * Check if remote file exists and when it was last updated.
 *
227 228
 * @param string $uri
 *   URI of remote file.
229
 *
230
 * @return array|bool
231 232 233 234 235
 *   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.
236
 */
237
function locale_translation_http_check($uri) {
238
  $logger = \Drupal::logger('locale');
239
  try {
240
    $actual_uri = NULL;
241 242
    $response = \Drupal::service('http_client_factory')->fromOptions([
      'allow_redirects' => [
243
        'on_redirect' => function (RequestInterface $request, ResponseInterface $response, UriInterface $request_uri) use (&$actual_uri) {
244
          $actual_uri = (string) $request_uri;
245
        },
246 247
      ],
    ])->head($uri);
248
    $result = [];
249

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

255
    $result['last_modified'] = $response->hasHeader('Last-Modified') ? strtotime($response->getHeaderLine('Last-Modified')) : 0;
256 257
    return $result;
  }
258
  catch (RequestException $e) {
259
    // Handle 4xx and 5xx http responses.
260 261 262 263 264 265 266
    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.
267
        $logger->notice('Translation file not found: @uri.', ['@uri' => $uri]);
268 269
        return TRUE;
      }
270
      $logger->notice('HTTP request to @url failed with error: @error.', ['@url' => $uri, '@error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()]);
271
    }
272
  }
273

274 275 276 277
  return FALSE;
}

/**
278
 * Downloads a translation file from a remote server.
279 280 281 282 283 284 285 286
 *
 * @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.
287 288 289
 * @param string $directory
 *   Directory where the downloaded file will be saved. Defaults to the
 *   temporary file path.
290 291 292
 *
 * @return object
 *   File object if download was successful. FALSE on failure.
293
 */
294
function locale_translation_download_source($source_file, $directory = 'temporary://') {
295
  if ($uri = system_retrieve_file($source_file->uri, $directory, FALSE, FILE_EXISTS_REPLACE)) {
296 297
    $file = clone($source_file);
    $file->type = LOCALE_TRANSLATION_LOCAL;
298
    $file->uri = $uri;
299 300
    $file->directory = $directory;
    $file->timestamp = filemtime($uri);
301
    return $file;
302
  }
303
  \Drupal::logger('locale')->error('Unable to download translation file @uri.', ['@uri' => $source_file->uri]);
304
  return FALSE;
305
}