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