<?php

/**
 * @file
 * Download translations from remote localization server.
 */

/**
 * Translation update mode: Use local files only.
 *
 * When checking for available translation updates, only local files will be
 * used. Any remote translation file will be ignored. Also custom modules and
 * themes which have set a "server pattern" to use a remote translation server
 * will be ignored.
 */
define('L10N_UPDATE_USE_SOURCE_LOCAL', 2);

/**
 * Translation update mode: Use both remote and local files.
 *
 * When checking for available translation updates, both local and remote files
 * will be checked.
 */
define('L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL', 3);

/**
 * Default location of gettext file on the translation server.
 *
 * @see l10n_update_default_translation_server().
 */
define('L10N_UPDATE_DEFAULT_SERVER_PATTERN', 'http://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po');

/**
 * Default gettext file name on the translation server.
 */
define('L10N_UPDATE_DEFAULT_FILE_NAME', '%project-%release.%language.po');

/**
 * Default gettext file name on the translation server.
 */
define('L10N_UPDATE_DEFAULT_TRANSLATION_PATH', 'sites/all/translations');

/**
 * The number of seconds that the translations status entry is valid.
 */
define('L10N_UPDATE_STATUS_TTL', 600);

/**
 * UI option for override of existing translations.
 *
 * Only override non-customized translations.
 */
define('L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED', 2);

/**
 * Translation source is a remote file.
 */
define('L10N_UPDATE_REMOTE', 'remote');

/**
 * Translation source is a local file.
 */
define('L10N_UPDATE_LOCAL', 'local');

/**
 * Translation source is the current translation.
 */
define('L10N_UPDATE_CURRENT', 'current');

/**
 * The delimiter used to split plural strings.
 *
 * This is the ETX (End of text) character and is used as a minimal means to
 * separate singular and plural variants in source and translation text. It
 * was found to be the most compatible delimiter for the supported databases.
 */
define('L10N_UPDATE_PLURAL_DELIMITER', "\03");

/**
 * Flag for locally not customized interface translation.
 *
 * Such translations are imported from .po files downloaded from
 * localize.drupal.org for example.
 */
define('L10N_UPDATE_NOT_CUSTOMIZED', 0);

/**
 * Flag for locally customized interface translation.
 *
 * Strings are customized when translated or edited using the build in
 * string translation form. Strings can also be marked as customized when a po
 * file is imported.
 */
define('L10N_UPDATE_STRING_CUSTOM', 1);

/**
 * Flag for locally customized interface translation.
 *
 * Such translations are edited from their imported originals on the user
 * interface or are imported as customized.
 */
define('L10N_UPDATE_CUSTOMIZED', 1);

/**
 * Implements hook_help().
 */
function l10n_update_help($path, $arg) {
  $output = '';

  switch ($path) {
    case 'admin/config/regional/translate/update':
      $output = '<p>' . t('Status of interface translations for each of the enabled languages.') . '</p>';
      $output .= '<p>' . t('If there are available updates you can click on "Update translation" for them to be downloaded and imported now or you can edit the configuration for them to be updated automatically on the <a href="@update-settings">Update settings page</a>', array('@update-settings' => url('admin/config/regional/language/update'))) . '</p>';
      break;

    case 'admin/config/regional/language/update':
      $output = '<p>' . t('These are the settings for the translation update system. To update your translations now, check out the <a href="@update-admin">Translation update administration page</a>.', array('@update-admin' => url('admin/config/regional/translate/update'))) . '</p>';
      break;
  }
  return $output;
}

/**
 * Implements hook_menu().
 */
function l10n_update_menu() {
  $items['admin/config/regional/translate/update'] = array(
    'title' => 'Update',
    'description' => 'Available updates',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('l10n_update_status_form'),
    'access arguments' => array('translate interface'),
    'file' => 'l10n_update.admin.inc',
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/config/regional/translate/check'] = array(
    'title' => 'Update',
    'description' => 'Available updates',
    'page callback' => 'l10n_update_manual_status',
    'access arguments' => array('translate interface'),
    'file' => 'l10n_update.admin.inc',
    'weight' => 20,
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/regional/language/update'] = array(
    'title' => 'Translation updates',
    'description' => 'Automatic update configuration',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('l10n_update_admin_settings_form'),
    'access arguments' => array('translate interface'),
    'file' => 'l10n_update.admin.inc',
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implements hook_theme().
 */
function l10n_update_theme() {
  return array(
    'l10n_update_last_check' => array(
      'variables' => array('last' => NULL),
      'file' => 'l10n_update.admin.inc',
      'template' => 'l10n_update-translation-last-check',
    ),
    'l10n_update_update_info' => array(
      'variables' => array('updates' => array(), 'not_found' => array()),
      'file' => 'l10n_update.admin.inc',
      'template' => 'l10n_update-translation-update-info',
    ),
  );
}
/**
 * Implements hook_menu_alter().
 */
function l10n_update_menu_alter(&$menu) {
  // Redirect l10n_client AJAX callback path for strings.
  if (module_exists('l10n_client')) {
    $menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
  }
}

/**
 * Implements hook_cron().
 *
 * Check one project/language at a time, download and import if update
 * available.
 */
function l10n_update_cron() {
  // Update translations only when an update frequency was set by the admin
  // and a translatable language was set.
  // Update tasks are added to the queue here but processed by Drupal's cron
  // using the cron worker defined in l10n_update_queue_info().
  if ($frequency = variable_get('l10n_update_check_frequency', '0') && l10n_update_translatable_language_list()) {
    module_load_include('translation.inc', 'l10n_update');
    l10n_update_cron_fill_queue();
  }
}

/**
 * Implements hook_cron_queue_info().
 */
function l10n_update_cron_queue_info() {
  $queues['l10n_update'] = array(
    'worker callback' => 'l10n_update_worker',
    'time' => 30,
  );
  return $queues;
}

/**
 * Callback: Executes interface translation queue tasks.
 *
 * The translation update functions executed here are batch operations which
 * are also used in translation update batches. The batch functions may need to
 * be executed multiple times to complete their task, typically this is the
 * translation import function. When a batch function is not finished, a new
 * queue task is created and added to the end of the queue. The batch context
 * data is needed to continue the batch task is stored in the queue with the
 * queue data.
 *
 * @param array $data
 *   Queue data array containing:
 *   - Function name.
 *   - Array of function arguments. Optionally contains the batch context data.
 *
 * @see l10n_update_queue_info()
 */
function l10n_update_worker(array $data) {
  module_load_include('batch.inc', 'l10n_update');
  list($function, $args) = $data;

  // We execute batch operation functions here to check, download and import the
  // translation files. Batch functions use a context variable as last argument
  // which is passed by reference. When a batch operation is called for the
  // first time a default batch context is created. When called iterative
  // (usually the batch import function) the batch context is passed through via
  // the queue and is part of the $data.
  $last = count($args) - 1;
  if (!is_array($args[$last]) || !isset($args[$last]['finished'])) {
    $batch_context = array(
      'sandbox'  => array(),
      'results'  => array(),
      'finished' => 1,
      'message'  => '',
    );
  }
  else {
    $batch_context = $args[$last];
    unset($args[$last]);
  }
  $args = array_merge($args, array(&$batch_context));

  // Call the batch operation function.
  call_user_func_array($function, $args);

  // If the batch operation is not finished we create a new queue task to
  // continue the task. This is typically the translation import task.
  if ($batch_context['finished'] < 1) {
    unset($batch_context['strings']);
    $queue = DrupalQueue::get('l10n_update', TRUE);
    $queue->createItem(array($function, $args));
  }
}

/**
 * Implements hook_stream_wrappers().
 */
function l10n_update_stream_wrappers() {
  // Load the stream wrapper class if not automatically loaded. This happens
  // before update.php is executed.
  if (!class_exists('TranslationsStreamWrapper')) {
    require_once 'includes/locale/TranslationsStreamWrapper.php';
  }

  $wrappers['translations'] = array(
    'name' => t('Translation files'),
    'class' => 'TranslationsStreamWrapper',
    'description' => t('Translation files.'),
    'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
  );

  return $wrappers;
}

/**
 * Implements hook_form_alter().
 */
function l10n_update_form_alter(&$form, $form_state, $form_id) {
  switch ($form_id) {
    case 'locale_translate_edit_form':
    case 'i18n_string_locale_translate_edit_form':
      $form['#submit'][] = 'l10n_update_locale_translate_edit_form_submit';
      break;

    case 'locale_languages_predefined_form':
    case 'locale_languages_custom_form':
      $form['#submit'][] = 'l10n_update_languages_changed_submit';
      break;

    case 'locale_languages_delete_form':
      // A language is being deleted.
      $form['#submit'][] = 'l10n_update_languages_delete_submit';
      break;
  }
}

/**
 * Implements hook_modules_enabled().
 *
 * Refresh project translation status and get translations if required.
 */
function l10n_update_modules_enabled($modules) {
  $components['module'] = $modules;
  l10n_update_system_update($components);
}

/**
 * Implements hook_modules_disabled().
 *
 * Set disabled modules to be ignored when updating translations.
 */
function l10n_update_modules_disabled($modules) {
  if (!variable_get('l10n_update_check_disabled', FALSE)) {
    db_update('l10n_update_project')
      ->fields(array(
        'status' => 0,
      ))
      ->condition('name', $modules)
      ->execute();
  }
}

/**
 * Implements hook_modules_uninstalled().
 *
 * Remove data of uninstalled modules from {l10n_update_file} table and
 * rebuild the projects cache.
 */
function l10n_update_modules_uninstalled($modules) {
  $components['module'] = $modules;
  l10n_update_system_remove($components);
}

/**
 * Implements hook_themes_enabled().
 *
 * Refresh project translation status and get translations if required.
 */
function l10n_update_themes_enabled($themes) {
  $components['theme'] = $themes;
  l10n_update_system_update($components);
}

/**
 * Additional submit handler for language forms.
 *
 * We need to refresh status when a new language is enabled / disabled.
 */
function l10n_update_languages_changed_submit($form, $form_state) {
  if (variable_get('l10n_update_import_enabled', TRUE)) {
    if (empty($form_state['values']['predefined_langcode']) || $form_state['values']['predefined_langcode'] == 'custom') {
      $langcode = $form_state['values']['langcode'];
    }
    else {
      $langcode = $form_state['values']['predefined_langcode'];
    }

    // Download and import translations for the newly added language.
    module_load_include('fetch.inc', 'l10n_update');
    $options = _l10n_update_default_update_options();
    $batch = l10n_update_batch_update_build(array(), array($langcode), $options);
    batch_set($batch);
  }
}

/**
 * Additional submit handler for language deletion form.
 *
 * When a language is deleted, the file history of this language is cleared.
 */
function l10n_update_languages_delete_submit($form, $form_state) {
  $langcode = $form_state['values']['langcode'];
  l10n_update_file_history_delete(array(), $langcode);
}

/**
 * Additional submit handler for locale and i18n_string translation edit form.
 *
 * Mark locally edited translations as customized.
 *
 * @see l10n_update_form_alter()
 */
function l10n_update_locale_translate_edit_form_submit($form, &$form_state) {
  $lid = $form_state['values']['lid'];
  foreach ($form_state['values']['translations'] as $langcode => $value) {
    if (!empty($value) && $value != $form_state['complete form']['translations'][$langcode]['#default_value']) {
      // An update has been made, mark the string as customized.
      db_update('locales_target')
        ->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
        ->condition('lid', $lid)
        ->condition('language', $langcode)
        ->execute();
    }
  }
}

/**
 * Menu callback. Saves a string translation coming as POST data.
 */
function l10n_update_client_save_string() {
  global $user, $language;

  if (l10n_client_access()) {
    if (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['textgroup']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
      // Ensure we have this source string before we attempt to save it.
      // @todo: add actual context support.
      $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $_POST['source'], ':context' => '', ':textgroup' => $_POST['textgroup']))->fetchField();

      if (!empty($lid)) {
        module_load_include('translation.inc', 'l10n_update');
        $report = array('skips' => 0, 'additions' => 0, 'updates' => 0, 'deletes' => 0);
        // @todo: add actual context support.
        _l10n_update_locale_import_one_string_db($report, $language->language, '', $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE, L10N_UPDATE_STRING_CUSTOM);
        cache_clear_all('locale:', 'cache', TRUE);
        _locale_invalidate_js($language->language);
        if (!empty($report['skips'])) {
          $message = theme('l10n_client_message', array('message' => t('Not saved locally due to invalid HTML content.')));
        }
        elseif (!empty($report['additions']) || !empty($report['updates'])) {
          $message = theme('l10n_client_message', array('message' => t('Translation saved locally.'), 'level' => WATCHDOG_INFO));
        }
        elseif (!empty($report['deletes'])) {
          $message = theme('l10n_client_message', array('message' => t('Translation successfully removed locally.'), 'level' => WATCHDOG_INFO));
        }
        else {
          $message = theme('l10n_client_message', array('message' => t('Unknown error while saving translation locally.')));
        }

        // Submit to remote server if enabled.
        if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server') && ($_POST['textgroup'] == 'default')) {
          if (!empty($user->data['l10n_client_key'])) {
            $remote_result = l10n_client_submit_translation($language->language, $_POST['source'], $_POST['target'], $user->data['l10n_client_key'], l10n_client_user_token($user));
            $message .= theme('l10n_client_message', array('message' => $remote_result[1], 'level' => $remote_result[0] ? WATCHDOG_INFO : WATCHDOG_ERROR));
          }
          else {
            $server_url = variable_get('l10n_client_server', 'http://localize.drupal.org');
            $user_edit_url = url('user/' . $user->uid . '/edit', array('absolute' => TRUE));
            $message .= theme('l10n_client_message', array('message' => t('You could share your work with !l10n_server if you set your API key at !user_link.', array('!l10n_server' => l($server_url, $server_url), '!user_link' => l($user_edit_url, 'user/' . $user->uid . '/edit'))), 'level' => WATCHDOG_WARNING));
          }
        }
      }
      else {
        $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
      }
    }
    else {
      $message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
    }
  }
  else {
    $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
  }
  drupal_json_output($message);
  exit;
}

