wsdl_docs-3.0.0-beta3/src/Services/SoapClientManager.php

src/Services/SoapClientManager.php
<?php

declare(strict_types = 1);

namespace Drupal\wsdl_docs\Services;

use DOMDocument;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Utility\Error;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use SoapClient;

/**
 * Class SoapClientManager.
 *
 * Import and manage WSDL documents.
 *
 * @package Drupal\wsdl_docs\Services
 */
class SoapClientManager {

  use StringTranslationTrait;

  /**
   * The logger factory service.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected LoggerChannelInterface $logger;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected FileSystemInterface $fileSystem;

  /**
   * Current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected AccountInterface $currentUser;

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected Connection $connection;

  /**
   * The messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * ProductManager constructor.
   *
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger_factory
   *   Logger factory service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   File system service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Database\Connection $connection
   *   Database connnection.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   */
  public function __construct(LoggerChannelInterface $logger, EntityTypeManagerInterface $entity_type_manager, FileSystemInterface $file_system, AccountInterface $current_user, Connection $connection, MessengerInterface $messenger) {
    $this->logger = $logger;
    $this->entityTypeManager = $entity_type_manager;
    $this->fileSystem = $file_system;
    $this->currentUser = $current_user;
    $this->connection = $connection;
    $this->messenger = $messenger;
  }

  /**
   * Returns a SOAP endpoint URI given a soap_service node.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Soap service node.
   *
   * @return null|string
   *   URI string, or NULL if error.
   */
  public function getUrl(NodeInterface $node): ?string {
    $uri = $node->get('field_wsdl_docs_source')->getValue();
    if (empty($uri)) {
      return NULL;
    }
    return $uri[0]['uri'];
  }

    /**
   * Create a SoapClient given a SOAP endpoint url.
   *
   * @param string $uri
   *   URI of the WSDL file.
   *
   * @return \SoapClient|null
   *   SoapClient service loaded with the provided URI, or null if error.
   */
  public function loadUrl(string $uri): ?SoapClient {
    $client = NULL;
    try {

      // Get the file contents from the WSDL Source.
      $wsdl_docs_contents = file_get_contents($uri);

      if (!empty($wsdl_docs_contents)) {
        // Validate WSDL Source content.
        $dom_document = $this->validateXml($wsdl_docs_contents);

        // Invalid XML. Do not process WSDL Source URL.
        if (empty($dom_document)) {
          $this->logger->error('Error: The provided WSDL Source URL does not contain valid XML. WSDL Source URL: @uri', [
              '@uri' => $uri,
            ]);
          $this->messenger->addError('Error: The provided WSDL Source URL does not contain valid XML.');
          return NULL;
        }

        // Create a new SoapClient using the WSDL Source URL.
        $client = new \SoapClient($uri, [
          "trace" => 1,
          "exceptions" => TRUE,
          "cache_wsdl" => WSDL_CACHE_NONE,
        ]);
      }
    }
    catch (\Exception $e) {
      $this->logger->warning('Problem loading SOAP client with uri: @uri. Message: @message', [
          '@uri' => $uri,
          '@message' => $e->getMessage(),
        ]);
      return NULL;
    }
    return $client;
  }

  /**
   * Parse a SOAP Service WSDL into Operations.
   *
   * Function called during create/update of soap_service nodes to process
   * linked WSDL file into wsdl_docs_operation nodes.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Soap Service node.
   *
   * @return bool
   *   Success status of operation.
   */
  public function saveSoapNode(NodeInterface $node): bool {
    $this->logger->debug('saveSoapNode');
    if ($node->getType() !== 'soap_service') {
      return FALSE;
    }

    $saved = FALSE;

    try {
      $saved = $this->updateOperations($node);
    }
    catch (\Exception $e) {
      Error::logException($this->logger, $e);
    }

    return $saved;
  }

