Commit | Line | Data |
---|---|---|
0b6f220f FT |
1 | """Module for handling server-side-include formatted files |
2 | ||
3 | This module is quite incomplete. I might complete it with more | |
4 | features as I need them. It will probably never be entirely compliant | |
5 | with Apache's version due to architectural differences. | |
6 | """ | |
7 | ||
8 | import sys, os, io, time, logging, functools | |
9 | from . import wsgiutil | |
10 | ||
11 | log = logging.getLogger("ssi") | |
12 | ||
13 | def parsecmd(text, p): | |
14 | try: | |
15 | while text[p].isspace(): p += 1 | |
16 | cmd = "" | |
17 | while not text[p].isspace(): | |
18 | cmd += text[p] | |
19 | p += 1 | |
20 | pars = {} | |
21 | while True: | |
22 | while text[p].isspace(): p += 1 | |
23 | if text[p:p + 3] == "-->": | |
24 | return cmd, pars, p + 3 | |
25 | key = "" | |
26 | while text[p].isalnum(): | |
27 | key += text[p] | |
28 | p += 1 | |
29 | if key == "": | |
30 | return None, {}, p | |
31 | while text[p].isspace(): p += 1 | |
32 | if text[p] != '=': | |
33 | continue | |
34 | p += 1 | |
35 | while text[p].isspace(): p += 1 | |
36 | q = text[p] | |
37 | if q != '"' and q != "'" and q != '`': | |
38 | continue | |
39 | val = "" | |
40 | p += 1 | |
41 | while text[p] != q: | |
42 | val += text[p] | |
43 | p += 1 | |
44 | p += 1 | |
45 | pars[key] = val | |
46 | except IndexError: | |
47 | return None, {}, len(text) | |
48 | ||
49 | class context(object): | |
50 | def __init__(self, out, root): | |
51 | self.out = out | |
52 | self.vars = {} | |
53 | now = time.time() | |
54 | self.vars["DOCUMENT_NAME"] = os.path.basename(root.path) | |
55 | self.vars["DATE_GMT"] = time.asctime(time.gmtime(now)) | |
56 | self.vars["DATE_LOCAL"] = time.asctime(time.localtime(now)) | |
57 | self.vars["LAST_MODIFIED"] = time.asctime(time.localtime(root.mtime)) | |
58 | ||
59 | class ssifile(object): | |
60 | def __init__(self, path): | |
61 | self.path = path | |
47da1b3d FT |
62 | sb = os.stat(self.path) |
63 | self.cache = (sb.st_mode & 0o010) != 0 | |
64 | self.mtime = int(sb.st_mtime) | |
0b6f220f FT |
65 | with open(path) as fp: |
66 | self.parts = self.parse(fp.read()) | |
67 | ||
68 | def text(self, text, ctx): | |
69 | ctx.out.write(text) | |
70 | ||
71 | def echo(self, var, enc, ctx): | |
72 | if var in ctx.vars: | |
73 | ctx.out.write(enc(ctx.vars[var])) | |
74 | ||
75 | def include(self, path, ctx): | |
76 | try: | |
77 | nest = getfile(os.path.join(os.path.dirname(self.path), path)) | |
78 | except Exception: | |
79 | log.warning("%s: could not find included file %s" % (self.path, path)) | |
80 | return | |
81 | nest.process(ctx) | |
82 | ||
83 | def process(self, ctx): | |
84 | for part in self.parts: | |
85 | part(ctx) | |
86 | ||
87 | def resolvecmd(self, cmd, pars): | |
88 | if cmd == "include": | |
89 | if "file" in pars: | |
90 | return functools.partial(self.include, pars["file"]) | |
91 | elif "virtual" in pars: | |
92 | # XXX: For now, just include the file as-is. Change | |
93 | # when necessary. | |
94 | return functools.partial(self.include, pars["virtual"]) | |
95 | else: | |
96 | log.warning("%s: invalid `include' directive" % self.path) | |
97 | return None | |
98 | elif cmd == "echo": | |
99 | if not "var" in pars: | |
100 | log.warning("%s: invalid `echo' directive" % self.path) | |
101 | return None | |
102 | enc = wsgiutil.htmlquote | |
103 | if "encoding" in pars: | |
104 | if pars["encoding"] == "entity": | |
105 | enc = wsgiutil.htmlquote | |
106 | return functools.partial(self.echo, pars["var"], enc) | |
107 | else: | |
108 | log.warning("%s: unknown SSI command `%s'" % (self.path, cmd)) | |
109 | return None | |
110 | ||
111 | def parse(self, text): | |
112 | ret = [] | |
113 | p = 0 | |
114 | while True: | |
115 | p2 = text.find("<!--#", p) | |
116 | if p2 < 0: | |
117 | ret.append(functools.partial(self.text, text[p:])) | |
118 | return ret | |
119 | ret.append(functools.partial(self.text, text[p:p2])) | |
120 | cmd, pars, p = parsecmd(text, p2 + 5) | |
121 | if cmd is not None: | |
122 | cmd = self.resolvecmd(cmd, pars) | |
123 | if cmd is not None: | |
124 | ret.append(cmd) | |
125 | ||
126 | filecache = {} | |
127 | ||
128 | def getfile(path): | |
129 | path = os.path.normpath(path) | |
130 | cf = filecache.get(path) | |
131 | if not cf: | |
132 | cf = filecache[path] = ssifile(path) | |
47da1b3d | 133 | elif int(os.stat(path).st_mtime) != cf.mtime: |
0b6f220f FT |
134 | cf = filecache[path] = ssifile(path) |
135 | return cf | |
136 | ||
137 | def wsgi(env, startreq): | |
138 | try: | |
139 | if env["PATH_INFO"] != "": | |
140 | return wsgiutil.simpleerror(env, startreq, 404, "Not Found", "The resource specified by the URL does not exist.") | |
141 | root = getfile(env["SCRIPT_FILENAME"]) | |
47da1b3d FT |
142 | |
143 | if root.cache and "HTTP_IF_MODIFIED_SINCE" in env: | |
144 | try: | |
145 | lmt = wsgiutil.phttpdate(env["HTTP_IF_MODIFIED_SINCE"]) | |
146 | if root.mtime <= lmt: | |
147 | startreq("304 Not Modified", [("Content-Length", "0")]) | |
148 | return [] | |
149 | except: | |
150 | pass | |
151 | ||
0b6f220f FT |
152 | buf = io.StringIO() |
153 | root.process(context(buf, root)) | |
154 | except Exception: | |
47da1b3d | 155 | return wsgiutil.simpleerror(env, startreq, 500, "Internal Error", "The server encountered an unpexpected error while handling SSI.") |
0b6f220f | 156 | ret = buf.getvalue().encode("utf8") |
47da1b3d FT |
157 | head = [("Content-Type", "text/html; charset=UTF-8"), ("Content-Length", str(len(ret)))] |
158 | if root.cache: | |
159 | head.append(("Last-Modified", wsgiutil.httpdate(root.mtime))) | |
160 | startreq("200 OK", head) | |
0b6f220f | 161 | return [ret] |