xmlmask.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. """
  2. SleekXMPP: The Sleek XMPP Library
  3. Copyright (C) 2010 Nathanael C. Fritz
  4. This file is part of SleekXMPP.
  5. See the file LICENSE for copying permission.
  6. """
  7. import logging
  8. from xml.parsers.expat import ExpatError
  9. from sleekxmpp.xmlstream.stanzabase import ET
  10. from sleekxmpp.xmlstream.matcher.base import MatcherBase
  11. log = logging.getLogger(__name__)
  12. class MatchXMLMask(MatcherBase):
  13. """
  14. The XMLMask matcher selects stanzas whose XML matches a given
  15. XML pattern, or mask. For example, message stanzas with body elements
  16. could be matched using the mask:
  17. .. code-block:: xml
  18. <message xmlns="jabber:client"><body /></message>
  19. Use of XMLMask is discouraged, and
  20. :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or
  21. :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath`
  22. should be used instead.
  23. :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML
  24. object or XML string to use as a mask.
  25. """
  26. def __init__(self, criteria, default_ns='jabber:client'):
  27. MatcherBase.__init__(self, criteria)
  28. if isinstance(criteria, str):
  29. self._criteria = ET.fromstring(self._criteria)
  30. self.default_ns = default_ns
  31. def setDefaultNS(self, ns):
  32. """Set the default namespace to use during comparisons.
  33. :param ns: The new namespace to use as the default.
  34. """
  35. self.default_ns = ns
  36. def match(self, xml):
  37. """Compare a stanza object or XML object against the stored XML mask.
  38. Overrides MatcherBase.match.
  39. :param xml: The stanza object or XML object to compare against.
  40. """
  41. if hasattr(xml, 'xml'):
  42. xml = xml.xml
  43. return self._mask_cmp(xml, self._criteria, True)
  44. def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
  45. """Compare an XML object against an XML mask.
  46. :param source: The :class:`~xml.etree.ElementTree.Element` XML object
  47. to compare against the mask.
  48. :param mask: The :class:`~xml.etree.ElementTree.Element` XML object
  49. serving as the mask.
  50. :param use_ns: Indicates if namespaces should be respected during
  51. the comparison.
  52. :default_ns: The default namespace to apply to elements that
  53. do not have a specified namespace.
  54. Defaults to ``"__no_ns__"``.
  55. """
  56. if source is None:
  57. # If the element was not found. May happend during recursive calls.
  58. return False
  59. # Convert the mask to an XML object if it is a string.
  60. if not hasattr(mask, 'attrib'):
  61. try:
  62. mask = ET.fromstring(mask)
  63. except ExpatError:
  64. log.warning("Expat error: %s\nIn parsing: %s", '', mask)
  65. mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
  66. if source.tag not in [mask.tag, mask_ns_tag]:
  67. return False
  68. # If the mask includes text, compare it.
  69. if mask.text and source.text and \
  70. source.text.strip() != mask.text.strip():
  71. return False
  72. # Compare attributes. The stanza must include the attributes
  73. # defined by the mask, but may include others.
  74. for name, value in mask.attrib.items():
  75. if source.attrib.get(name, "__None__") != value:
  76. return False
  77. # Recursively check subelements.
  78. matched_elements = {}
  79. for subelement in mask:
  80. matched = False
  81. for other in source.findall(subelement.tag):
  82. matched_elements[other] = False
  83. if self._mask_cmp(other, subelement, use_ns):
  84. if not matched_elements.get(other, False):
  85. matched_elements[other] = True
  86. matched = True
  87. if not matched:
  88. return False
  89. # Everything matches.
  90. return True