  /**
   * Create/update/delete nodes derived from WSDL.
   *
   * @param \Drupal\node\NodeInterface $node
   *   SOAP Service node with link to WSDL.
   * @param \DOMDocument|null $domDocument
   *   DOMDocument of the WSDL.
   *
   * @return bool
   *   Returns early on error.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function updateOperations(NodeInterface $node, DOMDocument $domDocument = NULL): bool {
    $user = $this->currentUser;
    $user = $this->entityTypeManager->getStorage('user')->load($user->id());
    $uri = $this->getUrl($node);
    if (empty($uri)) {
      return FALSE;
    }
    $client = $this->loadUrl($uri);

    if (empty($client)) {
      // Client cannot be loaded. Do not continue processing the SOAP Service.
      $this->logger->warning('Error: SOAP client could not be loaded. WSDL Operation nodes could not be created/updated.');
      $this->messenger->addError('Error: SOAP client could not be loaded. WSDL Operation nodes could not be created/updated.');

      return FALSE;
    }

    $data_types = $this->parseSoapTypes($client->__getTypes());

    if (!$domDocument) {
      $wsdl = file_get_contents($uri);
      $domDocument = $this->getDomdocument($wsdl);
      if (!$domDocument) {
        // XML didn't validate so stop here.
        return FALSE;
      }
    }

    $documentations = $styles = $outputs = $outputs_messages = $inputs = $inputs_messages = $messages_elements = $elements_types = [];
    $xsd_files = $complex_type_schema_map = $complex_type_min_occurs_map = $_messages_elements = $_inputs_messages = $_outputs_messages = $_elements_types = $_types_properties = $complex_type_base_params = [];
    $portTypes = $domDocument->getElementsByTagName('portType');
    foreach ($portTypes as $portType) {
      $operations = $portType->getElementsByTagName('operation');
      foreach ($operations as $operation) {
        $operation_name = $operation->getAttribute('name');
        // Parse documentation element.
        if (!isset($documentations[$operation_name])) {
          $documentation = $operation->getElementsByTagName('documentation');
          if ($documentation->length > 0) {
            $documentations[$operation_name] = $documentation[0]->nodeValue;
          }
        }

        // For input/output (step 1):
        // parsing correct input/output like WSDL viewer is multi step process.
        $message = $operation->getElementsByTagName('output')[0]->getAttribute('message');
        // Remove "tns:" namespace so we can parse the element.
        $message = $this->removeMethodNamespace($message);
        $outputs_messages[$message] = $operation_name;
        $_outputs_messages[$operation_name][] = $message;

        // Repeat for input.
        $message = $operation->getElementsByTagName('input')[0]->getAttribute('message');
        // Remove "tns:" namespace so we can parse the element.
        $message = $this->removeMethodNamespace($message);
        $inputs_messages[$message] = $operation_name;
        $_inputs_messages[$operation_name][] = $message;
      }
    }

    // For input/output (step 2)
    $messages = $domDocument->getElementsByTagName('message');
    foreach ($messages as $message) {
      $message_name = $message->getAttribute('name');
      if (isset($outputs_messages[$message_name]) || isset($inputs_messages[$message_name])) {
        $part = $message->getElementsByTagName('part')[0];
        $part_name = $part->getAttribute('name');
        if ($part->hasAttribute('element')) {
          $element = $part->getAttribute('element');
          // Remove "tns:".
          $element = $this->removeMethodNamespace($element);
          $messages_elements[$element][] = $message_name;
          $_messages_elements[$message_name] = [
            'name' => $part_name,
            'element' => $element,
          ];
        }
        elseif ($part->hasAttribute('type')) {
          $type = $part->getAttribute('type');
          // Remove "tns:" namespace so we can parse the element.
          $type = $this->removeMethodNamespace($type);
          $messages_elements[$type][] = $message_name;
          $_messages_elements[$message_name] = [
            'name' => $part_name,
            'type' => $type,
          ];
        }
      }
    }

    // Parse input/output (step 3)
    $schemas = $domDocument->getElementsByTagName('types')[0]->getElementsByTagName('schema');
    foreach ($schemas as $schema) {
      $elements = $schema->childNodes;
      foreach ($elements as $element) {
        if ($element->localName == 'element') {
          $element_name = $element->getAttribute('name');
          $element2_name = $element_name;
          if (isset($messages_elements[$element_name])) {
            // Parse complexType element.
            if (!$element->hasAttribute('type')) {
              $element = $element->getElementsByTagName('element')[0];
              $element2_name = $element->getAttribute('name');
            }
            if ($element->hasAttribute('type')) {
              $element_type = $element->getAttribute('type');
              // Remove "tns:".
              $element_type = $this->removeMethodNamespace($element_type);
              $elements_types[$element_type][] = $element_name;
              $_elements_types[$element_name] = [
                'name' => $element2_name,
                'type' => $element_type,
              ];
            }
          }
        }
        // Gather any xsd files for this wsdl file.
        if (property_exists($element, 'tagName') && $element->tagName === 'xsd:import') {
          $schema_location = $element->getAttribute('schemaLocation');
          if (!empty($schema_location) && !in_array($schema_location, $xsd_files)) {
            $xsd_files[] = $schema_location;
          }
        }
      }
    }

    // Parse input/output (step 4)
    foreach ($elements_types as $type => $element) {
      $_types_properties[$type] = isset($data_types[$type]) ? $data_types[$type]['property info'] : [];
    }
    $operations = $domDocument->getElementsByTagName('binding')[0]->childNodes;
    foreach ($operations as $operation) {
      if ($operation->localName == 'operation') {
        $name = $operation->getAttribute('name');
        // Parse style element.
        $operation2 = $operation->getElementsByTagName('operation')[0];
        if ($operation2->hasAttribute('style')) {
          $styles[$name] = $operation2->getAttribute('style');
        }
        // Parse body element.
        $outputs[$name] = $this->renderOperation($name, $_outputs_messages, $_messages_elements, $_elements_types, $_types_properties, $data_types);
        $inputs[$name] = $this->renderOperation($name, $_inputs_messages, $_messages_elements, $_elements_types, $_types_properties, $data_types);
      }
    }

    // Load each xsd file and search for complex types implementing
    // xsd:extension base, we'll use this to determine if our complex
    // type has a complex base field in which to derive additional fields.
    foreach ($xsd_files as $xsd_file) {
      $xsd_filename = ltrim($xsd_file, '/');
      $xsd_uri = preg_match('/^(http|https)\:\/{2}/', $xsd_filename)
        ? $xsd_filename
        : dirname($uri) . '/' . $xsd_filename;

      $xsd = file_get_contents($xsd_uri);
      $xsd_document = $this->getDomdocument($xsd);
      if (!$xsd_document) {
        continue;
      }
      // Loop each complex type and look for an extension.
      foreach ($xsd_document->getElementsByTagName('complexType') as $complex_type) {
        $complex_type_base = '';
        $complex_type_name = $complex_type->getAttribute('name');
        foreach ($complex_type->getElementsByTagName('extension') as $extension) {
          $complex_type_base = $extension->getAttribute('base');
          if ($complex_type_base && strpos($complex_type_base, ':') !== FALSE) {
            $components = explode(':', $complex_type_base);
            $complex_type_base = $components[1] ?? '';
            // Recursively extract the complex type parameters schema uris if namespaced.
            $complex_type_schema_map[$complex_type_name] = $this->mapComplexTypeBaseParamSchema($complex_type_name, $xsd_document, $uri);
            // Recursively extract the complex type parameters minOccurs attribute.
            $complex_type_min_occurs_map[$complex_type_name] = $this->mapComplexTypeBaseParamMinOccurs($complex_type_name, $xsd_document, $uri);
            break;
          }
        }
        // Map the complex type to the base type for later use.
        if ($complex_type_name && $complex_type_base) {
          $complex_type_base_params[$complex_type_name] = $complex_type_base;
        }
      }
    }

    // List of new operations.
    $operations = $this->parseSoapOperations($client->__getFunctions(), $complex_type_base_params, $complex_type_schema_map, $complex_type_min_occurs_map);
    // Add operations and data types for later use.
    $this->connection->merge('wsdl_docs_soap_services')
      ->key('nid', $node->id())
      ->fields([
        'nid' => $node->id(),
        'label' => $node->label(),
        'url' => $node->field_wsdl_docs_source->uri,
        'operations' => serialize($operations),
        'datatypes' => serialize($data_types),
      ])->execute();
    // Get old operations for this service.
    $current_operations = $this->entityTypeManager->getStorage('node')
      ->loadByProperties([
        'type' => 'wsdl_docs_operation',
        'field_wsdl_docs_soap_ref' => $node->id(),
      ]);
    foreach ($current_operations as $current_operation) {
      // If old operation exists in new operations list.
      if (isset($operations[$current_operation->label()])) {
        // Get new operation data.
        $operation = $operations[$current_operation->label()];
        // Set new documentation, style and output data.
        $current_operation->set('field_wsdl_docs_style', $styles[$operation['label']] ?? '');
        $current_operation->set('field_wsdl_docs_documentation', isset($documentations[$operation['label']]) ? [
          'value' => $documentations[$operation['label']],
          'format' => 'full_html',
        ] : '');
        $current_operation->set('field_wsdl_docs_output', isset($outputs[$operation['label']]) ? [
          'value' => $outputs[$operation['label']],
          'format' => 'full_html',
        ] : '');
        $current_operation->set('field_wsdl_docs_input', isset($inputs[$operation['label']]) ? [
          'value' => $inputs[$operation['label']],
          'format' => 'full_html',
        ] : '');
        // Update old operation node.
        $current_operation->save();
        // Unset old operation from new operations list.
        unset($operations[$current_operation->label()]);
      }
      // Else if old operation does not exist in new operations list.
      else {
        $current_operation->delete();
      }
    }

    // New operations left.
    foreach ($operations as $name => $operation) {
      // Create operation node.
      $new_operation = Node::create([
        'type' => 'wsdl_docs_operation',
        'title' => $operation['label'],
        'status' => TRUE,
        'field_wsdl_docs_soap_ref' => [
          'target_id' => $node->id(),
        ],
        'field_wsdl_docs_style' => [
          'value' => $styles[$operation['label']] ?? '',
        ],
        'field_wsdl_docs_documentation' => [
          'value' => $documentations[$operation['label']] ?? '',
          'format' => 'full_html',
        ],
        'field_wsdl_docs_output' => [
          'value' => $outputs[$operation['label']] ?: '',
          'format' => 'full_html',
        ],
        'field_wsdl_docs_input' => [
          'value' => $inputs[$operation['label']] ?: '',
          'format' => 'full_html',
        ],
      ]);
      $new_operation->setOwner($user);
      $new_operation->save();
    }

    return TRUE;
  }

  /**
   * Parse Types.
   *
   * Convert metadata about data types provided by a SOAPClient into a
   * compatible data type array.
   *
   * @param array $types
   *   The array containing the struct strings.
   *
   * @return array
   *   A data type array with property information.
   */
  public function parseSoapTypes(array $types): array {
    $wsclient_types = [];
    foreach ($types as $type_string) {
      if (str_starts_with($type_string, 'struct')) {
        $parts = explode('{', $type_string);
        // Cut off struct and whitespaces from type name.
        $type_name = trim(substr($parts[0], 6));
        $wsclient_types[$type_name] = ['label' => $type_name];
        $property_string = $parts[1];
        // Cut off trailing '}'.
        $property_string = substr($property_string, 0, -1);
        $properties = explode(';', $property_string);
        // Remove last empty element.
        array_pop($properties);
        // Initialize empty property information.
        $wsclient_types[$type_name]['property info'] = [];
        foreach ($properties as $property_string) {
          // Cut off white spaces.
          $property_string = trim($property_string);
          $parts = explode(' ', $property_string);
          $property_type = $parts[0];
          $property_name = $parts[1];
          $wsclient_types[$type_name]['property info'][$property_name] = [
            'type' => $this->soapMapper($property_type),
          ];
        }
      }
    }
    return $wsclient_types;
  }

