You are here

SmartlingTranslator.php in TMGMT Translator Smartling 8

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\Entity\Translator;
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 Drupal\tmgmt_smartling\Smartling\SmartlingApi;
use GuzzleHttp\ClientInterface;
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('api_url') && $translator
      ->getSetting('project_id') && $translator
      ->getSetting('key')) {
      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();
  }

  /**
   * {@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 {
      $upload_params = [
        'approved' => 0,
        'smartling.placeholder_format_custom' => $job
          ->getSetting('custom_regexp_placeholder'),
      ];
      if ($job
        ->getSetting('auto_authorize_locales')) {
        $upload_params['localesToApprove[0]'] = $job
          ->getRemoteTargetLanguage();
      }
      if ($job
        ->getTranslator()
        ->getSetting('callback_url_use')) {
        $upload_params['callbackUrl'] = Url::fromRoute('tmgmt_smartling.push_callback', [
          'job' => $job
            ->id(),
        ])
          ->setOptions(array(
          'absolute' => TRUE,
        ))
          ->toString();
      }
      $this
        ->getSmartlingApi($job
        ->getTranslator())
        ->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));
    } 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_languages = $this
        ->getSmartlingApi($translator)
        ->getLocaleList();
      foreach ($smartling_languages['locales'] as $language) {
        $languages[$language['locale']] = $language['locale'];
      }
    } catch (\Exception $e) {
      drupal_set_message($e
        ->getMessage(), 'Cannot get languages from the translator');
      watchdog_exception('tmgmt_smartling', $e);
      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;
  }

  /**
   * Execute a request against the Smartling API.
   *
   * @param Translator $translator
   *   The translator entity to get the settings from.
   * @param $path
   *   The path that should be appended to the base uri, e.g. Translate or
   *   GetLanguagesForTranslate.
   * @param $query
   *   (Optional) Array of GET query arguments.
   * @param $headers
   *   (Optional) Array of additional HTTP headers.
   *
   * @return array
   *   The HTTP response.
   */
  protected function doRequest(Translator $translator, $path, array $query = array(), array $headers = array()) {

    // @todo We don't need it at all.
    $response = $this->smartlingApi
      ->uploadFile($path);
    return $response;
  }

  /**
   * @return \Drupal\tmgmt_smartling\Smartling\SmartlingApi
   */
  public function getSmartlingApi(TranslatorInterface $translator) {
    if (empty($this->smartlingApi)) {
      $this->smartlingApi = new SmartlingApi($translator
        ->getSetting('key'), $translator
        ->getSetting('project_id'), $this->client, $translator
        ->getSetting('api_url'));
    }
    return $this->smartlingApi;
  }

  /**
   * 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');
      $filename = "JobID" . $job
        ->id() . '_' . $job
        ->getSourceLangcode() . '_' . $job
        ->getTargetLangcode() . '.' . $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,
      '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_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) {
    try {
      $api = $this
        ->getSmartlingApi($job
        ->getTranslator());
      $filename = $this
        ->getFileName($job);
      $locale = $job
        ->getRemoteTargetLanguage();
      $request_result = $api
        ->getStatus($filename, $locale);
    } catch (\Exception $e) {
      watchdog_exception('tmgmt_smartling', $e);
      return FALSE;
    }
    \Drupal::logger('tmgmt_smartling')
      ->info('Check status for file: @filename found @approved approved words and @completed completed ones', [
      '@filename' => $filename,
      '@approved' => @$request_result['approvedStringCount'],
      '@completed' => @$request_result['completedStringCount'],
    ]);
    return $request_result['approvedStringCount'] > 0 && $request_result['approvedStringCount'] === $request_result['completedStringCount'];
  }

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

    // TODO: Implement cancelTranslation() method.
  }

}

Classes

Namesort descending Description
SmartlingTranslator Smartling translator plugin.