lionbridge_translation_provider-8.x-2.4/tmgmt_contentapi/src/Services/AnalysisCodeApi.php
tmgmt_contentapi/src/Services/AnalysisCodeApi.php
<?php
namespace Drupal\tmgmt_contentapi\Services;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface;
use Drupal\tmgmt\TranslatorInterface;
/**
* Class AnalysisCodeApi.
*
* This class provides methods to interact with the analysis code configuration
* for the TMGMT Content API module.
*/
class AnalysisCodeApi {
/**
* Guzzle client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $client;
/**
* Logger Factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* Configuration Factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Key-Value Store.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $kvStore;
/**
* AnalysisCodeClient constructor.
*
* @param \GuzzleHttp\ClientInterface $client
* The Guzzle HTTP client.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory service.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $kv_store
* The key-value store service.
*/
public function __construct($client, $logger_factory, $config_factory, $kv_store) {
$this->client = $client;
$this->loggerFactory = $logger_factory;
$this->configFactory = $config_factory;
$this->kvStore = $kv_store;
}
/**
* Factory method to create an instance of AnalysisCodeApi.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The service container.
*
* @return static
* A new instance of AnalysisCodeApi.
*/
public static function create($container) {
return new static(
$container->get('http_client'),
$container->get('logger.factory'),
$container->get('config.factory'),
$container->get('keyvalue.expirable'),
);
}
/**
* Get API credentials and endpoint from configuration.
*
* @return array
* Associative array with keys: freeway_auth_url, freeway_service_url, api_login_name, api_login_password, analysis_code.
*/
public function getApiConfig() {
$analysis_settings = $this->configFactory->get('tmgmt.translator.contentapi')->get('settings.code-analysis-settings');
return [
'freeway_auth_url' => $analysis_settings['freeway_auth_url'] ?? 'https://fwapi.lionbridge.com/Obvibundles/freewayauth.asmx',
'freeway_service_url' => $analysis_settings['freeway_service_url'] ?? 'https://fwapi.lionbridge.com/Obvibundles/freewayservice.asmx',
'analysis_code_username' => $analysis_settings['analysis_code_username'] ?? '',
'analysis_code_password' => $analysis_settings['analysis_code_password'] ?? '',
'analysis_code' => $analysis_settings['code_group'] ?? '',
];
}
/**
* Authenticates with FreewayAuth SOAP API and retrieves a ticket.
*
* @param array $freeway_auth_creds
* Optional array of credentials to override the default configuration.
*
* @return string|null
* The authentication ticket, or NULL on failure.
*/
protected function getApiTicketFromConfig($freeway_auth_creds = []) {
// If credentials are provided, use them; otherwise, use the config.
if (!empty($freeway_auth_creds)) {
$api = $freeway_auth_creds;
}
else {
// Get API configuration from the service.
$api = $this->getApiConfig();
}
$url = $api['freeway_auth_url'];
$username = $api['analysis_code_username'];
$password = $api['analysis_code_password'];
$soapBody = sprintf(<<< XML
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Logon xmlns="http://tempuri.org/">
<Username>%s</Username>
<Password>%s</Password>
</Logon>
</soap:Body>
</soap:Envelope>
XML,
htmlspecialchars($username),
htmlspecialchars($password)
);
try {
$response = $this->soapRequestWithRetry('Logon', $url, $soapBody, 'POST', 3, 1000);
// Check if the response is valid.
if ($response === NULL) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Logon: Failed to get response from SOAP API.');
return NULL;
}
$body = (string) $response->getBody();
$xml = @simplexml_load_string($body);
if ($xml === FALSE) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Failed to parse SOAP response for ticket.');
return NULL;
}
$xml->registerXPathNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
$resultNodes = $xml->xpath('//soap:Body/soap:LogonResponse/soap:LogonResult');
if (!empty($resultNodes)) {
return (string) $resultNodes[0];
}
$resultNodes = $xml->xpath('//*[local-name()="LogonResult"]');
if (!empty($resultNodes)) {
return (string) $resultNodes[0];
}
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('No ticket found in SOAP response.');
return NULL;
}
catch (\Exception $e) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Ticket SOAP API call failed: @msg', ['@msg' => $e->getMessage()]);
return NULL;
}
}
/**
* Calls the Freeway SOAP API to fetch customer analysis codes using Guzzle.
*
* @param string $ticket
* The authentication ticket.
* @param string $analysisCodeLabel
* The analysis code label to filter.
*
* @return array|null
* Parsed analysis code groups or NULL on failure.
*/
protected function fetchCustomerAnalysisCodes(string $ticket, string $analysisCodeLabel = '') {
$api = $this->getApiConfig();
if ($analysisCodeLabel != '') {
$analysisCodeLabel = htmlspecialchars($analysisCodeLabel);
}
$url = $api['freeway_service_url'];
$soapBody = sprintf(<<<XML
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetCustomerAnalysisCodes xmlns="http://tempuri.org/">
<Ticket>%s</Ticket>
<AnalysisCodeLabel>%s</AnalysisCodeLabel>
</GetCustomerAnalysisCodes>
</soap:Body>
</soap:Envelope>
XML,
htmlspecialchars($ticket),
$analysisCodeLabel
);
try {
$response = $this->soapRequestWithRetry('GetCustomerAnalysisCodes', $url, $soapBody, 'POST', 3, 1000);
// Check if the response is valid.
if ($response === NULL) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('GetCustomerAnalysisCodes: Failed to get response from SOAP API.');
return NULL;
}
$body = (string) $response->getBody();
$xml = @simplexml_load_string($body);
if ($xml === FALSE) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Failed to parse SOAP response.');
return NULL;
}
$xml->registerXPathNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
$xml->registerXPathNamespace('t', 'http://tempuri.org/');
$resultNodes = $xml->xpath('//soap:Body//t:AnalysisCodeGroup');
$groups = [];
foreach ($resultNodes as $group) {
$level = (string) $group['Level'];
$name = (string) $group['Name'];
$codes = [];
if (isset($group->AnalysisCodes->AnalysisCode)) {
foreach ($group->AnalysisCodes->AnalysisCode as $code) {
$codes[] = (array) $code;
}
}
$groups[] = [
'level' => $level,
'name' => $name,
'codes' => $codes,
];
}
return $groups;
}
catch (\Exception $e) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('SOAP API call failed: @msg', ['@msg' => $e->getMessage()]);
return NULL;
}
}
/**
* Checks if the given ticket is still valid using CheckTicketExpiry SOAP API.
*
* @param string $ticket
* The authentication ticket.
*
* @return bool
* TRUE if the ticket is valid for at least 10 minutes, FALSE otherwise.
*/
protected function isTicketValid($ticket) {
$api = $this->getApiConfig();
$url = $api['freeway_auth_url'] ?: 'https://fwapi.staging.lionbridge.com/Obvibundles/freewayauth.asmx';
$soapBody = sprintf(<<<XML
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">'
<soap:Body>
<CheckTicketExpiry xmlns="http://tempuri.org/">
<Ticket>%s</Ticket>
</CheckTicketExpiry>
</soap:Body>
</soap:Envelope>
XML,
htmlspecialchars($ticket)
);
// Implement retry logic with exponential backoff
// and error handling for the SOAP request.
// Maximum number of retry attempts.
$maxRetries = 3;
// Delay between retries in milliseconds (1000ms = 1 second)
$retryDelay = 1000;
$attempt = 0;
$response = NULL;
try {
$response = $this->soapRequestWithRetry('CheckTicketExpiry', $url, $soapBody, 'POST', 3, 1000);
if ($response === NULL) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('CheckTicketExpiry: Failed to get response from SOAP API.');
return NULL;
}
$body = (string) $response->getBody();
$xml = @simplexml_load_string($body);
if ($xml === FALSE) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Failed to parse SOAP response for ticket expiry.');
return FALSE;
}
$xml->registerXPathNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
$resultNodes = $xml->xpath('//soap:Body/soap:CheckTicketExpiryResponse/soap:CheckTicketExpiryResult');
if (!empty($resultNodes)) {
$minutes = (int) $resultNodes[0];
// Consider valid if more than 10 minutes left.
return $minutes > 10;
}
$resultNodes = $xml->xpath('//*[local-name()="CheckTicketExpiryResult"]');
if (!empty($resultNodes)) {
$minutes = (int) $resultNodes[0];
return $minutes > 10;
}
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('No expiry info found in SOAP response.');
return FALSE;
}
catch (\Exception $e) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Ticket expiry SOAP API call failed: @msg', ['@msg' => $e->getMessage()]);
return FALSE;
}
}
/**
* Gets a valid ticket, reusing the last one if still valid, else fetches a new one.
*
* @param array $freeway_auth_creds
* Optional array of credentials to override the default configuration.
* @param bool $force_new_ticket
* If TRUE, forces the retrieval of a new ticket even if the existing one is still valid.
*
* @return string|null
* The valid authentication ticket, or NULL on failure.
*/
public function getValidApiTicket($freeway_auth_creds = [], $force_new_ticket = FALSE) {
$kv_store = $this->kvStore->get('lionbridge_translation_provider');
$analysis_ticket = $kv_store->get('analysis_ticket', NULL);
$expire_analysis_ticket_time = $kv_store->get('analysis_ticket_expiration', 0);
$current_time = time();
// Calculate the time remaining until the ticket expires.
if ($expire_analysis_ticket_time) {
$time_remaining = $expire_analysis_ticket_time - $current_time;
}
// If the ticket null generate new one. Else return existing ticket.
// If ticket suppose expire in 10 minutes, then get new ticket.
if ($analysis_ticket == NULL || $time_remaining < 600 || $force_new_ticket) {
// If the ticket is null or about to expire in less than 10 minutes, fetch a new one.
$analysis_ticket = $this->getApiTicketFromConfig($freeway_auth_creds);
// Set expiration time to 1 hour from now.
$expiration_time = time() + 3600;
// Store the new ticket and expiration time in the key-value store.
$this->kvStore->get('lionbridge_translation_provider')->setWithExpire('analysis_ticket', $analysis_ticket, 3600);
// Store the expiration time for the ticket.
$this->kvStore->get('lionbridge_translation_provider')->setWithExpire('analysis_ticket_expiration', $expiration_time, 3600);
// Log the new ticket retrieval.
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->info('Retrieved new analysis ticket');
}
return $analysis_ticket;
}
/**
* Function to make a SOAP request with retry logic.
*
* @param string $method_name
* The name of the SOAP method to call.
* @param string $url
* The URL of the SOAP service endpoint.
* @param string $soapBody
* The SOAP body to send in the request.
* @param string $method
* The HTTP method to use (default is 'POST').
* @param int $maxRetries
* The maximum number of retry attempts (default is 3).
* @param int $retryDelay
* The initial delay between retries in milliseconds (default is 1000).
*
* @return \Psr\Http\Message\ResponseInterface|
* Returns the response from the SOAP request, or NULL on failure.
*/
protected function soapRequestWithRetry($method_name, $url, $soapBody, $method = 'POST', $maxRetries = 3, $retryDelay = 1000) {
$response = NULL;
$attempt = 0;
while ($attempt < $maxRetries) {
try {
$response = $this->client->request($method, $url, [
'headers' => [
'Content-Type' => 'text/xml; charset=utf-8',
],
'body' => $soapBody,
'timeout' => 30,
]);
// Exit loop if request is successful.
break;
}
catch (\Exception $e) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('@method_name SOAP API call failed: @msg',
[
'@method_name' => $method_name,
'@msg' => $e->getMessage()
]);
$attempt++;
if ($attempt < $maxRetries) {
// Convert milliseconds to microseconds.
usleep($retryDelay * 1000);
// Exponential backoff.
$retryDelay *= 2;
}
}
}
return $response;
}
/**
* Function to test the connection to the SOAP API. with provided crdentials.
*/
public function testFreewayConnection($freeway_auth_creds = [], $force_new_ticket = FALSE) {
$ticket = $this->getValidApiTicket($freeway_auth_creds, $force_new_ticket);
if (!$ticket) {
return FALSE;
}
return TRUE;
}
/**
* Parses the analysis codes from the API response.
*
* @param array $freeway_auth_creds
*
* @param bool $raw
* If TRUE, returns the raw analysis codes without parsing.
*
* @return array
* Parsed analysis codes grouped by level and label.
*/
public function freewayAnalysisCodes($freeway_auth_creds = [], $raw = FALSE) {
if(empty($freeway_auth_creds)) {
// If no credentials are provided, use the default configuration.
$freeway_auth_creds = $this->getApiConfig();
}
$ticket = $this->getValidApiTicket($freeway_auth_creds);
if (!$ticket) {
$this->loggerFactory->get('TMGMT_CONTENTAPI_ANALYSIS_CODE')->error('Failed to retrieve a valid API ticket.');
return [];
}
// If raw is true, return the raw analysis codes without parsing.
$analysisCodes = $this->fetchCustomerAnalysisCodes($ticket);
if ($raw) {
return $analysisCodes;
}
// Initialize an empty array to hold the parsed codes.
if (empty($analysisCodes)) {
return [];
}
$parsedCodes = [];
foreach ($analysisCodes as $codeGroup) {
if (isset($codeGroup['level']) && isset($codeGroup['name']) && isset($codeGroup['codes']) && is_array($codeGroup['codes'])) {
$label = $codeGroup['name'];
$level = 'level_' . $codeGroup['level'];
$values = [];
foreach ($codeGroup['codes'] as $code) {
if (isset($code['@attributes']['Name'])) {
$values[] = $code['@attributes']['Name'];
}
}
$parsedCodes[$level][$label] = $values;
}
}
return $parsedCodes ?? ['level_1' => [], 'level_2' => [], 'level_3' => []];
}
/**
* Function to generate a form element of type 'select' with analysis codes.
*
* @param \Drupal\tmgmt\TranslatorInterface $translator
* Translator object to get code analysis settings.
*/
public function verifyAndGenerateAnalysisCodeSelect(TranslatorInterface $translator) {
// Get code analysis settings from translator.
$code_analysis_settings = $translator->getSetting('code-analysis-settings');
// Validate required settings are present.
if (empty($code_analysis_settings['freeway_auth_url']) ||
empty($code_analysis_settings['freeway_service_url']) ||
empty($code_analysis_settings['analysis_code_username']) ||
empty($code_analysis_settings['analysis_code_password']) ||
!isset($code_analysis_settings['code_group']) ||
!is_array($code_analysis_settings['code_group'])) {
return [];
}
// If code_group all false, return empty array.
$all_visibility_false = TRUE;
foreach ($code_analysis_settings['code_group'] as $selected) {
if ($selected) {
$all_visibility_false = FALSE;
break;
}
}
if ($all_visibility_false) {
return [];
}
// Prepare credentials for API authentication.
$freeway_auth_creds = [
'freeway_auth_url' => $code_analysis_settings['freeway_auth_url'],
'freeway_service_url' => $code_analysis_settings['freeway_service_url'],
'analysis_code_username' => $code_analysis_settings['analysis_code_username'] ?? '',
'analysis_code_password' => $code_analysis_settings['analysis_code_password'] ?? '',
'analysis_code' => $code_analysis_settings['code_group'] ?? [],
];
// Fetch analysis codes from the API.
$analysis_codes = $this->freewayAnalysisCodes($freeway_auth_creds);
if (empty($analysis_codes)) {
return [];
}
// Build options array from analysis codes.
$options = [];
foreach ($analysis_codes as $level => $codes) {
foreach ($codes as $label => $values) {
foreach ($values as $value) {
$options[$level][$label][$value] = $value;
}
}
}
// Filter options based on selected code groups.
$code_group = $code_analysis_settings['code_group'];
if (is_array($code_group)) {
foreach ($code_group as $level => $selected) {
if (!$selected) {
unset($options[$level]);
}
}
}
return $options;
}
function jsonToDynamicTable($data) {
if(empty($data)) {
return '';
}
$return_string = 'Freeway testing --- passed';
foreach ($data as $key => $value) {
if (is_array($value)) {
foreach ($value as $sub_key => $sub_value) {
$sub_string = $sub_key . ', [' . implode(', ', $sub_value) . ']';
}
$return_string .= "\n" .$key . ': ' . $sub_string;
} else {
$return_string .= "\n" .$key . ': ' . $sub_string;
}
}
return $return_string;
}
}
