locale.batch.inc

Batch process to check the availability of remote or local po files.

File

drupal/core/modules/locale/locale.batch.inc
View source
<?php

/**
 * @file
 *   Batch process to check the availability of remote or local po files.
 */
use Guzzle\Http\Exception\BadResponseException;
use Guzzle\Http\Exception\RequestException;

/**
 * Load the common translation API.
 */

// @todo Combine functions differently in files to avoid unnecessary includes.
// Follow-up issue http://drupal.org/node/1834298
require_once __DIR__ . '/locale.translation.inc';

/**
 * 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) {
  $t = get_t();

  // Check the translation file at the remote server and update the source
  // data with the remote status.
  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.
      if (isset($result['last_modified'])) {
        $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri;
        $remote_file->timestamp = $result['last_modified'];
        $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;
    }
    $context['results']['sources'][$source->name][$source->langcode] = $source;
    $context['message'] = $t('Checked translation for %project.', array(
      '%project' => $source->project,
    ));
  }
}

/**
 * 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) {
  $t = get_t();

  // Get the status of local translation files and store the result data in the
  // batch results for later processing.
  foreach ($sources as $source) {
    if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
      locale_translation_source_check_file($source);

      // If remote data was collected before, we merge it into the newly
      // collected result.
      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];
      }

      // Record success and store the updated source data.
      $context['results']['files'][$source->name] = $source->name;
      $context['results']['sources'][$source->name][$source->langcode] = $source;
    }
  }
  $context['message'] = $t('Checked all translations.');
}

/**
 * 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
 * stored in the 'locale.translation_status' state variable. Other
 * 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) {
  $t = get_t();
  $history = locale_translation_get_file_history();
  $results = array();
  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;
          }
        }
        $results[$project][$langcode] = $source;
      }
    }
    $context['message'] = $t('Updated translation status.');
  }
  locale_translation_status_save($results);
}

/**
 * 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) {
  $t = get_t();
  if ($success) {
    if (isset($results['failed_files'])) {
      if (module_exists('dblog')) {
        $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'])) {
      drupal_set_message(format_plural(count($results['sources']), '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.'));
    }
  }
  else {
    drupal_set_message($t('An error occurred trying to check available interface translation updates.'), 'error');
  }
}

/**
 * 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) {
      $t = get_t();
      if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE])) {
        $context['message'] = $t('Downloaded translation for %project.', array(
          '%project' => $source->project,
        ));
        $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) {
        $t = get_t();

        // 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(
          'message' => $t('Importing translation for %project.', array(
            '%project' => $source->project,
          )),
        );

        // 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])) {
            $context['message'] = $t('Imported translation for %project.', array(
              '%project' => $source->project,
            ));

            // 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.
            if ($import_type == LOCALE_TRANSLATION_DOWNLOADED && config('locale.settings')
              ->get('translation.path') && isset($source_result->files[LOCALE_TRANSLATION_LOCAL])) {
              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) {
  $t = get_t();
  $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;
      }
    }
    $context['message'] = $t('Updated translations.');

    // 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);
}

/**
 * Check if remote file exists and when it was last updated.
 *
 * @param string $uri
 *   URI of remote file.
 *
 * @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.
 */
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();
      }
    }
    $result['last_modified'] = $response
      ->getLastModified() ? strtotime($response
      ->getLastModified()) : 0;
    return $result;
  } 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;
    }
    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(),
    ));
  }
  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.
 */
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;
  }
  watchdog('locale', 'Unable to download translation file @uri.', array(
    '@uri' => $source->files[LOCALE_TRANSLATION_REMOTE]->uri,
  ), WATCHDOG_ERROR);
  return FALSE;
}

Functions

Namesort descending Description
locale_translation_batch_fetch_download Batch operation: Download a remote translation file.
locale_translation_batch_fetch_finished Batch finished callback: Set result message.
locale_translation_batch_fetch_import Batch process: Import translation file.
locale_translation_batch_fetch_sources Loads translation source data for the projects to be updated.
locale_translation_batch_fetch_update_status Batch process: Update the download history table.
locale_translation_batch_status_compare Batch operation callback: Compare states and store the result.
locale_translation_batch_status_fetch_local Batch operation callback: Check the availability of local po files.
locale_translation_batch_status_fetch_remote Batch operation callback: Check the availability of a remote po file.
locale_translation_batch_status_finished Batch finished callback: Set result message.
locale_translation_download_source Downloads source file from a remote server.
locale_translation_http_check Check if remote file exists and when it was last updated.