locale.batch.inc 21.2 KB
Newer Older
1 2 3 4 5 6 7
<?php

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

8 9 10
use Guzzle\Http\Exception\BadResponseException;
use Guzzle\Http\Exception\RequestException;

11
/**
12
 * Load the common translation API.
13
 */
14 15
// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue http://drupal.org/node/1834298
16
require_once __DIR__ . '/locale.translation.inc';
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

/**
 * Batch operation callback: Check the availability of a remote po file.
 *
 * Checks the presence and creation time of one po file per batch process. The
 * file URL and timestamp are stored.
 *
 * @param array $source
 *   A translation source object of the project for which to check the state of
 *   a remote po file.
 * @param array $context
 *   The batch context array. The collected state is stored in the 'results'
 *   parameter of the context.
 *
 * @see locale_translation_batch_status_fetch_local()
 * @see locale_translation_batch_status_compare()
*/
function locale_translation_batch_status_fetch_remote($source, &$context) {
  // Check the translation file at the remote server and update the source
  // data with the remote status.
37 38 39 40 41 42 43 44
  if (isset($source->files[LOCALE_TRANSLATION_REMOTE])) {
    $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE];
    $result = locale_translation_http_check($remote_file->uri);

    if ($result) {
      // Update the file object with the result data. In case of a redirect we
      // store the resulting uri. If a file is not found we don't update the
      // file object, and store it unchanged.
45 46 47
      if (isset($result['last_modified'])) {
        $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
        $remote_file->timestamp = $result['last_modified'];
48 49 50 51 52 53 54 55 56
        $source->files[LOCALE_TRANSLATION_REMOTE] = $remote_file;
      }
      // Record success.
      $context['results']['files'][$source->name] = $source->name;
    }
    else {
      // An error occured when checking the file. Record the failure for
      // reporting at the end of the batch.
      $context['results']['failed_files'][] = $source->name;
57
    }
58
    $context['results']['sources'][$source->name][$source->langcode] = $source;
59
    $context['message'] = t('Checked translation for %project.', array('%project' => $source->project));
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  }
}

/**
 * Batch operation callback: Check the availability of local po files.
 *
 * Checks the presence and creation time of po files in the local file system.
 * The file path and the timestamp are stored.
 *
 * @param array $sources
 *   Array of translation source objects of projects for which to check the
 *   state of local po files.
 * @param array $context
 *   The batch context array. The collected state is stored in the 'results'
 *   parameter of the context.
 *
 * @see locale_translation_batch_status_fetch_remote()
 * @see locale_translation_batch_status_compare()
 */
function locale_translation_batch_status_fetch_local($sources, &$context) {
  // Get the status of local translation files and store the result data in the
  // batch results for later processing.
  foreach ($sources as $source) {
83
    if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
84 85 86 87
      locale_translation_source_check_file($source);

      // If remote data was collected before, we merge it into the newly
      // collected result.
88 89
      if (isset($context['results']['sources'][$source->name][$source->langcode])) {
        $source->files[LOCALE_TRANSLATION_REMOTE] = $context['results']['sources'][$source->name][$source->langcode]->files[LOCALE_TRANSLATION_REMOTE];
90
      }
91 92 93 94

      // Record success and store the updated source data.
      $context['results']['files'][$source->name] = $source->name;
      $context['results']['sources'][$source->name][$source->langcode] = $source;
95 96
    }
  }
97
  $context['message'] = t('Checked all translations.');
98 99 100 101 102 103 104 105
}

/**
 * Batch operation callback: Compare states and store the result.
 *
 * In the preceding batch processes data of remote and local translation sources
 * is collected. Here we compare the collected results and update the source
 * object with the data of the most recent translation file. The end result is
106
 * stored in the 'locale.translation_status' state variable. Other
107 108 109 110 111 112 113 114 115 116
 * processes can collect this data after the batch process is completed.
 *
 * @param array $context
 *   The batch context array. The 'results' element contains a structured array
 *   of project data with languages, local and remote source data.
 *
 * @see locale_translation_batch_status_fetch_remote()
 * @see locale_translation_batch_status_fetch_local()
 */
