tmgmt_smartling-8.x-4.11/src/Context/ContextUploader.php
src/Context/ContextUploader.php
<?php
namespace Drupal\tmgmt_smartling\Context;
use Drupal\Core\File\FileSystemInterface;
use Drupal;
use Drupal\tmgmt_smartling\Exceptions\EmptyContextParameterException;
use Drupal\tmgmt_smartling\Smartling\SmartlingApiWrapper;
use Exception;
use Psr\Log\LoggerInterface;
use Smartling\Context\Params\MatchContextParameters;
use Smartling\Context\Params\UploadContextParameters;
use Smartling\Context\Params\UploadResourceParameters;
use Smartling\Exceptions\SmartlingApiException;
class ContextUploader {
/**
* @var TranslationJobToUrl
*/
protected $urlConverter;
/**
* @var ContextCurrentUserAuth
*/
protected $authenticator;
/**
* @var HtmlAssetInliner
*/
protected $assetInliner;
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @var SmartlingApiWrapper
*/
protected $apiWrapper;
const FILE_SIZE_LIMIT = 1024 * 1024 * 20;
public function __construct(
SmartlingApiWrapper $api_wrapper,
TranslationJobToUrl $url_converter,
ContextUserAuth $auth,
HtmlAssetInliner $html_asset_inliner,
LoggerInterface $logger
) {
$this->apiWrapper = $api_wrapper;
$this->urlConverter = $url_converter;
$this->authenticator = $auth;
$this->assetInliner = $html_asset_inliner;
$this->logger = $logger;
}
public function jobItemToUrl($job_item) {
return $this->urlConverter->convert($job_item);
}
/**
* @param $url
* @param array $settings
* @param bool $debug
*
* @return mixed|string|void
* @throws \Drupal\tmgmt_smartling\Exceptions\EmptyContextParameterException
* @throws Exception
*/
public function getContextualizedPage($url, array $settings, $debug = FALSE) {
if (empty($url)) {
throw new EmptyContextParameterException('Context url must be a non-empty string.');
}
$username = $settings['contextUsername'];
if (empty($username)) {
$username = $this->authenticator->getCurrentAccount()->getAccountName();
}
// Override URL host if context_url_host is configured
$url = $this->applyContextUrlHost($url, $settings);
// Generate a one-time login URL for the context user
$login_url = $this->authenticator->generateOneTimeLoginUrl($username);
// Ensure login URL uses same protocol and host as target URL to avoid cookie domain issues
// This handles cases where Drush is run without --uri and generates 'default' as hostname
$url_parts = parse_url($url);
$login_url_parts = parse_url($login_url);
if ($url_parts['scheme'] !== $login_url_parts['scheme'] || $url_parts['host'] !== $login_url_parts['host']) {
$login_url = $url_parts['scheme'] . '://' . $url_parts['host'] . $login_url_parts['path'];
if (!empty($login_url_parts['query'])) {
$login_url .= '?' . $login_url_parts['query'];
}
}
// Login under cntext user and catch cookies into cookie jar.
$this->assetInliner->getUrlContents($login_url, 0,
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10',
$settings,
$debug
);
// Get entity's context page.
$html = $this->assetInliner->getCompletePage($url, FALSE, $settings, $debug);
$html = str_replace('<p></p>', "\n", $html);
if (empty($html)) {
throw new Exception("Got empty context for $url url.");
}
return $html;
}
/**
* @param string $url
* @param string $filename
* @param array $proj_settings
* @return bool
* @throws \Drupal\tmgmt_smartling\Exceptions\EmptyContextParameterException
*/
public function upload($url, $filename = '', $proj_settings = []) {
$response = [];
$api_wrapper = $this->getApiWrapper($proj_settings);
if (empty($url)) {
$api_wrapper->createFirebaseRecord("tmgmt_smartling", "notifications", 10, [
"message" => t('Context upload failed: context url is empty.'),
"type" => "error",
]);
throw new EmptyContextParameterException('Context url must be a non-empty field.');
}
$smartling_context_directory = $proj_settings['scheme'] . '://tmgmt_smartling_context';
$smartling_context_file = $smartling_context_directory . '/' . str_replace('.', '_', $filename) . '.html';
$error_message = t(
'Error while uploading context for file @filename. See logs for more info.',
['@filename' => $filename]
)->render();
// Upload context body.
try {
$html = $this->getContextualizedPage($url, $proj_settings);
// Save context file.
if (\Drupal::service('file_system')->prepareDirectory($smartling_context_directory, FileSystemInterface::CREATE_DIRECTORY) &&
($file = \Drupal::service('file.repository')->writeData($html, $smartling_context_file, FileSystemInterface::EXISTS_REPLACE))
) {
$response = $this->uploadContextBody($url, $file, $proj_settings, $filename);
$this->uploadContextMissingResources($smartling_context_directory, $proj_settings);
if (!empty($response)) {
$this->logger->info('Context upload for file @filename completed successfully.', ['@filename' => $filename]);
$api_wrapper->createFirebaseRecord("tmgmt_smartling", "notifications", 10, [
"message" => t(
'Context upload for file @filename completed successfully.',
['@filename' => $filename]
)->render(),
"type" => "status",
]);
}
else {
$api_wrapper->createFirebaseRecord("tmgmt_smartling", "notifications", 10, [
"message" => $error_message,
"type" => "error",
]);
}
// Store content in state for testing before deletion.
if (\Drupal::moduleHandler()->moduleExists('tmgmt_smartling_test')) {
$state_key = 'tmgmt_smartling_test.context_html.' . str_replace('.', '_', $filename);
\Drupal::state()->set($state_key, $html);
}
$file->delete();
}
else {
$this->logger->error("Can't save context file: @path", [
'@path' => $smartling_context_file,
]);
$api_wrapper->createFirebaseRecord("tmgmt_smartling", "notifications", 10, [
"message" => $error_message,
"type" => "error",
]);
}
} catch (Exception $e) {
$this->logger->error($e->getMessage());
$api_wrapper->createFirebaseRecord("tmgmt_smartling", "notifications", 10, [
"message" => $error_message,
"type" => "error",
]);
}
return $response;
}
/**
* @param $proj_settings
*
* @return mixed
*/
protected function getApiWrapper($proj_settings) {
$this->apiWrapper->setSettings($proj_settings);
return $this->apiWrapper;
}
/**
* @param $url
* @param $file
* @param $proj_settings
* @param null $content_filename
* @return array
*/
protected function uploadContextBody($url, $file, $proj_settings, $content_filename = NULL) {
try {
// Override URL host if context_url_host is configured
$url = $this->applyContextUrlHost($url, $proj_settings);
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager')->getViaUri($file->getFileUri());
$api = $this->getApiWrapper($proj_settings)->getApi('context');
$match_params = new MatchContextParameters();
$match_params->setContentFileUri($content_filename);
$match_params->setOverrideContextOlderThanDays(0);
$upload_params_with_matching = new UploadContextParameters();
$upload_params_with_matching->setContent($stream_wrapper_manager->realpath());
$upload_params_with_matching->setName($url);
$upload_params_with_matching->setMatchParams($match_params);
$api->uploadAndMatchContext($upload_params_with_matching);
$response = TRUE;
} catch (Exception $e) {
$response = [];
if (str_starts_with('Async operation is not completed after', $e->getMessage())) {
$this->logger->warning($e->getMessage());
} else {
$this->logger->error($e->getMessage());
}
}
return $response;
}
/**
* @param $smartling_context_directory
* @param $proj_settings
*/
protected function uploadContextMissingResources($smartling_context_directory, $proj_settings) {
// Cache for resources which we can't upload. Do not try to re-upload them
// for 1 hour. After 1 hour cache will be reset and we will try again.
$cache_name = 'smartling_context_resources_cache';
$time_to_live = 60 * 60;
$two_days = 2 * 24 * 60 * 60;
$cache = \Drupal::cache()->get($cache_name);
$cached_data = empty($cache) ? [] : $cache->data;
$update_cache = FALSE;
$smartling_context_resources_directory = $smartling_context_directory . '/resources';
// Do nothing if directory for resources isn't accessible.
if (!\Drupal::service('file_system')->prepareDirectory($smartling_context_resources_directory, FileSystemInterface::CREATE_DIRECTORY)) {
$this->logger->error("Context resources directory @dir doesn't exist or is not writable. Missing resources were not uploaded. Context might look incomplete.", [
'@dir' => $smartling_context_directory,
]);
return;
}
try {
$api = $this->getApiWrapper($proj_settings)->getApi('context');
$time_out = PHP_SAPI == 'cli' ? 300 : 30;
$start_time = time();
do {
$delta = time() - $start_time;
if ($delta > $time_out) {
throw new SmartlingApiException(vsprintf('Not all context resources are uploaded after %s seconds.', [$delta]));
}
$all_missing_resources = $api->getAllMissingResources();
// Method getAllMissingResources can return not all missing resources
// in case it took to much time. Log this information.
if (!$all_missing_resources['all']) {
$this->logger->warning('Not all missing context resources are received. Context might look incomplete.');
}
$fresh_resources = [];
foreach ($all_missing_resources['items'] as $item) {
if (!in_array($item['resourceId'], $cached_data)) {
$fresh_resources[] = $item;
}
}
$urls_to_fetch = [];
foreach ($fresh_resources as $item) {
if ((time() - strtotime($item['created'])) < $two_days
&& !in_array($item['resourceId'], $cached_data)
) {
$urls_to_fetch[$item['resourceId']] = $item['url'];
}
}
$results = $this->assetInliner->getUrlContentsPooled(
$urls_to_fetch,
0,
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10',
$proj_settings
);
// Walk through missing resources and try to upload them.
foreach ($fresh_resources as $item) {
if ((time() - strtotime($item['created'])) >= $two_days) {
$update_cache = TRUE;
$cached_data[] = $item['resourceId'];
continue;
}
if (isset($results[$item['resourceId']]) && $results[$item['resourceId']] !== -1) {
$smartling_context_resource_file = $smartling_context_resources_directory . '/' . $item['resourceId'];
$smartling_context_resource_file_content = $results[$item['resourceId']];
// Ensure that resources directory is accessible, resource
// downloaded properly and only then upload it. ContextAPI will not
// be able to fopen() resource which is behind basic auth. So
// download it first, save it to smartling's directory and then upload.
if ($file = \Drupal::service('file.repository')->writeData($smartling_context_resource_file_content, $smartling_context_resource_file, FileSystemInterface::EXISTS_REPLACE)) {
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager')->getViaUri($file->getFileUri());
$params = new UploadResourceParameters();
$params->setFile($stream_wrapper_manager->realpath());
$actual_file_size = $file->getSize();
if ($actual_file_size <= ContextUploader::FILE_SIZE_LIMIT) {
$is_resource_uploaded = $api->uploadResource($item['resourceId'], $params);
} else {
$is_resource_uploaded = FALSE;
$this->logger->warning("Context resource file exceeds its maximum permitted_size = @permitted_size, actual_size = @actual_size, id = @id and url = @url", [
'@permitted_size' => ContextUploader::FILE_SIZE_LIMIT,
'@actual_size' => $actual_file_size,
'@id' => $item['resourceId'],
'@url' => $item['url'],
]);
}
// Resource isn't uploaded for some reason. Log this info and set
// resource id into the cache. We will not try to upload this
// resource for the next hour.
if (!$is_resource_uploaded) {
$update_cache = TRUE;
$cached_data[] = $item['resourceId'];
$this->logger->warning("Can't upload context resource file with id = @id and url = @url. Context might look incomplete.", [
'@id' => $item['resourceId'],
'@url' => $item['url'],
]);
}
$file->delete();
}
// We can't save context resource file. Log this info.
else {
$this->logger->error("Can't save context resource file: @path", [
'@path' => $smartling_context_resource_file,
]);
}
}
else {
// Current resource isn't accessible (or already in the cache).
// If first case then add inaccessible resource into the cache.
if (!in_array($item['resourceId'], $cached_data)) {
$update_cache = TRUE;
$cached_data[] = $item['resourceId'];
}
}
}
// Set failed resources into the cache for the next hour.
if ($update_cache) {
\Drupal::cache()->set($cache_name, $cached_data, time() + $time_to_live);
}
} while (!empty($fresh_resources));
}
catch (Exception $e) {
if (class_exists(\Drupal\Component\Utility\DeprecationHelper::class)) {
\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => \Drupal\Core\Utility\Error::logException(\Drupal::logger('tmgmt_smartling'), $e), fn() => watchdog_exception('tmgmt_smartling', $e));
} else {
watchdog_exception('tmgmt_smartling', $e);
}
}
}
/**
* @param $filename
* @return bool
*/
public function isReadyAcceptContext($filename, $proj_settings) {
try {
$api = $this->getApiWrapper($proj_settings)->getApi('file');
$res = $api->getStatusAllLocales($filename);
if (!$res) {
$this->logger->warning('File "@filename" is not ready to accept context. Most likely it is being processed by Smartling right now.',
['@filename' => $filename]);
}
return $res;
}
catch (Exception $e) {
$this->logger->warning($e->getMessage());
return FALSE;
}
}
/**
* Apply context URL host override if configured.
*
* @param string $url
* The original URL.
* @param array $settings
* The translator settings.
*
* @return string
* The URL with host override applied if configured.
*/
public function applyContextUrlHost($url, array $settings) {
if (empty($settings['context_url_host'])) {
return $url;
}
$url_parts = parse_url($url);
if ($url_parts && isset($url_parts['scheme']) && isset($url_parts['path'])) {
$context_host = $settings['context_url_host'];
// Remove any protocol from context_url_host if it was included
$context_host = preg_replace('#^https?://#', '', $context_host);
$modified_url = $url_parts['scheme'] . '://' . $context_host . $url_parts['path'];
if (!empty($url_parts['query'])) {
$modified_url .= '?' . $url_parts['query'];
}
if (!empty($url_parts['fragment'])) {
$modified_url .= '#' . $url_parts['fragment'];
}
return $modified_url;
}
return $url;
}
}
