You are here

SmartlingTranslator.php in TMGMT Translator Smartling 8.2

File

src/Plugin/tmgmt/Translator/SmartlingTranslator.php
View source
<?php

/**
 * @file
 * Contains \Drupal\tmgmt_smartling\Plugin\tmgmt\Translator\SmartlingTranslator.
 */
namespace Drupal\tmgmt_smartling\Plugin\tmgmt\Translator;

use Drupal;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\file\FileUsage\DatabaseFileUsageBackend;
use Drupal\tmgmt\Translator\TranslatableResult;
use Drupal\tmgmt\TranslatorPluginBase;
use Drupal\tmgmt\TranslatorInterface;
use Drupal\tmgmt\JobInterface;
use Drupal\tmgmt_extension_suit\ExtendedTranslatorPluginInterface;
use Drupal\tmgmt_file\Format\FormatManager;
use GuzzleHttp\ClientInterface;
use Smartling\File\Params\UploadFileParameters;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\tmgmt\Translator\AvailableResult;
use Drupal\tmgmt\ContinuousTranslatorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\tmgmt_smartling\Event\RequestTranslationEvent;

/**
 * Smartling translator plugin.
 *
 * @TranslatorPlugin(
 *   id = "smartling",
 *   label = @Translation("Smartling translator"),
 *   description = @Translation("Smartling Translator service."),
 *   ui = "Drupal\tmgmt_smartling\SmartlingTranslatorUi"
 * )
 */
class SmartlingTranslator extends TranslatorPluginBase implements ExtendedTranslatorPluginInterface, ContainerFactoryPluginInterface, ContinuousTranslatorInterface {

  /**
   * Guzzle HTTP client.
   *
   * @var \Drupal\tmgmt_smartling\Smartling\SmartlingApi
   */
  protected $smartlingApi;

  /**
   * @var \GuzzleHttp\ClientInterface
   */
  protected $client;

  /**
   * @var \Drupal\tmgmt_file\Format\FormatManager
   */
  protected $formatPluginsManager;

  /**
   * @var \Drupal\file\FileUsage\DatabaseFileUsageBackend
   */
  protected $fileUsage;