function locale_translation_batch_status_compare(&$context) {
117
  $history = locale_translation_get_file_history();
118 119
  $results = array();

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
  if (isset($context['results']['sources'])) {
    foreach ($context['results']['sources'] as $project => $langcodes) {
      foreach ($langcodes as $langcode => $source) {
        $local = isset($source->files[LOCALE_TRANSLATION_LOCAL]) ? $source->files[LOCALE_TRANSLATION_LOCAL] : NULL;
        $remote = isset($source->files[LOCALE_TRANSLATION_REMOTE]) ? $source->files[LOCALE_TRANSLATION_REMOTE] : NULL;

        // The available translation files are compared and data of the most
        // recent file is used to update the source object.
        $file = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local;
        if (isset($file->timestamp)) {
          $source->type = $file->type;
          $source->timestamp = $file->timestamp;
        }

        // Compare the available translation with the current translations
        // status. If the project/language was translated before and it is more
        // recent than the most recent translation, the translation is up to
        // date. Which is marked in the source object with type "current".
        if (isset($history[$source->project][$source->langcode])) {
          $current = $history[$source->project][$source->langcode];
          // Add the current translation to the source object to save it in
          // the status cache.
          $source->files[LOCALE_TRANSLATION_CURRENT] = $current;

          if (isset($source->type)) {
            $available = $source->files[$source->type];
            $result = _locale_translation_source_compare($current, $available) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $available : $current;
            $source->type = $result->type;
            $source->timestamp = $result->timestamp;
          }
          else {
            $source->type = $current->type;
            $source->timestamp = $current->timestamp;
          }
        }

156 157 158
        $results[$project][$langcode] = $source;
      }
    }
159
    $context['message'] = t('Updated translation status.');
160
  }
161
  locale_translation_status_save($results);
162 163 164 165 166 167 168 169 170 171 172
}

/**
 * Batch finished callback: Set result message.
 *
 * @param boolean $success
 *   TRUE if batch succesfully completed.
 * @param array $results
 *   Batch results.
 */
function locale_translation_batch_status_finished($success, $results) {
173 174
  if ($success) {
    if (isset($results['failed_files'])) {
175
        if (Drupal::moduleHandler()->moduleExists('dblog')) {
176 177 178 179 180 181 182 183
          $message = format_plural(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.', array('@url' => url('admin/reports/dblog')));
        }
        else {
          $message = format_plural(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'])) {
184
      drupal_set_message(format_plural(
185
        count($results['sources']),
186 187 188 189
        'Checked available interface translation updates for one project.',
        'Checked available interface translation updates for @count projects.'
      ));
    }
190 191 192
    if (!isset($results['failed_files']) && !isset($results['files'])) {
      drupal_set_message(t('Nothing to check.'));
    }
193 194
  }
  else {
195
    drupal_set_message(t('An error occurred trying to check available interface translation updates.'), 'error');
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
/**
 * Loads translation source data for the projects to be updated.
 *
 * Source data is loaded from cache and stored in the context results array.
 * Source data contains the translations status per project / per language
 * and whether translation updates are available and where the updates can be
 * retrieved from. The data is stored in the $context['results'] parameter
 * so that other batch operations can take this data as input for their
 * operation.
 *
 * @see locale_translation_batch_fetch_download()
 * @see locale_translation_batch_fetch_import()
 * @see locale_translation_batch_fetch_update_status()
 * @see locale_translation_batch_status_compare()
 */
function locale_translation_batch_fetch_sources($projects, $langcodes, &$context) {
  $context['results']['input'] = locale_translation_load_sources($projects, $langcodes);

  // If this batch operation is preceded by the status check operations, the
  // results of those operation are stored in the context. We remove them here
  // to keep the result records clean.
  unset($context['results']['files']);
  unset($context['results']['failed_files']);
}

/**
 * Batch operation: Download a remote translation file.
 *
 * This operation downloads a remote gettext file and saves it in the temporary
 * directory. The remote file URL is taken from the input data in
 * $context['results']['input']. The result of the operation is stored in
 * $context['results']['sources'] and contains the URL of the temporary file.
 *
 * @param object $project
 *   Source object of the translatable project.
 * @param string $langcode
 *   Language code.
 * @param $context
 *   Batch context array.
 *
 * @see locale_translation_batch_fetch_sources()
 * @see locale_translation_batch_fetch_import()
 * @see locale_translation_batch_fetch_update_status()
 * @see locale_translation_batch_status_compare()
 */
function locale_translation_batch_fetch_download($project, $langcode, &$context) {
  $sources = $context['results']['input'];
  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])) {
250
        $context['message'] = t('Downloaded translation for %project.', array('%project' => $source->project));
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 306 307 308
        $source->files[LOCALE_TRANSLATION_DOWNLOADED] = $file;
      }
      else {
        $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE];
      }
      $context['results']['sources'][$project][$langcode] = $source;
    }
  }
}

