You are here

LingotekApi.php in Lingotek Translation 7.7

Defines Drupal\lingotek\LingotekApi

File

lib/Drupal/lingotek/LingotekApi.php
View source
<?php

/**
 * @file
 * Defines Drupal\lingotek\LingotekApi
 */
class LingotekApi {

  /**
   * The status string representing a successful API call.
   */
  const RESPONSE_STATUS_SUCCESS = 'success';

  /**
   * The faux Lingotek user ID to use for anonymous user operations.
   */
  const ANONYMOUS_LINGOTEK_ID = 'anonymous';

  /**
   * The endpoint for API version 4
   */
  const API_ENDPOINT_V4 = '/lingopoint/api/4';

  /**
   * Methods that require an unstripped response
   */
  const REQUIRE_COMPLETE_RESPONSE = array(
    'updateContentDocumentAsync',
    'getDocument',
    'addTranslationTarget',
    'addContentDocumentAsync',
    'addContentDocumentWithTargetsAsync',
  );

  /**
   * Holds the static instance of the singleton object.
   *
   * @var LingotekApi
   */
  private static $instance;

  /**
   * Debug status for extra logging of API calls.
   *
   * @var bool
   */
  private $debug;

  /**
   * The endpoint for API calls.
   *
   * @var string
   */
  private $api_url;

  /**
   * Gets the singleton instance of the API class.
   *
   * @return LingotekApi
   *   An instantiated LingotekApi object.
   */
  public static function instance() {
    if (!isset(self::$instance)) {
      $class_name = __CLASS__;
      self::$instance = new $class_name();
    }
    return self::$instance;
  }

  /**
   * Add a document to the Lingotek platform.
   *
   * Uploads the translatable object's content in the selected language.
   *
   * @param object $translatable_object
   *   A Drupal node object or lingotek ConfigChunk object
   * @param mixed $with_targets
   *   An optional array of locales to include for translation, or TRUE for all enabled languages.
   */
  public function addContentDocument(LingotekTranslatableEntity $translatable_object, $with_targets = FALSE) {
    $success = FALSE;
    $project_id = $translatable_object
      ->getProjectId();
    $source_language = $translatable_object
      ->getSourceLocale();
    if (empty($source_language)) {
      drupal_set_message('Some entities not uploaded because the source language was language neutral.', 'warning', FALSE);
      LingotekLog::warning('Document @docname not uploaded. Language was language neutral.', array(
        '@docname' => $translatable_object
          ->getDocumentName(),
      ));
      return FALSE;
    }
    if ($project_id) {
      $parameters = array(
        'projectId' => $project_id,
        'format' => $this
          ->xmlFormat(),
        'sourceLanguage' => $source_language,
        'tmVaultId' => $translatable_object
          ->getVaultId(),
      );
      $parameters['documentName'] = $translatable_object
        ->getDocumentName();
      $parameters['documentDesc'] = $translatable_object
        ->getDescription();
      $parameters['content'] = $this
        ->check_url_alias_translation($translatable_object, $translatable_object
        ->documentLingotekXML());
      $parameters['url'] = $translatable_object
        ->getUrl(TRUE);
      $parameters['workflowId'] = $translatable_object
        ->getWorkflowId();
      $this
        ->addAdvancedParameters($parameters, $translatable_object);

      // If the document has invalid characters, return without uploading
      $invalid_xml = lingotek_keystore($translatable_object
        ->getEntityType(), $translatable_object
        ->getId(), 'invalid_xml');
      if ($invalid_xml == LingotekSync::INVALID_XML_PRESENT) {
        drupal_set_message(t('Unable to upload to Lingotek because entity contains invalid XML characters.'), 'warning');
        return FALSE;
      }

      // If the entity is empty, also return without uploading
      $empty_entity = lingotek_keystore($translatable_object
        ->getEntityType(), $translatable_object
        ->getId(), 'empty_entity');
      if ($empty_entity === LingotekSync::EMPTY_ENTITY) {
        drupal_set_message(t('Entity was not uploaded to Lingotek because it is empty.'), 'warning');
        return FALSE;
      }
      if ($with_targets) {
        if (is_array($with_targets)) {

          // Assumes language-specific profiles are enabled, so handle adding
          // target locales with *custom workflows* separately, and include
          // all the other target locales here.
          $default_targets = array();
          $language_specific_targets = array();
          foreach ($with_targets as $l => $v) {
            if (empty($v['workflow_id'])) {
              $default_targets[] = $l;
            }
            else {
              $language_specific_targets[$l] = $v;
            }
          }
          if (!empty($default_targets)) {
            $parameters['targetAsJSON'] = json_encode($default_targets);
            $parameters['applyWorkflow'] = 'true';

            // API expects a 'true' string
            $result = $this
              ->request('addContentDocumentWithTargetsAsync', $parameters);
          }
          else {
            $result = $this
              ->request('addContentDocumentAsync', $parameters);
          }
        }
        else {
          $parameters['targetAsJSON'] = Lingotek::getLanguagesWithoutSourceAsJSON($source_language);
          $parameters['applyWorkflow'] = 'true';

          // API expects a 'true' string
          $result = $this
            ->request('addContentDocumentWithTargetsAsync', $parameters);
        }
      }
      else {
        $result = $this
          ->request('addContentDocumentAsync', $parameters);
      }
      $code = isset($result['code']) ? intval($result['code']) : NULL;
      $result = isset($result['body']) ? $result['body'] : NULL;
      if ($code == 402) {
        return $this
          ->handleCommunityError($translatable_object);
      }
      if ($result) {
        if (isset($result->processId)) {
          $translatable_object
            ->setMetadataValue("process_id", $result->processId);
          $translatable_object
            ->setMetadataValue("initial_upload", TRUE);
        }
        if (isset($result->errors) && $result->errors) {
          LingotekLog::error(t('Request to send document to Lingotek failed: ') . print_r($result->errors, TRUE), array());
          $translatable_object
            ->setStatus(LingotekSync::STATUS_ERROR);
          $translatable_object
            ->setLastError(is_array($result->errors) ? array_shift($result->errors) : $result->errors);
          return FALSE;
        }
        if (get_class($translatable_object) == 'LingotekConfigSet') {
          $translatable_object
            ->setDocumentId($result->id);
          $translatable_object
            ->setProjectId($project_id);
          $translatable_object
            ->setStatus(LingotekSync::STATUS_CURRENT);
          $translatable_object
            ->setTargetsStatus(LingotekSync::STATUS_PENDING, $with_targets);

          // WTD: there is a race condition here where a user could modify a locales-
          // source entry between the time the dirty segments are pulled and the time
          // they are set to current at this point.  This same race condition exists
          // for nodes as well; however, the odds may be lower due to number of entries.
          LingotekConfigSet::setSegmentStatusToCurrentById($translatable_object
            ->getId());
        }
        else {
          $entity_type = $translatable_object
            ->getEntityType();
          lingotek_keystore($entity_type, $translatable_object
            ->getId(), 'document_id', $result->id);
          lingotek_keystore($entity_type, $translatable_object
            ->getId(), 'last_uploaded', time());
          if ($with_targets) {
            if (is_array($with_targets)) {
              foreach ($default_targets as $default_target) {
                lingotek_keystore($entity_type, $translatable_object
                  ->getId(), 'target_sync_status_' . $default_target, LingotekSync::STATUS_PENDING);
              }
              if (!empty($language_specific_targets)) {
                $this
                  ->upload_language_specific_targets($entity_type, $translatable_object
                  ->getId(), $result->id, $language_specific_targets);
              }
            }
            else {
              $target_locales = Lingotek::getLanguagesWithoutSource($source_language);
              foreach (array_keys($target_locales) as $locale) {
                lingotek_keystore($entity_type, $translatable_object
                  ->getId(), 'target_sync_status_' . $locale, LingotekSync::STATUS_PENDING);
              }
            }
          }
        }
        $success = TRUE;
      }
    }
    return $success;
  }
  public function sourceCompleted($process_id) {
    $params = array(
      'id' => $process_id,
    );
    $result = $this
      ->request('getProcessProgress', $params);
    return !empty($result->entries->{$process_id}->error) ? $result->entries->{$process_id}->error : !empty($result->entries->{$process_id}->done);
  }

