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