/**
 * Batch process: Import translation file.
 *
 * This batch operation imports either a local gettext file or a downloaded
 * remote gettext file. In case of a downloaded file the location of the
 * temporary file is found in the $context['results']['sources']. The temporary
 * file will be deleted after importing or will be moved to the local
 * translations directory. In case of a local file the file will just be
 * imported.
 *
 * @param object $project
 *   Source object of the translatable project.
 * @param string $langcode
 *   Language code.
 * @param array $options
 *   Array of import options.
 * @param $context
 *   Batch context array.
 *
 * @see locale_translate_batch_import_files()
 * @see locale_translation_batch_fetch_sources()
 * @see locale_translation_batch_fetch_download()
 * @see locale_translation_batch_fetch_update_status()
 * @see locale_translation_batch_status_compare()
 */
function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) {
  $sources = $context['results']['input'];
  if (isset($sources[$project . ':' . $langcode])) {
    $source = $sources[$project . ':' . $langcode];
    if (isset($source->type)) {
      if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) {

        // If we are working on a remote file we will import the downloaded
        // file. If the file was local just mark the result as such.
        if ($source->type == LOCALE_TRANSLATION_REMOTE) {
          if (isset($context['results']['sources'][$source->project][$source->langcode]->files[LOCALE_TRANSLATION_DOWNLOADED])) {
            $import_type = LOCALE_TRANSLATION_DOWNLOADED;
            $source_result = $context['results']['sources'][$source->project][$source->langcode];
          }
        }
        else {
          $import_type = LOCALE_TRANSLATION_LOCAL;
          $source_result = $source;
        }

        $file = $source_result->files[$import_type];
        module_load_include('bulk.inc', 'locale');
        $options += array(
309
          'message' => t('Importing translation for %project.', array('%project' => $source->project)),
310 311 312 313 314 315 316 317 318
        );
        // Import the translation file. For large files the batch operations is
        // progressive and will be called repeatedly untill finished.
        locale_translate_batch_import($file, $options, $context);

        // The import is finished.
        if (isset($context['finished']) && $context['finished'] == 1) {
          // The import is successfull.
          if (isset($context['results']['files'][$file->uri])) {
319
            $context['message'] = t('Imported translation for %project.', array('%project' => $source->project));
320 321 322 323 324 325 326 327 328

            // Keep the data of imported source. In the following batch
            // operation it will be saved in the {locale_file} table.
            $source_result->files[LOCALE_TRANSLATION_IMPORTED] = $source_result->files[$source->type];

            // Downloaded files are stored in the temporary files directory. If
            // files should be kept locally, they will be moved to the local
            // translations after successfull import. Otherwise the temporary
            // file is deleted after being imported.
329
            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED && config('locale.settings')->get('translation.path') && isset($source_result->files[LOCALE_TRANSLATION_LOCAL])) {
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
              if (file_unmanaged_move($file->uri, $source_result->files[LOCALE_TRANSLATION_LOCAL]->uri, FILE_EXISTS_REPLACE)) {
                // The downloaded file is now moved to the local file location.
                // From this point forward we can treat it as if we imported a
                // local file.
                $import_type = LOCALE_TRANSLATION_LOCAL;
              }
            }
            // The downloaded file is imported but will not be stored locally.
            // Store the timestamp and delete the file.
            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED) {
              $timestamp = filemtime($source_result->files[$import_type]->uri);
              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->timestamp = $timestamp;
              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->last_checked = REQUEST_TIME;
              file_unmanaged_delete($file->uri);
            }
            // If the translation file is stored in the local directory. The
            // timestamp of the file is stored.
            if ($import_type == LOCALE_TRANSLATION_LOCAL) {
              $timestamp = filemtime($source_result->files[$import_type]->uri);
              $source_result->files[LOCALE_TRANSLATION_LOCAL]->timestamp = $timestamp;
              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->timestamp = $timestamp;
              $source_result->files[LOCALE_TRANSLATION_IMPORTED]->last_checked = REQUEST_TIME;

            }
          }
          else {
            // File import failed. We can delete the temporary file.
            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED) {
              file_unmanaged_delete($file->uri);
            }
          }
        }
        $context['results']['sources'][$source->project][$source->langcode] = $source_result;
      }
    }
  }
}