  /**
   * Waits until document import status is complete, then adds targets, or logs an error
   */
  private function upload_language_specific_targets($entity_type, $entity_id, $doc_id, $targets) {
    $params = array(
      'id' => $doc_id,
    );
    for ($i = 0; $i <= 10; $i++) {
      $response = $this
        ->request('getDocumentImportStatus', $params);
      if ($response->status === 'COMPLETE') {
        break;
      }
      elseif ($i > 9) {
        drupal_set_message(t('Uploading some language-specific targets failed because of network timeout. Please use the "Request language-specific translations" bulk action.'), 'warning', FALSE);
        LingotekLog::error('Adding language-specific targets failed for document ID  @id.', array(
          '@id' => $doc_id,
        ));
        foreach ($targets as $target_locale => $target_info) {
          lingotek_keystore($entity_type, $entity_id, 'target_sync_status_' . $target_locale, LingotekSync::STATUS_ERROR);
        }

        // Store the failed language-specific targets
        LingotekSync::lingotek_store_failed_language_specific_targets($entity_type, $entity_id);
        return;
      }
      else {
        sleep(3);
      }
    }

    // The document has been imported successfully, now add the targets
    if (is_array($targets)) {
      foreach ($targets as $target_locale => $target_attribs) {
        if (!empty($doc_id) && !empty($target_attribs['workflow_id'])) {
          $tl_result = $this
            ->addTranslationTarget($doc_id, NULL, $target_locale, $target_attribs['workflow_id']);
          if ($tl_result) {
            lingotek_keystore($entity_type, $entity_id, 'target_sync_status_' . $target_locale, LingotekSync::STATUS_PENDING);
          }
          else {
            lingotek_keystore($entity_type, $entity_id, 'target_sync_status_' . $target_locale, LingotekSync::STATUS_ERROR);
          }
        }
      }
    }
  }

  /**
   * Checks if this content needs to have it's url translated, strips it out if it doesn't.
   * Best place for this function as the function that puts the url alias into the content
   * is used in other places.
   * @param type $translatable_object
   * @param string $content
   * @return string
   */
  private function check_url_alias_translation($translatable_object, $content) {
    if ($translatable_object->lingotek['url_alias_translation'] != 1) {
      $openTagPosition = strpos($content, '<url_alias>');
      if ($openTagPosition !== FALSE) {
        $closeTagPosition = strpos($content, '</url_alias>') + strlen('</url_alias>');
        $firstChunk = substr($content, 0, $openTagPosition);
        $lastChunk = substr($content, $closeTagPosition);
        $content = $firstChunk . $lastChunk;
      }
    }
    return $content;
  }
  function handleCommunityError(LingotekTranslatableEntity $translatable_object) {
    $translatable_object
      ->deleteMetadataValue('document_id');
    $translatable_object
      ->setStatus(LingotekSync::STATUS_ERROR);
    $translatable_object
      ->setExclusiveTargetsStatus(LingotekSync::STATUS_NONE, [
      LingotekSync::STATUS_CURRENT,
      LingotekSync::STATUS_READY,
    ]);
    $this
      ->communityDisabledErrorMessage();
    return FALSE;
  }
  function communityDisabledErrorMessage() {
    $community = lingotek_get_community_name() . ' (' . variable_get('lingotek_community_identifier', '') . ')';
    drupal_set_message(t('Community ' . $community . ' has been disabled. Please contact support@lingotek.com to re-enable your community.'), 'error');
  }

