1 #!/usr/bin/env python 2 3 """ 4 SOAP support using libxml2dom. Support for the archaic SOAP namespaces is also 5 provided. 6 7 See: http://www.w3.org/TR/2007/REC-soap12-part0-20070427/ 8 9 Copyright (C) 2007, 2008, 2014 Paul Boddie <paul@boddie.org.uk> 10 11 This program is free software; you can redistribute it and/or modify it under 12 the terms of the GNU Lesser General Public License as published by the Free 13 Software Foundation; either version 3 of the License, or (at your option) any 14 later version. 15 16 This program is distributed in the hope that it will be useful, but WITHOUT 17 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 19 details. 20 21 You should have received a copy of the GNU Lesser General Public License along 22 with this program. If not, see <http://www.gnu.org/licenses/>. 23 24 -------- 25 26 The sending and receiving of SOAP messages can be done using traditional HTTP 27 libraries. 28 29 See tests/test_soap.py for more details. 30 """ 31 32 import libxml2dom 33 from libxml2dom.macrolib import * 34 from libxml2dom.macrolib import \ 35 createDocument as Node_createDocument 36 from libxml2dom.values import ContentValue, SequenceValue 37 38 # SOAP-related namespaces. 39 40 SOAP_ENVELOPE_NAMESPACE = "http://www.w3.org/2003/05/soap-envelope" 41 SOAP_ENCODING_NAMESPACE = "http://www.w3.org/2003/05/soap-encoding" 42 SOAP_RPC_NAMESPACE = "http://www.w3.org/2003/05/soap-rpc" 43 XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema" 44 XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" 45 46 # Archaic namespaces. 47 48 OLD_SOAP_ENVELOPE_NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" 49 OLD_SOAP_ENCODING_NAMESPACE = "http://schemas.xmlsoap.org/soap/encoding/" 50 51 # Default namespace bindings for XPath. 52 53 default_ns = { 54 "env" : SOAP_ENVELOPE_NAMESPACE, 55 "enc" : SOAP_ENCODING_NAMESPACE, 56 "rpc" : SOAP_RPC_NAMESPACE, 57 "xs" : XS_NAMESPACE, 58 "xsi" : XSI_NAMESPACE, 59 "SOAP-ENV" : OLD_SOAP_ENVELOPE_NAMESPACE, 60 "SOAP-ENC" : OLD_SOAP_ENCODING_NAMESPACE 61 } 62 63 # Node classes. 64 65 class SOAPNode(libxml2dom.Node): 66 67 "Convenience modifications to nodes specific to libxml2dom.soap." 68 69 def add_or_replace_element(self, new_element): 70 71 """ 72 Add or replace the given 'new_element', using its localName to find any 73 element to be replaced. 74 """ 75 76 elements = self.xpath(new_element.localName) 77 if elements: 78 self.replaceChild(new_element, elements[0]) 79 else: 80 self.appendChild(new_element) 81 82 class SOAPElement(ContentValue, SequenceValue, SOAPNode): 83 84 "A SOAP element." 85 86 def convert(self, node): 87 return node.textContent.strip() 88 89 def values(self): 90 return [v.contents for v in self.xpath("*")] 91 92 def _contents(self): 93 # NOTE: Should check whether this should be a leaf element. 94 if not self.xpath("*"): 95 return (self.localName, getattr(self.ownerDocument, "convert", self.convert)(self)) 96 else: 97 return (self.localName, self) 98 99 def __len__(self): 100 if not self.xpath("*"): 101 return 2 102 else: 103 return SequenceValue.__len__(self) 104 105 def __eq__(self, other): 106 if not self.xpath("*"): 107 return ContentValue.__eq__(self, other) 108 else: 109 return SequenceValue.__eq__(self, other) 110 111 def __ne__(self, other): 112 if not self.xpath("*"): 113 return ContentValue.__ne__(self, other) 114 else: 115 return SequenceValue.__ne__(self, other) 116 117 def __repr__(self): 118 if self.contents[1] is self: 119 return "<%s: %r>" % (self.__class__.__name__, self.values()) 120 else: 121 return "<%s: %r>" % (self.__class__.__name__, self.contents) 122 123 # Node construction methods. 124 125 def createSOAPElement(self, localName): 126 127 "Create an element with the appropriate namespace and prefix." 128 129 ref_element = self.ownerDocument.documentElement 130 prefix = ref_element.prefix 131 if prefix: 132 name = prefix + ":" + localName 133 else: 134 name = localName 135 return self.createElementNS(ref_element.namespaceURI, name) 136 137 def makeSOAPElement(self, localName): 138 139 """ 140 Create and insert an element with the appropriate namespace and prefix. 141 """ 142 143 element = self.createSOAPElement(localName) 144 self.appendChild(element) 145 return element 146 147 contents = property(_contents) 148 149 class SOAPDocument(libxml2dom._Document, SOAPNode): 150 151 "A SOAP document fragment." 152 153 def __init__(self, node, impl, namespaces=None): 154 155 """ 156 Initialise the document with the given 'node', implementation 'impl', 157 and 'namespaces' details. 158 """ 159 160 libxml2dom._Document.__init__(self, node, impl, None) 161 self._update_namespaces([default_ns, namespaces]) 162 163 def _envelope(self): 164 return (self.xpath("env:Envelope|SOAP-ENV:Envelope") or [None])[0] 165 166 envelope = property(_envelope) 167 168 # Convenience methods and properties. 169 170 def _fault(self): 171 if self.envelope is not None: 172 return self.envelope.fault 173 else: 174 return None 175 176 def _method(self): 177 if self.envelope is not None: 178 return self.envelope.method 179 else: 180 return None 181 182 fault = property(_fault) 183 method = property(_method) 184 185 class SOAPEnvelopeElement(SOAPElement): 186 187 "A SOAP envelope element." 188 189 def _body(self): 190 return (self.xpath("env:Body|SOAP-ENV:Body") or [None])[0] 191 192 def _setBody(self, body): 193 self.appendChild(body) 194 195 def _delBody(self): 196 self.removeChild(self.body) 197 198 # Convenience methods and properties. 199 200 def _fault(self): 201 if self.body is not None: 202 return self.body.fault 203 else: 204 return None 205 206 def _method(self): 207 if self.body is not None: 208 return self.body.method 209 else: 210 return None 211 212 fault = property(_fault) 213 method = property(_method) 214 215 # Node construction methods. 216 217 def createBody(self): 218 return self.createSOAPElement("Body") 219 220 def makeBody(self): 221 element = self.createBody() 222 self.add_or_replace_element(element) 223 return element 224 225 body = property(_body, _setBody, _delBody) 226 227 class SOAPHeaderElement(SOAPElement): 228 229 "A SOAP header element." 230 231 pass 232 233 class SOAPBodyElement(SOAPElement): 234 235 "A SOAP body element." 236 237 def _fault(self): 238 return (self.xpath("env:Fault|SOAP-ENV:Fault") or [None])[0] 239 240 def _method(self): 241 if self.namespaceURI == SOAP_ENVELOPE_NAMESPACE: 242 return (self.xpath("*[@env:encodingStyle = '%s']" % SOAP_ENCODING_NAMESPACE) or [None])[0] 243 else: 244 return (self.xpath("*") or [None])[0] 245 246 # Node construction methods. 247 248 def createMethod(self, namespaceURI, name): 249 if self.method is not None: 250 self.removeChild(self.method) 251 element = self.createElementNS(namespaceURI, name) 252 element.setAttributeNS(SOAP_ENVELOPE_NAMESPACE, "env:encodingStyle", SOAP_ENCODING_NAMESPACE) 253 return element 254 255 def makeMethod(self, namespaceURI, name): 256 element = self.createMethod(namespaceURI, name) 257 self.appendChild(element) 258 return element 259 260 def createFault(self): 261 return self.createSOAPElement("Fault") 262 263 def makeFault(self): 264 element = self.createFault() 265 self.add_or_replace_element(element) 266 return element 267 268 fault = property(_fault) 269 method = property(_method) 270 271 class SOAPMethodElement(SOAPElement): 272 273 "A SOAP method element." 274 275 def _methodName(self): 276 return self.localName 277 278 def _resultParameter(self): 279 return (self.xpath(".//rpc:result") or [None])[0] 280 281 def _resultParameterValue(self): 282 if self.resultParameter: 283 name = self.resultParameter.textContent.strip() 284 result = self.xpath(".//" + name, namespaces={self.prefix : self.namespaceURI}) 285 if result: 286 return result[0].textContent.strip() 287 else: 288 return None 289 else: 290 return None 291 292 def _parameters(self): 293 return self.xpath("*") 294 295 def _parameterValues(self): 296 return self.values() 297 298 def __repr__(self): 299 return "<SOAPMethodElement: %r>" % self.parameters 300 301 methodName = property(_methodName) 302 resultParameter = property(_resultParameter) 303 resultParameterValue = property(_resultParameterValue) 304 parameterValues = property(_parameterValues) 305 parameters = property(_parameters) 306 307 class SOAPFaultElement(SOAPElement): 308 309 "A SOAP fault element." 310 311 def _code(self): 312 code = self.xpath("env:Code|SOAP-ENV:Code") 313 if code: 314 return code[0].value 315 else: 316 return None 317 318 def _subcode(self): 319 subcode = self.xpath("./env:Code/env:Subcode|./SOAP-ENV:Code/SOAP-ENV:Subcode") 320 if subcode: 321 return subcode[0].value 322 else: 323 return None 324 325 def _reason(self): 326 return (self.xpath("env:Reason|SOAP-ENV:Reason") or [None])[0] 327 328 def _detail(self): 329 return (self.xpath("env:Detail|SOAP-ENV:Detail") or [None])[0] 330 331 # Node construction methods. 332 333 def createCode(self): 334 return self.createSOAPElement("Code") 335 336 def makeCode(self): 337 element = self.createCode() 338 self.add_or_replace_element(element) 339 return element 340 341 code = property(_code) 342 subcode = property(_subcode) 343 reason = property(_reason) 344 detail = property(_detail) 345 346 class SOAPSubcodeElement(SOAPElement): 347 348 "A SOAP subcode element." 349 350 def _value(self): 351 value = self.xpath("env:Value|SOAP-ENV:Value") 352 if value: 353 return value[0].textContent.strip() 354 else: 355 return None 356 357 def _setValue(self, value): 358 nodes = self.xpath("env:Value|SOAP-ENV:Value") 359 v = self.createValue() 360 if nodes: 361 self.replaceChild(v, nodes[0]) 362 else: 363 self.appendChild(v) 364 v.value = value 365 366 # Node construction methods. 367 368 def createValue(self, value=None): 369 code_value = self.createSOAPElement("Value") 370 if value is not None: 371 code_value.value = code 372 return code_value 373 374 def makeValue(self, value=None): 375 code_value = self.createValue(value) 376 self.add_or_replace_element(code_value) 377 return code_value 378 379 value = property(_value, _setValue) 380 381 class SOAPCodeElement(SOAPSubcodeElement): 382 383 "A SOAP code element." 384 385 def _subcode(self): 386 return (self.xpath("env:Subcode|SOAP-ENV:Subcode") or [None])[0] 387 388 # Node construction methods. 389 390 def createSubcode(self): 391 return self.createSOAPElement("Subcode") 392 393 def makeSubcode(self): 394 element = self.createSubcode() 395 self.add_or_replace_element(element) 396 return element 397 398 subcode = property(_subcode) 399 400 class SOAPValueElement(SOAPElement): 401 402 "A SOAP value element." 403 404 def _value(self): 405 return self.textContent 406 407 def _setValue(self, value): 408 for node in self.childNodes: 409 self.removeChild(node) 410 text = self.ownerDocument.createTextNode(value) 411 self.appendChild(text) 412 413 value = property(_value, _setValue) 414 415 class SOAPTextElement(SOAPValueElement): 416 417 "A SOAP text element." 418 419 def _lang(self): 420 return self.getAttributeNS(libxml2dom.XML_NAMESPACE, "lang") 421 422 def _setLang(self, value): 423 self.setAttributeNS(libxml2dom.XML_NAMESPACE, "xml:lang", value) 424 425 lang = property(_lang, _setLang) 426 427 # Implementation-related functionality. 428 429 class SOAPImplementation(libxml2dom.Implementation): 430 431 "Contains a SOAP-specific implementation." 432 433 # Mapping of element names to wrappers. 434 435 _class_for_name = { 436 "Envelope" : SOAPEnvelopeElement, 437 "Header" : SOAPHeaderElement, 438 "Body" : SOAPBodyElement, 439 "Fault" : SOAPFaultElement, 440 "Code" : SOAPCodeElement, 441 "Subcode" : SOAPSubcodeElement, 442 "Value" : SOAPValueElement, 443 "Text" : SOAPTextElement 444 } 445 446 # Wrapping of documents. 447 448 def adoptDocument(self, node): 449 return SOAPDocument(node, self) 450 451 # Factory functions. 452 453 def get_node(self, _node, context_node): 454 455 """ 456 Get a libxml2dom node for the given low-level '_node' and libxml2dom 457 'context_node'. 458 """ 459 460 if Node_nodeType(_node) == context_node.ELEMENT_NODE: 461 462 # Make special envelope elements. 463 464 if Node_namespaceURI(_node) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 465 cls = self._class_for_name[Node_localName(_node)] 466 return cls(_node, self, context_node.ownerDocument) 467 468 # Detect the method element. 469 470 if Node_parentNode(_node) and Node_localName(Node_parentNode(_node)) == "Body" and \ 471 Node_namespaceURI(Node_parentNode(_node)) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 472 473 return SOAPMethodElement(_node, self, context_node.ownerDocument) 474 475 # Otherwise, make generic SOAP elements. 476 477 return SOAPElement(_node, self, context_node.ownerDocument) 478 479 else: 480 return libxml2dom.Implementation.get_node(self, _node, context_node) 481 482 # Convenience functions. 483 484 def createSOAPMessage(self): 485 486 "Create a new SOAP message document (fragment)." 487 488 return SOAPDocument(Node_createDocument(SOAP_ENVELOPE_NAMESPACE, "env:Envelope", None), self).documentElement 489 490 # Utility functions. 491 492 createDocument = libxml2dom.createDocument 493 createDocumentType = libxml2dom.createDocumentType 494 495 def createSOAPMessage(): 496 return default_impl.createSOAPMessage() 497 498 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 499 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 500 501 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 502 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 503 504 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 505 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 506 507 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 508 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 509 510 # Single instance of the implementation. 511 512 default_impl = SOAPImplementation() 513 514 # vim: tabstop=4 expandtab shiftwidth=4