1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
|
"""
Common functions and classes for services and cores.
"""
#c Copyright 2008-2020, the GAVO project
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import re
import os
import pkg_resources
from gavo import base
from gavo import utils
from gavo.formal import nevowc
class Error(base.ExecutiveAction):
def __init__(self, msg, rd=None, hint=None):
self.rd = rd
self.msg = msg
self.hint = hint
base.ExecutiveAction.__init__(self, msg)
class BadMethod(Error):
"""raised to generate an HTTP 405 response.
"""
def __str__(self):
return "This resource cannot respond to the HTTP '%s' method"%self.msg
class UnknownURI(Error, base.NotFoundError):
"""raised to generate an HTTP 404 response.
"""
def __str__(self):
return Error.__str__(self)
class ForbiddenURI(Error):
"""raised to generate an HTTP 403 response.
"""
class Authenticate(Error):
"""raised to initiate an authentication request.
Authenticates are optionally constructed with the realm the user
shall authenticate in. If you leave the realm out, the DC-wide default
will be used.
"""
def __init__(self, realm=base.getConfig("web", "realm"), hint=None):
self.realm = realm
Error.__init__(self, "This is a request to authenticate against %s"%realm,
hint=hint)
class RedirectBase(Error):
def __init__(self, dest, hint=None):
if isinstance(dest, bytes):
dest = dest.decode("utf-8")
self.rawDest = dest
dest = str(dest)
if not dest.startswith("http"):
dest = base.getConfig("web", "serverURL")+base.makeSitePath(dest)
self.dest = dest
Error.__init__(self, "This is supposed to redirect to %s"%dest,
hint=hint)
class WebRedirect(RedirectBase):
"""raised to redirect a user agent to a different resource (HTTP 301).
WebRedirectes are constructed with the destination URL that can be
relative (to webRoot) or absolute (starting with http).
"""
class SeeOther(RedirectBase):
"""raised to redirect a user agent to a different resource (HTTP 303).
SeeOthers are constructed with the destination URL that can be
relative (to webRoot) or absolute (starting with http).
They are essentially like WebRedirect, except they put out a 303
instead of a 301.
"""
class Found(RedirectBase):
"""raised to redirect a user agent to a different resource (HTTP 302).
Found instances are constructed with the destination URL that can be
relative (to webRoot) or absolute (starting with http).
They are essentially like WebRedirect, except they put out a 302
instead of a 301.
"""
def parseServicePath(serviceParts):
"""returns a tuple of resourceDescriptor, serviceName.
A serivce id consists of an inputsDir-relative path to a resource
descriptor, a slash, and the name of a service within this descriptor.
This function returns a tuple of inputsDir-relative path and service name.
It raises a gavo.Error if sid has an invalid format. The existence of
the resource or the service are not checked.
"""
return "/".join(serviceParts[:-1]), serviceParts[-1]
class QueryMeta(dict):
"""A class keeping information on the query environment.
It is constructed with a plain dictionary (there are alternative
constructors for t.w requests are below) mapping
certain keys (you'll currently have to figure out which from the
source) to values, mostly strings, except for the keys listed in
listKeys, which should be sequences of strings.
If you pass an empty dict, some sane defaults will be used. You
can get that "empty" query meta as common.emptyQueryMeta, but make
sure you don't mutate it.
QueryMetas constructed from request will have the user and password
items filled out.
If you're using formal, you should set the formal_data item
to the dictionary created by formal. This will let people use
the parsed parameters in templates.
Note: You cannot trust qm["user"] -- it is not validated against
any credentials.
"""
# a set of keys handled by query meta to be ignored in parameter
# lists because they are used internally. This covers everything
# QueryMeta interprets, but also keys by introduced by certain gwidgets
# and the formal infrastructure
metaKeys = set(["_FILTER", "_OUTPUT", "_charset_", "_ADDITEM",
"__nevow_form__", "_FORMAT", "_VERB", "_TDENC", "formal_data",
"_SET", "_TIMEOUT", "_VOTABLE_VERSION", "FORMAT"])
# a set of keys that have sequences as values (needed for construction
# from t.w request.strargs)
listKeys = set(["_ADDITEM", "_DBOPTIONS_ORDER", "_SET"])
def __init__(self, initArgs=None, defaultLimit=None):
if initArgs is None:
initArgs = {}
self.ctxArgs = utils.CaseSemisensitiveDict(initArgs)
if defaultLimit is None:
self.defaultLimit = base.getConfig("db", "defaultLimit")
else:
self.defaultLimit = defaultLimit
self["formal_data"] = {}
self["user"] = self["password"] = None
self["accept"] = {}
self["inputTable"] = None
self._fillOutput(self.ctxArgs)
self._fillDbOptions(self.ctxArgs)
self._fillSet(self.ctxArgs)
@classmethod
def fromRequestArgs(cls, inArgs, **kwargs):
"""constructs a QueryMeta from a gavo.web.common.Request.strargs
"""
args = {}
for key, value in inArgs.items():
# defense against broken legacy code: listify if necessary
if not isinstance(value, list):
value = [value]
if key in cls.listKeys:
args[key] = value
else:
if value:
args[key] = value[0]
return cls(args, **kwargs)
@classmethod
def fromRequest(cls, request, **kwargs):
"""constructs a QueryMeta from a gavo.web.common.Request.
In addition to getting information from the arguments, this
also sets user and password.
"""
res = cls.fromRequestArgs(request.strargs, **kwargs)
res["accept"] = utils.parseAccept(request.getHeader("accept"))
res["user"] = request.getUser() or None
res["password"] = utils.debytify(request.getPassword(), "utf-8") or None
return res
def _fillOutput(self, args):
"""interprets values left by the OutputFormat widget.
"""
if "RESPONSEFORMAT" in args:
self["format"] = args["RESPONSEFORMAT"]
else:
self["format"] = args.get("_FORMAT", "HTML")
try:
# prefer fine-grained "verbosity" over _VERB or VERB
# Hack: malformed _VERBs result in None verbosity, which is taken to
# mean about "use fields of HTML". Absent _VERB or VERB, on the other
# hand, means VERB=2, i.e., a sane default
if "verbosity" in args:
self["verbosity"] = int(args["verbosity"])
elif "_VERB" in args: # internal verb parameter
self["verbosity"] = int(args["_VERB"])*10
elif "VERB" in args: # verb parameter for SCS and such
self["verbosity"] = int(args["VERB"])*10
else:
self["verbosity"] = 20
except ValueError:
self["verbosity"] = "HTML" # VERB given, but not an int.
self["tdEnc"] = base.getConfig("ivoa", "votDefaultEncoding")=="td"
if "_TDENC" in args:
try:
self["tdEnc"] = base.parseBooleanLiteral(args["_TDENC"])
except ValueError:
pass
try:
self["VOTableVersion"] = tuple(int(v) for v in
args["_VOTABLE_VERSION"].split("."))
except: # simple ignore malformed version specs
pass
self["additionalFields"] = args.get("_ADDITEM", [])
def _fillSet(self, args):
"""interprets the output of a ColumnSet widget.
"""
self["columnSet"] = None
if "_SET" in args:
self["columnSet"] = set(args["_SET"])
def _fillDbOptions(self, args):
self["dbSortKeys"] = [s.strip()
for s in args.get("_DBOPTIONS_ORDER", []) if s.strip()]
self["direction"] = {"ASC": "ASC", "DESC": "DESC"}[
args.get("_DBOPTIONS_DIR", "ASC")]
try:
self["dbLimit"] = int(args["MAXREC"])
except (ValueError, KeyError):
self["dbLimit"] = self.defaultLimit
try:
self["timeout"] = max(float(args["_TIMEOUT"]), 0.001)
except (ValueError, KeyError):
self["timeout"] = base.getConfig("web", "sqlTimeout")
def overrideDbOptions(self, sortKeys=None, limit=None, sortFallback=None,
direction=None):
if sortKeys is not None:
self["dbSortKeys"] = sortKeys
if not self["dbSortKeys"] and sortFallback is not None:
self["dbSortKeys"] = sortFallback.split(",")
if limit is not None:
self["dbLimit"] = int(limit)
if direction is not None:
self["direction"] = direction
def asSQL(self):
"""returns the dbLimit and dbSortKey values as an SQL fragment.
"""
frag, pars = [], {}
sortKeys = self["dbSortKeys"]
dbLimit = self["dbLimit"]
# TODO: Sorting needs a thorough redesign, presumably giving column instance
# as column keys. These could carry "default up" or "default down" in
# properties. Meanwhile, there should be a UI to let users decide on
# sort direction.
# Meanwhile, let's do an emergency hack.
if sortKeys:
# Ok, we need to do some emergency securing here. There should be
# pre-validation that we're actually seeing a column key, but
# just in case let's make sure we're seeing an SQL identifier.
# (We can't rely on dbapi's escaping since we're not talking values here)
frag.append("ORDER BY %s %s"%(",".join(
re.sub('[^A-Za-z0-9"_]+', "", key) for key in sortKeys),
self["direction"]))
if dbLimit:
frag.append("LIMIT %(_matchLimit)s")
pars["_matchLimit"] = int(dbLimit)+1
return " ".join(frag), pars
emptyQueryMeta = QueryMeta()
def getTemplatePath(key):
"""see loadSystemTemplate.
"""
userPath = os.path.join(base.getConfig("rootDir"), "web/templates", key)
if os.path.exists(userPath):
return userPath
else:
resPath = "resources/templates/"+key
if pkg_resources.resource_exists('gavo', resPath):
return pkg_resources.resource_filename('gavo', resPath)
else:
raise base.NotFoundError(key, "template", "system templates")
def loadSystemTemplate(key):
"""returns a nevow template for system pages from key.
path is interpreted as relative to gavo_root/web/templates (first)
and package internal (last). If no template is found, None is
returned (this harmonizes with the fallback in CustomTemplateMixin).
"""
try:
tp = getTemplatePath(key)
if tp is not None:
return nevowc.XMLFile(tp)
except IOError:
pass
|