############################################################################## # # Copyright (c) 2002 Zope Foundation and Contributors. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """ Basic ZPublisher request management. """ import types from AccessControl.ZopeSecurityPolicy import getRoles from Acquisition import aq_base, aq_inner from Acquisition.interfaces import IAcquirer from ExtensionClass import Base import pkg_resources from six.moves.urllib.parse import quote as urllib_quote from zExceptions import Forbidden from zExceptions import NotFound from zope.component import queryMultiAdapter from zope.event import notify from zope.interface import implementer from zope.interface import Interface from zope.location.interfaces import LocationError from zope.publisher.defaultview import queryDefaultViewName from zope.publisher.interfaces import EndRequestEvent from zope.publisher.interfaces import IPublishTraverse from zope.publisher.interfaces import NotFound as ztkNotFound from zope.publisher.interfaces.browser import IBrowserPublisher from zope.traversing.namespace import namespaceLookup from zope.traversing.namespace import nsParse from ZPublisher.Converters import type_converters from ZPublisher.interfaces import UseTraversalDefault HAS_ZSERVER = True try: dist = pkg_resources.get_distribution('ZServer') except pkg_resources.DistributionNotFound: HAS_ZSERVER = False def is_xmlrpc_response(response): return False else: from ZServer.ZPublisher.xmlrpc import is_xmlrpc_response _marker = [] UNSPECIFIED_ROLES = '' def quote(text): # quote url path segments, but leave + and @ intact return urllib_quote(text, '/+@') class RequestContainer(Base): __roles__ = None def __init__(self, **kw): for k, v in kw.items(): self.__dict__[k] = v def manage_property_types(self): return list(type_converters.keys()) @implementer(IBrowserPublisher) class DefaultPublishTraverse(object): def __init__(self, context, request): self.context = context self.request = request def publishTraverse(self, request, name): object = self.context URL = request['URL'] if name[:1] == '_': raise Forbidden( "Object name begins with an underscore at: %s" % URL) subobject = UseTraversalDefault # indicator try: if hasattr(object, '__bobo_traverse__'): try: subobject = object.__bobo_traverse__(request, name) if isinstance(subobject, tuple) and len(subobject) > 1: # Add additional parents into the path # XXX There are no tests for this: request['PARENTS'][-1:] = list(subobject[:-1]) object, subobject = subobject[-2:] except (AttributeError, KeyError, NotFound) as e: # Try to find a view subobject = queryMultiAdapter( (object, request), Interface, name) if subobject is not None: # OFS.Application.__bobo_traverse__ calls # REQUEST.RESPONSE.notFoundError which sets the HTTP # status code to 404 request.response.setStatus(200) # We don't need to do the docstring security check # for views, so lets skip it and # return the object here. if IAcquirer.providedBy(subobject): subobject = subobject.__of__(object) return subobject # No view found. Reraise the error # raised by __bobo_traverse__ raise e except UseTraversalDefault: pass if subobject is UseTraversalDefault: # No __bobo_traverse__ or default traversal requested # Try with an unacquired attribute: if hasattr(aq_base(object), name): subobject = getattr(object, name) else: # We try to fall back to a view: subobject = queryMultiAdapter((object, request), Interface, name) if subobject is not None: if IAcquirer.providedBy(subobject): subobject = subobject.__of__(object) return subobject # And lastly, of there is no view, try acquired attributes, but # only if there is no __bobo_traverse__: try: subobject = getattr(object, name) # Again, clear any error status created by # __bobo_traverse__ because we actually found something: request.response.setStatus(200) except AttributeError: pass # Lastly we try with key access: if subobject is None: try: subobject = object[name] except TypeError: # unsubscriptable raise KeyError(name) # Ensure that the object has a docstring, or that the parent # object has a pseudo-docstring for the object. Objects that # have an empty or missing docstring are not published. doc = getattr(subobject, '__doc__', None) if not doc: raise Forbidden( "The object at %s has an empty or missing " "docstring. Objects must have a docstring to be " "published." % URL ) # Check that built-in types aren't publishable. if not typeCheck(subobject): raise Forbidden( "The object at %s is not publishable." % URL) return subobject def browserDefault(self, request): if hasattr(self.context, '__browser_default__'): return self.context.__browser_default__(request) # Zope 3.2 still uses IDefaultView name when it # registeres default views, even though it's # deprecated. So we handle that here: default_name = queryDefaultViewName(self.context, request) if default_name is not None: # Adding '@@' here forces this to be a view. # A neater solution might be desireable. return self.context, ('@@' + default_name,) return self.context, () class BaseRequest(object): """Provide basic ZPublisher request management This object provides access to request data. Request data may vary depending on the protocol used. Request objects are created by the object publisher and will be passed to published objects through the argument name, REQUEST. The request object is a mapping object that represents a collection of variable to value mappings. """ maybe_webdav_client = HAS_ZSERVER # While the following assignment is not strictly necessary, it # prevents alot of unnecessary searches because, without it, # acquisition of REQUEST is disallowed, which penalizes access # in DTML with tags. __roles__ = None _file = None common = {} # Common request data _auth = None _held = () # Allow (reluctantly) access to unprotected attributes __allow_access_to_unprotected_subobjects__ = 1 def __init__(self, other=None, **kw): """The constructor is not allowed to raise errors """ self.__doc__ = None # Make BaseRequest objects unpublishable if other is None: other = kw else: other.update(kw) self.other = other def clear(self): self.other.clear() self._held = None def close(self): try: notify(EndRequestEvent(None, self)) finally: # subscribers might need the zodb, so `clear` must come afterwards # (since `self._held=None` might close the connection, see above) self.clear() def processInputs(self): """Do any input processing that could raise errors """ def __len__(self): return 1 def __setitem__(self, key, value): """Set application variables This method is used to set a variable in the requests "other" category. """ self.other[key] = value set = __setitem__ def get(self, key, default=None): """Get a variable value Return a value for the required variable name. The value will be looked up from one of the request data categories. The search order is environment variables, other variables, form data, and then cookies. """ if key == 'REQUEST': return self v = self.other.get(key, _marker) if v is not _marker: return v v = self.common.get(key, default) if v is not _marker: return v if key == 'BODY' and self._file is not None: p = self._file.tell() self._file.seek(0) v = self._file.read() self._file.seek(p) self.other[key] = v return v if key == 'BODYFILE' and self._file is not None: v = self._file self.other[key] = v return v return default def __getitem__(self, key, default=_marker): v = self.get(key, default) if v is _marker: raise KeyError(key) return v def __bobo_traverse__(self, name): raise KeyError(name) def __getattr__(self, key, default=_marker): v = self.get(key, default) if v is _marker: raise AttributeError(key) return v def set_lazy(self, key, callable): pass # MAYBE, we could do more, but let HTTPRequest do it def has_key(self, key): return key in self def __contains__(self, key): return self.get(key, _marker) is not _marker def keys(self): keys = {} keys.update(self.common) keys.update(self.other) return list(keys.keys()) def items(self): result = [] for k in self.keys(): result.append((k, self.get(k))) return result def values(self): result = [] for k in self.keys(): result.append(self.get(k)) return result def __str__(self): L1 = list(self.items()) L1.sort() return '\n'.join("%s:\t%s" % item for item in L1) __repr__ = __str__ # Original version: see zope.traversing.publicationtraverse def traverseName(self, ob, name): if name and name[:1] in '@+': # Process URI segment parameters. ns, nm = nsParse(name) if ns: try: ob2 = namespaceLookup(ns, nm, ob, self) except LocationError: raise ztkNotFound(ob, name) if IAcquirer.providedBy(ob2): ob2 = ob2.__of__(ob) return ob2 if name == '.': return ob if IPublishTraverse.providedBy(ob): ob2 = ob.publishTraverse(self, name) else: adapter = queryMultiAdapter((ob, self), IPublishTraverse) if adapter is None: # Zope2 doesn't set up its own adapters in a lot of cases # so we will just use a default adapter. adapter = DefaultPublishTraverse(ob, self) ob2 = adapter.publishTraverse(self, name) return ob2 traverseName__roles__ = () def traverse(self, path, response=None, validated_hook=None): """Traverse the object space The REQUEST must already have a PARENTS item with at least one object in it. This is typically the root object. """ request = self request_get = request.get if response is None: response = self.response # remember path for later use browser_path = path # Cleanup the path list if path[:1] == '/': path = path[1:] if path[-1:] == '/': path = path[:-1] clean = [] for item in path.split('/'): # Make sure that certain things that dont make sense # cannot be traversed. if item in ('REQUEST', 'aq_self', 'aq_base'): return response.notFoundError(path) if not item or item == '.': continue elif item == '..': del clean[-1] else: clean.append(item) path = clean # How did this request come in? (HTTP GET, PUT, POST, etc.) method = request_get('REQUEST_METHOD', 'GET').upper() # Probably a browser no_acquire_flag = 0 if (method in ('GET', 'POST', 'PURGE') and not is_xmlrpc_response(response)): # index_html is still the default method, only any object can # override it by implementing its own __browser_default__ method method = 'index_html' elif self.maybe_webdav_client: # Probably a WebDAV client. no_acquire_flag = 1 URL = request['URL'] parents = request['PARENTS'] object = parents[-1] del parents[:] self.roles = getRoles(None, None, object, UNSPECIFIED_ROLES) # if the top object has a __bobo_traverse__ method, then use it # to possibly traverse to an alternate top-level object. if hasattr(object, '__bobo_traverse__'): try: object = object.__bobo_traverse__(request) self.roles = getRoles(None, None, object, UNSPECIFIED_ROLES) except Exception: pass if not path and not method: return response.forbiddenError(self['URL']) # Traverse the URL to find the object: if hasattr(object, '__of__'): # Try to bind the top-level object to the request # This is how you get 'self.REQUEST' object = object.__of__(RequestContainer(REQUEST=request)) parents.append(object) steps = self.steps self._steps = _steps = list(map(quote, steps)) path.reverse() request['TraversalRequestNameStack'] = request.path = path request['ACTUAL_URL'] = request['URL'] + quote(browser_path) # Set the posttraverse for duration of the traversal here self._post_traverse = post_traverse = [] # import time ordering problem try: from webdav.NullResource import NullResource except ImportError: NullResource = None entry_name = '' try: # We build parents in the wrong order, so we # need to make sure we reverse it when we're done. while 1: bpth = getattr(object, '__before_publishing_traverse__', None) if bpth is not None: bpth(object, self) path = request.path = request['TraversalRequestNameStack'] # Check for method: if path: entry_name = path.pop() else: # If we have reached the end of the path, we look to see # if we can find IBrowserPublisher.browserDefault. If so, # we call it to let the object tell us how to publish it. # BrowserDefault returns the object to be published # (usually self) and a sequence of names to traverse to # find the method to be published. # This is webdav support. The last object in the path # should not be acquired. Instead, a NullResource should # be given if it doesn't exist: if (NullResource is not None and no_acquire_flag and hasattr(object, 'aq_base') and not hasattr(object, '__bobo_traverse__')): if (object.__parent__ is not aq_inner(object).__parent__): object = NullResource(parents[-2], object.getId(), self).__of__(parents[-2]) if IBrowserPublisher.providedBy(object): adapter = object else: adapter = queryMultiAdapter((object, self), IBrowserPublisher) if adapter is None: # Zope2 doesn't set up its own adapters in a lot # of cases so we will just use a default adapter. adapter = DefaultPublishTraverse(object, self) object, default_path = adapter.browserDefault(self) if default_path: request._hacked_path = 1 if len(default_path) > 1: path = list(default_path) method = path.pop() request['TraversalRequestNameStack'] = path continue else: entry_name = default_path[0] elif (method and hasattr(object, method) and entry_name != method and getattr(object, method) is not None): request._hacked_path = 1 entry_name = method method = 'index_html' else: if hasattr(object, '__call__'): self.roles = getRoles( object, '__call__', object.__call__, self.roles) if request._hacked_path: i = URL.rfind('/') if i > 0: response.setBase(URL[:i]) break step = quote(entry_name) _steps.append(step) request['URL'] = URL = '%s/%s' % (request['URL'], step) try: subobject = self.traverseName(object, entry_name) if (hasattr(object, '__bobo_traverse__') or hasattr(object, entry_name)): check_name = entry_name else: check_name = None self.roles = getRoles( object, check_name, subobject, self.roles) object = subobject # traverseName() might raise ZTK's NotFound except (KeyError, AttributeError, ztkNotFound): if response.debug_mode: return response.debugError( "Cannot locate object at: %s" % URL) else: return response.notFoundError(URL) except Forbidden as e: if self.response.debug_mode: return response.debugError(e.args) else: return response.forbiddenError(entry_name) parents.append(object) steps.append(entry_name) finally: parents.reverse() # Note - no_acquire_flag is necessary to support # things like DAV. We have to make sure # that the target object is not acquired # if the request_method is other than GET # or POST. Otherwise, you could never use # PUT to add a new object named 'test' if # an object 'test' existed above it in the # heirarchy -- you'd always get the # existing object :( if (no_acquire_flag and hasattr(parents[1], 'aq_base') and not hasattr(parents[1], '__bobo_traverse__')): base = aq_base(parents[1]) if not hasattr(base, entry_name): try: if entry_name not in base: raise AttributeError(entry_name) except TypeError: raise AttributeError(entry_name) # After traversal post traversal hooks aren't available anymore del self._post_traverse request['PUBLISHED'] = parents.pop(0) # Do authorization checks user = groups = None i = 0 if 1: # Always perform authentication. last_parent_index = len(parents) if hasattr(object, '__allow_groups__'): groups = object.__allow_groups__ inext = 0 else: inext = None for i in range(last_parent_index): if hasattr(parents[i], '__allow_groups__'): groups = parents[i].__allow_groups__ inext = i + 1 break if inext is not None: i = inext if hasattr(groups, 'validate'): v = groups.validate else: v = old_validation auth = request._auth if v is old_validation and self.roles is UNSPECIFIED_ROLES: # No roles, so if we have a named group, get roles from # group keys if hasattr(groups, 'keys'): self.roles = list(groups.keys()) else: try: groups = groups() except Exception: pass try: self.roles = list(groups.keys()) except Exception: pass if groups is None: # Public group, hack structures to get it to validate self.roles = None auth = '' if v is old_validation: user = old_validation(groups, request, auth, self.roles) elif self.roles is UNSPECIFIED_ROLES: user = v(request, auth) else: user = v(request, auth, self.roles) while user is None and i < last_parent_index: parent = parents[i] i = i + 1 if hasattr(parent, '__allow_groups__'): groups = parent.__allow_groups__ else: continue if hasattr(groups, 'validate'): v = groups.validate else: v = old_validation if v is old_validation: user = old_validation( groups, request, auth, self.roles) elif self.roles is UNSPECIFIED_ROLES: user = v(request, auth) else: user = v(request, auth, self.roles) if user is None and self.roles != UNSPECIFIED_ROLES: response.unauthorized() if user is not None: if validated_hook is not None: validated_hook(self, user) request['AUTHENTICATED_USER'] = user request['AUTHENTICATION_PATH'] = '/'.join(steps[:-i]) # Remove http request method from the URL. request['URL'] = URL # Run post traversal hooks if post_traverse: result = exec_callables(post_traverse) if result is not None: object = result return object def post_traverse(self, f, args=()): """Add a callable object and argument tuple to be post-traversed. If traversal and authentication succeed, each post-traversal pair is processed in the order in which they were added. Each argument tuple is passed to its callable. If a callable returns a value other than None, no more pairs are processed, and the return value replaces the traversal result. """ try: pairs = self._post_traverse except AttributeError: raise RuntimeError('post_traverse() may only be called ' 'during publishing traversal.') else: pairs.append((f, tuple(args))) retry_count = 0 def supports_retry(self): return 0 def _hold(self, object): """Hold a reference to an object to delay it's destruction until mine """ if self._held is not None: self._held = self._held + (object, ) def exec_callables(callables): result = None for (f, args) in callables: # Don't catch exceptions here. And don't hide them anyway. result = f(*args) if result is not None: return result def old_validation(groups, request, auth, roles=UNSPECIFIED_ROLES): if auth: auth = request._authUserPW() if auth: name, password = auth elif roles is None: return '' else: return None elif 'REMOTE_USER' in request.environ: name = request.environ['REMOTE_USER'] password = None else: if roles is None: return '' return None if roles is None: return name keys = None try: keys = groups.keys except Exception: try: groups = groups() # Maybe it was a method defining a group keys = groups.keys except Exception: pass if keys is not None: # OK, we have a named group, so apply the roles to the named # group. if roles is UNSPECIFIED_ROLES: roles = keys() g = [] for role in roles: if role in groups: g.append(groups[role]) groups = g for d in groups: if name in d and (d[name] == password or password is None): return name if keys is None: # Not a named group, so don't go further raise Forbidden( """<strong>You are not authorized to access this resource""") return None itypes = { bool: 0, types.CodeType: 0, complex: 0, dict: 0, float: 0, types.FrameType: 0, frozenset: 0, int: 0, list: 0, type(None): 0, set: 0, slice: 0, str: 0, types.TracebackType: 0, tuple: 0, } for name in ('BufferType', 'DictProxyType', 'EllipsisType', 'LongType', 'UnicodeType', 'XRangeType'): if hasattr(types, name): itypes[getattr(types, name)] = 0 def typeCheck(obj, deny=itypes): # Return true if its ok to publish the type, false otherwise. return deny.get(type(obj), 1)