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;
  }
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc