예제 #1
0
class DecoratedController(with_metaclass(_DecoratedControllerMeta, object)):
    """Decorated controller object.

    Creates an interface to hang decoration attributes on
    controller methods for the purpose of rendering web content.

    """
    def _is_exposed(self, controller, name):
        method = getattr(controller, name, None)
        if method and inspect.ismethod(method) and hasattr(method, 'decoration'):
            return method.decoration.exposed

    def _call(self, controller, params, remainder=None, tgl=None):
        """Run the controller with the given parameters.

        _call is called by _perform_call in CoreDispatcher.

        Any of the before_validate hook, the validation, the before_call hook,
        and the controller method can return a FormEncode Invalid exception,
        which will give the validation error handler the opportunity to provide
        a replacement decorated controller method and output that will
        subsequently be rendered.

        This allows for validation to display the original page or an
        abbreviated form with validation errors shown on validation failure.

        The before_render hook provides a place for functions that are called
        before the template is rendered. For example, you could use it to
        add and remove from the dictionary returned by the controller method,
        before it is passed to rendering.

        The after_render hook can act upon and modify the response out of
        rendering.

        """
        if tgl is None: #pragma: no cover
            #compatibility with old code that didn't pass request locals explicitly
            tgl = tg.request.environ['tg.locals']

        self._initialize_validation_context(tgl)

        #This is necessary to prevent spurious Content Type header which would
        #cause problems to paste.response.replace_header calls and cause
        #responses wihout content type to get out with a wrong content type
        resp_headers = tgl.response.headers
        if not resp_headers.get('Content-Type'):
            resp_headers.pop('Content-Type', None)

        if remainder:
            remainder = tuple(map(url2pathname, remainder or []))
        else:
            remainder = tuple()

        tg_decoration = controller.decoration
        try:
            tg_decoration.run_hooks(tgl, 'before_validate', remainder, params)

            validate_params = get_params_with_argspec(controller, params, remainder)

            # Validate user input
            params = self._perform_validate(controller, validate_params)

            tgl.tmpl_context.form_values = params

            tg_decoration.run_hooks(tgl, 'before_call', remainder, params)

            params, remainder = remove_argspec_params_from_params(controller, params, remainder)

            #apply controller wrappers
            try:
                controller_caller = tgl.config['controller_caller']
            except KeyError:
                controller_caller = call_controller

            # call controller method
            output = controller_caller(controller, remainder, params)

        except validation_errors as inv:
            controller, output = self._handle_validation_errors(controller, remainder, params, inv)

        #Be sure that we run hooks if the controller changed due to validation errors
        tg_decoration = controller.decoration

        # Render template
        tg_decoration.run_hooks(tgl, 'before_render', remainder, params, output)

        response = self._render_response(tgl, controller, output)
        
        tg_decoration.run_hooks(tgl, 'after_render', response)
        
        return response['response']

    def _perform_validate(self, controller, params):
        """Run validation for the controller with the given parameters.

        Validation is stored on the "validation" attribute of the controller's
        decoration.

        If can be in three forms:

        1) A dictionary, with key being the request parameter name, and value a
           FormEncode validator.

        2) A FormEncode Schema object

        3) Any object with a "validate" method that takes a dictionary of the
           request variables.

        Validation can "clean" or otherwise modify the parameters that were
        passed in, not just raise an exception.  Validation exceptions should
        be FormEncode Invalid objects.

        """

        validation = getattr(controller.decoration, 'validation', None)

        if validation is None:
            return params

        # An object used by FormEncode to get translator function
        formencode_state = type('state', (), {'_': staticmethod(pylons_formencode_gettext)})

        #Initialize new_params -- if it never gets updated just return params
        new_params = {}

        # The validator may be a dictionary, a FormEncode Schema object, or any
        # object with a "validate" method.
        if isinstance(validation.validators, dict):
            # TG developers can pass in a dict of param names and FormEncode
            # validators.  They are applied one by one and builds up a new set
            # of validated params.

            errors = {}
            for field, validator in validation.validators.items():
                try:
                    if isinstance(validator, _FormEncodeValidator):
                        new_params[field] = validator.to_python(params.get(field),
                                                                formencode_state)
                    else:
                        new_params[field] = validator.to_python(params.get(field))
                # catch individual validation errors into the errors dictionary
                except validation_errors as inv:
                    errors[field] = inv

            # Parameters that don't have validators are returned verbatim
            for param, param_value in params.items():
                if not param in new_params:
                    new_params[param] = param_value

            # If there are errors, create a compound validation error based on
            # the errors dictionary, and raise it as an exception
            if errors:
                raise TGValidationError(TGValidationError.make_compound_message(errors),
                                        value=params,
                                        error_dict=errors)

        elif isinstance(validation.validators, _FormEncodeSchema):
            # A FormEncode Schema object - to_python converts the incoming
            # parameters to sanitized Python values
            new_params = validation.validators.to_python(params, formencode_state)

        elif (hasattr(validation.validators, 'validate')
              and getattr(validation, 'needs_controller', False)):
            # An object with a "validate" method - call it with the parameters
            new_params = validation.validators.validate(
                controller, params, formencode_state)

        elif hasattr(validation.validators, 'validate'):
            # An object with a "validate" method - call it with the parameters
            new_params = validation.validators.validate(params, formencode_state)

        # Theoretically this should not happen...
        # if new_params is None:
        #     return params

        return new_params

    def _render_response(self, tgl, controller, response):
        """
        Render response takes the dictionary returned by the
        controller calls the appropriate template engine. It uses
        information off of the decoration object to decide which engine
        and template to use, and removes anything in the exclude_names
        list from the returned dictionary.

        The exclude_names functionality allows you to pass variables to
        some template rendering engines, but not others. This behavior
        is particularly useful for rendering engines like JSON or other
        "web service" style engines which don't use and explicit
        template, or use a totally generic template.

        All of these values are populated into the context object by the
        expose decorator.
        """

        req = tgl.request
        resp = tgl.response

        (content_type, engine_name, template_name, exclude_names, render_params
            ) = controller.decoration.lookup_template_engine(tgl)

        result = dict(response=response, content_type=content_type,
                      engine_name=engine_name, template_name=template_name)

        if content_type is not None:
            resp.headers['Content-Type'] = content_type

        # if it's a string return that string and skip all the stuff
        if not isinstance(response, dict):
            if engine_name == 'json' and isinstance(response, list):
                raise JsonEncodeError(
                    'You may not expose with JSON a list return value because'
                    ' it leaves your application open to CSRF attacks.')
            return result

        # Save these objects as locals from the SOP to avoid expensive lookups
        tmpl_context = tgl.tmpl_context

        # If there is an identity, push it to the Pylons template context
        tmpl_context.identity = req.environ.get('repoze.who.identity')

        # Setup the template namespace, removing anything that the user
        # has marked to be excluded.
        namespace = response
        for name in exclude_names:
            namespace.pop(name, None)

        # If we are in a test request put the namespace where it can be
        # accessed directly
        if 'paste.testing' in req.environ:
            testing_variables = req.environ['paste.testing_variables']
            testing_variables['namespace'] = namespace
            testing_variables['template_name'] = template_name
            testing_variables['exclude_names'] = exclude_names
            testing_variables['render_params'] = render_params
            testing_variables['controller_output'] = response

        # Render the result.
        rendered = tg_render(template_vars=namespace, template_engine=engine_name,
                             template_name=template_name, **render_params)

        result['response'] = rendered
        return result

    def _handle_validation_errors(self,
            controller, remainder, params, exception):
        """Handle validation errors.

        Sets up tg.tmpl_context.form_values
        and tg.tmpl_context.form_errors
        to assist generating a form with given values
        and the validation failure messages.

        The error handler in decoration.validation.error_handler is called.
        If an error_handler isn't given, the original controller is used
        as the error handler instead.

        """
        tmpl_context = tg.tmpl_context
        tmpl_context.validation_exception = exception
        tmpl_context.form_errors = {}

        if isinstance(exception, _Tw2ValidationError):
            #Fetch all the children and grandchildren of a widget
            widget = exception.widget
            widget_children = _navigate_tw2form_children(widget.child)

            errors = [(child.key, child.error_msg) for child in widget_children]
            tmpl_context.form_errors.update(errors)
            tmpl_context.form_values = widget.child.value
        elif isinstance(exception, TGValidationError):
            tmpl_context.form_errors = exception.error_dict
            tmpl_context.form_values = exception.value
        else:
            # Most Invalid objects come back with a list of errors in the format:
            #"fieldname1: error\nfieldname2: error"
            error_list = exception.__str__().split('\n')
            for error in error_list:
                field_value = map(strip_string, error.split(':', 1))

                #if the error has no field associated with it,
                #return the error as a global form error
                if len(field_value) == 1:
                    tmpl_context.form_errors['_the_form'] = field_value[0]
                    continue

                tmpl_context.form_errors[field_value[0]] = field_value[1]

            tmpl_context.form_values = getattr(exception, 'value', {})

        error_handler = controller.decoration.validation.error_handler
        if error_handler is None:
            error_handler = controller
            output = error_handler(*remainder, **dict(params))
        else:
            output = error_handler(im_self(controller), *remainder, **dict(params))

        return error_handler, output

    def _initialize_validation_context(self, tgl):
        tgl.tmpl_context.form_errors = {}
        tgl.tmpl_context.form_values = {}

    def _check_security(self):
        predicate = getattr(self, 'allow_only', None)
        if predicate is None:
            return True
        try:
            predicate.check_authorization(tg.request.environ)
        except NotAuthorizedError as e:
            reason = unicode_text(e)
            if hasattr(self, '_failed_authorization'):
                # Should shortcircuit the rest, but if not we will still
                # deny authorization
                self._failed_authorization(reason)
            if not_anonymous().is_met(tg.request.environ):
                # The user is authenticated but not allowed.
                code = 403
                status = 'error'
            else:
                # The user has not been not authenticated.
                code = 401
                status = 'warning'
            tg.response.status = code
            flash(reason, status=status)
            abort(code, comment=reason)
