drupal/core/modules/locale/locale.batch.inc

510 lines
21 KiB
PHP
Raw Normal View History

<?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) {
// 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) {
// 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) {
$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) {
if ($success) {
if (isset($results['failed_files'])) {
if (Drupal::moduleHandler()->moduleExists('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) {
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) {
// 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) {
$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;
}