  /**
   * Updates the content of an existing Lingotek document with the current object contents.
   *
   * @param stdClass $translatable_object
   *   A Drupal node object or another object, such as a config chunk, etc.
   *
   * @return bool
   *   TRUE on success, FALSE on failure.
   */
  public function updateContentDocument(LingotekTranslatableEntity $translatable_object) {
    $parameters['documentId'] = $translatable_object
      ->getMetadataValue('document_id');
    $parameters['documentName'] = $translatable_object
      ->getDocumentName();
    $parameters['documentDesc'] = $translatable_object
      ->getDescription();
    $parameters['content'] = $this
      ->check_url_alias_translation($translatable_object, $translatable_object
      ->documentLingotekXML());
    $parameters['url'] = $translatable_object
      ->getUrl();
    $parameters['format'] = $this
      ->xmlFormat();
    $parameters['projectId'] = $translatable_object
      ->getProjectId();

    // If the document has invalid characters, return without uploading
    $invalid_xml = lingotek_keystore($translatable_object
      ->getEntityType(), $translatable_object
      ->getId(), 'invalid_xml');
    if ($invalid_xml == LingotekSync::INVALID_XML_PRESENT) {
      drupal_set_message(t('Unable to upload to Lingotek because entity contains invalid XML characters.'), 'warning');
      return FALSE;
    }
    $this
      ->addAdvancedParameters($parameters, $translatable_object);
    $response = $this
      ->request('updateContentDocumentAsync', $parameters);
    $result = isset($response['body']) ? $response['body'] : NULL;
    $code = isset($response['code']) ? intval($response['code']) : NULL;
    if ($result) {
      if ($code === 200) {
        if (get_class($translatable_object) == 'LingotekConfigSet') {

          // WTD: there is a race condition here where a user could modify a locales-
          // source entry between the time the dirty segments are pulled and the time
          // they are set to current at this point.  This same race condition exists
          // for nodes as well; however, the odds may be lower due to number of entries.
          LingotekConfigSet::setSegmentStatusToCurrentById($translatable_object
            ->getId());
        }
        $translatable_object
          ->setStatus(LingotekSync::STATUS_PENDING);
        $translatable_object
          ->setTargetsStatus(LingotekSync::STATUS_PENDING);
        if (isset($result->next_document_id) && isset($result->process_id)) {
          $next_doc_id = $result->next_document_id;
          $process_id = $result->process_id;
          $translatable_object
            ->setMetadataValue('previous_doc_id', $translatable_object
            ->getMetadataValue('document_id'));
          $translatable_object
            ->setMetadataValue('document_id', $next_doc_id);
          $translatable_object
            ->setMetadataValue('process_id', $process_id);
        }
        return TRUE;
      }
      elseif ($code == 402) {
        return $this
          ->handleCommunityError($translatable_object);
      }
      elseif ($code == 410) {
        $translatable_object
          ->setStatus(LingotekSync::STATUS_NONE);
        return $this
          ->addContentDocument($translatable_object, true);
      }
      elseif ($code == 423) {
        $params = [];
        $params['documentId'] = $translatable_object
          ->getMetadataValue('document_id');

        // Query the API to get the updated Lingotek DocumentId
        $response = $this
          ->request('getDocument', $params);
        $body = $response['body'];
        if (intval($response['code']) > 200) {
          throw Error("Document retrieval unsuccessful");
        }
        if (empty($body->nextDocumentId)) {
          LingotekLog::error('Error updating document: unable to retrieve next document id for record @id', params['documentId']);
          throw Error("Error updating document: unable to retrieve next document id");
        }
        $translatable_object
          ->setMetadataValue('document_id', $body->nextDocumentId);
        $this
          ->updateContentDocument($translatable_object);
        return TRUE;
      }
      else {
        throw new OAuthException2('Request failed with code ' . $code . ': ' . json_encode($result));
      }
    }
    return isset($result);
  }