/**
 * Batch process: Update the download history table.
 *
 * This batch process updates the {local_file} table with the data of imported
 * gettext files. Import data is taken from $context['results']['sources'].
 *
 * @param $context
 *   Batch context array.
 *
 * @see locale_translation_batch_fetch_sources()
 * @see locale_translation_batch_fetch_download()
 * @see locale_translation_batch_fetch_import()
 * @see locale_translation_batch_status_compare()
 */
function locale_translation_batch_fetch_update_status(&$context) {
  $results = array();

  if (isset($context['results']['sources'])) {
    foreach ($context['results']['sources'] as $project => $langcodes) {
      foreach ($langcodes as $langcode => $source) {

        // Store the state of the imported translations in {locale_file} table.
        // During the batch execution the data of the imported files is
        // temporary stored in $context['results']['sources']. Now it will be
        // stored in the database. Afterwards the temporary import and download
        // data can be deleted.
        if (isset($source->files[LOCALE_TRANSLATION_IMPORTED])) {
          $file = $source->files[LOCALE_TRANSLATION_IMPORTED];
          locale_translation_update_file_history($file);
          unset($source->files[LOCALE_TRANSLATION_IMPORTED]);
        }
        unset($source->files[LOCALE_TRANSLATION_DOWNLOADED]);

        // The source data is now up to date. Data of local and/or remote source
        // file is up to date including an updated time stamp. In a next batch
        // operation this can be used to update the translation status.
        $context['results']['sources'][$project][$langcode] = $source;
      }
    }
407
    $context['message'] = t('Updated translations.');
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426

    // The file history has changed, flush the static cache now.
    drupal_static_reset('locale_translation_get_file_history');
  }
}

/**
 * Batch finished callback: Set result message.
 *
 * @param boolean $success
 *   TRUE if batch succesfully completed.
 * @param array
 *   Batch results.
 */
function locale_translation_batch_fetch_finished($success, $results) {
  module_load_include('bulk.inc', 'locale');
  return locale_translate_batch_finished($success, $results);
}

427 428 429
/**
 * Check if remote file exists and when it was last updated.
 *
430 431
 * @param string $uri
 *   URI of remote file.
432 433 434 435 436 437 438
 *
 * @return array|boolean
 *   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.
439
 */
440 441 442 443 444 445 446 447 448 449 450 451 452
function locale_translation_http_check($uri) {

  try {
    $response = Drupal::httpClient()
      ->head($uri)
      ->send();
    $result = array();

    // In case of a permanent redirected response, return the final location.
    if ($previous = $response->getPreviousResponse()) {
      if ($previous->getStatusCode() == 301) {
        $result['location'] = $previous->getLocation();
      }
453
    }
454 455

    $result['last_modified'] = $response->getLastModified() ? strtotime($response->getLastModified()) : 0;
456 457
    return $result;
  }
458 459 460 461 462 463 464 465 466 467 468
  catch (BadResponseException $e) {
    // Handle 4xx and 5xx http responses.
    $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.
      watchdog('locale', 'Translation file not found: @uri.', array('@uri' => $uri));
      return TRUE;
469
    }
470 471 472 473 474 475
    watchdog('locale', 'HTTP request to @url failed with error: @error.', array('@url' => $uri, '@error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()));
  }
  catch (RequestException $e) {
    // Handle connection problems and cURL specific errors (CurlException) and
    // other http related errors.
    watchdog('locale', 'HTTP request to @url failed with error: @error.', array('@url' => $uri, '@error' => $e->getMessage()));
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
  }
  return FALSE;
}

/**
 * Downloads source file from a remote server.
 *
 * The downloaded file is stored in the temporary files directory.
 *
 * @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.
 *
 * @return object
 *   File object if download was successful. FALSE on failure.
495
 */
496 497 498 499 500 501 502 503 504 505
function locale_translation_download_source($source_file) {
  if ($uri = system_retrieve_file($source_file->uri, 'temporary://')) {
    $file = new stdClass();
    $file->project = $source_file->project;
    $file->langcode = $source_file->langcode;
    $file->version = $source_file->version;
    $file->type = LOCALE_TRANSLATION_DOWNLOADED;
    $file->uri = $uri;
    $file->filename = $source_file->filename;
    return $file;
506
  }
507 508
  watchdog('locale', 'Unable to download translation file @uri.', array('@uri' => $source->files[LOCALE_TRANSLATION_REMOTE]->uri), WATCHDOG_ERROR);
  return FALSE;
509
}