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