1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
21 import profile
as profiler
27 from datetime
import datetime
, timedelta
28 from functools
import wraps
, partial
29 from urlparse
import urljoin
30 from xml
.sax
.saxutils
import escape
33 from flask
import request
, session
, g
, current_app
, redirect
34 from itsdangerous
import BadData
35 from sqlalchemy
.orm
.exc
import NoResultFound
36 from werkzeug
.exceptions
import BadRequest
, MethodNotAllowed
, NotFound
, Forbidden
37 from werkzeug
.wrappers
import Response
38 from ZEO
.Exceptions
import ClientDisconnected
39 from ZODB
.POSException
import ConflictError
, POSKeyError
41 from MaKaC
.accessControl
import AccessWrapper
43 from MaKaC
.common
import fossilize
, security
44 from MaKaC
.common
.contextManager
import ContextManager
45 from MaKaC
.common
.utils
import truncate
46 from MaKaC
.errors
import (
49 ConferenceClosedError
,
55 from MaKaC
.webinterface
.mail
import GenericMailer
56 import MaKaC
.webinterface
.pages
.errors
as errors
57 from MaKaC
.webinterface
.pages
.error
import WErrorWSGI
58 from MaKaC
.webinterface
.pages
.conferences
import WPConferenceModificationClosed
59 from indico
.core
import signals
60 from indico
.core
.config
import Config
61 from indico
.core
.db
import DBMgr
62 from indico
.core
.logger
import Logger
63 from indico
.modules
.auth
.util
import url_for_login
64 from indico
.util
import json
65 from indico
.core
.db
.util
import flush_after_commit_queue
66 from indico
.util
.decorators
import jsonify_error
67 from indico
.util
.i18n
import _
68 from indico
.util
.redis
import RedisError
69 from indico
.web
.flask
.util
import ResponseUtil
, url_for
72 HTTP_VERBS
= {'GET', 'PATCH', 'POST', 'PUT', 'DELETE'}
75 class RequestHandlerBase():
79 def _checkProtection(self
):
80 """This method is called after _checkParams and is a good place
81 to check if the user is permitted to perform some actions.
83 If you only want to run some code for GET or POST requests, you can create
84 a method named e.g. _checkProtection_POST which will be executed AFTER this one.
90 Returns True if current user is a user or has either a modification key in their session.
92 return session
.get('modifKeys') or self
._getUser
()
96 Returns the access wrapper related to this session/user
100 accessWrapper
= property(getAW
)
104 Returns the current user
106 return self
._aw
.getUser()
108 def _setUser(self
, new_user
=None):
110 Sets the current user
112 self
._aw
.setUser(new_user
)
114 def getRequestURL(self
, secure
=False):
116 Reconstructs the request URL
118 query_string
= ('?' + request
.query_string
) if request
.query_string
else ''
120 return urljoin(Config
.getInstance().getBaseSecureURL(), request
.path
) + query_string
126 If the RH must be HTTPS and there is a BaseSecurURL, then use it!
128 return self
._tohttps
and Config
.getInstance().getBaseSecureURL()
130 def getRequestParams(self
):
133 def _getTruncatedParams(self
):
134 """Truncates params"""
136 for key
, value
in self
._reqParams
.iteritems():
137 if key
in {'password', 'confirm_password'}:
138 params
[key
] = '[password hidden, len=%d]' % len(value
)
139 elif isinstance(value
, basestring
):
140 params
[key
] = truncate(value
, 1024)
146 class RH(RequestHandlerBase
):
147 """This class is the base for request handlers of the application. A request
148 handler will be instantiated when a web request arrives to mod_python;
149 the mp layer will forward the request to the corresponding request
150 handler which will know which action has to be performed (displaying a
151 web page or performing some operation and redirecting to another page).
152 Request handlers will be responsible for parsing the parameters coming
153 from a mod_python request, handle the errors which occurred during the
154 action to perform, managing the sessions, checking security for each
155 operation (thus they implement the access control system of the web
157 It is important to encapsulate all this here as in case of changing
158 the web application framework we'll just need to adapt this layer (the
159 rest of the system wouldn't need any change).
162 _uh - (URLHandler) Associated URLHandler which points to the
164 _req - UNUSED/OBSOLETE, always None
165 _requestStarted - (bool) Flag which tells whether a DB transaction
166 has been started or not.
167 _aw - (AccessWrapper) Current access information for the rh.
168 _target - (Locable) Reference to an object which is the destination
169 of the operations needed to carry out the rh. If set it must
170 provide (through the standard Locable interface) the methods
171 to get the url parameters in order to reproduce the access to
173 _reqParams - (dict) Dictionary containing the received HTTP
174 parameters (independently of the method) transformed into
175 python data types. The key is the parameter name while the
176 value should be the received paramter value (or values).
178 _tohttps
= False # set this value to True for the RH that must be HTTPS when there is a BaseSecureURL
179 _doNotSanitizeFields
= []
180 _isMobile
= True # this value means that the generated web page can be mobile
181 CSRF_ENABLED
= False # require a csrf_token when accessing the RH with anything but GET
183 #: A dict specifying how the url should be normalized.
184 #: `args` is a dictionary mapping view args keys to callables
185 #: used to retrieve the expected value for those arguments if they
186 #: are present in the request's view args.
187 #: `locators` is a set of callables returning objects with locators.
188 #: `preserved_args` is a set of view arg names which will always
189 #: be copied from the current request if present.
190 #: The callables are always invoked with a single `self` argument
191 #: containing the RH instance.
192 #: Arguments specified in the `defaults` of any rule matching the
193 #: current endpoint are always excluded when checking if the args
194 #: match or when building a new URL.
195 #: If the view args built from the returned objects do not match
196 #: the request's view args, a redirect is issued automatically.
197 #: If the request is not using GET/HEAD, a 404 error is raised
198 #: instead of a redirect since such requests cannot be redirected
199 #: but executing them on the wrong URL may pose a security risk in
200 #: case and of the non-relevant URL segments is used for access
202 normalize_url_spec
= {
205 'preserved_args': set()
209 self
._responseUtil
= ResponseUtil()
210 self
._requestStarted
= False
211 self
._aw
= AccessWrapper() # Fill in the aw instance with the current information
214 self
._startTime
= None
216 self
._tempFilesToDelete
= []
217 self
._redisPipeline
= None
218 self
._doProcess
= True # Flag which indicates whether the RH process
219 # must be carried out; this is useful for
220 # the checkProtection methods when they
221 # detect that an immediate redirection is
224 # Methods =============================================================
230 return self
._isMobile
232 def _setSessionUser(self
):
233 self
._aw
.setUser(session
.avatar
)
236 def csrf_token(self
):
237 return session
.csrf_token
if session
.csrf_protected
else ''
239 def _getRequestParams(self
):
240 return self
._reqParams
242 def getRequestParams(self
):
243 return self
._getRequestParams
()
245 def _disableCaching(self
):
246 """Disables caching"""
248 # IE doesn't seem to like 'no-cache' Cache-Control headers...
249 if request
.user_agent
.browser
== 'msie':
250 # actually, the only way to safely disable caching seems to be this one
251 self
._responseUtil
.headers
["Cache-Control"] = "private"
252 self
._responseUtil
.headers
["Expires"] = "-1"
254 self
._responseUtil
.headers
["Cache-Control"] = "no-store, no-cache, must-revalidate"
255 self
._responseUtil
.headers
["Pragma"] = "no-cache"
257 def _redirect(self
, targetURL
, status
=303):
258 if isinstance(targetURL
, Response
):
259 status
= targetURL
.status_code
260 targetURL
= targetURL
.headers
['Location']
262 targetURL
= str(targetURL
)
263 if "\r" in targetURL
or "\n" in targetURL
:
264 raise MaKaCError(_("http header CRLF injection detected"))
265 self
._responseUtil
.redirect
= (targetURL
, status
)
267 def _changeRH(self
, rh
, params
):
268 """Calls the specified RH after processing this one"""
269 self
._responseUtil
.call
= lambda: rh().process(params
)
271 def _checkHttpsRedirect(self
):
272 """If HTTPS must be used but it is not, redirect!"""
273 if self
.use_https() and not request
.is_secure
:
274 self
._redirect
(self
.getRequestURL(secure
=True))
279 def _normaliseListParam(self
, param
):
280 if not isinstance(param
, list):
284 def _processError(self
, e
):
287 def _legacy_check(self
):
289 This method can be overridden to check if you are dealing with
290 legacy data not supported by the RH. It is called before
291 `checkParams` and should raise an exception if necessary.
294 def normalize_url(self
):
295 """Performs URL normalization.
297 This uses the :attr:`normalize_url_spec` to check if the URL
298 params are what they should be and redirects or fails depending
299 on the HTTP method used if it's not the case.
301 :return: ``None`` or a redirect response
303 if not self
.normalize_url_spec
or not any(self
.normalize_url_spec
.itervalues()):
306 'args': self
.normalize_url_spec
.get('args', {}),
307 'locators': self
.normalize_url_spec
.get('locators', set()),
308 'preserved_args': self
.normalize_url_spec
.get('preserved_args', set()),
310 # Initialize the new view args with preserved arguments (since those would be lost otherwise)
311 new_view_args
= {k
: v
for k
, v
in request
.view_args
.iteritems() if k
in spec
['preserved_args']}
312 # Retrieve the expected values for all simple arguments (if they are currently present)
313 for key
, getter
in spec
['args'].iteritems():
314 if key
in request
.view_args
:
315 new_view_args
[key
] = getter(self
)
316 # Retrieve the expected values from locators
317 for getter
in spec
['locators']:
320 expected
= value
.locator
321 except AttributeError:
323 expected
= value
.getLocator()
324 except AttributeError:
325 raise AttributeError("'{}' object has neither 'locator' nor 'getLocator'".format(type(value
)))
326 new_view_args
.update(expected
)
327 # Get all default values provided by the url map for the endpoint
328 defaults
= set(itertools
.chain
.from_iterable(r
.defaults
329 for r
in current_app
.url_map
.iter_rules(request
.endpoint
)
331 provided
= {k
: v
for k
, v
in request
.view_args
.iteritems() if k
not in defaults
}
332 if new_view_args
!= provided
:
333 if request
.method
in {'GET', 'HEAD'}:
334 return redirect(url_for(request
.endpoint
, **dict(request
.args
.to_dict(), **new_view_args
)))
336 raise NotFound('The URL contains invalid data. Please go to the previous page and refresh it.')
338 def _checkParams(self
, params
):
339 """This method is called before _checkProtection and is a good place
340 to assign variables from request params to member variables.
342 Note that in any new code the params argument SHOULD be IGNORED.
343 Use the following objects provided by Flask instead:
344 from flask import request
345 request.view_args (URL route params)
346 request.args (GET params (from the query string))
347 request.form (POST params)
348 request.values (GET+POST params - use only if ABSOLUTELY NECESSARY)
350 If you only want to run some code for GET or POST requests, you can create
351 a method named e.g. _checkParams_POST which will be executed AFTER this one.
352 The method is called without any arguments (except self).
357 """The default process method dispatches to a method containing
358 the HTTP verb used for the current request, e.g. _process_POST.
359 When implementing this please consider that you most likely want/need
360 only GET and POST - the other verbs are not supported everywhere!
362 method
= getattr(self
, '_process_' + request
.method
, None)
364 valid_methods
= [m
for m
in HTTP_VERBS
if hasattr(self
, '_process_' + m
)]
365 raise MethodNotAllowed(valid_methods
)
368 def _checkCSRF(self
):
369 token
= request
.headers
.get('X-CSRF-Token', request
.form
.get('csrf_token'))
370 if self
.CSRF_ENABLED
and request
.method
!= 'GET' and token
!= session
.csrf_token
:
371 msg
= _(u
"It looks like there was a problem with your current session. Please use your browser's back "
372 u
"button, reload the page and try again.")
373 raise BadRequest(msg
)
374 elif not self
.CSRF_ENABLED
and current_app
.debug
and request
.method
!= 'GET':
375 # Warn if CSRF is not enabled for a RH in new code
376 module
= self
.__class
__.__module
__
377 if module
.startswith('indico.modules.') or module
.startswith('indico.core.'):
378 msg
= (u
'{} request sent to {} which has no CSRF checks. Set `CSRF_ENABLED = True` in the class to '
379 u
'enable them.').format(request
.method
, self
.__class
__.__name
__)
380 warnings
.warn(msg
, RuntimeWarning)
381 # legacy csrf check (referer-based):
382 # Check referer for POST requests. We do it here so we can properly use indico's error handling
383 if Config
.getInstance().getCSRFLevel() < 3 or request
.method
!= 'POST':
385 referer
= request
.referrer
386 # allow empty - otherwise we might lock out paranoid users blocking referers
390 if referer
.startswith(Config
.getInstance().getBaseURL()):
392 # valid https referer - if https is enabled
393 base_secure
= Config
.getInstance().getBaseSecureURL()
394 if base_secure
and referer
.startswith(base_secure
):
396 raise BadRefererError('This operation is not allowed from an external referer.')
399 def _processGeneralError(self
, e
):
400 """Treats general errors occured during the process of a RH."""
402 if Config
.getInstance().getPropagateAllExceptions():
404 return errors
.WPGenericError(self
).display()
406 @jsonify_error(status
=500, logging_level
='exception')
407 def _processUnexpectedError(self
, e
):
408 """Unexpected errors"""
410 self
._responseUtil
.redirect
= None
411 if Config
.getInstance().getEmbeddedWebserver() or Config
.getInstance().getPropagateAllExceptions():
413 return errors
.WPUnexpectedError(self
).display()
416 def _processHostnameResolveError(self
, e
):
417 """Unexpected errors"""
419 return errors
.WPHostnameResolveError(self
).display()
421 @jsonify_error(status
=403)
422 def _processForbidden(self
, e
):
423 message
= _("Access Denied")
424 if e
.description
== Forbidden
.description
:
425 explanation
= _("You are not allowed to access this page.")
427 explanation
= e
.description
428 return WErrorWSGI((message
, explanation
)).getHTML()
430 @jsonify_error(status
=400)
431 def _processBadRequest(self
, e
):
432 message
= _("Bad Request")
433 return WErrorWSGI((message
, e
.description
)).getHTML()
435 @jsonify_error(status
=400)
436 def _processBadData(self
, e
):
437 message
= _("Invalid or expired token")
438 return WErrorWSGI((message
, e
.message
)).getHTML()
440 @jsonify_error(status
=403)
441 def _processAccessError(self
, e
):
442 """Treats access errors occured during the process of a RH."""
443 return errors
.WPAccessError(self
).display()
446 def _processKeyAccessError(self
, e
):
447 """Treats access errors occured during the process of a RH."""
449 # We are going to redirect to the page asking for access key
450 # and so it must be https if there is a BaseSecureURL. And that's
451 # why we set _tohttps to True.
453 if self
._checkHttpsRedirect
():
455 return errors
.WPKeyAccessError(self
).display()
458 def _processModificationError(self
, e
):
459 """Handles modification errors occured during the process of a RH."""
460 # Redirect to HTTPS in case the user is logged in
462 if self
._checkHttpsRedirect
():
464 return errors
.WPModificationError(self
).display()
466 @jsonify_error(status
=400)
467 def _processBadRequestKeyError(self
, e
):
468 """Request lacks a necessary key for processing"""
469 msg
= _('Required argument missing: %s') % e
.message
470 return errors
.WPFormValuesError(self
, msg
).display()
473 def _processConferenceClosedError(self
, e
):
474 """Treats access to modification pages for conferences when they are closed."""
476 return WPConferenceModificationClosed(self
, e
._conf
).display()
479 def _processTimingError(self
, e
):
480 """Treats timing errors occured during the process of a RH."""
482 return errors
.WPTimingError(self
, e
).display()
485 def _processNoReportError(self
, e
):
486 """Process errors without reporting"""
488 return errors
.WPNoReportError(self
, e
).display()
490 @jsonify_error(status
=404)
491 def _processNotFoundError(self
, e
):
492 if isinstance(e
, NotFound
):
493 message
= _("Page not found") # that's a bit nicer than "404: Not Found"
494 if e
.description
== NotFound
.description
:
495 explanation
= _("The page you are looking for doesn't exist.")
497 explanation
= e
.description
499 message
= e
.getMessage()
500 explanation
= e
.getExplanation()
501 return WErrorWSGI((message
, explanation
)).getHTML()
504 def _processParentTimingError(self
, e
):
505 """Treats timing errors occured during the process of a RH."""
507 return errors
.WPParentTimingError(self
, e
).display()
510 def _processEntryTimingError(self
, e
):
511 """Treats timing errors occured during the process of a RH."""
513 return errors
.WPEntryTimingError(self
, e
).display()
516 def _processFormValuesError(self
, e
):
517 """Treats user input related errors occured during the process of a RH."""
519 return errors
.WPFormValuesError(self
, e
).display()
522 def _processLaTeXError(self
, e
):
523 """Treats access errors occured during the process of a RH."""
525 return errors
.WPLaTeXError(self
, e
).display()
528 def _processRestrictedHTML(self
, e
):
530 return errors
.WPRestrictedHTML(self
, escape(str(e
))).display()
533 def _processHtmlScriptError(self
, e
):
535 return errors
.WPHtmlScriptError(self
, escape(str(e
))).display()
538 def _processHtmlForbiddenTag(self
, e
):
541 return errors
.WPRestrictedHTML(self
, escape(str(e
))).display()
543 def _process_retry_setup(self
):
544 # clear the fossile cache at the start of each request
545 fossilize
.clearCache()
546 # clear after-commit queue
547 flush_after_commit_queue(False)
548 # delete all queued emails
549 GenericMailer
.flushQueue(False)
550 # clear the existing redis pipeline
551 if self
._redisPipeline
:
552 self
._redisPipeline
.reset()
554 def _process_retry_auth_check(self
, params
):
555 # keep a link to the web session in the access wrapper
556 # this is used for checking access/modification key existence
557 # in the user session
558 self
._setSessionUser
()
561 Logger
.get('requestHandler').info('Request %s identified with user %s (%s)' % (
562 request
, self
._getUser
().getFullName(), self
._getUser
().getId()))
563 if not self
._tohttps
and Config
.getInstance().getAuthenticatedEnforceSecure():
565 if self
._checkHttpsRedirect
():
566 return self
._responseUtil
.make_redirect()
569 self
._reqParams
= copy
.copy(params
)
571 def _process_retry_do(self
, profile
):
572 profile_name
, res
= '', ''
576 # old code gets parameters from call
577 # new code utilizes of flask.request
578 if len(inspect
.getargspec(self
._checkParams
).args
) < 2:
579 cp_result
= self
._checkParams
()
581 cp_result
= self
._checkParams
(self
._reqParams
)
583 if isinstance(cp_result
, (current_app
.response_class
, Response
)):
586 func
= getattr(self
, '_checkParams_' + request
.method
, None)
589 if isinstance(cp_result
, (current_app
.response_class
, Response
)):
592 except NoResultFound
: # sqlalchemy .one() not finding anything
593 raise NotFoundError(_('The specified item could not be found.'), title
=_('Item not found'))
595 rv
= self
.normalize_url()
599 self
._checkProtection
()
600 func
= getattr(self
, '_checkProtection_' + request
.method
, None)
604 security
.Sanitization
.sanitizationCheck(self
._target
,
607 self
._doNotSanitizeFields
)
611 profile_name
= os
.path
.join(Config
.getInstance().getTempDir(), 'stone{}.prof'.format(random
.random()))
613 profiler
.runctx('result[0] = self._process()', globals(), locals(), profile_name
)
616 res
= self
._process
()
617 return profile_name
, res
619 def _process_retry(self
, params
, retry
, profile
, forced_conflicts
):
620 self
._process
_retry
_setup
()
621 self
._process
_retry
_auth
_check
(params
)
622 DBMgr
.getInstance().sync()
623 return self
._process
_retry
_do
(profile
)
625 def _process_success(self
):
626 Logger
.get('requestHandler').info('Request {} successful'.format(request
))
627 # request is succesfull, now, doing tasks that must be done only once
629 flush_after_commit_queue(True)
630 GenericMailer
.flushQueue(True) # send emails
631 self
._deleteTempFiles
()
633 Logger
.get('mail').exception('Mail sending operation failed')
634 # execute redis pipeline if we have one
635 if self
._redisPipeline
:
637 self
._redisPipeline
.execute()
639 Logger
.get('redis').exception('Could not execute pipeline')
641 def process(self
, params
):
642 if request
.method
not in HTTP_VERBS
:
643 # Just to be sure that we don't get some crappy http verb we don't expect
646 cfg
= Config
.getInstance()
647 forced_conflicts
, max_retries
, profile
= cfg
.getForceConflicts(), cfg
.getMaxRetries(), cfg
.getProfile()
648 profile_name
, res
, textLog
= '', '', []
650 self
._startTime
= datetime
.now()
653 ContextManager
.destroy()
654 ContextManager
.set('currentRH', self
)
657 #redirect to https if necessary
658 if self
._checkHttpsRedirect
():
659 return self
._responseUtil
.make_redirect()
661 DBMgr
.getInstance().startRequest()
662 textLog
.append("%s : Database request started" % (datetime
.now() - self
._startTime
))
663 Logger
.get('requestHandler').info('[pid=%s] Request %s started' % (
664 os
.getpid(), request
))
667 for i
, retry
in enumerate(transaction
.attempts(max_retries
)):
670 signals
.before_retry
.send()
673 Logger
.get('requestHandler').info('\t[pid=%s] from host %s' % (os
.getpid(), request
.remote_addr
))
674 profile_name
, res
= self
._process
_retry
(params
, i
, profile
, forced_conflicts
)
675 signals
.after_process
.send()
676 if i
< forced_conflicts
: # raise conflict error if enabled to easily handle conflict error case
679 DBMgr
.getInstance().endRequest(commit
=False)
681 except (ConflictError
, POSKeyError
):
684 # only log conflict if it wasn't forced
685 if i
>= forced_conflicts
:
686 Logger
.get('requestHandler').warning('Conflict in Database! (Request %s)\n%s' % (request
, traceback
.format_exc()))
687 except ClientDisconnected
:
689 Logger
.get('requestHandler').warning('Client Disconnected! (Request {})'.format(request
))
691 self
._process
_success
()
692 except Exception as e
:
694 res
= self
._getMethodByExceptionName
(e
)(e
)
696 totalTime
= (datetime
.now() - self
._startTime
)
697 textLog
.append('{} : Request ended'.format(totalTime
))
700 if profile
and totalTime
> timedelta(0, 1) and os
.path
.isfile(profile_name
):
701 rep
= Config
.getInstance().getTempDir()
702 stats
= pstats
.Stats(profile_name
)
704 stats
.sort_stats('cumulative', 'time', 'calls')
705 stats
.dump_stats(os
.path
.join(rep
, 'IndicoRequestProfile.log'))
706 output
= StringIO
.StringIO()
708 stats
.print_stats(100)
709 sys
.stdout
= sys
.__stdout
__
710 s
= output
.getvalue()
711 f
= file(os
.path
.join(rep
, 'IndicoRequest.log'), 'a+')
712 f
.write('--------------------------------\n')
713 f
.write('URL : {}\n'.format(request
.url
))
714 f
.write('{} : start request\n'.format(self
._startTime
))
715 f
.write('params:{}'.format(params
))
716 f
.write('\n'.join(textLog
))
718 f
.write('retried : {}\n'.format(10-retry
))
720 f
.write('--------------------------------\n\n')
722 if profile
and profile_name
and os
.path
.exists(profile_name
):
723 os
.remove(profile_name
)
725 if self
._responseUtil
.call
:
726 return self
._responseUtil
.make_call()
728 # In case of no process needed, we should return empty string to avoid erroneous output
729 # specially with getVars breaking the JS files.
730 if not self
._doProcess
or res
is None:
731 return self
._responseUtil
.make_empty()
733 return self
._responseUtil
.make_response(res
)
735 def _getMethodByExceptionName(self
, e
):
737 'NotFound': 'NotFoundError',
738 'MaKaCError': 'GeneralError',
739 'IndicoError': 'GeneralError',
740 'ValueError': 'UnexpectedError',
741 'Exception': 'UnexpectedError',
742 'AccessControlError': 'AccessError'
743 }.get(type(e
).__name
__, type(e
).__name
__)
744 if isinstance(e
, BadData
): # we also want its subclasses
745 exception_name
= 'BadData'
746 return getattr(self
, '_process{}'.format(exception_name
), self
._processUnexpectedError
)
748 def _deleteTempFiles(self
):
749 if len(self
._tempFilesToDelete
) > 0:
750 for f
in self
._tempFilesToDelete
:
758 """A simple RH that calls a function to build the response
760 The main purpose of this RH is to allow external library to
761 display something within the Indico layout (which requires a
762 RH / a ZODB connection in most cases).
764 The preferred way to use this class is by using the
765 `RHSimple.wrap_function` decorator.
767 :param func: A function returning HTML
769 def __init__(self
, func
):
778 def wrap_function(cls
, func
):
779 """Decorates a function to run within the RH's framework"""
781 def wrapper(*args
, **kwargs
):
782 return cls(partial(func
, *args
, **kwargs
)).process({})
787 class RHProtected(RH
):
789 def _getLoginURL(self
):
790 return url_for_login(request
.relative_url
)
792 def _checkSessionUser(self
):
793 if self
._getUser
() is None:
794 if request
.headers
.get("Content-Type", "text/html").find("application/json") != -1:
795 raise NotLoggedError("You are currently not authenticated. Please log in again.")
797 self
._redirect
(self
._getLoginURL
())
798 self
._doProcess
= False
800 def _checkProtection(self
):
801 self
._checkSessionUser
()
804 class RHDisplayBaseProtected(RHProtected
):
806 def _checkProtection(self
):
807 if not self
._target
.canAccess( self
.getAW() ):
808 from MaKaC
.conference
import Link
, LocalFile
, Category
809 if isinstance(self
._target
,Link
) or isinstance(self
._target
,LocalFile
):
810 target
= self
._target
.getOwner()
812 target
= self
._target
813 if not isinstance(self
._target
, Category
) and target
.isProtected():
814 if target
.getAccessKey() != "" or target
.getConference() and \
815 target
.getConference().getAccessKey() != "":
816 raise KeyAccessError()
817 elif target
.getModifKey() != "" or target
.getConference() and \
818 target
.getConference().getModifKey() != "":
819 raise ModificationError()
820 if self
._getUser
() is None:
821 self
._checkSessionUser
()
826 class RHModificationBaseProtected(RHProtected
):
830 def _checkProtection(self
):
831 if not self
._target
.canModify( self
.getAW() ):
832 if self
._target
.getModifKey() != "":
833 raise ModificationError()
834 if self
._getUser
() is None:
835 self
._checkSessionUser
()
837 raise ModificationError()
838 if hasattr(self
._target
, "getConference") and not self
._allowClosed
:
839 if self
._target
.getConference().isClosed():
840 raise ConferenceClosedError(self
._target
.getConference())