예제 #2
0
class DecoratedController(with_metaclass(_DecoratedControllerMeta, object)):
    """Decorated controller object.

    Creates an interface to hang decoration attributes on
    controller methods for the purpose of rendering web content.

    """
    def _is_exposed(self, controller, name):
        method = getattr(controller, name, None)
        if method and inspect.ismethod(method) and hasattr(
                method, 'decoration'):
            return method.decoration.exposed

    def _call(self, action, params, remainder=None, context=None):
        """Run the controller with the given parameters.

        _call is called by _perform_call in CoreDispatcher.

        Any of the before_validate hook, the validation, the before_call hook,
        and the controller method can return a FormEncode Invalid exception,
        which will give the validation error handler the opportunity to provide
        a replacement decorated controller method and output that will
        subsequently be rendered.

        This allows for validation to display the original page or an
        abbreviated form with validation errors shown on validation failure.

        The before_render hook provides a place for functions that are called
        before the template is rendered. For example, you could use it to
        add and remove from the dictionary returned by the controller method,
        before it is passed to rendering.

        The after_render hook can act upon and modify the response out of
        rendering.

        """
        if context is None:  #pragma: no cover
            #compatibility with old code that didn't pass request locals explicitly
            context = tg.request.environ['tg.locals']

        hooks = tg.hooks
        context_config = tg.config._current_obj()
        context.request._fast_setattr('validation', _ValidationStatus())

        # This is necessary to prevent spurious Content Type header which would
        # cause problems to paste.response.replace_header calls and cause
        # responses without content type to get out with a wrong content type
        resp_headers = context.response.headers
        if not resp_headers.get('Content-Type'):
            resp_headers.pop('Content-Type', None)

        if remainder:
            remainder = tuple(map(url2pathname, remainder or []))
        else:
            remainder = tuple()

        hooks.notify('before_validate',
                     args=(remainder, params),
                     controller=action)

        validate_params = get_params_with_argspec(action, params, remainder)
        context.request.args_params = validate_params  # Update args_params with positional args

        try:
            params = self._perform_validate(action, validate_params, context)
        except validation_errors as inv:
            instance, error_handler, chain_validation = self._process_validation_errors(
                action, remainder, params, inv, context=context)
            while chain_validation:
                # The validation asked for chained validation,
                # go on and validate the error_handler too.
                try:
                    params = self._perform_validate(error_handler,
                                                    validate_params, context)
                except validation_errors as inv:
                    instance, error_handler, chain_validation = self._process_validation_errors(
                        error_handler, remainder, params, inv, context=context)
                else:
                    chain_validation = False
            action = error_handler
            bound_controller_callable = partial(error_handler, instance)
        else:
            bound_controller_callable = action
            context.request.validation.values = params
            remainder, params = flatten_arguments(action, params, remainder)

        hooks.notify('before_call',
                     args=(remainder, params),
                     controller=action)

        # call controller method with applied wrappers
        controller_caller = action.decoration.controller_caller
        output = controller_caller(context_config, bound_controller_callable,
                                   remainder, params)

        # Render template
        hooks.notify('before_render',
                     args=(remainder, params, output),
                     controller=action)

        response = self._render_response(context, action, output)

        hooks.notify('after_render', args=(response, ), controller=action)

        return response['response']

    @classmethod
    def _perform_validate(cls, controller, params, context=None):
        """Run validation for the controller with the given parameters.

        Validation is stored on the "validation" attribute of the controller's
        decoration.

        If can be in three forms:

        1) A dictionary, with key being the request parameter name, and value a
           FormEncode validator.

        2) A FormEncode Schema object

        3) Any object with a "validate" method that takes a dictionary of the
           request variables.

        Validation can "clean" or otherwise modify the parameters that were
        passed in, not just raise an exception.  Validation exceptions should
        be FormEncode Invalid objects.

        """
        if context is None:  # pragma: no cover
            warnings.warn(
                "Calling DecoratedController._perform_validate without a Context is now deprecated."
                " Please provide the context argument when calling it.",
                DeprecationWarning,
                stacklevel=2)
            context = tg.request.environ['tg.locals']

        validations = controller.decoration.validations
        if not validations:
            return params

        req = context.request
        validation_status = req.validation

        validated_params = params
        for validation_intent in validations:
            validation_status.intent = validation_intent
            validated_params = validation_intent.check(controller,
                                                       validated_params)
        return validated_params

    def _render_response(self, tgl, controller, response):
        """
        Render response takes the dictionary returned by the
        controller calls the appropriate template engine. It uses
        information off of the decoration object to decide which engine
        and template to use, and removes anything in the exclude_names
        list from the returned dictionary.

        The exclude_names functionality allows you to pass variables to
        some template rendering engines, but not others. This behavior
        is particularly useful for rendering engines like JSON or other
        "web service" style engines which don't use and explicit
        template, or use a totally generic template.

        All of these values are populated into the context object by the
        expose decorator.
        """

        req = tgl.request
        resp = tgl.response

        (engine_content_type, engine_name, template_name, exclude_names,
         render_params) = controller.decoration.lookup_template_engine(tgl)

        result = dict(response=response,
                      content_type=engine_content_type,
                      engine_name=engine_name,
                      template_name=template_name)

        if resp.content_type is None and engine_content_type is not None:
            # User didn't set a specific content type during controller
            # and template engine has a suggested one. Use template engine one.
            resp.headers['Content-Type'] = engine_content_type

            content_type = resp.headers['Content-Type']
            if 'charset' not in content_type and (
                    content_type.startswith('text') or content_type
                    in ('application/xhtml+xml', 'application/xml',
                        'application/json')):
                resp.content_type = content_type + '; charset=utf-8'

        # if it's a string return that string and skip all the stuff
        if not isinstance(response, dict):
            return result

        # Setup the template namespace, removing anything that the user
        # has marked to be excluded.
        namespace = response
        for name in exclude_names:
            namespace.pop(name, None)

        # If we are in a test request put the namespace where it can be
        # accessed directly
        if 'paste.testing' in req.environ:
            testing_variables = req.environ['paste.testing_variables']
            testing_variables['namespace'] = namespace
            testing_variables['template_name'] = template_name
            testing_variables['exclude_names'] = exclude_names
            testing_variables['render_params'] = render_params
            testing_variables['controller_output'] = response

        # Render the result.
        rendered = tg_render(template_vars=namespace,
                             template_engine=engine_name,
                             template_name=template_name,
                             **render_params)

        result['response'] = rendered
        return result

    @classmethod
    def _process_validation_errors(cls, controller, remainder, params,
                                   exception, context):
        """Process validation errors.

        Sets up validation status and error tracking
        to assist generating a form with given values
        and the validation failure messages.

        The error handler in decoration.validation.error_handler resolved
        and returned to be called as a controller.
        If an error_handler isn't given, the original controller is returned instead.

        """
        req = context.request

        validation_status = req.validation
        validation_status.exception = exception

        if isinstance(exception, _Tw2ValidationError):
            # Fetch all the children and grandchildren of a widget
            widget = exception.widget
            widget_children = _navigate_tw2form_children(widget.child)

            errors = dict((child.compound_key, child.error_msg)
                          for child in widget_children)
            validation_status.errors = errors
            validation_status.values = widget.child.value
        elif isinstance(exception, TGValidationError):
            validation_status.errors = exception.error_dict
            validation_status.values = exception.value
        else:
            # Most Invalid objects come back with a list of errors in the format:
            # "fieldname1: error\nfieldname2: error"
            error_list = exception.__str__().split('\n')
            for error in error_list:
                field_value = list(map(strip_string, error.split(':', 1)))

                #if the error has no field associated with it,
                #return the error as a global form error
                if len(field_value) == 1:
                    validation_status.errors['_the_form'] = field_value[0]
                    continue

                validation_status.errors[field_value[0]] = field_value[1]

            validation_status.values = getattr(exception, 'value', {})

        # Get the error handler associated to the current validation status.
        error_handler = validation_status.error_handler
        chain_validation = validation_status.chain_validation
        if error_handler is None:
            error_handler = default_im_func(controller)
            chain_validation = False

        return im_self(controller), error_handler, chain_validation

    @classmethod
    def _handle_validation_errors(cls,
                                  controller,
                                  remainder,
                                  params,
                                  exception,
                                  tgl=None):
        """Handle validation errors.

        Processes validation errors and call the error_handler,
        this is not used by TurboGears itself and is mostly provided
        for backward compatibility.
        """
        warnings.warn(
            "DecoratedController._handle_validation_errors is deprecated and will be removed",
            DeprecationWarning)

        if tgl is None:  # pragma: no cover
            # compatibility with old code that didn't pass request locals explicitly
            tgl = tg.request.environ['tg.locals']

        obj, error_handler, _ = cls._process_validation_errors(
            controller, remainder, params, exception, tgl)
        return error_handler, error_handler(obj, *remainder, **dict(params))

    def _check_security(self):
        requirement = getattr(self, 'allow_only', None)
        if requirement is None:
            return True

        if hasattr(requirement, 'predicate'):
            # It is a full requirement, let it build the response
            requirement._check_authorization()
            return True

        # It is directly a predicate, build the response ourselves
        predicate = requirement
        try:
            predicate.check_authorization(tg.request.environ)
        except NotAuthorizedError as e:
            reason = unicode_text(e)
            if hasattr(self, '_failed_authorization'):
                # Should shortcircuit the rest, but if not we will still
                # deny authorization
                self._failed_authorization(reason)
            if not_anonymous().is_met(tg.request.environ):
                # The user is authenticated but not allowed.
                code = 403
                status = 'error'
            else:
                # The user has not been not authenticated.
                code = 401
                status = 'warning'
            tg.response.status = code
            flash(reason, status=status)
            abort(code, comment=reason)