  /**
   * Maps data type names from SOAPClient to wsclient/rules internals.
   *
   * @param string $type
   *   Type string.
   *
   * @return bool|string
   *   Mapped type.
   */
  private function soapMapper(string $type): bool|string {
    $primitive_types = [
      'string',
      'int',
      'long',
      'float',
      'boolean',
      'double',
      'short',
      'decimal',
    ];

    if (in_array($type, $primitive_types)) {
      switch ($type) {
        case 'double':
        case 'float':
          return 'decimal';

        case 'int':
        case 'long':
        case 'short':
          return 'integer';

        case 'string':
          return 'text';
      }
    }
    // Check for list types.
    if (str_starts_with($type, 'ArrayOf')) {
      $type = substr($type, 7);
      $primitive = strtolower($type);
      if (in_array($primitive, $primitive_types)) {
        return 'list<' . $primitive . '>';
      }
      return 'list<' . $type . '>';
    }
    // Otherwise return the type as is.
    return $type;
  }

  /**
   * Validate XML and create DOMDocument object if valid.
   *
   * @param string $xml
   *   XML loaded from WSDL/XSD file.
   *
   * @return \DOMDocument|false
   *   returns loaded DOMDocument object if XML validates, FALSE if invalid.
   */
  public function getDomdocument(string $xml): bool|DOMDocument {
    $domDocument = new DOMDocument();
    // Validate XML when loading.
    libxml_use_internal_errors(TRUE);
    $domDocument->loadXML($xml);
    $errors = libxml_get_errors();
    if (!empty($errors)) {
      $this->logger->warning('Problem parsing DOM document, errors: @errors', [
          '@errors' => $errors,
        ]);
      return FALSE;
    }
    libxml_clear_errors();
    return $domDocument;
  }

