wordsonline_connector-1.0.x-dev/src/Plugin/tmgmt/Translator/WordsOnlineTranslator.php
src/Plugin/tmgmt/Translator/WordsOnlineTranslator.php
<?php
namespace Drupal\wordsonline_connector\Plugin\tmgmt\Translator;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\tmgmt\ContinuousTranslatorInterface;
use Drupal\tmgmt\TMGMTException;
use Drupal\tmgmt\TranslatorPluginBase;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use GuzzleHttp\ClientInterface;
use Drupal\tmgmt\JobInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Messenger\Messenger;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\File\FileSystemInterface;
use Psr\Log\LoggerInterface;
use Drupal\tmgmt_file\Format\FormatManager;
use Drupal\file\FileRepository;
use Drupal\wordsonline_connector\WordsOnlineConst;
use Drupal\wordsonline_connector\WordsOnlineMessage;
use Drupal\Core\Archiver\ArchiverManager;
use Drupal\tmgmt\JobItemInterface;
use Drupal\tmgmt\Entity\Job;
use Drupal\tmgmt\Entity\JobItem;
use Drupal\tmgmt\Data;
use Drupal\node\Entity\Node;
/**
* WordsOnline translation plugin controller.
*
* @TranslatorPlugin(
* id = "wordsonline",
* label = @Translation("WordsOnline"),
* description = @Translation("WordsOnline translator service."),
* ui = "Drupal\wordsonline_connector\WordsOnlineTranslatorUi",
* logo = "icons/wordsonline-startup.png",
* )
*/
class WordsOnlineTranslator extends TranslatorPluginBase implements ContainerFactoryPluginInterface, ContinuousTranslatorInterface {
use StringTranslationTrait;
/**
* Guzzle HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $client;
/**
* Database.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Messenger.
*
* @var \Drupal\Core\Messenger\Messenger
*/
protected $messenger;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The file format manager.
*
* @var \Drupal\tmgmt_file\Format\FormatManager
*/
protected $formatManager;
/**
* The file repository service.
*
* @var \Drupal\file\FileRepository
*/
protected $fileRepository;
/**
* Archiver.
*
* @var \Drupal\Core\Archiver\ArchiverManager
*/
protected $archiver;
/**
* The tmgmt_data service.
*
* @var \Drupal\tmgmt\Data
*/
protected $data_service;
/**
* Constructs a WordsOnlineTranslator object.
*
* @param \GuzzleHttp\ClientInterface $client
* The Guzzle HTTP client.
* @param \Drupal\Core\Database\Connection $database
* Database connection.
* @param \Drupal\Core\Messenger\Messenger $messenger
* Messenger.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* File System.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
* @param \Drupal\tmgmt_file\Format\FormatManager $formatManager
* The file format manager.
* @param \Drupal\file\FileRepository $fileRepository
* The file format manager.
* @param \Drupal\Core\Archiver\ArchiverManager $archiver
* Archiver manager.
* @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.
* @param \Drupal\tmgmt\Data $data_service
* The tmgmt data service.
*/
public function __construct(ClientInterface $client, Connection $database, Messenger $messenger, FileSystemInterface $file_system, LoggerInterface $logger, FormatManager $formatManager, FileRepository $fileRepository, ArchiverManager $archiver, array $configuration, $plugin_id, array $plugin_definition, Data $data_service) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->client = $client;
$this->database = $database;
$this->messenger = $messenger;
$this->fileSystem = $file_system;
$this->logger = $logger;
$this->formatManager = $formatManager;
$this->fileRepository = $fileRepository;
$this->archiver = $archiver;
$this->data_service = $data_service;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$client = $container->get('http_client');
$database = $container->get('database');
$messenger = $container->get('messenger');
$fileSystem = $container->get('file_system');
$logger = $container->get('logger.factory')
->get("wordsonline_connector");
$manager = $container->get('plugin.manager.tmgmt_file.format');
$fileRepository = $container->get('file.repository');
$archiver = $container->get('plugin.manager.archiver');
$data_service = $container->get('tmgmt.data');
return new static(
$client,
$database,
$messenger,
$fileSystem,
$logger,
$manager,
$fileRepository,
$archiver,
$configuration,
$plugin_id,
$plugin_definition,
$data_service
);
}
/**
* {@inheritdoc}
*
* @param array $job_items
* List job item.
*/
public function requestJobItemsTranslation(array $job_items) {
/** @var \Drupal\tmgmt\Entity\Job $job */
$job = reset($job_items)->getJob();
/** create new job if job is continuous */
if($job->isContinuous()) {
$job_created_time = $job->getCreatedTime();
$jis =[];
$source_language = $job->getSourceLangcode();
$target_language = $job->getTargetLangcode();
foreach($job_items as $ji) {
$nid = $ji->getItemId();
$node = Node::load($nid);
if ($node) {
// if node is changed before continuous job is created then return
if($node->getChangedTime()< $job_created_time){
continue;
}
//Check is re-translate a translated note but content of this note no change.
if($this->isJobItemRetranslate($ji, $source_language, $target_language) == TRUE) {
continue;
}
if ($ji->getState() != JobItemInterface::STATE_ABORTED){
array_push($jis,$ji);
}
}
}
// if there is no job item that satisfies
if(count($jis) == 0) {
$this->abortJobItems($job_items);
return;
}
$new_job = $this->cloneJob($job);
foreach($jis as $ji) {
$this->cloneJobItem($ji,$new_job->id());
}
// check have conflict job items then remove all
$conflicting_items_by_item = $new_job->getConflictingItems();
if ($conflicting_items_by_item) {
foreach ($conflicting_items_by_item as $conflicting_items) {
foreach (JobItem::loadMultiple($conflicting_items) as $id => $conflicting_item) {
$job_item_id_not_change = $this->isHasChangeOriginSource($new_job,$conflicting_item);
if($this->isHasChangeOriginSource($new_job,$conflicting_item) == 0) {
$conflicting_item->setState(JobItemInterface::STATE_ABORTED);
} else {
$ji = JobItem::load($job_item_id_not_change);
$ji->setState(JobItemInterface::STATE_ABORTED);
}
}
}
}
$is_create_request = FALSE;
$new_job_items = array_values($new_job->getItems());
foreach($new_job_items as $ji) {
if ($ji->getState() != JobItemInterface::STATE_ABORTED){
$is_create_request = TRUE;
} else {
$ji->delete();
}
}
if ($is_create_request == TRUE || $is_create_request == 1) {
try{
$this->doTranslate($new_job, TRUE);
$f_ret = $this->getWordsOnlineConnectorJobRecordByJobId($new_job->id());
if ($f_ret != NULL && $f_ret["status"] == WordsOnlineConst::JOB_CREATED ) {
//$this->abortJobItems($job_items);
$submit_message = $this->getSubmitMessage();
$new_job->submitted($submit_message);
}
}
catch (TMGMTException $e) {
$new_job->delete();
}
} else {
$new_job->delete();
}
$this->abortJobItems($job_items);
}
return $job;
}
/**
* Check is origin source changed.
*
* @param Drupal\tmgmt\JobInterface $job
* Job.
* @param Drupal\tmgmt\JobItemInterface $conflict_job_item
* The conflict job item.
*/
public function isHasChangeOriginSource(JobInterface $job,JobItemInterface $conflict_job_item){
$item_id = $conflict_job_item->getItemId();
$job_items = array_values($job->getItems());
$result = 0;
$compare_data = $this->data_service->filterTranslatable($conflict_job_item->getData());
foreach($job_items as $ji){
if ($ji->getItemId() == $item_id) {
$result = $ji->id();
foreach ($compare_data as $key => $value) {
if (is_array($value) && isset($value['#translate']) && $value['#translate']) {
$key_array = $this->data_service->ensureArrayKey($key);
try {
$new_data = $this->data_service->flatten($ji->getSourceData());
}
catch (TMGMTException $e) {
continue;
}
$current_data = $conflict_job_item->getData($key_array);
if (!isset($new_data[$key])) {
$result = 0;
break;
}
elseif (!empty($current_data['#text']) && $current_data['#text'] != $new_data[$key]['#text']) {
$result = 0;
break;
}
}
}
}
}
return $result;
}
/**
* Get job item id by item id that belongs to a finished job.
*
* @param string $item_id
* The item id.
*/
public function getJobItemIdOfFinishJobByItemId($item_id, $source_language, $target_language) {
$job_item_ids = $this->database
->query(
WordsOnlineConst::GET_JOB_ITEM_HAS_TRANSLATED_QUERY,
[
':source_language' => $source_language,
':target_language' => $target_language,
':item_id' => $item_id
]
)
->fetchAssoc();
return $job_item_ids;
}
/**
* Check is re-translate a translated note but content of this note no change.
*
* @param Drupal\tmgmt\JobItemInterface $job_item
* The job item.
* @param string $source_language
* The source language.
* @param string $target_language
* The target language.
*/
public function isJobItemRetranslate(JobItemInterface $job_item, $source_language, $target_language) {
$item_id = $job_item->getItemId();
$jid = $this->getJobItemIdOfFinishJobByItemId($item_id, $source_language, $target_language);
if($jid == null) {
return FALSE;
}
$result = FALSE;
$ji = JobItem::load($jid["tjiid"]);
if($ji) {
$compare_data = $this->data_service->filterTranslatable($ji->getData());
$result = TRUE;
foreach ($compare_data as $key => $value) {
if (is_array($value) && isset($value['#translate']) && $value['#translate']) {
$key_array = $this->data_service->ensureArrayKey($key);
try {
$new_data = $this->data_service->flatten($job_item->getSourceData());
}
catch (TMGMTException $e) {
continue;
}
$current_data = $ji->getData($key_array);
if (!isset($new_data[$key])) {
$result = FALSE;
break;
}
elseif (!empty($current_data['#text']) && $current_data['#text'] != $new_data[$key]['#text']) {
$result = FALSE;
break;
}
}
}
} else {
$result = FALSE;
}
return $result;
}
/**
* Get submit message.
*/
public function getSubmitMessage(){
$submit_message = 'The translation job has been submitted. Please go to <a href="' .
base_path() .
'admin/tmgmt/wordsonline/jobs' .
'"> WordsOnline Translation Request</a> to see your quote.';
return $submit_message;
}
/**
* Delete exist job files.
*
* @param int $job_id
* The job id
* @param int $job_item_id
* The job item id
*
*/
public function deleteExistWordsOnlineJobFileRecord($job_id,$job_item_id) {
$this->database
->query(WordsOnlineConst::DELETE_JOB_FILE_QUERY,
[
':job_id' => $job_id,
':job_item_id' => $job_item_id,
]
)
->fetchAssoc();
}
/**
* Get the token.
*/
public function getToken(){
$results = $this->database->query(WordsOnlineConst::SELECT_CONFIG)->fetchAssoc();
$token = NULL;
if ($results != NULL) {
$auth["username"] = $results["username"];
$auth["password"] = $results["password"];
$auth["scope"] = $results["scope"];
$auth["grant_type"] = $results["grant_type"];
$response = wordsonline_connector_get_token('POST', $auth);
$response = json_decode($response, TRUE);
$token = $response['access_token'];
}
else {
$this->messenger->addError(
$this->pleaseContactHelpDeskMessage()
);
}
return $token;
}
/**
* Clone a job.
*
* @param Drupal\tmgmt\JobInterface $job
* Job.
*/
public function cloneJob($job){
$source_language = $job->getSourceLangcode();
$target_language = $job->getTargetLangcode();
$new_job = Job::create([
'source_language' => $source_language,
'target_language' => $target_language,
'uid' => 0,
'settings'=>[
'project_key' => $job->getSetting('project_key'),
'content_type' => $job->getSetting('content_type'),
'service_level' => $job->getSetting('service_level'),
'auto_approve_quote' => $job->getSetting('auto_approve_quote'),
'auto_import' => $job->getSetting('auto_import'),
// 'due_date' => $dueDate,
'request_name' => wordsonline_connector_get_request_name()
],
'translator' => $job->getTranslatorId()
]);
$new_job->save();
return $new_job;
}
/**
* Clone a job item
*
* @param Drupal\tmgmt\JobItemInterface $job_item
* Job item.
* @param int $job_id
* Id of Job.
*/
public function cloneJobItem($job_item,$job_id){
$new_job_item = JobItem::create([
'plugin' => $job_item->getPlugin(),
'item_type' => $job_item->getItemType(),
'item_id' => $job_item->getItemId(),
'tjid'=>$job_id,
'data' => $job_item->getData(),
'uid' => 0
]);
$new_job_item->save();
return $new_job_item;
}
/**
* Abort all job item.
*
* @param array $job_items
* Array job item.
*/
public function abortJobItems(array $job_items) {
foreach($job_items as $ji) {
if ($ji->getState() == JobItemInterface::STATE_ABORTED) {
continue;
}
try {
$ji->setState(JobItemInterface::STATE_ABORTED);
} catch (TMGMTException $ex) {
$ji->addMessage('Cannot abort job item.');
}
}
}
/**
* Get wordsonline job by job id.
*
* @param int $job_id
* Job id
*/
public function getWordsOnlineConnectorJobRecordByJobId($job_id){
$f_ret = $this->database
->query(WordsOnlineConst::GET_WORDSONLINE_JOB_BY_JOB_ID_QUERY,
[
':job_id' => $job_id
]
)
->fetchAssoc();
return $f_ret;
}
/**
* {@inheritdoc}
*
* @param Drupal\tmgmt\JobInterface $job
* Job.
* @param bool $is_continuous
* Job is created from continuous job
*/
public function doTranslate(JobInterface $job, $is_continuous = FALSE) {
$translator = $job->getTranslator();
$translator->setSetting('xliff_processing', TRUE);
$translator->save();
$source_lang = $job->getSourceLangcode();
$target_lang = $job->getTargetLangcode();
$request_name = $job->getSetting('request_name');
$projectId = $job->getSetting('project_key');
if ($projectId == NULL) {
$projectId = '';
}
$results = $this->database->query('SELECT code,wol_code FROM wordsonline_connector_support_languges')->fetchAll();
foreach ($results as $row) {
if ($row->code == $source_lang) {
$source_lang = $row->wol_code;
}
if ($row->code == $target_lang) {
$target_lang = $row->wol_code;
}
}
$params['request_name'] = $request_name;
$params['projectId'] = $projectId;
$params['sourceLanguage'] = $source_lang;
$params['targetLanguages'] = $target_lang;
$params['contentTypeId'] = $job->getSetting('content_type');
$params['serviceLevel'] = $job->getSetting('service_level');
$params['dueDate'] = $job->getSetting('due_date');
$auto_approve_quote = $job->getSetting('auto_approve_quote');
$params['auto_approve_quote'] = $auto_approve_quote == TRUE || $auto_approve_quote == 1 ? 'true' : 'false';
$xliff = $this->formatManager->createInstance('xlf', ['target' => 'source']);
$xliff_content = $xliff->export($job);
$file_zip = "Job{$job->id()}.zip";
$job_items = array_values($job->getItems());
$zip_files = [];
$path = "public://";
foreach ($job_items as $job_item) {
if ($job_item->getState() == JobItemInterface::STATE_ABORTED){
continue;
}
$job_item_id = $job_item->id();
$xliff = $this->formatManager->createInstance('xlf', ['target' => 'source']);
$conditions = ['tjiid' => ['value' => $job_item_id]];
$xliff_content = $xliff->export($job, $conditions);
// Build a file name.
$file_name = "Job_{$job->id()}_{$job_item_id}.xlf";
array_push($zip_files, "public://Job_{$job->id()}_{$job_item_id}.xlf");
if ($this->fileSystem->prepareDirectory(
$path,
FileSystemInterface::CREATE_DIRECTORY
)
) {
$version = (float) (str_replace("-dev", "", \Drupal::VERSION));
if ($version >= 9.3) {
$this->fileRepository->writeData(
$xliff_content,
$path . $file_name
);
}
else {
file_save_data(
$xliff_content,
$path . $file_name
);
}
}
$fields["job_id"] = $job->id();
$fields["job_item_id"] = $job_item_id;
$fields["file_name"] = $file_name;
$fields["file_path"] = $path . $file_name;
$fields["source_language"] = $source_lang;
$fields["target_language"] = $target_lang;
$this->deleteExistWordsOnlineJobFileRecord($job->id(), $job_item_id);
$this->database->insert('wordsonline_connector_job_files')
->fields($fields)->execute();
if ($job_item->getJob()->isContinuous()) {
$job_item->active();
}
}
$this->createZip($zip_files, $path . $file_zip);
$f_ret = $this->getWordsOnlineConnectorJobRecordByJobId($job->id());
if ($f_ret == NULL) {
$orders["job_id"] = $job->id();
$orders["file_guid"] = '';
$orders["request_guid"] = '';
$orders["request_name"] = $request_name;
$orders["project_guid"] = $projectId;
$orders["status"] = WordsOnlineConst::JOB_NEW;
$this->database->insert(WordsOnlineConst::JOB_TABLE)
->fields($orders)->execute();
}else{
$this->database->update(WordsOnlineConst::JOB_TABLE)
->condition('job_id', $job_id)
->fields(
[
'status' => WordsOnlineConst::JOB_NEW,
'request_guid' => '',
'request_name' => $request_name,
'project_guid' => $projectId,
'file_guid' => ''
]
)
->execute();
}
$params['job_id'] = $job->id();
$token = $this->getToken();
$is_created = FALSE;
if ($token != NULL) {
$files = $this->uploadFiles($path . $file_zip, $token);
if ($files["status"] >= 0) {
$params['fileList'] = $files["result"];
$is_created = $this->createOrder(WordsOnlineConst::CREATE_REQUEST_URL, 'POST', $params, $token, $is_continuous);
}
}
// if ($is_created == FALSE) {
// $this->fileSystem->delete($path . $file_zip);
// return;
// }
$this->fileSystem->delete($path . $file_zip);
}
/**
* Get error message when can't call wordsonline api.
*
*/
public function pleaseContactHelpDeskMessage(){
return $this->t(
'Cannot find WordsOnline provider settings.
Please contact <a href="mailto:@url">@url<a/>
to obtain WordsOnline provider settings.',
[
"@url" => WordsOnlineConst::HELPDESK_MAIL,
]
);
}
/**
* {@inheritdoc}
*
* @param Drupal\tmgmt\JobInterface $job
* Job.
*/
public function requestTranslation(JobInterface $job) {
try {
$this->doTranslate($job);
$submit_message = $this->getSubmitMessage();
$job->submitted($submit_message);
//$this->fileSystem->delete($path . $file_zip);
}
catch (TMGMTException $e) {
$job->rejected( WordsOnlineMessage::JOB_REJECT_MESSAGE, ['@error' => $e->getMessage()], 'error');
}
}
/**
* Creates a compressed zip file.
*
* @param array $files
* The path file array.
* @param string $destination
* The target zip path.
* @param bool $overwrite
* Is Overite.
*
* @return bool
* Check is succeess.
*/
private function createZip(array $files = [], $destination = '', $overwrite = FALSE) {
if (file_exists($destination) && !$overwrite) {
$overwrite = TRUE;
}
$valid_files = [];
if (is_array($files)) {
foreach ($files as $file) {
if (file_exists($file)) {
$valid_files[] = $file;
}
}
}
if (count($valid_files)) {
$zip = new \ZipArchive();
$destination = $this->fileSystem->realpath($destination);
if ($zip->open($destination, $overwrite ? \ZipArchive::OVERWRITE : \ZipArchive::CREATE) !== TRUE) {
return FALSE;
}
foreach ($valid_files as $file) {
$realFile = $this->fileSystem->realpath($file);
$zip->addFile($realFile, str_replace("public://", "", $file));
}
$zip->close();
return file_exists($destination);
}
else {
return FALSE;
}
}
/**
* Get quote.
*
* @param string $file_path
* Path of file.
* @param string $token
* Token.
*
* @return string
* Receive data.
*/
public function uploadFiles($file_path, $token) {
$url = WordsOnlineConst::API_URL . 'Files';
$options = [];
$options['headers'] = [
'Authorization' => 'Bearer ' . $token,
'Referer' => 'ClientAPI',
];
$options['multipart'] = [
[
'name' => 'Source',
'filename' => $file_path,
'contents' => fopen($file_path, 'r'),
'headers' => [
'Content-Type' => '<Content-type header>',
],
]
];
$options['timeout'] = 3600;
$result = $this->client->post($url, $options);
if ($result->getStatusCode() == 200) {
$data = json_decode($result->getBody()->getContents(), TRUE);
return $data;
}
else {
return new WordsOnlineResponse(NULL, -1, "B0001", "error");
}
}
/**
* Create WordsOnline order.
*
* @param string $path
* The path of api.
* @param string $method
* The method is used.
* @param array $params
* List params.
* @param string $token
* The token for authorization.
*
* @return bool
* Check is succeess.
*/
public function createOrder($path, $method = 'POST', array $params = [], $token = NULL,$is_continuous = FALSE) {
$options = [];
$job_id = $params['job_id'];
$url = WordsOnlineConst::API_URL . $path;
try {
$json =[];
if($is_continuous){
$json = [
'fileList' => $params['fileList'],
'requestName' => $params['request_name'],
'projectId' => $params['projectId'],
'sourceLanguage' => $params['sourceLanguage'],
'targetLanguages' => [
$params['targetLanguages'],
],
'contentTypeId' => $params['contentTypeId'],
'serviceLevel' => $params['serviceLevel'],
'description' => 'Request from drupal',
"clientRequestId" => "Default ID",
"isAutoApprove" => $params['auto_approve_quote'],
];
}else {
$json = [
'fileList' => $params['fileList'],
'requestName' => $params['request_name'],
'projectId' => $params['projectId'],
'sourceLanguage' => $params['sourceLanguage'],
'targetLanguages' => [
$params['targetLanguages'],
],
'contentTypeId' => $params['contentTypeId'],
'serviceLevel' => $params['serviceLevel'],
'description' => 'Request from drupal',
"clientRequestId" => "Default ID",
"isAutoApprove" => $params['auto_approve_quote'],
"dueDate" => $params['dueDate'],
];
}
$response = $this->client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
'Referer' => 'ClientAPI',
],
'json' => $json,
'timeout' => 3600,
]
);
}
catch (RequestException $e) {
$this->logger->error($e->getMessage());
$this->messenger->addError(WordsOnlineMessage::FAILED_TO_CREATE_NEW_ORDER);
return FALSE;
}
$received_data = $response->getBody()->getContents();
$err_msg = WordsOnlineMessage::FAILED_TO_CREATE_NEW_ORDER;
if ($response->getStatusCode() != 200) {
$this->messenger->addError(WordsOnlineMessage::FAILED_TO_CREATE_NEW_ORDER);
return FALSE;
}
$orderResponse = json_decode($received_data, TRUE);
if ($orderResponse["status"] == 1) {
$this->database->update(WordsOnlineConst::JOB_TABLE)
->condition('job_id', $job_id)
->fields(
[
'status' => WordsOnlineConst::JOB_CREATED,
'request_guid' => $orderResponse["result"],
]
)
->execute();
$this->messenger->addMessage(WordsOnlineMessage::NEW_ORDER_CREATED);
return TRUE;
}
else {
if ($orderResponse != NULL && $orderResponse['message'] != NULL) {
$err_msg = $err_msg . "\r\n" . $orderResponse['message'];
}
$this->messenger->addError($err_msg);
}
return FALSE;
}
}