/**
 * Imports translations when new modules or themes are installed.
 *
 * This function will start a batch to import translations for the added
 * components.
 *
 * @param array $components
 *   An array of arrays of component (theme and/or module) names to import
 *   translations for, indexed by type.
 */
function l10n_update_system_update(array $components) {
  $components += array('module' => array(), 'theme' => array());
  $list = array_merge($components['module'], $components['theme']);

  // Skip running the translation imports if in the installer,
  // because it would break out of the installer flow. We have
  // built-in support for translation imports in the installer.
  if (!drupal_installation_attempted() && l10n_update_translatable_language_list() && variable_get('l10n_update_import_enabled', TRUE)) {
    module_load_include('compare.inc', 'l10n_update');

    // Update the list of translatable projects and start the import batch.
    // Only when new projects are added the update batch will be triggered. Not
    // each enabled module will introduce a new project. E.g. sub modules.
    $projects = array_keys(l10n_update_build_projects());
    if ($list = array_intersect($list, $projects)) {
      module_load_include('fetch.inc', 'l10n_update');
      // Get translation status of the projects, download and update
      // translations.
      $options = _l10n_update_default_update_options();
      $batch = l10n_update_batch_update_build($list, array(), $options);
      batch_set($batch);
    }
  }
}

/**
 * Delete translation history of modules and themes.
 *
 * Only the translation history is removed, not the source strings or
 * translations. This is not possible because strings are shared between
 * modules and we have no record of which string is used by which module.
 *
 * @param array $components
 *   An array of arrays of component (theme and/or module) names to import
 *   translations for, indexed by type.
 */