  /**
   * Removes namespace from WSDL method name.
   *
   * @param string $str
   *   String to de-namespace.
   *
   * @return string
   *   String with namespace removed.
   */
  public function removeMethodNamespace(string $str): string {
    $arr = explode(':', $str, 2);
    return $arr[1] ?? $arr[0];
  }

  /**
   * Generate HTML output for SOAP operation.
   *
   * @param string $operation_name
   *   Name of service operation.
   * @param array $_outputs_messages
   *   List of messages in output tags.
   * @param array $_messages_elements
   *   List of elements in message tags.
   * @param array $_elements_types
   *   List of types in element tags.
   * @param array $_types_properties
   *   List of element properties in data types tags.
   * @param array $data_types
   *   Available data types.
   *
   * @return string
   *   HTML output.
   */
  public function renderOperation(string $operation_name, array &$_outputs_messages, array &$_messages_elements, array &$_elements_types, array &$_types_properties, array $data_types): string {
    $messages = $_outputs_messages[$operation_name];
    $base_field_types = [
      'text',
      'integer',
      'boolean',
      'dateTime',
      'date',
      'double',
      'float',
      'decimal',
    ];
    // Parse port name.
    $message = $messages[0];
    $part_name = $_messages_elements[$message]['name'];
    $text = '';
    if (isset($_messages_elements[$message]['element'])) {
      $element = $_messages_elements[$message]['element'];
      $element_name = $_elements_types[$element]['name'] ?? '';
      $element_type = $_elements_types[$element]['type'] ?? '';
      $properties = $_types_properties[$element_type] ?? [];
      // $text .= $part_name . ' type ' . $element . '<br>';
      $text .= '<table><thead><th>' . $this->t('Name') . '</th><th>' . $this->t('Type') . '</th></thead><tbody>';
      $text .= '<tr><td> ' . $part_name . '</td><td>' . $element . '</td></tr><tr><td colspan="2"><table><tbody>';
      $text .= '<tr><td>' . $element_name . '</td><td>' . $element_type . '</td></tr><tr><td colspan="2"><table><tbody>';
      foreach ($properties as $property_name => $property) {
        $property['name'] = $property_name;
        $text .= $this->getRecursiveDataTypes($property, $data_types, $base_field_types);
      }
      $text .= '</tbody></table></td></tr>';
      $text .= '</tbody></table></td></tr>';
      $text .= '</tbody></table>';
    }
    elseif (isset($_messages_elements[$message]['type'])) {
      $type = $_messages_elements[$message]['type'];
      $properties = $_types_properties[$type];
      // $text .= $part_name . ' type ' . $type . '<br>';
      $text .= '<table><thead><th>' . $this->t('Name') . '</th><th>' . $this->t('Type') . '</th></thead><tbody>';
      $text .= '<tr><td> ' . $part_name . '</td><td>' . $type . '</td></tr><tr><tr><td colspan="2"><table><tbody>';
      foreach ($properties as $property_name => $property) {
        $property['name'] = $property_name;
        $text .= $this->getRecursiveDataTypes($property, $data_types, $base_field_types);
      }
      $text .= '</tbody></table></td></tr>';
      $text .= '</tbody></table>';
    }
    return $text;
  }

