web/application.py
changeset 8312 6c2119509fac
parent 8311 76a44a0d7f4b
child 8390 637b934bc742
--- a/web/application.py	Thu Mar 15 17:42:31 2012 +0100
+++ b/web/application.py	Thu Mar 15 17:48:20 2012 +0100
@@ -24,6 +24,9 @@
 import sys
 from time import clock, time
 from contextlib import contextmanager
+from warnings import warn
+
+import httplib
 
 from logilab.common.deprecation import deprecated
 
@@ -39,6 +42,8 @@
     StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
     RemoteCallFailed, InvalidSession, RequestError)
 
+from cubicweb.web.request import CubicWebRequestBase
+
 # make session manager available through a global variable so the debug view can
 # print information about web session
 SESSION_MANAGER = None
@@ -288,11 +293,11 @@
         if config['query-log-file']:
             from threading import Lock
             self._query_log = open(config['query-log-file'], 'a')
-            self.publish = self.log_publish
+            self.handle_request = self.log_handle_request
             self._logfile_lock = Lock()
         else:
             self._query_log = None
-            self.publish = self.main_publish
+            self.handle_request = self.main_handle_request
         # instantiate session and url resolving helpers
         self.session_handler = session_handler_fact(self)
         self.set_urlresolver()
@@ -311,12 +316,12 @@
 
     # publish methods #########################################################
 
-    def log_publish(self, path, req):
+    def log_handle_request(self, req, path):
         """wrapper around _publish to log all queries executed for a given
         accessed path
         """
         try:
-            return self.main_publish(path, req)
+            return self.main_handle_request(req, path)
         finally:
             cnx = req.cnx
             if cnx:
@@ -332,7 +337,78 @@
                     except Exception:
                         self.exception('error while logging queries')
 