function l10n_update_system_remove(array $components) {
  $components += array('module' => array(), 'theme' => array());
  $list = array_merge($components['module'], $components['theme']);
  if ($language_list = l10n_update_translatable_language_list()) {
    module_load_include('compare.inc', 'l10n_update');
    module_load_include('bulk.inc', 'l10n_update');

    // Only when projects are removed, the translation files and records will be
    // deleted. Not each disabled module will remove a project. E.g. sub
    // modules.
    $projects = array_keys(l10n_update_get_projects());
    if ($list = array_intersect($list, $projects)) {
      l10n_update_file_history_delete($list);

      // Remove translation files.
      l10n_update_delete_translation_files($list, array());

      // Remove translatable projects.
      // Followup issue http://drupal.org/node/1842362 to replace the
      // {l10n_update_project} table. Then change this to a function call.
      db_delete('l10n_update_project')
        ->condition('name', $list)
        ->execute();

      // Clear the translation status.
      l10n_update_status_delete_projects($list);
    }

  }
}

/**
 * Gets current translation status from the {l10n_update_file} table.
 *
 * @return array
 *   Array of translation file objects.
 */
function l10n_update_get_file_history() {
  $history = &drupal_static(__FUNCTION__, array());

  if (empty($history)) {
    // Get file history from the database.
    $result = db_query('SELECT project, language, filename, version, uri, timestamp, last_checked FROM {l10n_update_file}');
    foreach ($result as $file) {
      $file->langcode = $file->language;
      $file->type = $file->timestamp ? L10N_UPDATE_CURRENT : '';
      $history[$file->project][$file->langcode] = $file;
    }
  }
  return $history;
}