  /**
   * Get renderable input and output parameters.
   *
   * @param array $property
   *   Property info.
   * @param array $data_types
   *   All available data types.
   * @param array $base_field_types
   *   Base field types.
   *
   * @return mixed|string
   *   Return html string.
   */
  public function getRecursiveDataTypes(array $property, array $data_types, array $base_field_types): mixed {
    $text = '';
    $data = &drupal_static(__METHOD__, []);
    $type = $property['type'];
    if (!in_array($property['type'], $base_field_types)) {
      if (!isset($data[$type])) {
        $child_property = $data_types[$property['type']]['property info'];
        $text .= '<tr><td>' . $property['name'] . '</td><td>' . $property['type'] . '</td></tr><tr><td colspan="2"><table><tbody>';
        foreach ($child_property as $name => $item) {
          $item['name'] = $name;
          $new_text = $this->getRecursiveDataTypes($item, $data_types, $base_field_types);
          if (!in_array($item['type'], $base_field_types)) {
            $text .= $new_text;
          }
          else {
            $text .= '<tr><td>' . $item['name'] . '</td><td>' . $item['type'] . '</td></tr>';
          }
        }
        $text .= '</td></tr></tbody></table>';
        $data[$type] = $text;
      }
      return $data[$type];
    }
    else {
      return '<tr><td>' . $property['name'] . '</td><td>' . $property['type'] . '</td></tr>';
    }
  }