  /**
   * Adds a target language to an existing Lingotek Document or Project.
   *
   * @param int $lingotek_document_id
   *   The document to which the new translation target should be added.  Or null if the target will be added to the project.
   * @param int $lingotek_project_id
   *   The project to which the new translation target should be added.  Or null if the target will be added to a document instead.
   * @param string $lingotek_locale
   *   The two letter code representing the language which should be added as a translation target.
   * @param string $workflow_id
   *   The optional workflow to associate with this target. If omitted, the project's default
   *   workflow will be applied.
   *
   * @return mixed
   *  The ID of the new translation target in the Lingotek system, or FALSE on error.
   */
  public function addTranslationTarget($lingotek_document_id, $lingotek_project_id, $lingotek_locale, $workflow_id = '') {
    $parameters = array(
      'targetLanguage' => $lingotek_locale,
    );
    if (isset($lingotek_document_id) && !isset($lingotek_project_id)) {
      $parameters['documentId'] = $lingotek_document_id;
    }
    elseif (isset($lingotek_project_id) && !isset($lingotek_document_id)) {
      $parameters['projectId'] = $lingotek_project_id;
    }
    if ($workflow_id) {
      $parameters['workflowId'] = $workflow_id;
    }
    else {

      // The associated project's Workflow template should be applied.
      $parameters['applyWorkflow'] = 'true';
    }
    if ($this
      ->request('addTranslationTarget', $parameters)) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Removes a target language to an existing Lingotek Document or Project.
   *
   * @param int $lingotek_document_id
   *   The document from which the translation target should be removed.  Or null if the target will be removed from the project.
   * @param int $lingotek_project_id
   *   The project from which the translation target should be removed.  Or null if the target will be removed from a document instead.
   * @param string $lingotek_locale
   *   The two letter code representing the language which should be added as a translation target.
   * @param string $workflow_id
   *   The optional workflow to associate with this target. If omitted, the project's default
   *   workflow will be applied.
   *
   * @return bool
   *  TRUE on success, or FALSE on error.
   */
  public function removeTranslationTarget($lingotek_document_id, $lingotek_project_id, $lingotek_locale) {
    $parameters = array(
      'targetLanguage' => $lingotek_locale,
    );
    if (isset($lingotek_document_id) && !isset($lingotek_project_id)) {
      $parameters['documentId'] = $lingotek_document_id;
    }
    elseif (isset($lingotek_project_id) && !isset($lingotek_document_id)) {
      $parameters['projectId'] = $lingotek_project_id;
    }
    if ($old_translation_target = $this
      ->request('removeTranslationTarget', $parameters)) {
      return $old_translation_target->results == 'success' ? TRUE : FALSE;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Gets the phase data for the active phase of the specified translation target.
   *
   * @param int $translation_target_id
   *   The ID of a translation target on the Lingotek system.
   *
   * @return mixed
   *   An object representing data for the current translation phase, or FALSE on error.
   */
  public function currentPhase($translation_target_id) {
    if ($target = $this
      ->getTranslationTarget($translation_target_id)) {
      if (!empty($target->phases)) {
        $current_phase = FALSE;
        foreach ($target->phases as $phase) {
          if (!$phase->isMarkedComplete) {
            $current_phase = $phase;
            break;
          }
        }

        // Return either the first uncompleted phase, or the last phase if all phases are complete.
        return $current_phase ? $current_phase : end($target->phases);
      }
      else {
        return FALSE;
      }
    }
    else {
      return FALSE;
    }
  }

  /**
   * Downloads the translated document for the specified document and language.
   *
   * @param int $document_id
   *   The Lingotek document ID that should be downloaded.
   * @param string $lingotek_locale
   *   A Lingotek language/locale code.
   *
   * @return mixed
   *   On success, a LingotekXMLElement object representing the translated document. FALSE on failure.
   *
   */
  public function downloadDocument($document_id, $lingotek_locale, $return_text = FALSE) {
    $document = FALSE;
    $params = array(
      'documentId' => $document_id,
      'targetLanguage' => $lingotek_locale,
    );
    $results = $this
      ->request('downloadDocument', $params);
    $code = 200;
    if (isset($results['code'])) {
      $code = $results['code'];
      $results = $results['body'];
      if ($code == 410) {
        $ln = LingotekEntity::loadByLingotekDocumentId($document_id);
        $ln
          ->setTargetsStatus(LingotekSync::STATUS_ERROR, $lingotek_locale);
        drupal_set_message(t('Document @id has been archived and needs to be reuploaded'), 'error');
        return FALSE;
      }
    }
    if ($results) {
      try {

        // TODO: This is borrowed from the now-deprecated LingotekSession::download()
        // and could use refactoring.
        $tmpFile = tempnam(file_directory_temp(), 'lingotek');
        $fp = fopen($tmpFile, 'w');
        fwrite($fp, $results);
        fclose($fp);
        $text = '';
        $file = FALSE;

        // downloadDocument returns zip-encoded data.
        $zip = new ZipArchive();
        $zip
          ->open($tmpFile);
        $name = $zip
          ->getNameIndex(0);
        $file = $zip
          ->getStream($name);
        if ($file) {
          while (!feof($file)) {
            $text .= fread($file, 2);
          }
        }
        fclose($file);
        unlink($tmpFile);
        if ($return_text) {
          return $text;
        }
        $document = new LingotekXMLElement($text);
      } catch (Exception $e) {
        LingotekLog::error('Unable to parse downloaded document. Error: @error. Text: !xml.', array(
          '!xml' => $text,
          '@error' => $e
            ->getMessage(),
        ));
      }
    }
    return $document;
  }

  /**
   * Gets Lingotek Document data for the specified document.
   *
   * @param int $document_id
   *   The ID of the Lingotek Document to retrieve.
   *
   * @return mixed
   *  The API response object with Lingotek Document data, or FALSE on error.
   */
  public function getDocument($document_id) {
    $documents =& drupal_static(__FUNCTION__);
    if (!empty($documents[$document_id])) {
      $document = $documents[$document_id];
    }
    else {
      $params = array(
        'documentId' => $document_id,
      );
      if ($document = $this
        ->request('getDocument', $params)) {
        $documents[$document_id] = $document['body'];
      }
    }
    return $document;
  }

  /**
   * Gets the workflow progress of the specified document.
   *
   * @param int $document_id
   *   The ID of the Lingotek Document to retrieve.
   *
   * @return mixed
   *  The API response object with Lingotek Document data, or FALSE on error.
   */
  public function getDocumentProgress($document_id) {
    $documents =& drupal_static(__FUNCTION__);
    if (!empty($documents[$document_id])) {
      $document = $documents[$document_id];
    }
    else {
      $params = array(
        'documentId' => $document_id,
      );
      if ($document = $this
        ->request('getDocumentProgress', $params)) {
        $documents[$document_id] = $document;
      }
    }
    return $document;
  }

  /**
   * Gets the workflow progress of the specified documents.
   *
   * @param int $document_id
   *   The IDs of the Lingotek Documents to retrieve.
   *
   * @return mixed
   *  The API response object with Lingotek Document data, or FALSE on error.
   */
  public function listDocumentProgress($document_ids) {
    $params = array();
    foreach ($document_ids as $document_id) {
      $params['documentId'][] = $document_id;
    }
    $documents = $this
      ->request('listDocumentProgress', $params);
    return $documents;
  }

  /**
   * Gets the Target Tranlsations of the specified document.
   *
   * @param int $document_id
   *   The ID of the Lingotek Document to retrieve.
   *
   * @return mixed
   *  The API response object with Lingotek Document data, or FALSE on error.
   */
  public function listTranslationTargets($document_id) {
    $params['documentId'] = $document_id;
    $targetTranslations = $this
      ->request('listTranslationTargets', $params);
    return $targetTranslations;
  }

  /**
   * Gets the workflow progress of the specified project (or list of document ids).
   *
   * @param int $project_id
   *
   * @param array<int> $document_ids
   *   An array of document IDs of the Lingotek Document to retrieve.
   *
   * @return mixed
   *  The API response object with Lingotek Document data, or FALSE on error.
   */
  public function getProgressReport($project_id = NULL, $document_ids = NULL) {
    $params = array();
    if (is_array($document_ids)) {
      $params['documentId'] = $document_ids;
    }
    elseif (!is_null($project_id)) {
      $params['projectId'] = $project_id;
    }
    return $this
      ->request('getProgressReport', $params);
  }

  /**
   * Gets data for a specific Workflow Phase.
   *
   * @param int $phase_id
   *   The ID of the phase to retrieve.
   *
   * @return mixed
   *   The API response object, or FALSE on error.
   */
  public function getPhase($phase_id) {
    $params = array(
      'phaseId' => $phase_id,
    );
    return $this
      ->request('getPhase', $params);
  }

  /**
   * Gets a translation target.
   *
   * This fetches an target language object for a specific document.
   *
   * @param int $translation_target_id
   *   ID for the target language object.
   * @return
   *   Object representing a target language for a specific document in the Lingotek platform, or FALSE on error.
   */
  public function getTranslationTarget($translation_target_id) {
    $targets =& drupal_static(__FUNCTION__);
    $params = array(
      'translationTargetId' => $translation_target_id,
    );
    if (isset($targets[$translation_target_id])) {
      return $targets[$translation_target_id];
    }
    elseif ($output = $this
      ->request('getTranslationTarget', $params)) {
      $targets[$translation_target_id] = $output;
      return $output;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Get a User Profile Attributes
   *
   * Note:  the Request() method will switch the ExternalID to whatever is passed in, instead of the regular ExternalID.
   */
  public function getProfileAttributes($externalId = NULL) {
    $result = FALSE;
    $parameters = array();
    if (isset($externalId)) {
      $parameters['externalId'] = $externalId;
    }
    if ($output = $this
      ->request('getProfileAttributes', $parameters)) {
      if ($output->results == 'success' && is_object($output->attributes)) {
        $result = $output->attributes;
      }
    }
    return $result;
  }

  /**
   * Uses getProfileAttributes to Get the User Profile Attributes, and return the ID.
   */
  public function getProfileId($externalId = NULL) {
    $result = FALSE;
    $profile = $this
      ->getProfileAttributes($externalId);
    if ($profile) {
      $result = $profile->id;
    }
    return $result;
  }

  /**
   * Assigns a role to a user.  (Must be done by an community admin)
   * Returns TRUE or FALSE
   */
  public function addRoleAssignment($role, $userId) {
    $result = FALSE;
    if (isset($role) && isset($userId)) {
      $parameters = array(
        'role' => $role,
        'clientId' => $userId,
      );
      if ($output = $this
        ->request('addRoleAssignment', $parameters)) {
        if ($output->results == 'success') {
          $result = TRUE;
        }
      }
    }
    return $result;
  }

  /**
   * Assigns a user to a project.  (Must be done by an community admin)
   * Returns TRUE or FALSE
   * This will only return TRUE the first time.
   */
  public function assignProjectManager($projectId, $userId) {
    $result = FALSE;
    if (isset($projectId) && isset($userId)) {
      $parameters = array(
        'projectId' => $projectId,
        'managerUserId' => $userId,
      );
      $output = $this
        ->request('assignProjectManager', $parameters);
      if ($output) {
        if ($output->results == 'success') {
          $result = TRUE;
        }
      }
    }
    return $result;
  }

  /**
   * Configures the Lingotek community to allow the local Drupal user to use the Workbench.
   *
   * @param str $username
   *   A Drupal username.
   */
  private function checkUserWorkbenchLinkPermissions($username = self::ANONYMOUS_LINGOTEK_ID) {

    // Don't do anything with the community_admin user.
    if ($username == 'community_admin') {
      return true;
    }

    // To use the workbench, users need to be tagged.  Check to see if the current user has already been tagged.
    $workbench_list = variable_get('lingotek_workbench_tagged_users', array());
    $found = array_search($username, $workbench_list);

    // If not, update his account to allow  him to use the workbench.
    if ($found === false) {
      $profileId = $this
        ->getProfileId($username);
      if ($profileId) {
        $projectId = variable_get('lingotek_project', NULL);
        $role = $this
          ->addRoleAssignment("project_manager", $profileId);
        if ($role && isset($projectId)) {
          $manager = $this
            ->assignProjectManager($projectId, $profileId);

          // This call will only return true the FIRST time.
          $workbench_list[] = $username;
          variable_set('lingotek_workbench_tagged_users', $workbench_list);
        }
      }
    }
  }

  /**
   * Gets a workbench URL for the specified document ID and phase.
   *
   * @param int $document_id
   *   A Lingotek Document ID.
   * @param int $phase_id
   *   A Lingotek workflow phase ID.
   *
   * @return mixed
   *   A workbench URL string on success, or FALSE on failure.
   */
  public function getWorkbenchLink($document_id, $phase_id) {
    $links =& drupal_static(__FUNCTION__);
    global $user;
    $externalId = isset($user->mail) ? $user->mail : '';

    // send a blank string for anonymous users (community translation)
    self::checkUserWorkbenchLinkPermissions($externalId);
    $static_id = $document_id . '-' . $phase_id;
    if (empty($links[$static_id])) {
      $params = array(
        'documentId' => $document_id,
        'phaseId' => $phase_id,
        'externalId' => $externalId,
      );
      $output = $this
        ->request('getWorkbenchLink', $params);
      if ($output && isset($output->url)) {
        $links[$static_id] = $url = $output->url;
      }
      else {
        $url = FALSE;
      }
    }
    else {
      $url = $links[$static_id];
    }
    return $url;
  }

  /**
   * Gets available Lingotek projects.
   *
   * @param $reset
   *   A boolean value to determin whether we need to query the API
   *
   * @return array
   *   An array of available projects with project IDs as keys, project labels as values.
   */
  public function listProjects($reset = FALSE) {
    $projects = variable_get('lingotek_project_defaults', array());
    if (!empty($projects) && $reset == FALSE) {
      return $projects;
    }
    $response = $this
      ->request('listProjects');
    if (empty($response) || $response->results == 'fail') {
      return FALSE;
    }
    $projects = array();
    foreach ($response->projects as $project) {
      $projects[$project->id] = $project->name;
    }
    variable_set('lingotek_project_defaults', $projects);
    return $projects;
  }

  /**
   * Gets available Lingotek Workflows.
   *
   * @param $reset
   *   A boolean value to determine whether we need to query the API
   * @param $include_public
   *   A boolean value to determine whether to show public workflows
   *
   * @return array
   *   An array of available Workflows with workflow IDs as keys, workflow labels as values.
   */
  public function listWorkflows($reset = FALSE, $include_public = FALSE) {
    $workflows = variable_get('lingotek_workflow_defaults', array());
    if (!empty($workflows) && $reset == FALSE) {
      return $workflows;
    }
    if ($workflows_raw = $this
      ->request('listWorkflows')) {
      $workflows = array();
      foreach ($workflows_raw->workflows as $workflow) {
        if ($include_public || !$workflow->is_public && $workflow->owner != LINGOTEK_DEFAULT_WORKFLOW_TEMPLATE) {
          $workflows[$workflow->id] = $workflow->name;
        }
      }
      variable_set('lingotek_workflow_defaults', $workflows);
    }
    return $workflows;
  }

  /**
   * Gets Lingotek Workflow by ID
   *
   * @param $id
   *   a string containing the workflow ID
   * @param $reset
   *   A boolean value to determine whether we need to query the API
   *
   * @return array
   *   An array of workflow details
   */
  public function getWorkflow($id, $reset = FALSE) {
    $workflow = variable_get('lingotek_workflow_' . $id, array());
    if (!empty($workflow) && $reset == FALSE) {
      return $workflow;
    }
    $response = $this
      ->request('getWorkflow', array(
      'id' => $id,
    ));
    if (!empty($response->results) && $response->results == 'success') {
      $workflow = $response->workflow;
      variable_set('lingotek_workflow_' . $id, $workflow);
    }
    return $workflow;
  }

  /**
   * Gets available Lingotek Translation Memory Vaults.
   *
   * @param $reset
   *   A boolean value to determin whether we need to query the API
   *
   * @return array
   *   An array of available vaults.
   */
  public function listVaults($reset = FALSE, $show_public_vaults = FALSE) {
    $vaults = variable_get('lingotek_vaults_defaults', array());
    if (!empty($vaults) && $reset == FALSE) {
      return $vaults;
    }
    if ($vaults_raw = $this
      ->request('listTMVaults')) {
      $vaults = array();
      if (!empty($vaults_raw->personalVaults)) {
        foreach ($vaults_raw->personalVaults as $vault) {
          $vaults['Personal Vaults'][$vault->id] = $vault->name;
        }
      }
      if (!empty($vaults_raw->communityVaults)) {
        foreach ($vaults_raw->communityVaults as $vault) {
          $vaults['Community Vaults'][$vault->id] = $vault->name;
        }
      }
      if ($show_public_vaults && !empty($vaults_raw->publicVaults)) {
        foreach ($vaults_raw->publicVaults as $vault) {
          $vaults['Public Vaults'][$vault->id] = $vault->name;
        }
      }
      variable_set('lingotek_vaults_defaults', $vaults);
    }
    return $vaults;
  }

  /**
   * Updates one or more nids to belong to a given workflow
   *
   * @param array $document_ids
   *   An array of document IDs
   * @param string $workflow_id
   *   A string containing the desired workflow_id
   * @param string $prefillPhase
   *   An optional parameter specifying the prefill phase
   *
   * @return bool
   *   TRUE on success, FALSE on failure.
   */
  public function changeWorkflow($document_ids, $workflow_id, $prefillPhase = NULL) {
    $parameters = array(
      'documentId' => $document_ids,
      'workflowId' => $workflow_id,
      'preserveTargets' => 'true',
    );
    if ($prefillPhase) {
      $parameters['prefillPhase'] = $prefillPhase;
    }
    return $this
      ->request('resetDocument', $parameters) ? TRUE : FALSE;
  }

  /**
   * Marks a phase as complete.
   *
   * @param int $phase_id
   *   The phase ID to be marked as complete.
   *
   * @return bool
   *   TRUE on success, FALSE on failure.
   */
  public function markPhaseComplete($phase_id) {
    $parameters = array(
      'phaseId' => $phase_id,
    );
    return $this
      ->request('markPhaseComplete', $parameters) ? TRUE : FALSE;
  }

  /**
   * Gets the appropriate format code for the current system state.
   *
   * @return string
   *   A XML format code.
   */
  public function xmlFormat() {
    return variable_get('lingotek_advanced_parsing', TRUE) ? 'XML_OKAPI' : 'XML';
  }

  /**
   * Tests the current configuration to ensure that API calls can be made.
   *
   * @return bool
   *   TRUE if the configuration is correct, FALSE otherwise.
   */
  public function testAuthentication($force = FALSE) {
    $valid_connection =& drupal_static(__FUNCTION__);
    if ($force || !isset($valid_connection)) {

      // Only test the connection if the oauth keys have been setup.
      $consumer_key = variable_get('lingotek_oauth_consumer_id', '');
      $consumer_secret = variable_get('lingotek_oauth_consumer_secret', '');
      if (!empty($consumer_key) && !empty($consumer_secret)) {
        $valid_connection = $this
          ->request('validateApiKeys');
        if (!empty($valid_connection->results) && $valid_connection->results != 'fail') {
          $valid_connection = TRUE;
          return $valid_connection;
        }
      }
    }
    $valid_connection = FALSE;
    return $valid_connection;
  }

  /**
   * Calls a Lingotek API method.
   *
   * @return mixed
   *   On success, a stdClass object of the returned response data, FALSE on error.
   */
  public function request($method, $parameters = array(), $request_method = 'POST', $credentials = NULL) {
    LingotekLog::trace('<h2>@method</h2> (trace)', array(
      '@method' => $method,
    ));
    $response_data = FALSE;

    // Every v4 API request needs to have the externalID parameter present.
    // Defaults the externalId to the lingotek_login_id, unless externalId is passed as a parameter
    if (!isset($parameters['externalId'])) {
      $parameters['externalId'] = variable_get('lingotek_login_id', '');
    }
    module_load_include('php', 'lingotek', 'lib/oauth-php/library/OAuthStore');
    module_load_include('php', 'lingotek', 'lib/oauth-php/library/LingotekOAuthRequester');
    $credentials = is_null($credentials) ? array(
      'consumer_key' => variable_get('lingotek_oauth_consumer_id', ''),
      'consumer_secret' => variable_get('lingotek_oauth_consumer_secret', ''),
    ) : $credentials;
    $timer_name = $method . '-' . microtime(TRUE);
    timer_start($timer_name);
    $result = NULL;
    $response = NULL;
    $api_url = $this->api_url . '/' . $method;
    try {
      OAuthStore::instance('2Leg', $credentials);
      $request = @new LingotekOAuthRequester($api_url, $request_method, $parameters);

      // There is an error right here.  The new LingotekOAuthRequester throws it, because it barfs on $parameters
      // The error:  Warning: rawurlencode() expects parameter 1 to be string, array given in LingotekOAuthRequest->urlencode() (line 619 of .../modules/lingotek/lib/oauth-php/library/LingotekOAuthRequest.php).
      // The thing is, if you encode the params, they just get translated back to an array by the object.  They have some type of error internal to the object code that is handling things wrong.
      // I couldn't find a way to get around this without changing the library.  For now, I am just supressing the warning w/ and @ sign.
      $curl_options = array(
        CURLOPT_SSL_VERIFYPEER => FALSE,
      );

      // Check and add proxy settings if needed
      $proxy_server = variable_get('proxy_server', '');
      if (!empty($proxy_server)) {
        $curl_options[CURLOPT_PROXY] = $proxy_server;
        $proxy_port = variable_get('proxy_port', 8080);
        $curl_options[CURLOPT_PROXYPORT] = $proxy_port;

        // Proxy user/password
        $proxy_username = variable_get('proxy_username', '');
        if (!empty($proxy_username)) {
          $proxy_password = variable_get('proxy_password', '');
          $str_usr_pwd = "{$proxy_username}:{$proxy_password}";
          $curl_options[CURLOPT_PROXYUSERPWD] = $str_usr_pwd;
          $proxy_auth_method = variable_get('proxy_auth_method', CURLAUTH_BASIC);
          $curl_options[CURLOPT_PROXYAUTH] = $proxy_auth_method;
        }
      }
      $result = $request
        ->doRequest(0, $curl_options);
      $response = json_decode($result['body']);
    } catch (OAuthException2 $e) {
      LingotekLog::error('Failed OAuth request. <br />Method: @method <br />Message: @message
      <br />API URL: @url
      <br />Parameters: !params.
      <br />Response: !response', array(
        '@method' => $method,
        '@message' => $e
          ->getMessage(),
        '@url' => $api_url,
        '!params' => $parameters,
        '!response' => $response,
      ), 'api');
    }
    $timer_results = timer_stop($timer_name);

    // cleanup parameters so that the logs aren't too long
    if (isset($parameters['fprmFileContents'])) {
      $parameters['fprmFileContents'] = 'removed for brevity';
    }
    if (isset($parameters['secondaryFprmFileContents'])) {
      $parameters['secondaryFprmFileContents'] = 'removed for brevity';
    }
    $message_params = array(
      '@url' => $api_url,
      '@method' => $method,
      '!params' => $parameters,
      '!request' => $request,
      '!response' => $method == 'downloadDocument' && !isset($response->results) ? "Zipped document" : $response,
      '@response_time' => number_format($timer_results['time']) . ' ms',
    );

    /*
     Exceptions:
     downloadDocument - Returns misc data (no $response->results), and should always be sent back.
     assignProjectManager - Returns fails/falses if the person is already a community manager (which should be ignored)
    */
    if ($method == 'downloadDocument') {

      // Exception downloadDocument
      LingotekLog::api('<h1>@method</h1> <strong>API URL:</strong> @url
        <br /><strong>Response Time:</strong> @response_time<br /><strong>Request Params</strong>: !params<br /><strong>Response:</strong> !response<br/><strong>Full Request:</strong> !request', $message_params);
      $response_data['code'] = !empty($result['code']) ? $result['code'] : "";
      $response_data['body'] = !empty($result) ? $result['body'] : "";
    }
    elseif ($this
      ->returnEntireResponse($method)) {
      $result['body'] = $response;
      $response_data = $result;
    }
    elseif (!is_null($response) && $response->results == self::RESPONSE_STATUS_SUCCESS || $method == 'assignProjectManager') {

      // SUCCESS
      LingotekLog::api('<h1>@method</h1> <strong>API URL:</strong> @url
        <br /><strong>Response Time:</strong> @response_time<br /><strong>Request Params</strong>: !params<br /><strong>Response:</strong> !response<br/><strong>Full Request:</strong> !request', $message_params);
      $response_data = $response;
    }
    else {

      // ERROR
      LingotekLog::error('<h1>@method (Failed)</h1> <strong>API URL:</strong> @url
        <br /><strong>Response Time:</strong> @response_time<br /><strong>Request Params</strong>: !params<br /><strong>Response:</strong> !response<br/><strong>Full Request:</strong> !request', $message_params, 'api');
      $response_data = $response;
    }
    return $response_data;
  }
  function returnEntireResponse($method) {
    return in_array($method, self::REQUIRE_COMPLETE_RESPONSE);
  }

  /**
   * Calls a Lingotek API to provision a new Community (account).
   * Modified version of the request() method.
   *
   * @return mixed
   *   On success, a stdClass object of the returned response data, FALSE on error.
   */
  public function createCommunity($parameters = array(), $callback_url = NULL) {
    $credentials = array(
      'consumer_key' => LINGOTEK_AP_OAUTH_KEY,
      'consumer_secret' => LINGOTEK_AP_OAUTH_SECRET,
    );
    if (isset($callback_url)) {
      $parameters['projectName'] = lingotek_get_site_name();
      $parameters['callbackUrl'] = $callback_url;
    }
    $response = $this
      ->request('autoProvisionCommunity', $parameters, 'POST', $credentials);
    return $response;
  }

  /**
   * Adds advanced parameters for use with addContentDocument and updateContentDocument.
   *
   * @param array $parameters
   *   An array of API request parameters.
   * @param object $entity
   *   A Drupal entity object.
   */
  private function addAdvancedParameters(&$parameters, LingotekTranslatableEntity $entity) {

    // Extra parameters when using advanced XML configuration.
    $entity_advanced_parsing_enabled = lingotek_keystore($entity
      ->getEntityType(), $entity
      ->getId(), 'use_advanced_parsing');
    $current_plan_type = variable_get('lingotek_account_plan_type', 'standard');
    if ($current_plan_type === 'advanced' || $entity_advanced_parsing_enabled) {
      $fprmFileContents = variable_get('lingotek_advanced_xml_config1', '');
      $secondaryFprmFileContents = variable_get('lingotek_advanced_xml_config2', '');
      if (!strlen($fprmFileContents) || !strlen($secondaryFprmFileContents)) {
        lingotek_set_default_advanced_xml();
        $fprmFileContents = variable_get('lingotek_advanced_xml_config1', '');
        $secondaryFprmFileContents = variable_get('lingotek_advanced_xml_config2', '');
      }
      $advanced_parameters = array(
        'fprmFileContents' => $fprmFileContents,
        'secondaryFprmFileContents' => $secondaryFprmFileContents,
        'secondaryFilter' => 'okf_html',
      );
      $parameters = array_merge($parameters, $advanced_parameters);
    }
  }

  /**
   * Private constructor.
   */
  private function __construct() {
    $this->debug = variable_get('lingotek_api_debug', FALSE);
    $host = LINGOTEK_API_SERVER;

    // Trim trailing slash from user-entered server name, if it exists.
    if (substr($host, -1) == '/') {
      $host = substr($host, 0, -1);
    }
    $this->api_url = $host . self::API_ENDPOINT_V4;
  }

  /**
   * Private clone implementation.
   */
  private function __clone() {
  }

}

Classes

Namesort descending Description
LingotekApi @file Defines Drupal\lingotek\LingotekApi