  /**
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * Constructs a LocalActionBase object.
   *
   * @param \GuzzleHttp\ClientInterface $client
   *   The Guzzle HTTP client.
   * @param \Drupal\tmgmt_file\Format\FormatManager $format_plugin_manager
   * @param \Drupal\file\FileUsage\DatabaseFileUsageBackend $file_usage
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param array $plugin_definition
   *   The plugin implementation definition.
   */
  public function __construct(ClientInterface $client, FormatManager $format_plugin_manager, DatabaseFileUsageBackend $file_usage, EventDispatcherInterface $event_dispatcher, array $configuration, $plugin_id, array $plugin_definition) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->client = $client;
    $this->formatPluginsManager = $format_plugin_manager;
    $this->fileUsage = $file_usage;
    $this->eventDispatcher = $event_dispatcher;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($container
      ->get('http_client'), $container
      ->get('plugin.manager.tmgmt_file.format'), $container
      ->get('file.usage'), $container
      ->get('event_dispatcher'), $configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function checkAvailable(TranslatorInterface $translator) {
    if ($translator
      ->getSetting('user_id') && $translator
      ->getSetting('token_secret') && $translator
      ->getSetting('project_id')) {
      return AvailableResult::yes();
    }
    return AvailableResult::no(t('@translator is not available. Make sure it is properly <a href=:configured>configured</a>.', [
      '@translator' => $translator
        ->label(),
      ':configured' => $translator
        ->url(),
    ]));
  }

  /**
   * {@inheritdoc}
   */
  public function checkTranslatable(TranslatorInterface $translator, JobInterface $job) {

    // Anything can be exported.
    return TranslatableResult::yes();
  }

  /**
   * Returns callback url.
   *
   * Host value can be overridden by value defined in translator settings.
   *
   * @param JobInterface $job
   *
   * @return Drupal\Core\GeneratedUrl|string
   */
  private function getCallbackUrl(JobInterface $job) {
    $callback_url = Url::fromRoute('tmgmt_smartling.push_callback', [
      'job' => $job
        ->id(),
    ])
      ->setOptions([
      'absolute' => TRUE,
    ])
      ->toString();
    $relative_callback_url = Url::fromRoute('tmgmt_smartling.push_callback', [
      'job' => $job
        ->id(),
    ])
      ->toString();
    $callback_url_host = rtrim($job
      ->getTranslator()
      ->getSetting('callback_url_host'), '/');
    if (!empty($callback_url_host)) {
      $callback_url = Url::fromUserInput($relative_callback_url, [
        'base_url' => $callback_url_host,
      ])
        ->toString();
    }
    return $callback_url;
  }

  /**
   * {@inheritdoc}
   */
  public function requestTranslation(JobInterface $job) {
    $name = $this
      ->getFileName($job);
    $export_format = pathinfo($name, PATHINFO_EXTENSION);
    $export = $this->formatPluginsManager
      ->createInstance($export_format);
    $path = $job
      ->getSetting('scheme') . '://tmgmt_sources/' . $name;
    $dirname = dirname($path);
    if (file_prepare_directory($dirname, FILE_CREATE_DIRECTORY)) {
      $data = $export
        ->export($job);
      $file = file_save_data($data, $path, FILE_EXISTS_REPLACE);
      $this->fileUsage
        ->add($file, 'tmgmt_smartling', 'tmgmt_job', $job
        ->id());
      $job
        ->submitted('Exported file can be downloaded <a href="@link">here</a>.', array(
        '@link' => file_create_url($path),
      ));
    }
    else {
      $e = new \Exception('It is not possible to create a directory ' . $dirname);
      watchdog_exception('tmgmt_smartling', $e);
      $job
        ->rejected('Job has been rejected with following error: @error', [
        '@error' => $e
          ->getMessage(),
      ], 'error');
    }
    try {
      $api = $this
        ->getApi($job
        ->getTranslator());
      $upload_params = new UploadFileParameters();
      $upload_params
        ->setAuthorized(0);
      $upload_params
        ->set('smartling.placeholder_format_custom', $job
        ->getSetting('custom_regexp_placeholder'));
      if ($job
        ->getSetting('auto_authorize_locales')) {
        $upload_params
          ->setLocalesToApprove($job
          ->getRemoteTargetLanguage());
      }
      if ($job
        ->getTranslator()
        ->getSetting('callback_url_use')) {
        $upload_params
          ->set('callbackUrl', $this
          ->getCallbackUrl($job));
      }
      $api
        ->uploadFile(\Drupal::service('file_system')
        ->realpath($file
        ->getFileUri()), $file
        ->getFilename(), $export_format === 'xlf' ? 'xliff' : $export_format, $upload_params);
      $this->eventDispatcher
        ->dispatch(RequestTranslationEvent::REQUEST_TRANSLATION_EVENT, new RequestTranslationEvent($job));
      Drupal::logger('tmgmt_smartling')
        ->info(t('File uploaded. Job id: @job_id, file name: @name.', [
        '@name' => $job
          ->getTranslatorPlugin()
          ->getFileName($job),
        '@job_id' => $job
          ->id(),
      ]));
    } catch (\Exception $e) {
      watchdog_exception('tmgmt_smartling', $e);
      $job
        ->rejected('Job has been rejected with following error: @error uploading @file', array(
        '@error' => $e
          ->getMessage(),
        '@file' => $file
          ->getFileUri(),
      ), 'error');
    }

    // @todo disallow to submit translation to unsupported language.
  }

  /**
   * {@inheritdoc}
   */
  public function getSupportedRemoteLanguages(TranslatorInterface $translator) {
    $languages = [];

    // Prevent access if the translator isn't configured yet.
    if (!$translator
      ->getSetting('project_id')) {

      // @todo should be implemented by an Exception.
      return $languages;
    }
    try {
      $smartling_project_details = $this
        ->getApi($translator, 'project')
        ->getProjectDetails();
      foreach ($smartling_project_details['targetLocales'] as $language) {
        $languages[$language['localeId']] = $language['localeId'];
      }
    } catch (\Exception $e) {
      Drupal::logger('tmgmt_smartling')
        ->error('Can not get languages from the translator: @message', [
        '@message' => $e
          ->getMessage(),
      ]);
      return $languages;
    }
    return $languages;
  }

  /**
   * {@inheritdoc}
   */
  public function getDefaultRemoteLanguagesMappings() {
    return array(
      'zh-hans' => 'zh-CH',
      'nl' => 'nl-NL',
      'en' => 'en-EN',
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getSupportedTargetLanguages(TranslatorInterface $translator, $source_language) {
    $remote_languages = $this
      ->getSupportedRemoteLanguages($translator);
    unset($remote_languages[$source_language]);
    return $remote_languages;
  }

  /**
   * {@inheritdoc}
   */
  public function hasCheckoutSettings(JobInterface $job) {
    return FALSE;
  }

  /**
   * @param \Drupal\tmgmt\TranslatorInterface $translator
   * @param string $api_type
   * @return \Drupal\tmgmt_smartling\Smartling\SmartlingApi
   */
  public function getApi(TranslatorInterface $translator, $api_type = 'file') {
    $api_factory = Drupal::service('tmgmt_smartling.smartling_api_factory');
    return $api_factory::create($translator
      ->getSettings(), $api_type);
  }

  /**
   * Returns file name.
   *
   * @param \Drupal\tmgmt\JobInterface $job
   * @return string
   */
  public function getFileName(JobInterface $job) {

    // TODO: identical filename task.
    // $extension = $job->getSetting('export_format');
    //
    // try {
    //   // Try to load existing file name from tmgmt_job table.
    //   $filename = $job->get('job_file_name');
    //
    //   if (!empty($filename->getValue())) {
    //     $filename = $filename->getValue()[0]['value'];
    //   }
    //   // Job item title should be included into a filename only if there is a
    //   // single JobItem in a Job. If there are 3 JobItems in a file - file name
    //   // should be "@entity_type_@entity_id>". And finally for every Job with
    //   // more than 3 JobItems - standard "JobId@id"
    //   elseif ($extension == 'xml') {
    //     $file_names = [];
    //     $job_items = $job->getItems();
    //     $job_items_count = count($job_items);
    //
    //     if ($job_items_count == 1) {
    //       $file_name_type = 'expanded';
    //     }
    //     else if ($job_items_count > 1 && $job_items_count <= 3) {
    //       $file_name_type = 'simplified';
    //     }
    //     else {
    //       $file_name_type = 'default';
    //     }
    //
    //     foreach ($job_items as $job_item) {
    //       $job_item_id = $job_item->getItemId();
    //       $job_item_type = $job_item->getItemType();
    //
    //       switch ($file_name_type) {
    //         case 'expanded':
    //           $temp_name = $job_item->getSourceLabel() . '_' . $job_item_type . '_' . $job_item_id;
    //
    //           break;
    //
    //         case 'simplified':
    //           $temp_name = $job_item_type . '_' . $job_item_id;
    //
    //           break;
    //
    //         default:
    //           $file_names[$job_item_id] = 'JobID' . $job->id() . '_' . $job->getSourceLangcode() . '_' . $job->getTargetLangcode();
    //
    //           break 2;
    //       }
    //
    //       $file_names[$job_item_id] = $temp_name;
    //     }
    //
    //     ksort($file_names);
    //     $filename = $this->cleanFileName(implode('_', $file_names) . '.' . $extension);
    //   }
    //   else {
    //     $filename = '';
    //   }
    // } catch (\Exception $e) {
    //   $filename = '';
    // }
    //
    // // Fallback to default file name.
    // if (empty($filename) || !$job->getSetting('identical_file_name')) {
    //   $filename = "JobID" . $job->id() . '_' . $job->getSourceLangcode() . '_' . $job->getTargetLangcode() . '.' . $extension;
    // }
    //
    // return $filename;
    try {
      $filename = $job
        ->get('job_file_name');
      $filename = !empty($filename
        ->getValue()) ? $filename
        ->getValue()[0]['value'] : '';
    } catch (\Exception $e) {
      $filename = '';
    }
    if (empty($filename)) {
      $extension = $job
        ->getSetting('export_format');
      $name = "JobID" . $job
        ->id() . '_' . $job
        ->getSourceLangcode() . '_' . $job
        ->getTargetLangcode();

      // Alter name before saving it into database.
      $cloned_job = clone $job;
      \Drupal::moduleHandler()
        ->alter('tmgmt_smartling_filename', $name, $cloned_job);
      $filename = $name . '.' . $extension;
    }
    return $filename;
  }

  /**
   * Return clean filename, sanitized for path traversal vulnerability.
   *
   * Url (https://code.google.com/p/teenage-mutant-ninja-turtles
   * /wiki/AdvancedObfuscationPathtraversal).
   *
   * @param string $filename
   *   File name.
   * @param bool $allow_dirs
   *   TRUE if allow dirs. FALSE by default.
   *
   * @return string
   *   Return clean filename.
   */
  private function cleanFileName($filename, $allow_dirs = FALSE) {

    // Prior to PHP 5.5, empty() only supports variables.
    // (http://www.php.net/manual/en/function.empty.php).
    $trim_filename = trim($filename);
    if (empty($trim_filename)) {
      return '';
    }
    $pattern = '/[^a-zA-Z0-9_\\-\\:]/i';
    $info = pathinfo(trim($filename));
    $filename = preg_replace($pattern, '_', $info['filename']);
    if (isset($info['extension']) && !empty($info['extension'])) {
      $filename .= '.' . preg_replace($pattern, '_', $info['extension']);
    }
    if ($allow_dirs && isset($info['dirname']) && !empty($info['dirname'])) {
      $filename = preg_replace('/[^a-zA-Z0-9_\\/\\-\\:]/i', '_', $info['dirname']) . '/' . $filename;
    }
    return (string) $filename;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultSettings() {
    return array(
      'export_format' => 'xml',
      'allow_override' => TRUE,
      'scheme' => 'public',
      'retrieval_type' => 'published',
      'callback_url_use' => FALSE,
      'callback_url_host' => '',
      'auto_authorize_locales' => TRUE,
      'xliff_processing' => TRUE,
      'context_silent_user_switching' => FALSE,
      'custom_regexp_placeholder' => '(@|%|!)[\\w-]+',
      'context_skip_host_verifying' => FALSE,
      'identical_file_name' => FALSE,
      'enable_smartling_logging' => TRUE,
      'enable_basic_auth' => FALSE,
      'basic_auth' => [
        'login' => '',
        'password' => '',
      ],
    );
  }

  /**
   * {@inheritdoc}
   */
  public function requestJobItemsTranslation(array $job_items) {

    /** @var \Drupal\tmgmt\Entity\Job $job */
    $job = reset($job_items)
      ->getJob();
    foreach ($job_items as $job_item) {

      //tmgmt_smartling_download_file($job_item->getJob());
      $this
        ->requestTranslation($job_item
        ->getJob());
      if ($job
        ->isContinuous()) {
        $job_item
          ->active();
      }
    }
  }

  /**
   * Downloads translation file and applies it.
   *
   * @param \Drupal\tmgmt\JobInterface $job
   *
   * @return bool
   */
  public function downloadTranslation(JobInterface $job) {
    return tmgmt_smartling_download_file($job);
  }

  /**
   * Checks if file is ready for download.
   *
   * @param JobInterface $job
   *
   * @return bool
   */
  public function isReadyForDownload(JobInterface $job) {
    $result = FALSE;
    try {
      $api = $this
        ->getApi($job
        ->getTranslator());
      $filename = $this
        ->getFileName($job);
      $locale = $job
        ->getRemoteTargetLanguage();
      $request_result = $api
        ->getStatus($filename, $locale);
      $authorized = intval($request_result['authorizedStringCount']);
      $completed = intval($request_result['completedStringCount']);
      $progress = $authorized + $completed > 0 ? (int) ($completed / ($authorized + $completed) * 100) : 0;
      $file_last_modified = 0;
      $job_last_modified = intval($job
        ->getChangedTime());
      $last_modified = $api
        ->lastModified($filename);
      foreach ($last_modified['items'] as $item) {
        if ($item['localeId'] == $locale) {
          $file_last_modified = $item['lastModified']
            ->getTimeStamp();
          break;
        }
      }

      // Chain of calls:
      // 1. Check status worker: SmartlingTranslator::isReadyForDownload().
      // 2. Download worker: SmartlingTranslator::isReadyForDownload() ->
      // SmartlingTranslator::downloadTranslation().
      //
      // In Drupal 7 connector we don't have second check "isReadyForDownload"
      // so there is a simpler condition - just if $file_last_modified GRATER
      // than $job_last_modified. In Drupal 8 connector we have a second call
      // of SmartlingTranslator::isReadyForDownload() so we need to take into
      // consideration this case.
      //
      // If $file_last_modified == $job_last_modified then it means that
      // this is second call of SmartlingTranslator::isReadyForDownload() for
      // this job (from JobDownload::processItem(), in other words it's happened
      // from download queue. In this case we should "lock" this check by
      // setting job's changed time as current time. Otherwise here will be
      // infinite loop of downloading process ($job_last_modified will always
      // be equal to $file_last_modified)).
      if ($file_last_modified >= $job_last_modified && $progress === 100) {

        // Case when $file_last_modified > $job_last_modified - the first check
        // "isReadyForDownload" from check status queue worker. Unlock the
        // condition for the second "isReadyForDownload" check from download
        // queue worker.
        if ($file_last_modified > $job_last_modified) {
          $job
            ->set('changed', $file_last_modified);
        }
        else {
          $job
            ->set('changed', time());
        }
        $job
          ->save();
        $result = TRUE;
      }
      else {
        Drupal::logger('tmgmt_smartling')
          ->warning(t('File @file is not ready for download.', [
          '@file' => $this
            ->getFileName($job),
        ]));
      }
    } catch (\Exception $e) {
      watchdog_exception('tmgmt_smartling', $e);
    }
    \Drupal::logger('tmgmt_smartling')
      ->info('Check status for file: @filename found @approved pending words and @completed completed ones', [
      '@filename' => $filename,
      '@approved' => @$request_result['authorizedStringCount'],
      '@completed' => @$request_result['completedStringCount'],
    ]);
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function cancelTranslation(JobInterface $job) {

    // TODO: Implement cancelTranslation() method.
  }

}

Classes

Namesort descending Description
SmartlingTranslator Smartling translator plugin.