  /**
   * Parse Operations.
   *
   * Convert metadata about operations provided by a SOAPClient into a
   * compatible operations array.
   *
   * @param array $operations
   *   The array containing the operation signature strings.
   * @param array $base_parameters
   *   An optional array of base_parameters.
   * @param array $schema_map
   *   An optional array containing complex type parameter mappings to their
   *   respective schema file.
   * @param array $min_occurs_map
   *   An optional array containing complex type parameters with
   *   a 'minOccurs' attribute set to '0'.
   *
   * @return array
   *   An operations array with parameter information.
   */
  public function parseSoapOperations(array $operations, array $base_parameters = [], array $schema_map = [], array $min_occurs_map = []): array {
    $wsclient_operations = [];
    foreach ($operations as $operation) {
      $parts = explode(' ', $operation);
      $return_type = $this->soapMapper($parts[0]);
      $name_parts = explode('(', $parts[1]);
      $op_name = $name_parts[0];
      $wsclient_operations[$op_name] = [
        'label' => $op_name,
        'result' => ['type' => $return_type, 'label' => $return_type],
      ];
      $parts = explode('(', $operation);
      // Cut off trailing ')'.
      $param_string = substr($parts[1], 0, -1);
      if ($param_string) {
        $parameters = explode(',', $param_string);
        foreach ($parameters as $parameter) {
          $parameter = trim($parameter);
          $parts = explode(' ', $parameter);
          $param_type = $parts[0];
          // Remove leading '$' from parameter name.
          $param_name = substr($parts[1], 1);
          $wsclient_operations[$op_name]['parameter'][$param_name] = [
            'type' => $this->soapMapper($param_type),
            'base' => !empty($base_parameters[$param_type]) ? $base_parameters[$param_type] : '',
            'schema_map' => $schema_map,
            'min_occurs_map' => $min_occurs_map,
          ];
        }
      }
    }
    return $wsclient_operations;
  }