-    def main_publish(self, path, req):
+
+
+    def main_handle_request(self, req, path):
+        if not isinstance(req, CubicWebRequestBase):
+            warn('[3.15] Application entry poin arguments are now (req, path) '
+                 'not (path, req)', DeprecationWarning, 2)
+            req, path = path, req
+        if req.authmode == 'http':
+            # activate realm-based auth
+            realm = self.vreg.config['realm']
+            req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
+        try:
+            self.connect(req)
+            # DENY https acces for anonymous_user
+            if (req.https
+                and req.session.anonymous_session
+                and self.vreg.config['https-deny-anonymous']):
+                # don't allow anonymous on https connection
+                raise AuthenticationError()
+            content = ''
+            # nested try to allow LogOut to delegate logic to AuthenticationError
+            # handler
+            try:
+                ### Try to generate the actual request content
+                content = self.core_handle(req, path)
+            # Handle user log-out
+            except LogOut, ex:
+                # When authentification is handled by cookie the code that
+                # raised LogOut must has invalidated the cookie. We can just
+                # reload the original url without authentification
+                if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
+                    req.headers_out.setHeader('location', str(ex.url))
+                if ex.status is not None:
+                    req.status_out = httplib.SEE_OTHER
+                # When the authentification is handled by http we must
+                # explicitly ask for authentification to flush current http
+                # authentification information
+                else:
+                    # Render "logged out" content.
+                    # assignement to ``content`` prevent standard
+                    # AuthenticationError code to overwrite it.
+                    content = self.loggedout_content(req)
+                    # let the explicitly reset http credential
+                    raise AuthenticationError()
+        # Wrong, absent or Reseted credential
+        except AuthenticationError:
+            # If there is an https url configured and
+            # the request do not used https, redirect to login form
+            https_url = self.vreg.config['https-url']
+            if https_url and req.base_url() != https_url:
+                req.status_out = httplib.SEE_OTHER
+                req.headers_out.setHeader('location', https_url + 'login')
+            else:
+                # We assume here that in http auth mode the user *May* provide
+                # Authentification Credential if asked kindly.
+                if self.vreg.config['auth-mode'] == 'http':
+                    req.status_out = httplib.UNAUTHORIZED
+                # In the other case (coky auth) we assume that there is no way
+                # for the user to provide them...
+                # XXX But WHY ?
+                else:
+                    req.status_out = httplib.FORBIDDEN
+                # If previous error handling already generated a custom content
+                # do not overwrite it. This is used by LogOut Except
+                # XXX ensure we don't actually serve content
+                if not content:
+                    content = self.need_login_content(req)
+        return content
+
+
+
+    def core_handle(self, req, path):
         """method called by the main publisher to process <path>
 
         should return a string containing the resulting page or raise a
@@ -355,6 +431,7 @@
         tstart = clock()
         commited = False
         try:
+            ### standard processing of the request
             try:
                 ctrlid, rset = self.url_resolver.process(req, path)
                 try:
@@ -364,76 +441,69 @@
                     raise Unauthorized(req._('not authorized'))
                 req.update_search_state()
                 result = controller.publish(rset=rset)
-                if req.cnx:
-                    # no req.cnx if anonymous aren't allowed and we are
-                    # displaying some anonymous enabled view such as the cookie
-                    # authentication form
-                    txuuid = req.cnx.commit()
-                    if txuuid is not None:
-                        req.data['last_undoable_transaction'] = txuuid
-                    commited = True
-            except (StatusResponse, DirectResponse):
-                if req.cnx:
-                    req.cnx.commit()
-                raise
-            except (AuthenticationError, LogOut):
-                raise
-            except Redirect:
-                # redirect is raised by edit controller when everything went fine,
-                # so try to commit
-                try:
-                    if req.cnx:
-                        txuuid = req.cnx.commit()
-                        if txuuid is not None:
-                            req.data['last_undoable_transaction'] = txuuid
-                except ValidationError, ex:
-                    self.validation_error_handler(req, ex)
-                except Unauthorized, ex:
-                    req.data['errmsg'] = req._('You\'re not authorized to access this page. '
-                                               'If you think you should, please contact the site administrator.')
-                    self.error_handler(req, ex, tb=False)
-                except Exception, ex:
-                    self.error_handler(req, ex, tb=True)
-                else:
-                    self.add_undo_link_to_msg(req)
-                    # delete validation errors which may have been previously set
-                    if '__errorurl' in req.form:
-                        req.session.data.pop(req.form['__errorurl'], None)
-                    raise
-            except RemoteCallFailed, ex:
-                req.set_header('content-type', 'application/json')
-                raise StatusResponse(500, ex.dumps())
-            except NotFound:
-                raise StatusResponse(404, self.notfound_content(req))
-            except ValidationError, ex:
-                self.validation_error_handler(req, ex)
-            except Unauthorized, ex:
-                self.error_handler(req, ex, tb=False, code=403)
-            except (BadRQLQuery, RequestError), ex:
-                self.error_handler(req, ex, tb=False)
-            except BaseException, ex:
-                self.error_handler(req, ex, tb=True)
-            except:
-                self.critical('Catch all triggered!!!')
-                self.exception('this is what happened')
-                result = 'oops'
+            except StatusResponse, ex:
+                warn('StatusResponse is deprecated use req.status_out',
+                     DeprecationWarning)
+                result = ex.content
+                req.status_out = ex.status
+            except Redirect, ex:
+                # handle redirect
+                # - comply to ex status
+                # - set header field
+                #
+                # Redirect Maybe be is raised by edit controller when
+                # everything went fine, so try to commit
+                self.debug('redirecting to %s', str(ex.location))
+                req.headers_out.setHeader('location', str(ex.location))
+                assert 300<= ex.status < 400
+                req.status_out = ex.status
+                result = ''
+            if req.cnx:
+                txuuid = req.cnx.commit()
+                commited = True
+                if txuuid is not None:
+                    req.data['last_undoable_transaction'] = txuuid
+        ### error case
+        except NotFound, ex:
+            result = self.notfound_content(req)
+            req.status_out = ex.status
+        except ValidationError, ex:
+            req.status_out = httplib.CONFLICT
+            result = self.validation_error_handler(req, ex)
+        except RemoteCallFailed, ex:
+            result = self.ajax_error_handler(req, ex)
+        except Unauthorized, ex:
+            req.data['errmsg'] = req._('You\'re not authorized to access this page. '
+                                       'If you think you should, please contact the site administrator.')
+            req.status_out = httplib.UNAUTHORIZED
+            result = self.error_handler(req, ex, tb=False)
+        except (BadRQLQuery, RequestError), ex:
+            result = self.error_handler(req, ex, tb=False)
+        ### pass through exception
+        except DirectResponse:
+            if req.cnx:
+                req.cnx.commit()
+            raise
+        except (AuthenticationError, LogOut):
+            # the rollback is handled in the finally
+            raise
+        ### Last defence line
+        except BaseException, ex:
+            result = self.error_handler(req, ex, tb=True)
         finally:
             if req.cnx and not commited:
                 try:
                     req.cnx.rollback()
                 except Exception:
                     pass # ignore rollback error at this point
+            # request may be referenced by "onetime callback", so clear its entity
+            # cache to avoid memory usage
+            req.drop_entity_cache()
         self.add_undo_link_to_msg(req)
         self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
         return result
 
-    def add_undo_link_to_msg(self, req):
-        txuuid = req.data.get('last_undoable_transaction')
-        if txuuid is not None:
-            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
-            req.build_url('undo', txuuid=txuuid), req._('undo'))
-            req.append_to_redirect_message(msg)
-
+    ### Error handler
     def validation_error_handler(self, req, ex):
         ex.errors = dict((k, v) for k, v in ex.errors.items())
         if '__errorurl' in req.form:
@@ -446,10 +516,13 @@
             # session key is 'url + #<form dom id', though we usually don't want
             # the browser to move to the form since it hides the global
             # messages.
-            raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
-        self.error_handler(req, ex, tb=False)
+            location = req.form['__errorurl'].rsplit('#', 1)[0]
+            req.headers_out.setHeader('location', str(location))
+            req.status_out = httplib.SEE_OTHER
+            return ''
+        return self.error_handler(req, ex, tb=False)
 
-    def error_handler(self, req, ex, tb=False, code=500):
+    def error_handler(self, req, ex, tb=False):
         excinfo = sys.exc_info()
         self.exception(repr(ex))
         req.set_header('Cache-Control', 'no-cache')
@@ -457,7 +530,7 @@
         req.reset_message()
         req.reset_headers()
         if req.ajax_request:
-            raise RemoteCallFailed(unicode(ex))
+            return ajax_error_handler(req, ex)
         try:
             req.data['ex'] = ex
             if tb:
@@ -468,7 +541,29 @@
             content = self.vreg['views'].main_template(req, template, view=errview)
         except Exception:
             content = self.vreg['views'].main_template(req, 'error-template')
-        raise StatusResponse(code, content)
+        if getattr(ex, 'status', None) is not None:
+            req.status_out = ex.status
+        return content
+
+    def add_undo_link_to_msg(self, req):
+        txuuid = req.data.get('last_undoable_transaction')
+        if txuuid is not None:
+            msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+            req.build_url('undo', txuuid=txuuid), req._('undo'))
+            req.append_to_redirect_message(msg)
+
+
+
+    def ajax_error_handler(self, req, ex):
+        req.set_header('content-type', 'application/json')
+        status = ex.status
+        if status is None:
+            status = httplib.INTERNAL_SERVER_ERROR
+        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        req.status_out = status
+        return json_dumper()
+
+    # special case handling
 
     def need_login_content(self, req):
         return self.vreg['views'].main_template(req, 'login')
@@ -482,6 +577,8 @@
         template = self.main_template_id(req)
         return self.vreg['views'].main_template(req, template, view=view)
 
+    # template stuff
+
     def main_template_id(self, req):
         template = req.form.get('__template', req.property_value('ui.main-template'))
         if template not in self.vreg['views']: