479 lines
14 KiB
Python
479 lines
14 KiB
Python
# vim: set sw=4 expandtab :
|
|
#
|
|
# Copyright (C) 2000, 2001, 2013 Gregory Trubetskoy
|
|
# Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 Apache Software Foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
# may not use this file except in compliance with the License. You
|
|
# may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied. See the License for the specific language governing
|
|
# permissions and limitations under the License.
|
|
#
|
|
# This file originally written by Sterling Hughes
|
|
#
|
|
|
|
from . import apache, Session, util, _psp
|
|
import _apache
|
|
|
|
import sys
|
|
import os
|
|
import marshal
|
|
import types
|
|
import dbm, dbm
|
|
import tempfile
|
|
try:
|
|
from html import escape
|
|
except:
|
|
from cgi import escape
|
|
|
|
PY2 = sys.version[0] == '2'
|
|
|
|
# dbm types for cache
|
|
dbm_types = {}
|
|
|
|
tempdir = tempfile.gettempdir()
|
|
|
|
def path_split(filename):
|
|
|
|
dir, fname = os.path.split(filename)
|
|
if sys.platform.startswith("win"):
|
|
dir += "\\"
|
|
else:
|
|
dir += "/"
|
|
|
|
return dir, fname
|
|
|
|
def code2str(c):
|
|
|
|
ctuple = (c.co_argcount, c.co_nlocals, c.co_stacksize, c.co_flags,
|
|
c.co_code, c.co_consts, c.co_names, c.co_varnames, c.co_filename,
|
|
c.co_name, c.co_firstlineno, c.co_lnotab)
|
|
|
|
return marshal.dumps(ctuple)
|
|
|
|
def str2code(s):
|
|
|
|
return types.CodeType(*marshal.loads(s))
|
|
|
|
class PSPInterface:
|
|
|
|
def __init__(self, req, filename, form):
|
|
self.req = req
|
|
self.filename = filename
|
|
self.error_page = None
|
|
self.form = form
|
|
|
|
def set_error_page(self, page):
|
|
if page and page[0] == '/':
|
|
# relative to document root
|
|
self.error_page = PSP(self.req, self.req.document_root() + page)
|
|
else:
|
|
# relative to same dir we're in
|
|
dir = path_split(self.filename)[0]
|
|
self.error_page = PSP(self.req, dir + page)
|
|
|
|
def apply_data(self, object):
|
|
|
|
if not self.form:
|
|
if not hasattr(self.req, 'form'):
|
|
# no existing form, so need to create one,
|
|
# form has to be saved back to request object
|
|
# so that error page can access it if need be
|
|
self.form = util.FieldStorage(self.req, keep_blank_values=1)
|
|
self.req.form = self.form
|
|
else:
|
|
self.form = self.req.form
|
|
|
|
return util.apply_fs_data(object, self.form, req=self.req)
|
|
|
|
def redirect(self, location, permanent=0):
|
|
|
|
util.redirect(self.req, location, permanent)
|
|
|
|
class PSP:
|
|
|
|
code = None
|
|
dbmcache = None
|
|
|
|
def __init__(self, req, filename=None, string=None, vars={}):
|
|
|
|
if (string and filename):
|
|
raise ValueError("Must specify either filename or string")
|
|
|
|
self.req, self.vars = req, vars
|
|
|
|
if not filename and not string:
|
|
filename = req.filename
|
|
|
|
self.filename, self.string = filename, string
|
|
|
|
if filename:
|
|
|
|
# if filename is not absolute, default to our guess
|
|
# of current directory
|
|
if not os.path.isabs(filename):
|
|
base = os.path.split(req.filename)[0]
|
|
self.filename = os.path.join(base, filename)
|
|
|
|
self.load_from_file()
|
|
else:
|
|
|
|
cached = mem_scache.get(string)
|
|
if cached:
|
|
self.code = cached
|
|
else:
|
|
source = _psp.parsestring(string)
|
|
code = compile(source, "__psp__", "exec")
|
|
mem_scache.store(string,code)
|
|
self.code = code
|
|
|
|
def cache_get(self, filename, mtime):
|
|
|
|
opts = self.req.get_options()
|
|
if "mod_python.psp.cache_database_filename" in opts:
|
|
self.dbmcache = opts["mod_python.psp.cache_database_filename"]
|
|
elif "PSPDbmCache" in opts:
|
|
# For backwards compatability with versions
|
|
# of mod_python prior to 3.3.
|
|
self.dbmcache = opts["PSPDbmCache"]
|
|
|
|
if self.dbmcache:
|
|
cached = dbm_cache_get(self.req.server, self.dbmcache,
|
|
filename, mtime)
|
|
if cached:
|
|
return cached
|
|
|
|
cached = mem_fcache.get(filename, mtime)
|
|
if cached:
|
|
return cached
|
|
|
|
def cache_store(self, filename, mtime, code):
|
|
|
|
if self.dbmcache:
|
|
dbm_cache_store(self.req.server, self.dbmcache,
|
|
filename, mtime, code)
|
|
else:
|
|
mem_fcache.store(filename, mtime, code)
|
|
|
|
def cfile_get(self, filename, mtime):
|
|
|
|
# check for a file ending with 'c' (precompiled file)
|
|
name, ext = os.path.splitext(filename)
|
|
cname = name + ext[:-1] + 'c'
|
|
|
|
if os.path.isfile(cname):
|
|
cmtime = os.path.getmtime(cname)
|
|
|
|
if cmtime >= mtime:
|
|
return str2code(open(cname).read())
|
|
|
|
def load_from_file(self):
|
|
|
|
filename = self.filename
|
|
|
|
if not os.path.isfile(filename):
|
|
raise apache.SERVER_RETURN(apache.HTTP_NOT_FOUND)
|
|
|
|
mtime = os.path.getmtime(filename)
|
|
|
|
# check cache
|
|
code = self.cache_get(filename, mtime)
|
|
|
|
# check for precompiled file
|
|
if not code:
|
|
code = self.cfile_get(filename, mtime)
|
|
|
|
# finally parse and compile
|
|
if not code:
|
|
dir, fname = path_split(self.filename)
|
|
source = _psp.parse(fname, dir)
|
|
code = compile(source, filename, "exec")
|
|
|
|
# store in cache
|
|
self.cache_store(filename, mtime, code)
|
|
|
|
self.code = code
|
|
|
|
def run(self, vars={}, flush=0):
|
|
|
|
code, req = self.code, self.req
|
|
|
|
# does this code use session?
|
|
session = None
|
|
if "session" in code.co_names:
|
|
if not hasattr(req, 'session'):
|
|
# no existing session, so need to create one,
|
|
# session has to be saved back to request object
|
|
# to avoid deadlock if error page tries to use it
|
|
req.session = session = Session.Session(req)
|
|
|
|
# does this code use form?
|
|
form = None
|
|
if "form" in code.co_names:
|
|
if not hasattr(req, 'form'):
|
|
# no existing form, so need to create one,
|
|
# form has to be saved back to request object
|
|
# so that error page can access it if need be
|
|
form = util.FieldStorage(req, keep_blank_values=1)
|
|
req.form = form
|
|
else:
|
|
form = req.form
|
|
|
|
# create psp interface object
|
|
psp = PSPInterface(req, self.filename, form)
|
|
|
|
try:
|
|
global_scope = globals().copy()
|
|
global_scope.update({"req":req, "form":form, "psp":psp})
|
|
|
|
# strictly speaking, session attribute only needs
|
|
# to be populated if referenced, but historically
|
|
# code has always populated it even if None, so
|
|
# preserve that just in case changing it breaks
|
|
# some users code
|
|
if hasattr(req, 'session'):
|
|
global_scope.update({"session":req.session})
|
|
else:
|
|
global_scope.update({"session":None})
|
|
|
|
global_scope.update(self.vars) # passed in __init__()
|
|
global_scope.update(vars) # passed in run()
|
|
|
|
class _InstanceInfo:
|
|
|
|
def __init__(self, label, file, cache):
|
|
self.label = label
|
|
self.file = file
|
|
self.cache = cache
|
|
self.children = {}
|
|
|
|
global_scope["__file__"] = req.filename
|
|
global_scope["__mp_info__"] = _InstanceInfo(
|
|
None, req.filename, None)
|
|
global_scope["__mp_path__"] = []
|
|
|
|
try:
|
|
exec(code, global_scope)
|
|
if flush:
|
|
req.flush()
|
|
|
|
# the mere instantiation of a session changes it
|
|
# (access time), so it *always* has to be saved
|
|
if hasattr(req, 'session'):
|
|
req.session.save()
|
|
except:
|
|
et, ev, etb = sys.exc_info()
|
|
if psp.error_page:
|
|
# run error page
|
|
psp.error_page.run({"exception": (et, ev, etb)}, flush)
|
|
else:
|
|
if PY2:
|
|
exec('raise et, ev, etb')
|
|
else:
|
|
raise et(ev).with_traceback(etb)
|
|
finally:
|
|
# if session was created here, unlock it and don't leave
|
|
# it behind in request object in unlocked state as it
|
|
# will just cause problems if then used by subsequent code
|
|
if session is not None:
|
|
session.unlock()
|
|
del req.session
|
|
|
|
def __str__(self):
|
|
self.req.content_type = 'text/html'
|
|
self.run()
|
|
return ""
|
|
|
|
def display_code(self):
|
|
"""
|
|
Display a niceliy HTML-formatted side-by-side of
|
|
what PSP generated next to orinial code.
|
|
"""
|
|
|
|
req, filename = self.req, self.filename
|
|
|
|
# Because of caching, source code is most often not
|
|
# available in this object, so we read it here
|
|
# (instead of trying to get it in __init__ somewhere)
|
|
|
|
dir, fname = path_split(filename)
|
|
|
|
source = open(filename).read().splitlines()
|
|
pycode = _psp.parse(fname, dir).splitlines()
|
|
|
|
source = [s.rstrip() for s in source]
|
|
pycode = [s.rstrip() for s in pycode]
|
|
|
|
req.write("<table>\n<tr>")
|
|
for s in ("", " PSP-produced Python Code:",
|
|
" %s:" % filename):
|
|
req.write("<td><tt>%s</tt></td>" % s)
|
|
req.write("</tr>\n")
|
|
|
|
n = 1
|
|
for line in pycode:
|
|
req.write("<tr>")
|
|
left = escape(line).replace("\t", " "*4).replace(" ", " ")
|
|
if len(source) < n:
|
|
right = ""
|
|
else:
|
|
right = escape(source[n-1]).replace("\t", " "*4).replace(" ", " ")
|
|
for s in ("%d. " % n,
|
|
"<font color=blue>%s</font>" % left,
|
|
" <font color=green>%s</font>" % right):
|
|
req.write("<td><tt>%s</tt></td>" % s)
|
|
req.write("</tr>\n")
|
|
|
|
n += 1
|
|
req.write("</table>\n")
|
|
|
|
|
|
def parse(filename, dir=None):
|
|
if dir:
|
|
return _psp.parse(filename, dir)
|
|
else:
|
|
return _psp.parse(filename)
|
|
|
|
def parsestring(str):
|
|
|
|
return _psp.parsestring(str)
|
|
|
|
def handler(req):
|
|
|
|
req.content_type = "text/html"
|
|
|
|
config = req.get_config()
|
|
debug = debug = int(config.get("PythonDebug", 0))
|
|
|
|
if debug and req.filename[-1] == "_":
|
|
p = PSP(req, req.filename[:-1])
|
|
p.display_code()
|
|
else:
|
|
p = PSP(req)
|
|
p.run()
|
|
|
|
return apache.OK
|
|
|
|
def dbm_cache_type(dbmfile):
|
|
|
|
global dbm_types
|
|
|
|
if dbmfile in dbm_types:
|
|
return dbm_types[dbmfile]
|
|
|
|
module = dbm.whichdb(dbmfile)
|
|
if module:
|
|
dbm_type = __import__(module)
|
|
dbm_types[dbmfile] = dbm_type
|
|
return dbm_type
|
|
else:
|
|
# this is a new file
|
|
return anydbm
|
|
|
|
def dbm_cache_store(srv, dbmfile, filename, mtime, val):
|
|
|
|
dbm_type = dbm_cache_type(dbmfile)
|
|
|
|
# NOTE: acquiring a lock for the dbm file (also applies to dbm_cache_get)
|
|
# See http://issues.apache.org/jira/browse/MODPYTHON-69
|
|
# In mod_python versions < 3.2 "pspcache" was used as the lock key.
|
|
# ie. _apache._global_lock(srv, key, index)
|
|
# Assuming there are 32 mutexes (the default in 3.1.x), "pspcache"
|
|
# will hash to one of 31 mutexes (index 0 is reserved). Therefore
|
|
# there is a 1 in 31 chance for a hash collision if a session is
|
|
# used in the same request, which would result in a deadlock. This
|
|
# has been confirmed by testing.
|
|
# We can avoid this by using index 0 and setting the key to None.
|
|
# Lock index 0 is also used by DbmSession for locking it's dbm file,
|
|
# but since the lock is not held for the duration of the request there
|
|
# should not be any additional deadlock issues. Likewise, the lock
|
|
# here is only held for a short time, so it will not interfere
|
|
# with DbmSession file locking.
|
|
|
|
_apache._global_lock(srv, None, 0)
|
|
try:
|
|
dbm = dbm_type.open(dbmfile, 'c')
|
|
dbm[filename] = "%d %s" % (mtime, code2str(val))
|
|
finally:
|
|
try: dbm.close()
|
|
except: pass
|
|
_apache._global_unlock(srv, None, 0)
|
|
|
|
def dbm_cache_get(srv, dbmfile, filename, mtime):
|
|
|
|
dbm_type = dbm_cache_type(dbmfile)
|
|
_apache._global_lock(srv, None, 0)
|
|
try:
|
|
dbm = dbm_type.open(dbmfile, 'c')
|
|
try:
|
|
entry = dbm[filename]
|
|
t, val = entry.split(" ", 1)
|
|
if int(t) == mtime:
|
|
return str2code(val)
|
|
except KeyError:
|
|
return None
|
|
finally:
|
|
try: dbm.close()
|
|
except: pass
|
|
_apache._global_unlock(srv, None, 0)
|
|
|
|
|
|
class HitsCache:
|
|
|
|
def __init__(self, size=512):
|
|
self.cache = {}
|
|
self.size = size
|
|
|
|
def store(self, key, val):
|
|
self.cache[key] = (1, val)
|
|
if len(self.cache) > self.size:
|
|
self.clean()
|
|
|
|
def get(self, key):
|
|
if key in self.cache:
|
|
hits, val = self.cache[key]
|
|
self.cache[key] = (hits+1, val)
|
|
return val
|
|
else:
|
|
return None
|
|
|
|
def clean(self):
|
|
|
|
byhits = [(n[1], n[0]) for n in list(self.cache.items())]
|
|
byhits.sort()
|
|
|
|
# delete enough least hit entries to make cache 75% full
|
|
for item in byhits[:len(self.cache)-int(self.size*.75)]:
|
|
val, key = item
|
|
del self.cache[key]
|
|
|
|
mem_scache = HitsCache()
|
|
|
|
class FileCache(HitsCache):
|
|
|
|
def store(self, filename, mtime, code):
|
|
self.cache[filename] = (1, mtime, code)
|
|
if len(self.cache) > self.size:
|
|
self.clean()
|
|
|
|
def get(self, filename, mtime):
|
|
try:
|
|
hits, c_mtime, code = self.cache[filename]
|
|
if mtime != c_mtime:
|
|
del self.cache[filename]
|
|
return None
|
|
else:
|
|
self.cache[filename] = (hits+1, mtime, code)
|
|
return code
|
|
except KeyError:
|
|
return None
|
|
|
|
mem_fcache = FileCache()
|
|
|