  /**
   * Recursively extract complex type parameters and their namespace schema uris.
   *
   * In order to attach namespaces to a soapcall, we need to exctract all
   * schema uris for each field in a complex type, including all fields
   * contained inside its base field type. Sometimes the base field type
   * may also be dependent upon another base field type, so we recursively
   * locate each element from all levels and build out the field schema
   * mapping.
   *
   * @param string $type
   *   The complex type name.
   * @param \DOMDocument $xsd_document
   *   A DomDocument object containing the complex types xsd contents.
   * @param string $xsd_uri
   *   The uri of the xsd file in case the location of the schema file
   *   is relative.
   *
   * @return array
   *   A mapping of parameter names to their respecting namespace schema uris.
   */
  public function mapComplexTypeBaseParamSchema(string $type, \DOMDocument $xsd_document, string $xsd_uri): array {
    $complex_type_schema_map = [];

    // Loop all elements in the existing complexType element, extract their
    // schema location from the namespace tag if present.
    foreach ($xsd_document->getElementsByTagName('complexType') as $complex_type) {
      if (!method_exists($complex_type, 'getAttribute') || $complex_type->getAttribute('name') !== $type) {
        continue;
      }

      foreach ($complex_type->getElementsByTagName('element') as $element) {
        if (!method_exists($element, 'getAttribute')) {
          continue;
        }
        // Try to get the ref attribute, which should contain the
        // xmlns and the parameter name, we'll map these for retrieval
        // later i.e. ref="tag:parameter".
        $ref = explode(':', $element->getAttribute('ref'));
        $xmlns_tag = !empty($ref) && count($ref) == 2 ? $ref[0] : '';
        $parameter_name = !empty($ref) && count($ref) == 2 ? $ref[1] : '';
        if (empty($xmlns_tag) || empty($parameter_name)) {
          continue;
        }

        // Locate the correct schema for the element and map the schema uri.
        foreach ($xsd_document->getElementsByTagName('schema') as $schema_node) {
          if (!method_exists($schema_node, 'getAttribute')) {
            continue;
          }
          $xmlns_schema = $schema_node->getAttribute('xmlns:' . $xmlns_tag);
          $complex_type_schema_map[$parameter_name] = $xmlns_schema;
        }
      }

      // Determine if the complex type has a dependency, this is located in the
      // complexType extension base attribute; if it does, then we need to
      // load it and get all schema values, either from the current xsd,
      // or by loading another one.
      $complex_type_base = '';
      $complex_type_base_namespace = '';
      foreach ($complex_type->getElementsByTagName('extension') as $extension) {
        $complex_type_base = $extension->getAttribute('base');
        if ($complex_type_base && strpos($complex_type_base, ':') !== FALSE) {
          $components = explode(':', $complex_type_base);
          $complex_type_base_namespace = $components[0] ?? '';
          $complex_type_base = $components[1] ?? '';
          break;
        }
      }
      // Map the complex type to the base type for later use.
      if ($complex_type_base_namespace && $complex_type_base) {
        // First get the schema uri from the namespace.
        $schema_uri = '';
        foreach ($xsd_document->getElementsByTagName('schema') as $schema_node) {
          if (!method_exists($schema_node, 'getAttribute')) {
            continue;
          }
          $schema_uri = $schema_node->getAttribute('xmlns:' . $complex_type_base_namespace);
          if ($schema_uri) {
            break;
          }
        }

        foreach ($xsd_document->getElementsByTagName('import') as $import_node) {
          if (!method_exists($import_node, 'getAttribute')) {
            continue;
          }
          if ($schema_uri !== $import_node->getAttribute('namespace')) {
            continue;
          }

          // Extract the schema uri from the import node's schemaLocation attribute
          // i.e. <xsd:import namespace="http://example.com/example"
          // schemaLocation="http://example.com/example.xsd" />.
          $next_xsd_uri = '';
          $schema_location = $import_node->getAttribute('schemaLocation');
          if (!empty($schema_location)) {
            $schema_location = ltrim($schema_location, '/');
            $next_xsd_uri = preg_match('/^(http|https)\:\/{2}/', $schema_location)
              ? $schema_location
              : dirname($xsd_uri) . '/' . $schema_location;
          }

          // Load the xsd file from the derived schemaLocation uri.
          $dom_document = NULL;
          if (!empty($next_xsd_uri)) {
            $wsdl = file_get_contents($next_xsd_uri);
            $dom_document = $this->getDomdocument($wsdl);
          }
          if (!empty($dom_document)) {
            // Recurse the mapping and merge the parameters.
            $dependency_complex_type_schema_map = $this->mapComplexTypeBaseParamSchema($complex_type_base, $dom_document, $next_xsd_uri);
            $complex_type_schema_map = array_merge($complex_type_schema_map, $dependency_complex_type_schema_map);
          }
        }
      }
    }
    return $complex_type_schema_map;
  }

  /**
   * Recursively extract the minOccurs values for the complex type parameters.
   *
   * @param string $type
   *   The complex type name.
   * @param \DOMDocument $xsd_document
   *   A DomDocument object containing the complex types xsd contents.
   * @param string $xsd_uri
   *   The uri of the xsd file in case the location of the schema file
   *   is relative.
   *
   * @return array
   *   A mapping of parameter names to their minOccurs value, if existing.
   */
  public function mapComplexTypeBaseParamMinOccurs(string $type, \DOMDocument $xsd_document, string $xsd_uri): array {
    $complex_type_min_occurs_map = [];

    // Loop all elements in the existing complexType element, extract their
    // schema location from the namespace tag if present.
    foreach ($xsd_document->getElementsByTagName('complexType') as $complex_type) {
      if (!method_exists($complex_type, 'getAttribute') || $complex_type->getAttribute('name') !== $type) {
        continue;
      }

      foreach ($complex_type->getElementsByTagName('element') as $element) {
        if (!method_exists($element, 'getAttribute')) {
          continue;
        }
        // Try to get the ref attribute, which should contain the
        // xmlns and the parameter name, we'll map these for retrieval
        // later i.e. ref="tag:parameter".
        $ref = explode(':', $element->getAttribute('ref'));
        $xmlns_tag = !empty($ref) && count($ref) == 2 ? $ref[0] : '';
        $parameter_name = !empty($ref) && count($ref) == 2 ? $ref[1] : '';
        if (empty($xmlns_tag) || empty($parameter_name)) {
          continue;
        }

        // Try to get the minOccurs attribute, which should contain the
        // minimum number of times an element can occur,
        // we'll map these for retrieval later. i.e. minOccurs="0".
        $minOccurs = $element->getAttribute('minOccurs');
        if ($minOccurs === '0') {
          // Optional parameter. Added to map array.
          $complex_type_min_occurs_map[$parameter_name] = TRUE;
        }
      }

      // Determine if the complex type has a dependency, this is located in the
      // complexType extension base attribute; if it does, then we need to
      // load it and get all schema values, either from the current xsd,
      // or by loading another one.
      $complex_type_base = '';
      $complex_type_base_namespace = '';
      foreach ($complex_type->getElementsByTagName('extension') as $extension) {
        $complex_type_base = $extension->getAttribute('base');
        if ($complex_type_base && strpos($complex_type_base, ':') !== FALSE) {
          $components = explode(':', $complex_type_base);
          $complex_type_base_namespace = $components[0] ?? '';
          $complex_type_base = $components[1] ?? '';
          break;
        }
      }
      // Map the complex type to the base type for later use.
      if ($complex_type_base_namespace && $complex_type_base) {
        // First get the schema uri from the namespace.
        $schema_uri = '';

        // Try to get the minOccurs attribute, which should contain the
        // minimum number of times an element can occur,
        // we'll map these for retrieval later. i.e. minOccurs="0".
        $minOccurs = $element->getAttribute('minOccurs');
        if ($minOccurs === '0') {
          // Optional parameter. Added to map array.
          $complex_type_min_occurs_map[$parameter_name] = TRUE;
        }

        foreach ($xsd_document->getElementsByTagName('import') as $import_node) {
          if (!method_exists($import_node, 'getAttribute')) {
            continue;
          }
          if ($schema_uri !== $import_node->getAttribute('namespace')) {
            continue;
          }

          // Extract the schema uri from the import node's schemaLocation attribute
          // i.e. <xsd:import namespace="http://example.com/example"
          // schemaLocation="http://example.com/example.xsd" />.
          $next_xsd_uri = '';
          $schema_location = $import_node->getAttribute('schemaLocation');
          if (!empty($schema_location)) {
            $schema_location = ltrim($schema_location, '/');
            $next_xsd_uri = preg_match('/^(http|https)\:\/{2}/', $schema_location)
              ? $schema_location
              : dirname($xsd_uri) . '/' . $schema_location;
          }

          // Load the xsd file from the derived schemaLocation uri.
          $dom_document = NULL;
          if (!empty($next_xsd_uri)) {
            $wsdl = file_get_contents($next_xsd_uri);
            $dom_document = $this->getDomdocument($wsdl);
          }
          if (!empty($dom_document)) {
            // Recurse the mapping and merge the parameters.
            $dependency_complex_type_min_occurs_map = $this->mapComplexTypeBaseParamMinOccurs($complex_type_base, $dom_document, $next_xsd_uri);
            $complex_type_min_occurs_map = array_merge($complex_type_min_occurs_map, $dependency_complex_type_min_occurs_map);
          }
        }
      }
    }
    return $complex_type_min_occurs_map;
  }

  /**
   * Validate XML and create DOMDocument object if valid.
   *
   * @param string $wsdl
   *   XML loaded from wsdl/xsd file.
   *
   * @return \DOMDocument|false
   *   Returns loaded DOMDocument object if XML validates, FALSE if invalid.
   */
  public function validateXml(string $wsdl): DOMDocument|false {
    $dom_document = new \DOMDocument();
    // Validate XML when loading.
    libxml_use_internal_errors(TRUE);
    $dom_document->loadXML($wsdl);
    $errors = libxml_get_errors();
    if (!empty($errors)) {
      return FALSE;
    }
    libxml_clear_errors();
    return $dom_document;
  }

}

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

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