1"""Load / save to libwww-perl (LWP) format files.
2
3Actually, the format is slightly extended from that used by LWP's
4(libwww-perl's) HTTP::Cookies, to avoid losing some RFC 2965 information
5not recorded by LWP.
6
7It uses the version string "2.0", though really there isn't an LWP Cookies
82.0 format.  This indicates that there is extra information in here
9(domain_dot and # port_spec) while still being compatible with
10libwww-perl, I hope.
11
12"""
13
14import time, re
15from cookielib import (_warn_unhandled_exception, FileCookieJar, LoadError,
16                       Cookie, MISSING_FILENAME_TEXT,
17                       join_header_words, split_header_words,
18                       iso2time, time2isoz)
19
20def lwp_cookie_str(cookie):
21    """Return string representation of Cookie in an the LWP cookie file format.
22
23    Actually, the format is extended a bit -- see module docstring.
24
25    """
26    h = [(cookie.name, cookie.value),
27         ("path", cookie.path),
28         ("domain", cookie.domain)]
29    if cookie.port is not None: h.append(("port", cookie.port))
30    if cookie.path_specified: h.append(("path_spec", None))
31    if cookie.port_specified: h.append(("port_spec", None))
32    if cookie.domain_initial_dot: h.append(("domain_dot", None))
33    if cookie.secure: h.append(("secure", None))
34    if cookie.expires: h.append(("expires",
35                               time2isoz(float(cookie.expires))))
36    if cookie.discard: h.append(("discard", None))
37    if cookie.comment: h.append(("comment", cookie.comment))
38    if cookie.comment_url: h.append(("commenturl", cookie.comment_url))
39
40    keys = cookie._rest.keys()
41    keys.sort()
42    for k in keys:
43        h.append((k, str(cookie._rest[k])))
44
45    h.append(("version", str(cookie.version)))
46
47    return join_header_words([h])
48
49class LWPCookieJar(FileCookieJar):
50    """
51    The LWPCookieJar saves a sequence of"Set-Cookie3" lines.
52    "Set-Cookie3" is the format used by the libwww-perl libary, not known
53    to be compatible with any browser, but which is easy to read and
54    doesn't lose information about RFC 2965 cookies.
55
56    Additional methods
57
58    as_lwp_str(ignore_discard=True, ignore_expired=True)
59
60    """
61
62    def as_lwp_str(self, ignore_discard=True, ignore_expires=True):
63        """Return cookies as a string of "\n"-separated "Set-Cookie3" headers.
64
65        ignore_discard and ignore_expires: see docstring for FileCookieJar.save
66
67        """
68        now = time.time()
69        r = []
70        for cookie in self:
71            if not ignore_discard and cookie.discard:
72                continue
73            if not ignore_expires and cookie.is_expired(now):
74                continue
75            r.append("Set-Cookie3: %s" % lwp_cookie_str(cookie))
76        return "\n".join(r+[""])
77
78    def save(self, filename=None, ignore_discard=False, ignore_expires=False):
79        if filename is None:
80            if self.filename is not None: filename = self.filename
81            else: raise ValueError(MISSING_FILENAME_TEXT)
82
83        f = open(filename, "w")
84        try:
85            # There really isn't an LWP Cookies 2.0 format, but this indicates
86            # that there is extra information in here (domain_dot and
87            # port_spec) while still being compatible with libwww-perl, I hope.
88            f.write("#LWP-Cookies-2.0\n")
89            f.write(self.as_lwp_str(ignore_discard, ignore_expires))
90        finally:
91            f.close()
92
93    def _really_load(self, f, filename, ignore_discard, ignore_expires):
94        magic = f.readline()
95        if not re.search(self.magic_re, magic):
96            msg = ("%r does not look like a Set-Cookie3 (LWP) format "
97                   "file" % filename)
98            raise LoadError(msg)
99
100        now = time.time()
101
102        header = "Set-Cookie3:"
103        boolean_attrs = ("port_spec", "path_spec", "domain_dot",
104                         "secure", "discard")
105        value_attrs = ("version",
106                       "port", "path", "domain",
107                       "expires",
108                       "comment", "commenturl")
109
110        try:
111            while 1:
112                line = f.readline()
113                if line == "": break
114                if not line.startswith(header):
115                    continue
116                line = line[len(header):].strip()
117
118                for data in split_header_words([line]):
119                    name, value = data[0]
120                    standard = {}
121                    rest = {}
122                    for k in boolean_attrs:
123                        standard[k] = False
124                    for k, v in data[1:]:
125                        if k is not None:
126                            lc = k.lower()
127                        else:
128                            lc = None
129                        # don't lose case distinction for unknown fields
130                        if (lc in value_attrs) or (lc in boolean_attrs):
131                            k = lc
132                        if k in boolean_attrs:
133                            if v is None: v = True
134                            standard[k] = v
135                        elif k in value_attrs:
136                            standard[k] = v
137                        else:
138                            rest[k] = v
139
140                    h = standard.get
141                    expires = h("expires")
142                    discard = h("discard")
143                    if expires is not None:
144                        expires = iso2time(expires)
145                    if expires is None:
146                        discard = True
147                    domain = h("domain")
148                    domain_specified = domain.startswith(".")
149                    c = Cookie(h("version"), name, value,
150                               h("port"), h("port_spec"),
151                               domain, domain_specified, h("domain_dot"),
152                               h("path"), h("path_spec"),
153                               h("secure"),
154                               expires,
155                               discard,
156                               h("comment"),
157                               h("commenturl"),
158                               rest)
159                    if not ignore_discard and c.discard:
160                        continue
161                    if not ignore_expires and c.is_expired(now):
162                        continue
163                    self.set_cookie(c)
164
165        except IOError:
166            raise
167        except Exception:
168            _warn_unhandled_exception()
169            raise LoadError("invalid Set-Cookie3 format file %r: %r" %
170                            (filename, line))
171