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 | |
62 | self.mtime = os.stat(self.path).st_mtime | |
63 | with open(path) as fp: | |
64 | self.parts = self.parse(fp.read()) | |
65 | ||
66 | def text(self, text, ctx): | |
67 | ctx.out.write(text) | |
68 | ||
69 | def echo(self, var, enc, ctx): | |
70 | if var in ctx.vars: | |
71 | ctx.out.write(enc(ctx.vars[var])) | |
72 | ||
73 | def include(self, path, ctx): | |
74 | try: | |
75 | nest = getfile(os.path.join(os.path.dirname(self.path), path)) | |
76 | except Exception: | |
77 | log.warning("%s: could not find included file %s" % (self.path, path)) | |
78 | return | |
79 | nest.process(ctx) | |
80 | ||
81 | def process(self, ctx): | |
82 | for part in self.parts: | |
83 | part(ctx) | |
84 | ||
85 | def resolvecmd(self, cmd, pars): | |
86 | if cmd == "include": | |
87 | if "file" in pars: | |
88 | return functools.partial(self.include, pars["file"]) | |
89 | elif "virtual" in pars: | |
90 | # XXX: For now, just include the file as-is. Change | |
91 | # when necessary. | |
92 | return functools.partial(self.include, pars["virtual"]) | |
93 | else: | |
94 | log.warning("%s: invalid `include' directive" % self.path) | |
95 | return None | |
96 | elif cmd == "echo": | |
97 | if not "var" in pars: | |
98 | log.warning("%s: invalid `echo' directive" % self.path) | |
99 | return None | |
100 | enc = wsgiutil.htmlquote | |
101 | if "encoding" in pars: | |
102 | if pars["encoding"] == "entity": | |
103 | enc = wsgiutil.htmlquote | |
104 | return functools.partial(self.echo, pars["var"], enc) | |
105 | else: | |
106 | log.warning("%s: unknown SSI command `%s'" % (self.path, cmd)) | |
107 | return None | |
108 | ||
109 | def parse(self, text): | |
110 | ret = [] | |
111 | p = 0 | |
112 | while True: | |
113 | p2 = text.find("<!--#", p) | |
114 | if p2 < 0: | |
115 | ret.append(functools.partial(self.text, text[p:])) | |
116 | return ret | |
117 | ret.append(functools.partial(self.text, text[p:p2])) | |
118 | cmd, pars, p = parsecmd(text, p2 + 5) | |
119 | if cmd is not None: | |
120 | cmd = self.resolvecmd(cmd, pars) | |
121 | if cmd is not None: | |
122 | ret.append(cmd) | |
123 | ||
124 | filecache = {} | |
125 | ||
126 | def getfile(path): | |
127 | path = os.path.normpath(path) | |
128 | cf = filecache.get(path) | |
129 | if not cf: | |
130 | cf = filecache[path] = ssifile(path) | |
131 | elif os.stat(path).st_mtime != cf.mtime: | |
132 | cf = filecache[path] = ssifile(path) | |
133 | return cf | |
134 | ||
135 | def wsgi(env, startreq): | |
136 | try: | |
137 | if env["PATH_INFO"] != "": | |
138 | return wsgiutil.simpleerror(env, startreq, 404, "Not Found", "The resource specified by the URL does not exist.") | |
139 | root = getfile(env["SCRIPT_FILENAME"]) | |
140 | buf = io.StringIO() | |
141 | root.process(context(buf, root)) | |
142 | except Exception: | |
143 | return wsgituil.simpleerror(env, startreq, 500, "Internal Error", "The server encountered an unpexpected error while handling SSI.") | |
144 | ret = buf.getvalue().encode("utf8") | |
145 | startreq("200 OK", [("Content-Type", "text/html; charset=UTF-8"), ("Content-Length", str(len(ret)))]) | |
146 | return [ret] |