diff options
| -rw-r--r-- | libmproxy/console.py | 2 | ||||
| -rw-r--r-- | libmproxy/contrib/BeautifulSoup.py | 2014 | ||||
| -rw-r--r-- | libmproxy/utils.py | 50 | ||||
| -rw-r--r-- | test/test_utils.py | 60 | ||||
| -rw-r--r-- | todo | 6 | 
5 files changed, 97 insertions, 2035 deletions
| diff --git a/libmproxy/console.py b/libmproxy/console.py index 84ef6a5f..c47de130 100644 --- a/libmproxy/console.py +++ b/libmproxy/console.py @@ -307,7 +307,7 @@ class ConnectionView(WWrap):              ])      def _view_pretty(self, conn, txt): -        for i in utils.prettybody(conn.content): +        for i in utils.pretty_xmlish(conn.content):              txt.append(                  ("text", i),              ) diff --git a/libmproxy/contrib/BeautifulSoup.py b/libmproxy/contrib/BeautifulSoup.py deleted file mode 100644 index 4b17b853..00000000 --- a/libmproxy/contrib/BeautifulSoup.py +++ /dev/null @@ -1,2014 +0,0 @@ -"""Beautiful Soup -Elixir and Tonic -"The Screen-Scraper's Friend" -http://www.crummy.com/software/BeautifulSoup/ - -Beautiful Soup parses a (possibly invalid) XML or HTML document into a -tree representation. It provides methods and Pythonic idioms that make -it easy to navigate, search, and modify the tree. - -A well-formed XML/HTML document yields a well-formed data -structure. An ill-formed XML/HTML document yields a correspondingly -ill-formed data structure. If your document is only locally -well-formed, you can use this library to find and process the -well-formed part of it. - -Beautiful Soup works with Python 2.2 and up. It has no external -dependencies, but you'll have more success at converting data to UTF-8 -if you also install these three packages: - -* chardet, for auto-detecting character encodings -  http://chardet.feedparser.org/ -* cjkcodecs and iconv_codec, which add more encodings to the ones supported -  by stock Python. -  http://cjkpython.i18n.org/ - -Beautiful Soup defines classes for two main parsing strategies: - - * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific -   language that kind of looks like XML. - - * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid -   or invalid. This class has web browser-like heuristics for -   obtaining a sensible parse tree in the face of common HTML errors. - -Beautiful Soup also defines a class (UnicodeDammit) for autodetecting -the encoding of an HTML or XML document, and converting it to -Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. - -For more than you ever wanted to know about Beautiful Soup, see the -documentation: -http://www.crummy.com/software/BeautifulSoup/documentation.html - -Here, have some legalese: - -Copyright (c) 2004-2010, Leonard Richardson - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -  * Redistributions of source code must retain the above copyright -    notice, this list of conditions and the following disclaimer. - -  * Redistributions in binary form must reproduce the above -    copyright notice, this list of conditions and the following -    disclaimer in the documentation and/or other materials provided -    with the distribution. - -  * Neither the name of the the Beautiful Soup Consortium and All -    Night Kosher Bakery nor the names of its contributors may be -    used to endorse or promote products derived from this software -    without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. - -""" -from __future__ import generators - -__author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "3.2.0" -__copyright__ = "Copyright (c) 2004-2010 Leonard Richardson" -__license__ = "New-style BSD" - -from sgmllib import SGMLParser, SGMLParseError -import codecs -import markupbase -import types -import re -import sgmllib -try: -  from htmlentitydefs import name2codepoint -except ImportError: -  name2codepoint = {} -try: -    set -except NameError: -    from sets import Set as set - -#These hacks make Beautiful Soup able to parse XML with namespaces -sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') -markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match - -DEFAULT_OUTPUT_ENCODING = "utf-8" - -def _match_css_class(str): -    """Build a RE to match the given CSS class.""" -    return re.compile(r"(^|.*\s)%s($|\s)" % str) - -# First, the classes that represent markup elements. - -class PageElement(object): -    """Contains the navigational information for some part of the page -    (either a tag or a piece of text)""" - -    def setup(self, parent=None, previous=None): -        """Sets up the initial relations between this element and -        other elements.""" -        self.parent = parent -        self.previous = previous -        self.next = None -        self.previousSibling = None -        self.nextSibling = None -        if self.parent and self.parent.contents: -            self.previousSibling = self.parent.contents[-1] -            self.previousSibling.nextSibling = self - -    def replaceWith(self, replaceWith): -        oldParent = self.parent -        myIndex = self.parent.index(self) -        if hasattr(replaceWith, "parent")\ -                  and replaceWith.parent is self.parent: -            # We're replacing this element with one of its siblings. -            index = replaceWith.parent.index(replaceWith) -            if index and index < myIndex: -                # Furthermore, it comes before this element. That -                # means that when we extract it, the index of this -                # element will change. -                myIndex = myIndex - 1 -        self.extract() -        oldParent.insert(myIndex, replaceWith) - -    def replaceWithChildren(self): -        myParent = self.parent -        myIndex = self.parent.index(self) -        self.extract() -        reversedChildren = list(self.contents) -        reversedChildren.reverse() -        for child in reversedChildren: -            myParent.insert(myIndex, child) - -    def extract(self): -        """Destructively rips this element out of the tree.""" -        if self.parent: -            try: -                del self.parent.contents[self.parent.index(self)] -            except ValueError: -                pass - -        #Find the two elements that would be next to each other if -        #this element (and any children) hadn't been parsed. Connect -        #the two. -        lastChild = self._lastRecursiveChild() -        nextElement = lastChild.next - -        if self.previous: -            self.previous.next = nextElement -        if nextElement: -            nextElement.previous = self.previous -        self.previous = None -        lastChild.next = None - -        self.parent = None -        if self.previousSibling: -            self.previousSibling.nextSibling = self.nextSibling -        if self.nextSibling: -            self.nextSibling.previousSibling = self.previousSibling -        self.previousSibling = self.nextSibling = None -        return self - -    def _lastRecursiveChild(self): -        "Finds the last element beneath this object to be parsed." -        lastChild = self -        while hasattr(lastChild, 'contents') and lastChild.contents: -            lastChild = lastChild.contents[-1] -        return lastChild - -    def insert(self, position, newChild): -        if isinstance(newChild, basestring) \ -            and not isinstance(newChild, NavigableString): -            newChild = NavigableString(newChild) - -        position =  min(position, len(self.contents)) -        if hasattr(newChild, 'parent') and newChild.parent is not None: -            # We're 'inserting' an element that's already one -            # of this object's children. -            if newChild.parent is self: -                index = self.index(newChild) -                if index > position: -                    # Furthermore we're moving it further down the -                    # list of this object's children. That means that -                    # when we extract this element, our target index -                    # will jump down one. -                    position = position - 1 -            newChild.extract() - -        newChild.parent = self -        previousChild = None -        if position == 0: -            newChild.previousSibling = None -            newChild.previous = self -        else: -            previousChild = self.contents[position-1] -            newChild.previousSibling = previousChild -            newChild.previousSibling.nextSibling = newChild -            newChild.previous = previousChild._lastRecursiveChild() -        if newChild.previous: -            newChild.previous.next = newChild - -        newChildsLastElement = newChild._lastRecursiveChild() - -        if position >= len(self.contents): -            newChild.nextSibling = None - -            parent = self -            parentsNextSibling = None -            while not parentsNextSibling: -                parentsNextSibling = parent.nextSibling -                parent = parent.parent -                if not parent: # This is the last element in the document. -                    break -            if parentsNextSibling: -                newChildsLastElement.next = parentsNextSibling -            else: -                newChildsLastElement.next = None -        else: -            nextChild = self.contents[position] -            newChild.nextSibling = nextChild -            if newChild.nextSibling: -                newChild.nextSibling.previousSibling = newChild -            newChildsLastElement.next = nextChild - -        if newChildsLastElement.next: -            newChildsLastElement.next.previous = newChildsLastElement -        self.contents.insert(position, newChild) - -    def append(self, tag): -        """Appends the given tag to the contents of this tag.""" -        self.insert(len(self.contents), tag) - -    def findNext(self, name=None, attrs={}, text=None, **kwargs): -        """Returns the first item that matches the given criteria and -        appears after this Tag in the document.""" -        return self._findOne(self.findAllNext, name, attrs, text, **kwargs) - -    def findAllNext(self, name=None, attrs={}, text=None, limit=None, -                    **kwargs): -        """Returns all items that match the given criteria and appear -        after this Tag in the document.""" -        return self._findAll(name, attrs, text, limit, self.nextGenerator, -                             **kwargs) - -    def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): -        """Returns the closest sibling to this Tag that matches the -        given criteria and appears after this Tag in the document.""" -        return self._findOne(self.findNextSiblings, name, attrs, text, -                             **kwargs) - -    def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, -                         **kwargs): -        """Returns the siblings of this Tag that match the given -        criteria and appear after this Tag in the document.""" -        return self._findAll(name, attrs, text, limit, -                             self.nextSiblingGenerator, **kwargs) -    fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x - -    def findPrevious(self, name=None, attrs={}, text=None, **kwargs): -        """Returns the first item that matches the given criteria and -        appears before this Tag in the document.""" -        return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) - -    def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, -                        **kwargs): -        """Returns all items that match the given criteria and appear -        before this Tag in the document.""" -        return self._findAll(name, attrs, text, limit, self.previousGenerator, -                           **kwargs) -    fetchPrevious = findAllPrevious # Compatibility with pre-3.x - -    def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): -        """Returns the closest sibling to this Tag that matches the -        given criteria and appears before this Tag in the document.""" -        return self._findOne(self.findPreviousSiblings, name, attrs, text, -                             **kwargs) - -    def findPreviousSiblings(self, name=None, attrs={}, text=None, -                             limit=None, **kwargs): -        """Returns the siblings of this Tag that match the given -        criteria and appear before this Tag in the document.""" -        return self._findAll(name, attrs, text, limit, -                             self.previousSiblingGenerator, **kwargs) -    fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x - -    def findParent(self, name=None, attrs={}, **kwargs): -        """Returns the closest parent of this Tag that matches the given -        criteria.""" -        # NOTE: We can't use _findOne because findParents takes a different -        # set of arguments. -        r = None -        l = self.findParents(name, attrs, 1) -        if l: -            r = l[0] -        return r - -    def findParents(self, name=None, attrs={}, limit=None, **kwargs): -        """Returns the parents of this Tag that match the given -        criteria.""" - -        return self._findAll(name, attrs, None, limit, self.parentGenerator, -                             **kwargs) -    fetchParents = findParents # Compatibility with pre-3.x - -    #These methods do the real heavy lifting. - -    def _findOne(self, method, name, attrs, text, **kwargs): -        r = None -        l = method(name, attrs, text, 1, **kwargs) -        if l: -            r = l[0] -        return r - -    def _findAll(self, name, attrs, text, limit, generator, **kwargs): -        "Iterates over a generator looking for things that match." - -        if isinstance(name, SoupStrainer): -            strainer = name -        # (Possibly) special case some findAll*(...) searches -        elif text is None and not limit and not attrs and not kwargs: -            # findAll*(True) -            if name is True: -                return [element for element in generator() -                        if isinstance(element, Tag)] -            # findAll*('tag-name') -            elif isinstance(name, basestring): -                return [element for element in generator() -                        if isinstance(element, Tag) and -                        element.name == name] -            else: -                strainer = SoupStrainer(name, attrs, text, **kwargs) -        # Build a SoupStrainer -        else: -            strainer = SoupStrainer(name, attrs, text, **kwargs) -        results = ResultSet(strainer) -        g = generator() -        while True: -            try: -                i = g.next() -            except StopIteration: -                break -            if i: -                found = strainer.search(i) -                if found: -                    results.append(found) -                    if limit and len(results) >= limit: -                        break -        return results - -    #These Generators can be used to navigate starting from both -    #NavigableStrings and Tags. -    def nextGenerator(self): -        i = self -        while i is not None: -            i = i.next -            yield i - -    def nextSiblingGenerator(self): -        i = self -        while i is not None: -            i = i.nextSibling -            yield i - -    def previousGenerator(self): -        i = self -        while i is not None: -            i = i.previous -            yield i - -    def previousSiblingGenerator(self): -        i = self -        while i is not None: -            i = i.previousSibling -            yield i - -    def parentGenerator(self): -        i = self -        while i is not None: -            i = i.parent -            yield i - -    # Utility methods -    def substituteEncoding(self, str, encoding=None): -        encoding = encoding or "utf-8" -        return str.replace("%SOUP-ENCODING%", encoding) - -    def toEncoding(self, s, encoding=None): -        """Encodes an object to a string in some encoding, or to Unicode. -        .""" -        if isinstance(s, unicode): -            if encoding: -                s = s.encode(encoding) -        elif isinstance(s, str): -            if encoding: -                s = s.encode(encoding) -            else: -                s = unicode(s) -        else: -            if encoding: -                s  = self.toEncoding(str(s), encoding) -            else: -                s = unicode(s) -        return s - -class NavigableString(unicode, PageElement): - -    def __new__(cls, value): -        """Create a new NavigableString. - -        When unpickling a NavigableString, this method is called with -        the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be -        passed in to the superclass's __new__ or the superclass won't know -        how to handle non-ASCII characters. -        """ -        if isinstance(value, unicode): -            return unicode.__new__(cls, value) -        return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) - -    def __getnewargs__(self): -        return (NavigableString.__str__(self),) - -    def __getattr__(self, attr): -        """text.string gives you text. This is for backwards -        compatibility for Navigable*String, but for CData* it lets you -        get the string without the CData wrapper.""" -        if attr == 'string': -            return self -        else: -            raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) - -    def __unicode__(self): -        return str(self).decode(DEFAULT_OUTPUT_ENCODING) - -    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): -        if encoding: -            return self.encode(encoding) -        else: -            return self - -class CData(NavigableString): - -    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): -        return "<![CDATA[%s]]>" % NavigableString.__str__(self, encoding) - -class ProcessingInstruction(NavigableString): -    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): -        output = self -        if "%SOUP-ENCODING%" in output: -            output = self.substituteEncoding(output, encoding) -        return "<?%s?>" % self.toEncoding(output, encoding) - -class Comment(NavigableString): -    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): -        return "<!--%s-->" % NavigableString.__str__(self, encoding) - -class Declaration(NavigableString): -    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): -        return "<!%s>" % NavigableString.__str__(self, encoding) - -class Tag(PageElement): - -    """Represents a found HTML tag with its attributes and contents.""" - -    def _invert(h): -        "Cheap function to invert a hash." -        i = {} -        for k,v in h.items(): -            i[v] = k -        return i - -    XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", -                                      "quot" : '"', -                                      "amp" : "&", -                                      "lt" : "<", -                                      "gt" : ">" } - -    XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) - -    def _convertEntities(self, match): -        """Used in a call to re.sub to replace HTML, XML, and numeric -        entities with the appropriate Unicode characters. If HTML -        entities are being converted, any unrecognized entities are -        escaped.""" -        x = match.group(1) -        if self.convertHTMLEntities and x in name2codepoint: -            return unichr(name2codepoint[x]) -        elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: -            if self.convertXMLEntities: -                return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] -            else: -                return u'&%s;' % x -        elif len(x) > 0 and x[0] == '#': -            # Handle numeric entities -            if len(x) > 1 and x[1] == 'x': -                return unichr(int(x[2:], 16)) -            else: -                return unichr(int(x[1:])) - -        elif self.escapeUnrecognizedEntities: -            return u'&%s;' % x -        else: -            return u'&%s;' % x - -    def __init__(self, parser, name, attrs=None, parent=None, -                 previous=None): -        "Basic constructor." - -        # We don't actually store the parser object: that lets extracted -        # chunks be garbage-collected -        self.parserClass = parser.__class__ -        self.isSelfClosing = parser.isSelfClosingTag(name) -        self.name = name -        if attrs is None: -            attrs = [] -        elif isinstance(attrs, dict): -            attrs = attrs.items() -        self.attrs = attrs -        self.contents = [] -        self.setup(parent, previous) -        self.hidden = False -        self.containsSubstitutions = False -        self.convertHTMLEntities = parser.convertHTMLEntities -        self.convertXMLEntities = parser.convertXMLEntities -        self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities - -        # Convert any HTML, XML, or numeric entities in the attribute values. -        convert = lambda(k, val): (k, -                                   re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", -                                          self._convertEntities, -                                          val)) -        self.attrs = map(convert, self.attrs) - -    def getString(self): -        if (len(self.contents) == 1 -            and isinstance(self.contents[0], NavigableString)): -            return self.contents[0] - -    def setString(self, string): -        """Replace the contents of the tag with a string""" -        self.clear() -        self.append(string) - -    string = property(getString, setString) - -    def getText(self, separator=u""): -        if not len(self.contents): -            return u"" -        stopNode = self._lastRecursiveChild().next -        strings = [] -        current = self.contents[0] -        while current is not stopNode: -            if isinstance(current, NavigableString): -                strings.append(current.strip()) -            current = current.next -        return separator.join(strings) - -    text = property(getText) - -    def get(self, key, default=None): -        """Returns the value of the 'key' attribute for the tag, or -        the value given for 'default' if it doesn't have that -        attribute.""" -        return self._getAttrMap().get(key, default) - -    def clear(self): -        """Extract all children.""" -        for child in self.contents[:]: -            child.extract() - -    def index(self, element): -        for i, child in enumerate(self.contents): -            if child is element: -                return i -        raise ValueError("Tag.index: element not in tag") - -    def has_key(self, key): -        return self._getAttrMap().has_key(key) - -    def __getitem__(self, key): -        """tag[key] returns the value of the 'key' attribute for the tag, -        and throws an exception if it's not there.""" -        return self._getAttrMap()[key] - -    def __iter__(self): -        "Iterating over a tag iterates over its contents." -        return iter(self.contents) - -    def __len__(self): -        "The length of a tag is the length of its list of contents." -        return len(self.contents) - -    def __contains__(self, x): -        return x in self.contents - -    def __nonzero__(self): -        "A tag is non-None even if it has no contents." -        return True - -    def __setitem__(self, key, value): -        """Setting tag[key] sets the value of the 'key' attribute for the -        tag.""" -        self._getAttrMap() -        self.attrMap[key] = value -        found = False -        for i in range(0, len(self.attrs)): -            if self.attrs[i][0] == key: -                self.attrs[i] = (key, value) -                found = True -        if not found: -            self.attrs.append((key, value)) -        self._getAttrMap()[key] = value - -    def __delitem__(self, key): -        "Deleting tag[key] deletes all 'key' attributes for the tag." -        for item in self.attrs: -            if item[0] == key: -                self.attrs.remove(item) -                #We don't break because bad HTML can define the same -                #attribute multiple times. -            self._getAttrMap() -            if self.attrMap.has_key(key): -                del self.attrMap[key] - -    def __call__(self, *args, **kwargs): -        """Calling a tag like a function is the same as calling its -        findAll() method. Eg. tag('a') returns a list of all the A tags -        found within this tag.""" -        return apply(self.findAll, args, kwargs) - -    def __getattr__(self, tag): -        #print "Getattr %s.%s" % (self.__class__, tag) -        if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: -            return self.find(tag[:-3]) -        elif tag.find('__') != 0: -            return self.find(tag) -        raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) - -    def __eq__(self, other): -        """Returns true iff this tag has the same name, the same attributes, -        and the same contents (recursively) as the given tag. - -        NOTE: right now this will return false if two tags have the -        same attributes in a different order. Should this be fixed?""" -        if other is self: -            return True -        if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): -            return False -        for i in range(0, len(self.contents)): -            if self.contents[i] != other.contents[i]: -                return False -        return True - -    def __ne__(self, other): -        """Returns true iff this tag is not identical to the other tag, -        as defined in __eq__.""" -        return not self == other - -    def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): -        """Renders this tag as a string.""" -        return self.__str__(encoding) - -    def __unicode__(self): -        return self.__str__(None) - -    BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" -                                           + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" -                                           + ")") - -    def _sub_entity(self, x): -        """Used with a regular expression to substitute the -        appropriate XML entity for an XML special character.""" -        return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" - -    def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, -                prettyPrint=False, indentLevel=0): -        """Returns a string or Unicode representation of this tag and -        its contents. To get Unicode, pass None for encoding. - -        NOTE: since Python's HTML parser consumes whitespace, this -        method is not certain to reproduce the whitespace present in -        the original string.""" - -        encodedName = self.toEncoding(self.name, encoding) - -        attrs = [] -        if self.attrs: -            for key, val in self.attrs: -                fmt = '%s="%s"' -                if isinstance(val, basestring): -                    if self.containsSubstitutions and '%SOUP-ENCODING%' in val: -                        val = self.substituteEncoding(val, encoding) - -                    # The attribute value either: -                    # -                    # * Contains no embedded double quotes or single quotes. -                    #   No problem: we enclose it in double quotes. -                    # * Contains embedded single quotes. No problem: -                    #   double quotes work here too. -                    # * Contains embedded double quotes. No problem: -                    #   we enclose it in single quotes. -                    # * Embeds both single _and_ double quotes. This -                    #   can't happen naturally, but it can happen if -                    #   you modify an attribute value after parsing -                    #   the document. Now we have a bit of a -                    #   problem. We solve it by enclosing the -                    #   attribute in single quotes, and escaping any -                    #   embedded single quotes to XML entities. -                    if '"' in val: -                        fmt = "%s='%s'" -                        if "'" in val: -                            # TODO: replace with apos when -                            # appropriate. -                            val = val.replace("'", "&squot;") - -                    # Now we're okay w/r/t quotes. But the attribute -                    # value might also contain angle brackets, or -                    # ampersands that aren't part of entities. We need -                    # to escape those to XML entities too. -                    val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) - -                attrs.append(fmt % (self.toEncoding(key, encoding), -                                    self.toEncoding(val, encoding))) -        close = '' -        closeTag = '' -        if self.isSelfClosing: -            close = ' /' -        else: -            closeTag = '</%s>' % encodedName - -        indentTag, indentContents = 0, 0 -        if prettyPrint: -            indentTag = indentLevel -            space = (' ' * (indentTag-1)) -            indentContents = indentTag + 1 -        contents = self.renderContents(encoding, prettyPrint, indentContents) -        if self.hidden: -            s = contents -        else: -            s = [] -            attributeString = '' -            if attrs: -                attributeString = ' ' + ' '.join(attrs) -            if prettyPrint: -                s.append(space) -            s.append('<%s%s%s>' % (encodedName, attributeString, close)) -            if prettyPrint: -                s.append("\n") -            s.append(contents) -            if prettyPrint and contents and contents[-1] != "\n": -                s.append("\n") -            if prettyPrint and closeTag: -                s.append(space) -            s.append(closeTag) -            if prettyPrint and closeTag and self.nextSibling: -                s.append("\n") -            s = ''.join(s) -        return s - -    def decompose(self): -        """Recursively destroys the contents of this tree.""" -        self.extract() -        if len(self.contents) == 0: -            return -        current = self.contents[0] -        while current is not None: -            next = current.next -            if isinstance(current, Tag): -                del current.contents[:] -            current.parent = None -            current.previous = None -            current.previousSibling = None -            current.next = None -            current.nextSibling = None -            current = next - -    def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): -        return self.__str__(encoding, True) - -    def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, -                       prettyPrint=False, indentLevel=0): -        """Renders the contents of this tag as a string in the given -        encoding. If encoding is None, returns a Unicode string..""" -        s=[] -        for c in self: -            text = None -            if isinstance(c, NavigableString): -                text = c.__str__(encoding) -            elif isinstance(c, Tag): -                s.append(c.__str__(encoding, prettyPrint, indentLevel)) -            if text and prettyPrint: -                text = text.strip() -            if text: -                if prettyPrint: -                    s.append(" " * (indentLevel-1)) -                s.append(text) -                if prettyPrint: -                    s.append("\n") -        return ''.join(s) - -    #Soup methods - -    def find(self, name=None, attrs={}, recursive=True, text=None, -             **kwargs): -        """Return only the first child of this Tag matching the given -        criteria.""" -        r = None -        l = self.findAll(name, attrs, recursive, text, 1, **kwargs) -        if l: -            r = l[0] -        return r -    findChild = find - -    def findAll(self, name=None, attrs={}, recursive=True, text=None, -                limit=None, **kwargs): -        """Extracts a list of Tag objects that match the given -        criteria.  You can specify the name of the Tag and any -        attributes you want the Tag to have. - -        The value of a key-value pair in the 'attrs' map can be a -        string, a list of strings, a regular expression object, or a -        callable that takes a string and returns whether or not the -        string matches for some custom definition of 'matches'. The -        same is true of the tag name.""" -        generator = self.recursiveChildGenerator -        if not recursive: -            generator = self.childGenerator -        return self._findAll(name, attrs, text, limit, generator, **kwargs) -    findChildren = findAll - -    # Pre-3.x compatibility methods -    first = find -    fetch = findAll - -    def fetchText(self, text=None, recursive=True, limit=None): -        return self.findAll(text=text, recursive=recursive, limit=limit) - -    def firstText(self, text=None, recursive=True): -        return self.find(text=text, recursive=recursive) - -    #Private methods - -    def _getAttrMap(self): -        """Initializes a map representation of this tag's attributes, -        if not already initialized.""" -        if not getattr(self, 'attrMap'): -            self.attrMap = {} -            for (key, value) in self.attrs: -                self.attrMap[key] = value -        return self.attrMap - -    #Generator methods -    def childGenerator(self): -        # Just use the iterator from the contents -        return iter(self.contents) - -    def recursiveChildGenerator(self): -        if not len(self.contents): -            raise StopIteration -        stopNode = self._lastRecursiveChild().next -        current = self.contents[0] -        while current is not stopNode: -            yield current -            current = current.next - - -# Next, a couple classes to represent queries and their results. -class SoupStrainer: -    """Encapsulates a number of ways of matching a markup element (tag or -    text).""" - -    def __init__(self, name=None, attrs={}, text=None, **kwargs): -        self.name = name -        if isinstance(attrs, basestring): -            kwargs['class'] = _match_css_class(attrs) -            attrs = None -        if kwargs: -            if attrs: -                attrs = attrs.copy() -                attrs.update(kwargs) -            else: -                attrs = kwargs -        self.attrs = attrs -        self.text = text - -    def __str__(self): -        if self.text: -            return self.text -        else: -            return "%s|%s" % (self.name, self.attrs) - -    def searchTag(self, markupName=None, markupAttrs={}): -        found = None -        markup = None -        if isinstance(markupName, Tag): -            markup = markupName -            markupAttrs = markup -        callFunctionWithTagData = callable(self.name) \ -                                and not isinstance(markupName, Tag) - -        if (not self.name) \ -               or callFunctionWithTagData \ -               or (markup and self._matches(markup, self.name)) \ -               or (not markup and self._matches(markupName, self.name)): -            if callFunctionWithTagData: -                match = self.name(markupName, markupAttrs) -            else: -                match = True -                markupAttrMap = None -                for attr, matchAgainst in self.attrs.items(): -                    if not markupAttrMap: -                         if hasattr(markupAttrs, 'get'): -                            markupAttrMap = markupAttrs -                         else: -                            markupAttrMap = {} -                            for k,v in markupAttrs: -                                markupAttrMap[k] = v -                    attrValue = markupAttrMap.get(attr) -                    if not self._matches(attrValue, matchAgainst): -                        match = False -                        break -            if match: -                if markup: -                    found = markup -                else: -                    found = markupName -        return found - -    def search(self, markup): -        #print 'looking for %s in %s' % (self, markup) -        found = None -        # If given a list of items, scan it for a text element that -        # matches. -        if hasattr(markup, "__iter__") \ -                and not isinstance(markup, Tag): -            for element in markup: -                if isinstance(element, NavigableString) \ -                       and self.search(element): -                    found = element -                    break -        # If it's a Tag, make sure its name or attributes match. -        # Don't bother with Tags if we're searching for text. -        elif isinstance(markup, Tag): -            if not self.text: -                found = self.searchTag(markup) -        # If it's text, make sure the text matches. -        elif isinstance(markup, NavigableString) or \ -                 isinstance(markup, basestring): -            if self._matches(markup, self.text): -                found = markup -        else: -            raise Exception, "I don't know how to match against a %s" \ -                  % markup.__class__ -        return found - -    def _matches(self, markup, matchAgainst): -        #print "Matching %s against %s" % (markup, matchAgainst) -        result = False -        if matchAgainst is True: -            result = markup is not None -        elif callable(matchAgainst): -            result = matchAgainst(markup) -        else: -            #Custom match methods take the tag as an argument, but all -            #other ways of matching match the tag name as a string. -            if isinstance(markup, Tag): -                markup = markup.name -            if markup and not isinstance(markup, basestring): -                markup = unicode(markup) -            #Now we know that chunk is either a string, or None. -            if hasattr(matchAgainst, 'match'): -                # It's a regexp object. -                result = markup and matchAgainst.search(markup) -            elif hasattr(matchAgainst, '__iter__'): # list-like -                result = markup in matchAgainst -            elif hasattr(matchAgainst, 'items'): -                result = markup.has_key(matchAgainst) -            elif matchAgainst and isinstance(markup, basestring): -                if isinstance(markup, unicode): -                    matchAgainst = unicode(matchAgainst) -                else: -                    matchAgainst = str(matchAgainst) - -            if not result: -                result = matchAgainst == markup -        return result - -class ResultSet(list): -    """A ResultSet is just a list that keeps track of the SoupStrainer -    that created it.""" -    def __init__(self, source): -        list.__init__([]) -        self.source = source - -# Now, some helper functions. - -def buildTagMap(default, *args): -    """Turns a list of maps, lists, or scalars into a single map. -    Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and -    NESTING_RESET_TAGS maps out of lists and partial maps.""" -    built = {} -    for portion in args: -        if hasattr(portion, 'items'): -            #It's a map. Merge it. -            for k,v in portion.items(): -                built[k] = v -        elif hasattr(portion, '__iter__'): # is a list -            #It's a list. Map each item to the default. -            for k in portion: -                built[k] = default -        else: -            #It's a scalar. Map it to the default. -            built[portion] = default -    return built - -# Now, the parser classes. - -class BeautifulStoneSoup(Tag, SGMLParser): - -    """This class contains the basic parser and search code. It defines -    a parser that knows nothing about tag behavior except for the -    following: - -      You can't close a tag without closing all the tags it encloses. -      That is, "<foo><bar></foo>" actually means -      "<foo><bar></bar></foo>". - -    [Another possible explanation is "<foo><bar /></foo>", but since -    this class defines no SELF_CLOSING_TAGS, it will never use that -    explanation.] - -    This class is useful for parsing XML or made-up markup languages, -    or when BeautifulSoup makes an assumption counter to what you were -    expecting.""" - -    SELF_CLOSING_TAGS = {} -    NESTABLE_TAGS = {} -    RESET_NESTING_TAGS = {} -    QUOTE_TAGS = {} -    PRESERVE_WHITESPACE_TAGS = [] - -    MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), -                       lambda x: x.group(1) + ' />'), -                      (re.compile('<!\s+([^<>]*)>'), -                       lambda x: '<!' + x.group(1) + '>') -                      ] - -    ROOT_TAG_NAME = u'[document]' - -    HTML_ENTITIES = "html" -    XML_ENTITIES = "xml" -    XHTML_ENTITIES = "xhtml" -    # TODO: This only exists for backwards-compatibility -    ALL_ENTITIES = XHTML_ENTITIES - -    # Used when determining whether a text node is all whitespace and -    # can be replaced with a single space. A text node that contains -    # fancy Unicode spaces (usually non-breaking) should be left -    # alone. -    STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } - -    def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, -                 markupMassage=True, smartQuotesTo=XML_ENTITIES, -                 convertEntities=None, selfClosingTags=None, isHTML=False): -        """The Soup object is initialized as the 'root tag', and the -        provided markup (which can be a string or a file-like object) -        is fed into the underlying parser. - -        sgmllib will process most bad HTML, and the BeautifulSoup -        class has some tricks for dealing with some HTML that kills -        sgmllib, but Beautiful Soup can nonetheless choke or lose data -        if your data uses self-closing tags or declarations -        incorrectly. - -        By default, Beautiful Soup uses regexes to sanitize input, -        avoiding the vast majority of these problems. If the problems -        don't apply to you, pass in False for markupMassage, and -        you'll get better performance. - -        The default parser massage techniques fix the two most common -        instances of invalid HTML that choke sgmllib: - -         <br/> (No space between name of closing tag and tag close) -         <! --Comment--> (Extraneous whitespace in declaration) - -        You can pass in a custom list of (RE object, replace method) -        tuples to get Beautiful Soup to scrub your input the way you -        want.""" - -        self.parseOnlyThese = parseOnlyThese -        self.fromEncoding = fromEncoding -        self.smartQuotesTo = smartQuotesTo -        self.convertEntities = convertEntities -        # Set the rules for how we'll deal with the entities we -        # encounter -        if self.convertEntities: -            # It doesn't make sense to convert encoded characters to -            # entities even while you're converting entities to Unicode. -            # Just convert it all to Unicode. -            self.smartQuotesTo = None -            if convertEntities == self.HTML_ENTITIES: -                self.convertXMLEntities = False -                self.convertHTMLEntities = True -                self.escapeUnrecognizedEntities = True -            elif convertEntities == self.XHTML_ENTITIES: -                self.convertXMLEntities = True -                self.convertHTMLEntities = True -                self.escapeUnrecognizedEntities = False -            elif convertEntities == self.XML_ENTITIES: -                self.convertXMLEntities = True -                self.convertHTMLEntities = False -                self.escapeUnrecognizedEntities = False -        else: -            self.convertXMLEntities = False -            self.convertHTMLEntities = False -            self.escapeUnrecognizedEntities = False - -        self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) -        SGMLParser.__init__(self) - -        if hasattr(markup, 'read'):        # It's a file-type object. -            markup = markup.read() -        self.markup = markup -        self.markupMassage = markupMassage -        try: -            self._feed(isHTML=isHTML) -        except StopParsing: -            pass -        self.markup = None                 # The markup can now be GCed - -    def convert_charref(self, name): -        """This method fixes a bug in Python's SGMLParser.""" -        try: -            n = int(name) -        except ValueError: -            return -        if not 0 <= n <= 127 : # ASCII ends at 127, not 255 -            return -        return self.convert_codepoint(n) - -    def _feed(self, inDocumentEncoding=None, isHTML=False): -        # Convert the document to Unicode. -        markup = self.markup -        if isinstance(markup, unicode): -            if not hasattr(self, 'originalEncoding'): -                self.originalEncoding = None -        else: -            dammit = UnicodeDammit\ -                     (markup, [self.fromEncoding, inDocumentEncoding], -                      smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) -            markup = dammit.unicode -            self.originalEncoding = dammit.originalEncoding -            self.declaredHTMLEncoding = dammit.declaredHTMLEncoding -        if markup: -            if self.markupMassage: -                if not hasattr(self.markupMassage, "__iter__"): -                    self.markupMassage = self.MARKUP_MASSAGE -                for fix, m in self.markupMassage: -                    markup = fix.sub(m, markup) -                # TODO: We get rid of markupMassage so that the -                # soup object can be deepcopied later on. Some -                # Python installations can't copy regexes. If anyone -                # was relying on the existence of markupMassage, this -                # might cause problems. -                del(self.markupMassage) -        self.reset() - -        SGMLParser.feed(self, markup) -        # Close out any unfinished strings and close all the open tags. -        self.endData() -        while self.currentTag.name != self.ROOT_TAG_NAME: -            self.popTag() - -    def __getattr__(self, methodName): -        """This method routes method call requests to either the SGMLParser -        superclass or the Tag superclass, depending on the method name.""" -        #print "__getattr__ called on %s.%s" % (self.__class__, methodName) - -        if methodName.startswith('start_') or methodName.startswith('end_') \ -               or methodName.startswith('do_'): -            return SGMLParser.__getattr__(self, methodName) -        elif not methodName.startswith('__'): -            return Tag.__getattr__(self, methodName) -        else: -            raise AttributeError - -    def isSelfClosingTag(self, name): -        """Returns true iff the given string is the name of a -        self-closing tag according to this parser.""" -        return self.SELF_CLOSING_TAGS.has_key(name) \ -               or self.instanceSelfClosingTags.has_key(name) - -    def reset(self): -        Tag.__init__(self, self, self.ROOT_TAG_NAME) -        self.hidden = 1 -        SGMLParser.reset(self) -        self.currentData = [] -        self.currentTag = None -        self.tagStack = [] -        self.quoteStack = [] -        self.pushTag(self) - -    def popTag(self): -        tag = self.tagStack.pop() - -        #print "Pop", tag.name -        if self.tagStack: -            self.currentTag = self.tagStack[-1] -        return self.currentTag - -    def pushTag(self, tag): -        #print "Push", tag.name -        if self.currentTag: -            self.currentTag.contents.append(tag) -        self.tagStack.append(tag) -        self.currentTag = self.tagStack[-1] - -    def endData(self, containerClass=NavigableString): -        if self.currentData: -            currentData = u''.join(self.currentData) -            if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and -                not set([tag.name for tag in self.tagStack]).intersection( -                    self.PRESERVE_WHITESPACE_TAGS)): -                if '\n' in currentData: -                    currentData = '\n' -                else: -                    currentData = ' ' -            self.currentData = [] -            if self.parseOnlyThese and len(self.tagStack) <= 1 and \ -                   (not self.parseOnlyThese.text or \ -                    not self.parseOnlyThese.search(currentData)): -                return -            o = containerClass(currentData) -            o.setup(self.currentTag, self.previous) -            if self.previous: -                self.previous.next = o -            self.previous = o -            self.currentTag.contents.append(o) - - -    def _popToTag(self, name, inclusivePop=True): -        """Pops the tag stack up to and including the most recent -        instance of the given tag. If inclusivePop is false, pops the tag -        stack up to but *not* including the most recent instqance of -        the given tag.""" -        #print "Popping to %s" % name -        if name == self.ROOT_TAG_NAME: -            return - -        numPops = 0 -        mostRecentTag = None -        for i in range(len(self.tagStack)-1, 0, -1): -            if name == self.tagStack[i].name: -                numPops = len(self.tagStack)-i -                break -        if not inclusivePop: -            numPops = numPops - 1 - -        for i in range(0, numPops): -            mostRecentTag = self.popTag() -        return mostRecentTag - -    def _smartPop(self, name): - -        """We need to pop up to the previous tag of this type, unless -        one of this tag's nesting reset triggers comes between this -        tag and the previous tag of this type, OR unless this tag is a -        generic nesting trigger and another generic nesting trigger -        comes between this tag and the previous tag of this type. - -        Examples: -         <p>Foo<b>Bar *<p>* should pop to 'p', not 'b'. -         <p>Foo<table>Bar *<p>* should pop to 'table', not 'p'. -         <p>Foo<table><tr>Bar *<p>* should pop to 'tr', not 'p'. - -         <li><ul><li> *<li>* should pop to 'ul', not the first 'li'. -         <tr><table><tr> *<tr>* should pop to 'table', not the first 'tr' -         <td><tr><td> *<td>* should pop to 'tr', not the first 'td' -        """ - -        nestingResetTriggers = self.NESTABLE_TAGS.get(name) -        isNestable = nestingResetTriggers != None -        isResetNesting = self.RESET_NESTING_TAGS.has_key(name) -        popTo = None -        inclusive = True -        for i in range(len(self.tagStack)-1, 0, -1): -            p = self.tagStack[i] -            if (not p or p.name == name) and not isNestable: -                #Non-nestable tags get popped to the top or to their -                #last occurance. -                popTo = name -                break -            if (nestingResetTriggers is not None -                and p.name in nestingResetTriggers) \ -                or (nestingResetTriggers is None and isResetNesting -                    and self.RESET_NESTING_TAGS.has_key(p.name)): - -                #If we encounter one of the nesting reset triggers -                #peculiar to this tag, or we encounter another tag -                #that causes nesting to reset, pop up to but not -                #including that tag. -                popTo = p.name -                inclusive = False -                break -            p = p.parent -        if popTo: -            self._popToTag(popTo, inclusive) - -    def unknown_starttag(self, name, attrs, selfClosing=0): -        #print "Start tag %s: %s" % (name, attrs) -        if self.quoteStack: -            #This is not a real tag. -            #print "<%s> is not real!" % name -            attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs]) -            self.handle_data('<%s%s>' % (name, attrs)) -            return -        self.endData() - -        if not self.isSelfClosingTag(name) and not selfClosing: -            self._smartPop(name) - -        if self.parseOnlyThese and len(self.tagStack) <= 1 \ -               and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): -            return - -        tag = Tag(self, name, attrs, self.currentTag, self.previous) -        if self.previous: -            self.previous.next = tag -        self.previous = tag -        self.pushTag(tag) -        if selfClosing or self.isSelfClosingTag(name): -            self.popTag() -        if name in self.QUOTE_TAGS: -            #print "Beginning quote (%s)" % name -            self.quoteStack.append(name) -            self.literal = 1 -        return tag - -    def unknown_endtag(self, name): -        #print "End tag %s" % name -        if self.quoteStack and self.quoteStack[-1] != name: -            #This is not a real end tag. -            #print "</%s> is not real!" % name -            self.handle_data('</%s>' % name) -            return -        self.endData() -        self._popToTag(name) -        if self.quoteStack and self.quoteStack[-1] == name: -            self.quoteStack.pop() -            self.literal = (len(self.quoteStack) > 0) - -    def handle_data(self, data): -        self.currentData.append(data) - -    def _toStringSubclass(self, text, subclass): -        """Adds a certain piece of text to the tree as a NavigableString -        subclass.""" -        self.endData() -        self.handle_data(text) -        self.endData(subclass) - -    def handle_pi(self, text): -        """Handle a processing instruction as a ProcessingInstruction -        object, possibly one with a %SOUP-ENCODING% slot into which an -        encoding will be plugged later.""" -        if text[:3] == "xml": -            text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" -        self._toStringSubclass(text, ProcessingInstruction) - -    def handle_comment(self, text): -        "Handle comments as Comment objects." -        self._toStringSubclass(text, Comment) - -    def handle_charref(self, ref): -        "Handle character references as data." -        if self.convertEntities: -            data = unichr(int(ref)) -        else: -            data = '&#%s;' % ref -        self.handle_data(data) - -    def handle_entityref(self, ref): -        """Handle entity references as data, possibly converting known -        HTML and/or XML entity references to the corresponding Unicode -        characters.""" -        data = None -        if self.convertHTMLEntities: -            try: -                data = unichr(name2codepoint[ref]) -            except KeyError: -                pass - -        if not data and self.convertXMLEntities: -                data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) - -        if not data and self.convertHTMLEntities and \ -            not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): -                # TODO: We've got a problem here. We're told this is -                # an entity reference, but it's not an XML entity -                # reference or an HTML entity reference. Nonetheless, -                # the logical thing to do is to pass it through as an -                # unrecognized entity reference. -                # -                # Except: when the input is "&carol;" this function -                # will be called with input "carol". When the input is -                # "AT&T", this function will be called with input -                # "T". We have no way of knowing whether a semicolon -                # was present originally, so we don't know whether -                # this is an unknown entity or just a misplaced -                # ampersand. -                # -                # The more common case is a misplaced ampersand, so I -                # escape the ampersand and omit the trailing semicolon. -                data = "&%s" % ref -        if not data: -            # This case is different from the one above, because we -            # haven't already gone through a supposedly comprehensive -            # mapping of entities to Unicode characters. We might not -            # have gone through any mapping at all. So the chances are -            # very high that this is a real entity, and not a -            # misplaced ampersand. -            data = "&%s;" % ref -        self.handle_data(data) - -    def handle_decl(self, data): -        "Handle DOCTYPEs and the like as Declaration objects." -        self._toStringSubclass(data, Declaration) - -    def parse_declaration(self, i): -        """Treat a bogus SGML declaration as raw data. Treat a CDATA -        declaration as a CData object.""" -        j = None -        if self.rawdata[i:i+9] == '<![CDATA[': -             k = self.rawdata.find(']]>', i) -             if k == -1: -                 k = len(self.rawdata) -             data = self.rawdata[i+9:k] -             j = k+3 -             self._toStringSubclass(data, CData) -        else: -            try: -                j = SGMLParser.parse_declaration(self, i) -            except SGMLParseError: -                toHandle = self.rawdata[i:] -                self.handle_data(toHandle) -                j = i + len(toHandle) -        return j - -class BeautifulSoup(BeautifulStoneSoup): - -    """This parser knows the following facts about HTML: - -    * Some tags have no closing tag and should be interpreted as being -      closed as soon as they are encountered. - -    * The text inside some tags (ie. 'script') may contain tags which -      are not really part of the document and which should be parsed -      as text, not tags. If you want to parse the text as tags, you can -      always fetch it and parse it explicitly. - -    * Tag nesting rules: - -      Most tags can't be nested at all. For instance, the occurance of -      a <p> tag should implicitly close the previous <p> tag. - -       <p>Para1<p>Para2 -        should be transformed into: -       <p>Para1</p><p>Para2 - -      Some tags can be nested arbitrarily. For instance, the occurance -      of a <blockquote> tag should _not_ implicitly close the previous -      <blockquote> tag. - -       Alice said: <blockquote>Bob said: <blockquote>Blah -        should NOT be transformed into: -       Alice said: <blockquote>Bob said: </blockquote><blockquote>Blah - -      Some tags can be nested, but the nesting is reset by the -      interposition of other tags. For instance, a <tr> tag should -      implicitly close the previous <tr> tag within the same <table>, -      but not close a <tr> tag in another table. - -       <table><tr>Blah<tr>Blah -        should be transformed into: -       <table><tr>Blah</tr><tr>Blah -        but, -       <tr>Blah<table><tr>Blah -        should NOT be transformed into -       <tr>Blah<table></tr><tr>Blah - -    Differing assumptions about tag nesting rules are a major source -    of problems with the BeautifulSoup class. If BeautifulSoup is not -    treating as nestable a tag your page author treats as nestable, -    try ICantBelieveItsBeautifulSoup, MinimalSoup, or -    BeautifulStoneSoup before writing your own subclass.""" - -    def __init__(self, *args, **kwargs): -        if not kwargs.has_key('smartQuotesTo'): -            kwargs['smartQuotesTo'] = self.HTML_ENTITIES -        kwargs['isHTML'] = True -        BeautifulStoneSoup.__init__(self, *args, **kwargs) - -    SELF_CLOSING_TAGS = buildTagMap(None, -                                    ('br' , 'hr', 'input', 'img', 'meta', -                                    'spacer', 'link', 'frame', 'base', 'col')) - -    PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) - -    QUOTE_TAGS = {'script' : None, 'textarea' : None} - -    #According to the HTML standard, each of these inline tags can -    #contain another tag of the same type. Furthermore, it's common -    #to actually use these tags this way. -    NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', -                            'center') - -    #According to the HTML standard, these block tags can contain -    #another tag of the same type. Furthermore, it's common -    #to actually use these tags this way. -    NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del') - -    #Lists can contain other lists, but there are restrictions. -    NESTABLE_LIST_TAGS = { 'ol' : [], -                           'ul' : [], -                           'li' : ['ul', 'ol'], -                           'dl' : [], -                           'dd' : ['dl'], -                           'dt' : ['dl'] } - -    #Tables can contain other tables, but there are restrictions. -    NESTABLE_TABLE_TAGS = {'table' : [], -                           'tr' : ['table', 'tbody', 'tfoot', 'thead'], -                           'td' : ['tr'], -                           'th' : ['tr'], -                           'thead' : ['table'], -                           'tbody' : ['table'], -                           'tfoot' : ['table'], -                           } - -    NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre') - -    #If one of these tags is encountered, all tags up to the next tag of -    #this type are popped. -    RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', -                                     NON_NESTABLE_BLOCK_TAGS, -                                     NESTABLE_LIST_TAGS, -                                     NESTABLE_TABLE_TAGS) - -    NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, -                                NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) - -    # Used to detect the charset in a META tag; see start_meta -    CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) - -    def start_meta(self, attrs): -        """Beautiful Soup can detect a charset included in a META tag, -        try to convert the document to that charset, and re-parse the -        document from the beginning.""" -        httpEquiv = None -        contentType = None -        contentTypeIndex = None -        tagNeedsEncodingSubstitution = False - -        for i in range(0, len(attrs)): -            key, value = attrs[i] -            key = key.lower() -            if key == 'http-equiv': -                httpEquiv = value -            elif key == 'content': -                contentType = value -                contentTypeIndex = i - -        if httpEquiv and contentType: # It's an interesting meta tag. -            match = self.CHARSET_RE.search(contentType) -            if match: -                if (self.declaredHTMLEncoding is not None or -                    self.originalEncoding == self.fromEncoding): -                    # An HTML encoding was sniffed while converting -                    # the document to Unicode, or an HTML encoding was -                    # sniffed during a previous pass through the -                    # document, or an encoding was specified -                    # explicitly and it worked. Rewrite the meta tag. -                    def rewrite(match): -                        return match.group(1) + "%SOUP-ENCODING%" -                    newAttr = self.CHARSET_RE.sub(rewrite, contentType) -                    attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], -                                               newAttr) -                    tagNeedsEncodingSubstitution = True -                else: -                    # This is our first pass through the document. -                    # Go through it again with the encoding information. -                    newCharset = match.group(3) -                    if newCharset and newCharset != self.originalEncoding: -                        self.declaredHTMLEncoding = newCharset -                        self._feed(self.declaredHTMLEncoding) -                        raise StopParsing -                    pass -        tag = self.unknown_starttag("meta", attrs) -        if tag and tagNeedsEncodingSubstitution: -            tag.containsSubstitutions = True - -class StopParsing(Exception): -    pass - -class ICantBelieveItsBeautifulSoup(BeautifulSoup): - -    """The BeautifulSoup class is oriented towards skipping over -    common HTML errors like unclosed tags. However, sometimes it makes -    errors of its own. For instance, consider this fragment: - -     <b>Foo<b>Bar</b></b> - -    This is perfectly valid (if bizarre) HTML. However, the -    BeautifulSoup class will implicitly close the first b tag when it -    encounters the second 'b'. It will think the author wrote -    "<b>Foo<b>Bar", and didn't close the first 'b' tag, because -    there's no real-world reason to bold something that's already -    bold. When it encounters '</b></b>' it will close two more 'b' -    tags, for a grand total of three tags closed instead of two. This -    can throw off the rest of your document structure. The same is -    true of a number of other tags, listed below. - -    It's much more common for someone to forget to close a 'b' tag -    than to actually use nested 'b' tags, and the BeautifulSoup class -    handles the common case. This class handles the not-co-common -    case: where you can't believe someone wrote what they did, but -    it's valid HTML and BeautifulSoup screwed up by assuming it -    wouldn't be.""" - -    I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ -     ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', -      'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', -      'big') - -    I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',) - -    NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, -                                I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, -                                I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) - -class MinimalSoup(BeautifulSoup): -    """The MinimalSoup class is for parsing HTML that contains -    pathologically bad markup. It makes no assumptions about tag -    nesting, but it does know which tags are self-closing, that -    <script> tags contain Javascript and should not be parsed, that -    META tags may contain encoding information, and so on. - -    This also makes it better for subclassing than BeautifulStoneSoup -    or BeautifulSoup.""" - -    RESET_NESTING_TAGS = buildTagMap('noscript') -    NESTABLE_TAGS = {} - -class BeautifulSOAP(BeautifulStoneSoup): -    """This class will push a tag with only a single string child into -    the tag's parent as an attribute. The attribute's name is the tag -    name, and the value is the string child. An example should give -    the flavor of the change: - -    <foo><bar>baz</bar></foo> -     => -    <foo bar="baz"><bar>baz</bar></foo> - -    You can then access fooTag['bar'] instead of fooTag.barTag.string. - -    This is, of course, useful for scraping structures that tend to -    use subelements instead of attributes, such as SOAP messages. Note -    that it modifies its input, so don't print the modified version -    out. - -    I'm not sure how many people really want to use this class; let me -    know if you do. Mainly I like the name.""" - -    def popTag(self): -        if len(self.tagStack) > 1: -            tag = self.tagStack[-1] -            parent = self.tagStack[-2] -            parent._getAttrMap() -            if (isinstance(tag, Tag) and len(tag.contents) == 1 and -                isinstance(tag.contents[0], NavigableString) and -                not parent.attrMap.has_key(tag.name)): -                parent[tag.name] = tag.contents[0] -        BeautifulStoneSoup.popTag(self) - -#Enterprise class names! It has come to our attention that some people -#think the names of the Beautiful Soup parser classes are too silly -#and "unprofessional" for use in enterprise screen-scraping. We feel -#your pain! For such-minded folk, the Beautiful Soup Consortium And -#All-Night Kosher Bakery recommends renaming this file to -#"RobustParser.py" (or, in cases of extreme enterprisiness, -#"RobustParserBeanInterface.class") and using the following -#enterprise-friendly class aliases: -class RobustXMLParser(BeautifulStoneSoup): -    pass -class RobustHTMLParser(BeautifulSoup): -    pass -class RobustWackAssHTMLParser(ICantBelieveItsBeautifulSoup): -    pass -class RobustInsanelyWackAssHTMLParser(MinimalSoup): -    pass -class SimplifyingSOAPParser(BeautifulSOAP): -    pass - -###################################################### -# -# Bonus library: Unicode, Dammit -# -# This class forces XML data into a standard format (usually to UTF-8 -# or Unicode).  It is heavily based on code from Mark Pilgrim's -# Universal Feed Parser. It does not rewrite the XML or HTML to -# reflect a new encoding: that happens in BeautifulStoneSoup.handle_pi -# (XML) and BeautifulSoup.start_meta (HTML). - -# Autodetects character encodings. -# Download from http://chardet.feedparser.org/ -try: -    import chardet -#    import chardet.constants -#    chardet.constants._debug = 1 -except ImportError: -    chardet = None - -# cjkcodecs and iconv_codec make Python know about more character encodings. -# Both are available from http://cjkpython.i18n.org/ -# They're built in if you use Python 2.4. -try: -    import cjkcodecs.aliases -except ImportError: -    pass -try: -    import iconv_codec -except ImportError: -    pass - -class UnicodeDammit: -    """A class for detecting the encoding of a *ML document and -    converting it to a Unicode string. If the source encoding is -    windows-1252, can replace MS smart quotes with their HTML or XML -    equivalents.""" - -    # This dictionary maps commonly seen values for "charset" in HTML -    # meta tags to the corresponding Python codec names. It only covers -    # values that aren't in Python's aliases and can't be determined -    # by the heuristics in find_codec. -    CHARSET_ALIASES = { "macintosh" : "mac-roman", -                        "x-sjis" : "shift-jis" } - -    def __init__(self, markup, overrideEncodings=[], -                 smartQuotesTo='xml', isHTML=False): -        self.declaredHTMLEncoding = None -        self.markup, documentEncoding, sniffedEncoding = \ -                     self._detectEncoding(markup, isHTML) -        self.smartQuotesTo = smartQuotesTo -        self.triedEncodings = [] -        if markup == '' or isinstance(markup, unicode): -            self.originalEncoding = None -            self.unicode = unicode(markup) -            return - -        u = None -        for proposedEncoding in overrideEncodings: -            u = self._convertFrom(proposedEncoding) -            if u: break -        if not u: -            for proposedEncoding in (documentEncoding, sniffedEncoding): -                u = self._convertFrom(proposedEncoding) -                if u: break - -        # If no luck and we have auto-detection library, try that: -        if not u and chardet and not isinstance(self.markup, unicode): -            u = self._convertFrom(chardet.detect(self.markup)['encoding']) - -        # As a last resort, try utf-8 and windows-1252: -        if not u: -            for proposed_encoding in ("utf-8", "windows-1252"): -                u = self._convertFrom(proposed_encoding) -                if u: break - -        self.unicode = u -        if not u: self.originalEncoding = None - -    def _subMSChar(self, orig): -        """Changes a MS smart quote character to an XML or HTML -        entity.""" -        sub = self.MS_CHARS.get(orig) -        if isinstance(sub, tuple): -            if self.smartQuotesTo == 'xml': -                sub = '&#x%s;' % sub[1] -            else: -                sub = '&%s;' % sub[0] -        return sub - -    def _convertFrom(self, proposed): -        proposed = self.find_codec(proposed) -        if not proposed or proposed in self.triedEncodings: -            return None -        self.triedEncodings.append(proposed) -        markup = self.markup - -        # Convert smart quotes to HTML if coming from an encoding -        # that might have them. -        if self.smartQuotesTo and proposed.lower() in("windows-1252", -                                                      "iso-8859-1", -                                                      "iso-8859-2"): -            markup = re.compile("([\x80-\x9f])").sub \ -                     (lambda(x): self._subMSChar(x.group(1)), -                      markup) - -        try: -            # print "Trying to convert document to %s" % proposed -            u = self._toUnicode(markup, proposed) -            self.markup = u -            self.originalEncoding = proposed -        except Exception, e: -            # print "That didn't work!" -            # print e -            return None -        #print "Correct encoding: %s" % proposed -        return self.markup - -    def _toUnicode(self, data, encoding): -        '''Given a string and its encoding, decodes the string into Unicode. -        %encoding is a string recognized by encodings.aliases''' - -        # strip Byte Order Mark (if present) -        if (len(data) >= 4) and (data[:2] == '\xfe\xff') \ -               and (data[2:4] != '\x00\x00'): -            encoding = 'utf-16be' -            data = data[2:] -        elif (len(data) >= 4) and (data[:2] == '\xff\xfe') \ -                 and (data[2:4] != '\x00\x00'): -            encoding = 'utf-16le' -            data = data[2:] -        elif data[:3] == '\xef\xbb\xbf': -            encoding = 'utf-8' -            data = data[3:] -        elif data[:4] == '\x00\x00\xfe\xff': -            encoding = 'utf-32be' -            data = data[4:] -        elif data[:4] == '\xff\xfe\x00\x00': -            encoding = 'utf-32le' -            data = data[4:] -        newdata = unicode(data, encoding) -        return newdata - -    def _detectEncoding(self, xml_data, isHTML=False): -        """Given a document, tries to detect its XML encoding.""" -        xml_encoding = sniffed_xml_encoding = None -        try: -            if xml_data[:4] == '\x4c\x6f\xa7\x94': -                # EBCDIC -                xml_data = self._ebcdic_to_ascii(xml_data) -            elif xml_data[:4] == '\x00\x3c\x00\x3f': -                # UTF-16BE -                sniffed_xml_encoding = 'utf-16be' -                xml_data = unicode(xml_data, 'utf-16be').encode('utf-8') -            elif (len(xml_data) >= 4) and (xml_data[:2] == '\xfe\xff') \ -                     and (xml_data[2:4] != '\x00\x00'): -                # UTF-16BE with BOM -                sniffed_xml_encoding = 'utf-16be' -                xml_data = unicode(xml_data[2:], 'utf-16be').encode('utf-8') -            elif xml_data[:4] == '\x3c\x00\x3f\x00': -                # UTF-16LE -                sniffed_xml_encoding = 'utf-16le' -                xml_data = unicode(xml_data, 'utf-16le').encode('utf-8') -            elif (len(xml_data) >= 4) and (xml_data[:2] == '\xff\xfe') and \ -                     (xml_data[2:4] != '\x00\x00'): -                # UTF-16LE with BOM -                sniffed_xml_encoding = 'utf-16le' -                xml_data = unicode(xml_data[2:], 'utf-16le').encode('utf-8') -            elif xml_data[:4] == '\x00\x00\x00\x3c': -                # UTF-32BE -                sniffed_xml_encoding = 'utf-32be' -                xml_data = unicode(xml_data, 'utf-32be').encode('utf-8') -            elif xml_data[:4] == '\x3c\x00\x00\x00': -                # UTF-32LE -                sniffed_xml_encoding = 'utf-32le' -                xml_data = unicode(xml_data, 'utf-32le').encode('utf-8') -            elif xml_data[:4] == '\x00\x00\xfe\xff': -                # UTF-32BE with BOM -                sniffed_xml_encoding = 'utf-32be' -                xml_data = unicode(xml_data[4:], 'utf-32be').encode('utf-8') -            elif xml_data[:4] == '\xff\xfe\x00\x00': -                # UTF-32LE with BOM -                sniffed_xml_encoding = 'utf-32le' -                xml_data = unicode(xml_data[4:], 'utf-32le').encode('utf-8') -            elif xml_data[:3] == '\xef\xbb\xbf': -                # UTF-8 with BOM -                sniffed_xml_encoding = 'utf-8' -                xml_data = unicode(xml_data[3:], 'utf-8').encode('utf-8') -            else: -                sniffed_xml_encoding = 'ascii' -                pass -        except: -            xml_encoding_match = None -        xml_encoding_match = re.compile( -            '^<\?.*encoding=[\'"](.*?)[\'"].*\?>').match(xml_data) -        if not xml_encoding_match and isHTML: -            regexp = re.compile('<\s*meta[^>]+charset=([^>]*?)[;\'">]', re.I) -            xml_encoding_match = regexp.search(xml_data) -        if xml_encoding_match is not None: -            xml_encoding = xml_encoding_match.groups()[0].lower() -            if isHTML: -                self.declaredHTMLEncoding = xml_encoding -            if sniffed_xml_encoding and \ -               (xml_encoding in ('iso-10646-ucs-2', 'ucs-2', 'csunicode', -                                 'iso-10646-ucs-4', 'ucs-4', 'csucs4', -                                 'utf-16', 'utf-32', 'utf_16', 'utf_32', -                                 'utf16', 'u16')): -                xml_encoding = sniffed_xml_encoding -        return xml_data, xml_encoding, sniffed_xml_encoding - - -    def find_codec(self, charset): -        return self._codec(self.CHARSET_ALIASES.get(charset, charset)) \ -               or (charset and self._codec(charset.replace("-", ""))) \ -               or (charset and self._codec(charset.replace("-", "_"))) \ -               or charset - -    def _codec(self, charset): -        if not charset: return charset -        codec = None -        try: -            codecs.lookup(charset) -            codec = charset -        except (LookupError, ValueError): -            pass -        return codec - -    EBCDIC_TO_ASCII_MAP = None -    def _ebcdic_to_ascii(self, s): -        c = self.__class__ -        if not c.EBCDIC_TO_ASCII_MAP: -            emap = (0,1,2,3,156,9,134,127,151,141,142,11,12,13,14,15, -                    16,17,18,19,157,133,8,135,24,25,146,143,28,29,30,31, -                    128,129,130,131,132,10,23,27,136,137,138,139,140,5,6,7, -                    144,145,22,147,148,149,150,4,152,153,154,155,20,21,158,26, -                    32,160,161,162,163,164,165,166,167,168,91,46,60,40,43,33, -                    38,169,170,171,172,173,174,175,176,177,93,36,42,41,59,94, -                    45,47,178,179,180,181,182,183,184,185,124,44,37,95,62,63, -                    186,187,188,189,190,191,192,193,194,96,58,35,64,39,61,34, -                    195,97,98,99,100,101,102,103,104,105,196,197,198,199,200, -                    201,202,106,107,108,109,110,111,112,113,114,203,204,205, -                    206,207,208,209,126,115,116,117,118,119,120,121,122,210, -                    211,212,213,214,215,216,217,218,219,220,221,222,223,224, -                    225,226,227,228,229,230,231,123,65,66,67,68,69,70,71,72, -                    73,232,233,234,235,236,237,125,74,75,76,77,78,79,80,81, -                    82,238,239,240,241,242,243,92,159,83,84,85,86,87,88,89, -                    90,244,245,246,247,248,249,48,49,50,51,52,53,54,55,56,57, -                    250,251,252,253,254,255) -            import string -            c.EBCDIC_TO_ASCII_MAP = string.maketrans( \ -            ''.join(map(chr, range(256))), ''.join(map(chr, emap))) -        return s.translate(c.EBCDIC_TO_ASCII_MAP) - -    MS_CHARS = { '\x80' : ('euro', '20AC'), -                 '\x81' : ' ', -                 '\x82' : ('sbquo', '201A'), -                 '\x83' : ('fnof', '192'), -                 '\x84' : ('bdquo', '201E'), -                 '\x85' : ('hellip', '2026'), -                 '\x86' : ('dagger', '2020'), -                 '\x87' : ('Dagger', '2021'), -                 '\x88' : ('circ', '2C6'), -                 '\x89' : ('permil', '2030'), -                 '\x8A' : ('Scaron', '160'), -                 '\x8B' : ('lsaquo', '2039'), -                 '\x8C' : ('OElig', '152'), -                 '\x8D' : '?', -                 '\x8E' : ('#x17D', '17D'), -                 '\x8F' : '?', -                 '\x90' : '?', -                 '\x91' : ('lsquo', '2018'), -                 '\x92' : ('rsquo', '2019'), -                 '\x93' : ('ldquo', '201C'), -                 '\x94' : ('rdquo', '201D'), -                 '\x95' : ('bull', '2022'), -                 '\x96' : ('ndash', '2013'), -                 '\x97' : ('mdash', '2014'), -                 '\x98' : ('tilde', '2DC'), -                 '\x99' : ('trade', '2122'), -                 '\x9a' : ('scaron', '161'), -                 '\x9b' : ('rsaquo', '203A'), -                 '\x9c' : ('oelig', '153'), -                 '\x9d' : '?', -                 '\x9e' : ('#x17E', '17E'), -                 '\x9f' : ('Yuml', ''),} - -####################################################################### - - -#By default, act as an HTML pretty-printer. -if __name__ == '__main__': -    import sys -    soup = BeautifulSoup(sys.stdin) -    print soup.prettify() diff --git a/libmproxy/utils.py b/libmproxy/utils.py index ee0d9b43..2a878676 100644 --- a/libmproxy/utils.py +++ b/libmproxy/utils.py @@ -12,9 +12,7 @@  #   # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. - -import re, os, subprocess, datetime -from contrib import BeautifulSoup +import re, os, subprocess, datetime, textwrap  def format_timestamp(s): @@ -48,14 +46,48 @@ def cleanBin(s):      return "".join(parts) -def prettybody(s): +TAG = r""" +        <\s* +        (?!\s*[!"]) +        (?P<close>\s*\/)? +        (?P<name>\w+) +        ( +                [a-zA-Z0-9_#:=().%\/]+ +            | +                "[^\"]*"['\"]* +            | +                '[^']*'['\"]* +            |  +                \s+ +        )* +        (?P<selfcont>\s*\/\s*)? +        \s*> +      """ +UNI = set(["br", "hr", "img", "input", "area", "link"]) +INDENT = " "*4 +def pretty_xmlish(s):      """ -        Return a list of pretty-printed lines. +        This is a robust, general pretty-printer for XML-ish data.  +        Returns a list of lines.      """ -    s = BeautifulSoup.BeautifulStoneSoup(s) -    s = s.prettify().strip() -    parts = s.split("\n") -    return [repr(i)[1:-1] for i in parts] +    data, offset, indent, prev = [], 0, 0, None +    for i in re.finditer(TAG, s, re.VERBOSE|re.MULTILINE): +        start, end = i.span() +        name = i.group("name") +        if start > offset: +            txt = [] +            for x in textwrap.dedent(s[offset:start]).split("\n"): +                if x.strip(): +                    txt.append(indent*INDENT + x) +            data.extend(txt) +        if i.group("close") and not (name in UNI and name==prev): +            indent = max(indent - 1, 0) +        data.append(indent*INDENT + i.group().strip()) +        offset = end +        if not any([i.group("close"), i.group("selfcont"), name in UNI]): +            indent += 1 +        prev = name +    return data  def hexdump(s): diff --git a/test/test_utils.py b/test/test_utils.py index 325664a4..1ec4f2f5 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,4 +1,4 @@ -import textwrap, cStringIO, os, time +import textwrap, cStringIO, os, time, re  import libpry  from libmproxy import utils @@ -228,13 +228,59 @@ class umake_bogus_cert(libpry.AutoTree):          assert "CERTIFICATE" in d -class uprettybody(libpry.AutoTree): +class upretty_xmlish(libpry.AutoTree): +    def test_tagre(self): +        def f(s): +            return re.search(utils.TAG, s, re.VERBOSE|re.MULTILINE) +        assert f(r"<body>") +        assert f(r"<body/>") +        assert f(r"< body/>") +        assert f(r"< body/ >") +        assert f(r"< body / >") +        assert f(r"<foo a=b>") +        assert f(r"<foo a='b'>") +        assert f(r"<foo a='b\"'>") +        assert f(r'<a b=(a.b) href="foo">') +        assert f('<td width=25%>') +      def test_all(self): -        s = "<html><p></p></html>" -        assert utils.prettybody(s) +        def isbalanced(ret): +            # The last tag should have no indent +            assert ret[-1].strip() == ret[-1] + +        s = "<html><br><br></br><p>one</p></html>" +        ret = utils.pretty_xmlish(s) +        isbalanced(ret) + +        s = r""" +<body bgcolor=#ffffff text=#000000 link=#0000cc vlink=#551a8b alink=#ff0000 onload="document.f.q.focus();if(document.images)new Image().src='/images/srpr/nav_logo27.png'" ><textarea id=csi style=display:none></textarea></body> +        """ +        isbalanced(utils.pretty_xmlish(textwrap.dedent(s))) + +        s = r""" +                <a href="http://foo.com" target=""> +                   <img src="http://foo.gif" alt="bar" height="25" width="132"> +                </a> +            """ +        isbalanced(utils.pretty_xmlish(textwrap.dedent(s))) + +        s = r""" +            <!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" +            \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"> +            <html></html> +        """ +        ret = utils.pretty_xmlish(textwrap.dedent(s)) +        isbalanced(ret) + +        s = "<html><br/><p>one</p></html>" +        ret = utils.pretty_xmlish(s) +        assert len(ret) == 6 +        isbalanced(ret) + +        s = "gobbledygook" +        print utils.pretty_xmlish(s) + -        s = "".join([chr(i) for i in range(256)]) -        assert utils.prettybody(s) @@ -249,5 +295,5 @@ tests = [      uMultiDict(),      uHeaders(),      uData(), -    uprettybody(), +    upretty_xmlish(),  ] @@ -1,15 +1,11 @@  Futures: -    - Timestamps -          - Strings view for binary responses.      - Post and URL field parsing and editing.      - On-the-fly generation of keys, signed with a CA      - Pass-through fast-track for things that don't match filter? -    - Reading contents from file      - Shortcut for viewing in pager -    - Serializing and de-serializing requests and responses.      - Upstream proxies.      - mitmdump          - Filters @@ -17,7 +13,9 @@ Futures:          - Pipe to script          - Command-line replay or serialized flows +  Bugs:      - In some circumstances, long URLs in list view are line-broken oddly.      - Termination sometimes hangs. +    - When a bug in mitmproxy causes a stack trace, we hang on exit. | 