/**
 * Updates the {locale_file} table.
 *
 * @param object $file
 *   Object representing the file just imported.
 *
 * @return int
 *   FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
 *
 * @see drupal_write_record()
 */
function l10n_update_update_file_history($file) {
  // Update or write new record.
  if (db_query("SELECT project FROM {l10n_update_file} WHERE project = :project AND language = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) {
    $update = array('project', 'language');
  }
  else {
    $update = array();
  }
  $file->language = $file->langcode;
  $result = drupal_write_record('l10n_update_file', $file, $update);
  // The file history has changed, flush the static cache now.
  // @todo Can we make this more fine grained?
  drupal_static_reset('l10n_update_get_file_history');
  return $result;
}

/**
 * Deletes the history of downloaded translations.
 *
 * @param array $projects
 *   Project name(s) to be deleted from the file history. If both project(s) and
 *   language code(s) are specified the conditions will be ANDed.
 * @param array $langcodes
 *   Language code(s) to be deleted from the file history.
 */
function l10n_update_file_history_delete($projects = array(), $langcodes = array()) {
  $query = db_delete('l10n_update_file');
  if (!empty($projects)) {
    $query->condition('project', $projects);
  }
  if (!empty($langcodes)) {
    $query->condition('language', $langcodes);
  }
  $query->execute();
}

/**
 * Gets the current translation status.
 *
 * @todo What is 'translation status'?
 */
function l10n_update_get_status($projects = NULL, $langcodes = NULL) {
  $result = array();
  $status = variable_get('l10n_update_translation_status', array());
  module_load_include('translation.inc', 'l10n_update');
  $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
  $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());

  // Get the translation status of each project-language combination. If no
  // status was stored, a new translation source is created.
  foreach ($projects as $project) {
    foreach ($langcodes as $langcode) {
      if (isset($status[$project][$langcode])) {
        $result[$project][$langcode] = $status[$project][$langcode];
      }
      else {
        $sources = l10n_update_build_sources(array($project), array($langcode));
        if (isset($sources[$project][$langcode])) {
          $result[$project][$langcode] = $sources[$project][$langcode];
        }
      }
    }
  }
  return $result;
}

/**
 * Saves the status of translation sources in static cache.
 *
 * @param string $project
 *   Machine readable project name.
 * @param string $langcode
 *   Language code.
 * @param string $type
 *   Type of data to be stored.
 * @param object $data
 *   File object also containing timestamp when the translation is last updated.
 */
function l10n_update_status_save($project, $langcode, $type, $data) {
  // Followup issue: http://drupal.org/node/1842362
  // Split status storage per module/language and expire individually. This will
  // improve performance for large sites.
  // Load the translation status or build it if not already available.
  module_load_include('translation.inc', 'l10n_update');
  $status = l10n_update_get_status();
  if (empty($status)) {
    $projects = l10n_update_get_projects(array($project));
    if (isset($projects[$project])) {
      $status[$project][$langcode] = l10n_update_source_build($projects[$project], $langcode);
    }
  }

  // Merge the new status data with the existing status.
  if (isset($status[$project][$langcode])) {
    switch ($type) {
      case L10N_UPDATE_REMOTE:
      case L10N_UPDATE_LOCAL:
        // Add the source data to the status array.
        $status[$project][$langcode]->files[$type] = $data;
        // Check if this translation is the most recent one. Set timestamp and
        // data type of the most recent translation source.
        if (isset($data->timestamp) && $data->timestamp) {
          if ($data->timestamp > $status[$project][$langcode]->timestamp) {
            $status[$project][$langcode]->timestamp = $data->timestamp;
            $status[$project][$langcode]->last_checked = REQUEST_TIME;
            $status[$project][$langcode]->type = $type;
          }
        }
        break;

      case L10N_UPDATE_CURRENT:
        $data->last_checked = REQUEST_TIME;
        $status[$project][$langcode]->timestamp = $data->timestamp;
        $status[$project][$langcode]->last_checked = $data->last_checked;
        $status[$project][$langcode]->type = $type;
        l10n_update_update_file_history($data);
        break;
    }

    variable_set('l10n_update_translation_status', $status);
    variable_set('l10n_update_last_check', REQUEST_TIME);
  }
}

/**
 * Delete language entries from the status cache.
 *
 * @param array $langcodes
 *   Language code(s) to be deleted from the cache.
 */
function l10n_update_status_delete_languages(array $langcodes) {
  if ($status = l10n_update_get_status()) {
    foreach ($status as $project => $languages) {
      foreach ($languages as $langcode => $source) {
        if (in_array($langcode, $langcodes)) {
          unset($status[$project][$langcode]);
        }
      }
    }
    variable_set('l10n_update_translation_status', $status);
  }
}

/**
 * Delete project entries from the status cache.
 *
 * @param array $projects
 *   Project name(s) to be deleted from the cache.
 */
function l10n_update_status_delete_projects(array $projects) {
  $status = l10n_update_get_status();

  foreach ($status as $project => $languages) {
    if (in_array($project, $projects)) {
      unset($status[$project]);
    }
  }
  variable_set('l10n_update_translation_status', $status);
}

/**
 * Returns list of translatable languages.
 *
 * @return array
 *   Array of enabled languages keyed by language name. English is omitted.
 */
function l10n_update_translatable_language_list() {
  $languages = locale_language_list('name');
  unset($languages['en']);
  return $languages;
}

/**
 * Clear the translation status cache.
 */
function l10n_update_clear_status() {
  variable_del('l10n_update_translation_status');
  variable_del('l10n_update_last_check');
}

/**
 * Checks whether remote translation sources are used.
 *
 * @return bool
 *   Returns TRUE if remote translations sources should be taken into account
 *   when checking or importing translation files, FALSE otherwise.
 */
function l10n_update_use_remote_source() {
  return variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL) == L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL;
}
