do not choke on missing defaults in normalize_url
[cds-indico.git] / indico / MaKaC / webinterface / rh / base.py
blob19e52ab454f88992af0389a3bea4a612bacf41a0
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/>.
16 import copy
17 import inspect
18 import itertools
19 import time
20 import os
21 import profile as profiler
22 import pstats
23 import sys
24 import random
25 import StringIO
26 from datetime import datetime, timedelta
27 from functools import wraps, partial
28 from urlparse import urljoin
29 from xml.sax.saxutils import escape
31 import transaction
32 from flask import request, session, g, current_app, redirect
33 from itsdangerous import BadData
34 from sqlalchemy.orm.exc import NoResultFound
35 from werkzeug.exceptions import BadRequest, MethodNotAllowed, NotFound, Forbidden
36 from werkzeug.wrappers import Response
37 from ZEO.Exceptions import ClientDisconnected
38 from ZODB.POSException import ConflictError, POSKeyError
40 from MaKaC.accessControl import AccessWrapper
42 from MaKaC.common import fossilize, security
43 from MaKaC.common.contextManager import ContextManager
44 from MaKaC.common.utils import truncate
45 from MaKaC.errors import (
46 AccessError,
47 BadRefererError,
48 ConferenceClosedError,
49 KeyAccessError,
50 MaKaCError,
51 ModificationError,
52 NotLoggedError,
53 NotFoundError)
54 from MaKaC.webinterface.mail import GenericMailer
55 import MaKaC.webinterface.pages.errors as errors
56 from MaKaC.webinterface.pages.error import WErrorWSGI
57 from MaKaC.webinterface.pages.conferences import WPConferenceModificationClosed
58 from indico.core import signals
59 from indico.core.config import Config
60 from indico.core.db import DBMgr
61 from indico.core.logger import Logger
62 from indico.modules.auth.util import url_for_login
63 from indico.util import json
64 from indico.core.db.util import flush_after_commit_queue
65 from indico.util.decorators import jsonify_error
66 from indico.util.i18n import _
67 from indico.util.redis import RedisError
68 from indico.web.flask.util import ResponseUtil, url_for
71 HTTP_VERBS = {'GET', 'PATCH', 'POST', 'PUT', 'DELETE'}
74 class RequestHandlerBase():
76 _uh = None
78 def _checkProtection(self):
79 """This method is called after _checkParams and is a good place
80 to check if the user is permitted to perform some actions.
82 If you only want to run some code for GET or POST requests, you can create
83 a method named e.g. _checkProtection_POST which will be executed AFTER this one.
84 """
85 pass
87 def _getAuth(self):
88 """
89 Returns True if current user is a user or has either a modification key in their session.
90 """
91 return session.get('modifKeys') or self._getUser()
93 def getAW(self):
94 """
95 Returns the access wrapper related to this session/user
96 """
97 return self._aw
99 accessWrapper = property(getAW)
101 def _getUser(self):
103 Returns the current user
105 return self._aw.getUser()
107 def _setUser(self, new_user=None):
109 Sets the current user
111 self._aw.setUser(new_user)
113 def getRequestURL(self, secure=False):
115 Reconstructs the request URL
117 query_string = ('?' + request.query_string) if request.query_string else ''
118 if secure:
119 return urljoin(Config.getInstance().getBaseSecureURL(), request.path) + query_string
120 else:
121 return request.url
123 def use_https(self):
125 If the RH must be HTTPS and there is a BaseSecurURL, then use it!
127 return self._tohttps and Config.getInstance().getBaseSecureURL()
129 def getRequestParams(self):
130 return self._params
132 def _getTruncatedParams(self):
133 """Truncates params"""
134 params = {}
135 for key, value in self._reqParams.iteritems():
136 if key in {'password', 'confirm_password'}:
137 params[key] = '[password hidden, len=%d]' % len(value)
138 elif isinstance(value, basestring):
139 params[key] = truncate(value, 1024)
140 else:
141 params[key] = value
142 return params
145 class RH(RequestHandlerBase):
146 """This class is the base for request handlers of the application. A request
147 handler will be instantiated when a web request arrives to mod_python;
148 the mp layer will forward the request to the corresponding request
149 handler which will know which action has to be performed (displaying a
150 web page or performing some operation and redirecting to another page).
151 Request handlers will be responsible for parsing the parameters coming
152 from a mod_python request, handle the errors which occurred during the
153 action to perform, managing the sessions, checking security for each
154 operation (thus they implement the access control system of the web
155 interface).
156 It is important to encapsulate all this here as in case of changing
157 the web application framework we'll just need to adapt this layer (the
158 rest of the system wouldn't need any change).
160 Attributes:
161 _uh - (URLHandler) Associated URLHandler which points to the
162 current rh.
163 _req - UNUSED/OBSOLETE, always None
164 _requestStarted - (bool) Flag which tells whether a DB transaction
165 has been started or not.
166 _aw - (AccessWrapper) Current access information for the rh.
167 _target - (Locable) Reference to an object which is the destination
168 of the operations needed to carry out the rh. If set it must
169 provide (through the standard Locable interface) the methods
170 to get the url parameters in order to reproduce the access to
171 the rh.
172 _reqParams - (dict) Dictionary containing the received HTTP
173 parameters (independently of the method) transformed into
174 python data types. The key is the parameter name while the
175 value should be the received paramter value (or values).
177 _tohttps = False # set this value to True for the RH that must be HTTPS when there is a BaseSecureURL
178 _doNotSanitizeFields = []
179 _isMobile = True # this value means that the generated web page can be mobile
180 CSRF_ENABLED = False # require a csrf_token when accessing the RH with anything but GET
182 #: A dict specifying how the url should be normalized.
183 #: `args` is a dictionary mapping view args keys to callables
184 #: used to retrieve the expected value for those arguments if they
185 #: are present in the request's view args.
186 #: `locators` is a set of callables returning objects with locators.
187 #: `preserved_args` is a set of view arg names which will always
188 #: be copied from the current request if present.
189 #: The callables are always invoked with a single `self` argument
190 #: containing the RH instance.
191 #: Arguments specified in the `defaults` of any rule matching the
192 #: current endpoint are always excluded when checking if the args
193 #: match or when building a new URL.
194 #: If the view args built from the returned objects do not match
195 #: the request's view args, a redirect is issued automatically.
196 #: If the request is not using GET/HEAD, a 404 error is raised
197 #: instead of a redirect since such requests cannot be redirected
198 #: but executing them on the wrong URL may pose a security risk in
199 #: case and of the non-relevant URL segments is used for access
200 #: checks.
201 normalize_url_spec = {
202 'args': {},
203 'locators': set(),
204 'preserved_args': set()
207 def __init__(self):
208 self._responseUtil = ResponseUtil()
209 self._requestStarted = False
210 self._aw = AccessWrapper() # Fill in the aw instance with the current information
211 self._target = None
212 self._reqParams = {}
213 self._startTime = None
214 self._endTime = None
215 self._tempFilesToDelete = []
216 self._redisPipeline = None
217 self._doProcess = True # Flag which indicates whether the RH process
218 # must be carried out; this is useful for
219 # the checkProtection methods when they
220 # detect that an immediate redirection is
221 # needed
223 # Methods =============================================================
225 def getTarget(self):
226 return self._target
228 def isMobile(self):
229 return self._isMobile
231 def _setSessionUser(self):
232 self._aw.setUser(session.avatar)
234 @property
235 def csrf_token(self):
236 return session.csrf_token if session.csrf_protected else ''
238 def _getRequestParams(self):
239 return self._reqParams
241 def getRequestParams(self):
242 return self._getRequestParams()
244 def _disableCaching(self):
245 """Disables caching"""
247 # IE doesn't seem to like 'no-cache' Cache-Control headers...
248 if request.user_agent.browser == 'msie':
249 # actually, the only way to safely disable caching seems to be this one
250 self._responseUtil.headers["Cache-Control"] = "private"
251 self._responseUtil.headers["Expires"] = "-1"
252 else:
253 self._responseUtil.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
254 self._responseUtil.headers["Pragma"] = "no-cache"
256 def _redirect(self, targetURL, status=303):
257 if isinstance(targetURL, Response):
258 status = targetURL.status_code
259 targetURL = targetURL.headers['Location']
260 else:
261 targetURL = str(targetURL)
262 if "\r" in targetURL or "\n" in targetURL:
263 raise MaKaCError(_("http header CRLF injection detected"))
264 self._responseUtil.redirect = (targetURL, status)
266 def _changeRH(self, rh, params):
267 """Calls the specified RH after processing this one"""
268 self._responseUtil.call = lambda: rh().process(params)
270 def _checkHttpsRedirect(self):
271 """If HTTPS must be used but it is not, redirect!"""
272 if self.use_https() and not request.is_secure:
273 self._redirect(self.getRequestURL(secure=True))
274 return True
275 else:
276 return False
278 def _normaliseListParam(self, param):
279 if not isinstance(param, list):
280 return [param]
281 return param
283 def _processError(self, e):
284 raise
286 def _legacy_check(self):
288 This method can be overridden to check if you are dealing with
289 legacy data not supported by the RH. It is called before
290 `checkParams` and should raise an exception if necessary.
293 def normalize_url(self):
294 """Performs URL normalization.
296 This uses the :attr:`normalize_url_spec` to check if the URL
297 params are what they should be and redirects or fails depending
298 on the HTTP method used if it's not the case.
300 :return: ``None`` or a redirect response
302 if not self.normalize_url_spec or not any(self.normalize_url_spec.itervalues()):
303 return
304 spec = {
305 'args': self.normalize_url_spec.get('args', {}),
306 'locators': self.normalize_url_spec.get('locators', set()),
307 'preserved_args': self.normalize_url_spec.get('preserved_args', set()),
309 # Initialize the new view args with preserved arguments (since those would be lost otherwise)
310 new_view_args = {k: v for k, v in request.view_args.iteritems() if k in spec['preserved_args']}
311 # Retrieve the expected values for all simple arguments (if they are currently present)
312 for key, getter in spec['args'].iteritems():
313 if key in request.view_args:
314 new_view_args[key] = getter(self)
315 # Retrieve the expected values from locators
316 for getter in spec['locators']:
317 value = getter(self)
318 try:
319 expected = value.locator
320 except AttributeError:
321 try:
322 expected = value.getLocator()
323 except AttributeError:
324 raise AttributeError("'{}' object has neither 'locator' nor 'getLocator'".format(type(value)))
325 new_view_args.update(expected)
326 # Get all default values provided by the url map for the endpoint
327 defaults = set(itertools.chain.from_iterable(r.defaults
328 for r in current_app.url_map.iter_rules(request.endpoint)
329 if r.defaults))
330 provided = {k: v for k, v in request.view_args.iteritems() if k not in defaults}
331 if new_view_args != provided:
332 if request.method in {'GET', 'HEAD'}:
333 return redirect(url_for(request.endpoint, **dict(request.args.to_dict(), **new_view_args)))
334 else:
335 raise NotFound('The URL contains invalid data. Please go to the previous page and refresh it.')
337 def _checkParams(self, params):
338 """This method is called before _checkProtection and is a good place
339 to assign variables from request params to member variables.
341 Note that in any new code the params argument SHOULD be IGNORED.
342 Use the following objects provided by Flask instead:
343 from flask import request
344 request.view_args (URL route params)
345 request.args (GET params (from the query string))
346 request.form (POST params)
347 request.values (GET+POST params - use only if ABSOLUTELY NECESSARY)
349 If you only want to run some code for GET or POST requests, you can create
350 a method named e.g. _checkParams_POST which will be executed AFTER this one.
351 The method is called without any arguments (except self).
353 pass
355 def _process(self):
356 """The default process method dispatches to a method containing
357 the HTTP verb used for the current request, e.g. _process_POST.
358 When implementing this please consider that you most likely want/need
359 only GET and POST - the other verbs are not supported everywhere!
361 method = getattr(self, '_process_' + request.method, None)
362 if method is None:
363 valid_methods = [m for m in HTTP_VERBS if hasattr(self, '_process_' + m)]
364 raise MethodNotAllowed(valid_methods)
365 return method()
367 def _checkCSRF(self):
368 token = request.headers.get('X-CSRF-Token', request.form.get('csrf_token'))
369 if self.CSRF_ENABLED and request.method != 'GET' and token != session.csrf_token:
370 msg = _(u"It looks like there was a problem with your current session. Please use your browser's back "
371 u"button, reload the page and try again.")
372 raise BadRequest(msg)
373 # legacy csrf check (referer-based):
374 # Check referer for POST requests. We do it here so we can properly use indico's error handling
375 if Config.getInstance().getCSRFLevel() < 3 or request.method != 'POST':
376 return
377 referer = request.referrer
378 # allow empty - otherwise we might lock out paranoid users blocking referers
379 if not referer:
380 return
381 # valid http referer
382 if referer.startswith(Config.getInstance().getBaseURL()):
383 return
384 # valid https referer - if https is enabled
385 base_secure = Config.getInstance().getBaseSecureURL()
386 if base_secure and referer.startswith(base_secure):
387 return
388 raise BadRefererError('This operation is not allowed from an external referer.')
390 @jsonify_error
391 def _processGeneralError(self, e):
392 """Treats general errors occured during the process of a RH."""
394 if Config.getInstance().getPropagateAllExceptions():
395 raise
396 return errors.WPGenericError(self).display()
398 @jsonify_error(status=500, logging_level='exception')
399 def _processUnexpectedError(self, e):
400 """Unexpected errors"""
402 self._responseUtil.redirect = None
403 if Config.getInstance().getEmbeddedWebserver() or Config.getInstance().getPropagateAllExceptions():
404 raise
405 return errors.WPUnexpectedError(self).display()
407 @jsonify_error
408 def _processHostnameResolveError(self, e):
409 """Unexpected errors"""
411 return errors.WPHostnameResolveError(self).display()
413 @jsonify_error(status=403)
414 def _processForbidden(self, e):
415 message = _("Access Denied")
416 if e.description == Forbidden.description:
417 explanation = _("You are not allowed to access this page.")
418 else:
419 explanation = e.description
420 return WErrorWSGI((message, explanation)).getHTML()
422 @jsonify_error(status=400)
423 def _processBadRequest(self, e):
424 message = _("Bad Request")
425 return WErrorWSGI((message, e.description)).getHTML()
427 @jsonify_error(status=400)
428 def _processBadData(self, e):
429 message = _("Invalid or expired token")
430 return WErrorWSGI((message, e.message)).getHTML()
432 @jsonify_error(status=403)
433 def _processAccessError(self, e):
434 """Treats access errors occured during the process of a RH."""
435 return errors.WPAccessError(self).display()
437 @jsonify_error
438 def _processKeyAccessError(self, e):
439 """Treats access errors occured during the process of a RH."""
441 # We are going to redirect to the page asking for access key
442 # and so it must be https if there is a BaseSecureURL. And that's
443 # why we set _tohttps to True.
444 self._tohttps = True
445 if self._checkHttpsRedirect():
446 return
447 return errors.WPKeyAccessError(self).display()
449 @jsonify_error
450 def _processModificationError(self, e):
451 """Handles modification errors occured during the process of a RH."""
452 # Redirect to HTTPS in case the user is logged in
453 self._tohttps = True
454 if self._checkHttpsRedirect():
455 return
456 return errors.WPModificationError(self).display()
458 @jsonify_error(status=400)
459 def _processBadRequestKeyError(self, e):
460 """Request lacks a necessary key for processing"""
461 msg = _('Required argument missing: %s') % e.message
462 return errors.WPFormValuesError(self, msg).display()
464 @jsonify_error
465 def _processConferenceClosedError(self, e):
466 """Treats access to modification pages for conferences when they are closed."""
468 return WPConferenceModificationClosed(self, e._conf).display()
470 @jsonify_error
471 def _processTimingError(self, e):
472 """Treats timing errors occured during the process of a RH."""
474 return errors.WPTimingError(self, e).display()
476 @jsonify_error
477 def _processNoReportError(self, e):
478 """Process errors without reporting"""
480 return errors.WPNoReportError(self, e).display()
482 @jsonify_error(status=404)
483 def _processNotFoundError(self, e):
484 if isinstance(e, NotFound):
485 message = _("Page not found") # that's a bit nicer than "404: Not Found"
486 if e.description == NotFound.description:
487 explanation = _("The page you are looking for doesn't exist.")
488 else:
489 explanation = e.description
490 else:
491 message = e.getMessage()
492 explanation = e.getExplanation()
493 return WErrorWSGI((message, explanation)).getHTML()
495 @jsonify_error
496 def _processParentTimingError(self, e):
497 """Treats timing errors occured during the process of a RH."""
499 return errors.WPParentTimingError(self, e).display()
501 @jsonify_error
502 def _processEntryTimingError(self, e):
503 """Treats timing errors occured during the process of a RH."""
505 return errors.WPEntryTimingError(self, e).display()
507 @jsonify_error
508 def _processFormValuesError(self, e):
509 """Treats user input related errors occured during the process of a RH."""
511 return errors.WPFormValuesError(self, e).display()
513 @jsonify_error
514 def _processLaTeXError(self, e):
515 """Treats access errors occured during the process of a RH."""
517 return errors.WPLaTeXError(self, e).display()
519 @jsonify_error
520 def _processRestrictedHTML(self, e):
522 return errors.WPRestrictedHTML(self, escape(str(e))).display()
524 @jsonify_error
525 def _processHtmlScriptError(self, e):
526 """ TODO """
527 return errors.WPHtmlScriptError(self, escape(str(e))).display()
529 @jsonify_error
530 def _processHtmlForbiddenTag(self, e):
531 """ TODO """
533 return errors.WPRestrictedHTML(self, escape(str(e))).display()
535 def _process_retry_setup(self):
536 # clear the fossile cache at the start of each request
537 fossilize.clearCache()
538 # clear after-commit queue
539 flush_after_commit_queue(False)
540 # delete all queued emails
541 GenericMailer.flushQueue(False)
542 # clear the existing redis pipeline
543 if self._redisPipeline:
544 self._redisPipeline.reset()
546 def _process_retry_auth_check(self, params):
547 # keep a link to the web session in the access wrapper
548 # this is used for checking access/modification key existence
549 # in the user session
550 self._setSessionUser()
551 if self._getAuth():
552 if self._getUser():
553 Logger.get('requestHandler').info('Request %s identified with user %s (%s)' % (
554 request, self._getUser().getFullName(), self._getUser().getId()))
555 if not self._tohttps and Config.getInstance().getAuthenticatedEnforceSecure():
556 self._tohttps = True
557 if self._checkHttpsRedirect():
558 return self._responseUtil.make_redirect()
560 self._checkCSRF()
561 self._reqParams = copy.copy(params)
563 def _process_retry_do(self, profile):
564 profile_name, res = '', ''
565 try:
566 self._legacy_check()
568 # old code gets parameters from call
569 # new code utilizes of flask.request
570 if len(inspect.getargspec(self._checkParams).args) < 2:
571 cp_result = self._checkParams()
572 else:
573 cp_result = self._checkParams(self._reqParams)
575 if isinstance(cp_result, (current_app.response_class, Response)):
576 return '', cp_result
578 func = getattr(self, '_checkParams_' + request.method, None)
579 if func:
580 cp_result = func()
581 if isinstance(cp_result, (current_app.response_class, Response)):
582 return '', cp_result
584 except NoResultFound: # sqlalchemy .one() not finding anything
585 raise NotFoundError(_('The specified item could not be found.'), title=_('Item not found'))
587 rv = self.normalize_url()
588 if rv is not None:
589 return '', rv
591 self._checkProtection()
592 func = getattr(self, '_checkProtection_' + request.method, None)
593 if func:
594 func()
596 security.Sanitization.sanitizationCheck(self._target,
597 self._reqParams,
598 self._aw,
599 self._doNotSanitizeFields)
601 if self._doProcess:
602 if profile:
603 profile_name = os.path.join(Config.getInstance().getTempDir(), 'stone{}.prof'.format(random.random()))
604 result = [None]
605 profiler.runctx('result[0] = self._process()', globals(), locals(), profile_name)
606 res = result[0]
607 else:
608 res = self._process()
609 return profile_name, res
611 def _process_retry(self, params, retry, profile, forced_conflicts):
612 self._process_retry_setup()
613 self._process_retry_auth_check(params)
614 DBMgr.getInstance().sync()
615 return self._process_retry_do(profile)
617 def _process_success(self):
618 Logger.get('requestHandler').info('Request {} successful'.format(request))
619 # request is succesfull, now, doing tasks that must be done only once
620 try:
621 flush_after_commit_queue(True)
622 GenericMailer.flushQueue(True) # send emails
623 self._deleteTempFiles()
624 except:
625 Logger.get('mail').exception('Mail sending operation failed')
626 # execute redis pipeline if we have one
627 if self._redisPipeline:
628 try:
629 self._redisPipeline.execute()
630 except RedisError:
631 Logger.get('redis').exception('Could not execute pipeline')
633 def process(self, params):
634 if request.method not in HTTP_VERBS:
635 # Just to be sure that we don't get some crappy http verb we don't expect
636 raise BadRequest
638 cfg = Config.getInstance()
639 forced_conflicts, max_retries, profile = cfg.getForceConflicts(), cfg.getMaxRetries(), cfg.getProfile()
640 profile_name, res, textLog = '', '', []
642 self._startTime = datetime.now()
644 # clear the context
645 ContextManager.destroy()
646 ContextManager.set('currentRH', self)
647 g.rh = self
649 #redirect to https if necessary
650 if self._checkHttpsRedirect():
651 return self._responseUtil.make_redirect()
653 DBMgr.getInstance().startRequest()
654 textLog.append("%s : Database request started" % (datetime.now() - self._startTime))
655 Logger.get('requestHandler').info('[pid=%s] Request %s started' % (
656 os.getpid(), request))
658 try:
659 for i, retry in enumerate(transaction.attempts(max_retries)):
660 with retry:
661 if i > 0:
662 signals.before_retry.send()
664 try:
665 Logger.get('requestHandler').info('\t[pid=%s] from host %s' % (os.getpid(), request.remote_addr))
666 profile_name, res = self._process_retry(params, i, profile, forced_conflicts)
667 signals.after_process.send()
668 if i < forced_conflicts: # raise conflict error if enabled to easily handle conflict error case
669 raise ConflictError
670 transaction.commit()
671 DBMgr.getInstance().endRequest(commit=False)
672 break
673 except (ConflictError, POSKeyError):
674 transaction.abort()
675 import traceback
676 # only log conflict if it wasn't forced
677 if i >= forced_conflicts:
678 Logger.get('requestHandler').warning('Conflict in Database! (Request %s)\n%s' % (request, traceback.format_exc()))
679 except ClientDisconnected:
680 transaction.abort()
681 Logger.get('requestHandler').warning('Client Disconnected! (Request {})'.format(request))
682 time.sleep(i)
683 self._process_success()
684 except Exception as e:
685 transaction.abort()
686 res = self._getMethodByExceptionName(e)(e)
688 totalTime = (datetime.now() - self._startTime)
689 textLog.append('{} : Request ended'.format(totalTime))
691 # log request timing
692 if profile and totalTime > timedelta(0, 1) and os.path.isfile(profile_name):
693 rep = Config.getInstance().getTempDir()
694 stats = pstats.Stats(profile_name)
695 stats.strip_dirs()
696 stats.sort_stats('cumulative', 'time', 'calls')
697 stats.dump_stats(os.path.join(rep, 'IndicoRequestProfile.log'))
698 output = StringIO.StringIO()
699 sys.stdout = output
700 stats.print_stats(100)
701 sys.stdout = sys.__stdout__
702 s = output.getvalue()
703 f = file(os.path.join(rep, 'IndicoRequest.log'), 'a+')
704 f.write('--------------------------------\n')
705 f.write('URL : {}\n'.format(request.url))
706 f.write('{} : start request\n'.format(self._startTime))
707 f.write('params:{}'.format(params))
708 f.write('\n'.join(textLog))
709 f.write('\n')
710 f.write('retried : {}\n'.format(10-retry))
711 f.write(s)
712 f.write('--------------------------------\n\n')
713 f.close()
714 if profile and profile_name and os.path.exists(profile_name):
715 os.remove(profile_name)
717 if self._responseUtil.call:
718 return self._responseUtil.make_call()
720 # In case of no process needed, we should return empty string to avoid erroneous output
721 # specially with getVars breaking the JS files.
722 if not self._doProcess or res is None:
723 return self._responseUtil.make_empty()
725 return self._responseUtil.make_response(res)
727 def _getMethodByExceptionName(self, e):
728 exception_name = {
729 'NotFound': 'NotFoundError',
730 'MaKaCError': 'GeneralError',
731 'IndicoError': 'GeneralError',
732 'ValueError': 'UnexpectedError',
733 'Exception': 'UnexpectedError',
734 'AccessControlError': 'AccessError'
735 }.get(type(e).__name__, type(e).__name__)
736 if isinstance(e, BadData): # we also want its subclasses
737 exception_name = 'BadData'
738 return getattr(self, '_process{}'.format(exception_name), self._processUnexpectedError)
740 def _deleteTempFiles(self):
741 if len(self._tempFilesToDelete) > 0:
742 for f in self._tempFilesToDelete:
743 if f is not None:
744 os.remove(f)
746 relativeURL = None
749 class RHSimple(RH):
750 """A simple RH that calls a function to build the response
752 The main purpose of this RH is to allow external library to
753 display something within the Indico layout (which requires a
754 RH / a ZODB connection in most cases).
756 The preferred way to use this class is by using the
757 `RHSimple.wrap_function` decorator.
759 :param func: A function returning HTML
761 def __init__(self, func):
762 RH.__init__(self)
763 self.func = func
765 def _process(self):
766 rv = self.func()
767 return rv
769 @classmethod
770 def wrap_function(cls, func):
771 """Decorates a function to run within the RH's framework"""
772 @wraps(func)
773 def wrapper(*args, **kwargs):
774 return cls(partial(func, *args, **kwargs)).process({})
776 return wrapper
779 class RHProtected(RH):
781 def _getLoginURL(self):
782 return url_for_login(request.relative_url)
784 def _checkSessionUser(self):
785 if self._getUser() is None:
786 if request.headers.get("Content-Type", "text/html").find("application/json") != -1:
787 raise NotLoggedError("You are currently not authenticated. Please log in again.")
788 else:
789 self._redirect(self._getLoginURL())
790 self._doProcess = False
792 def _checkProtection(self):
793 self._checkSessionUser()
796 class RHDisplayBaseProtected(RHProtected):
798 def _checkProtection(self):
799 if not self._target.canAccess( self.getAW() ):
800 from MaKaC.conference import Link, LocalFile, Category
801 if isinstance(self._target,Link) or isinstance(self._target,LocalFile):
802 target = self._target.getOwner()
803 else:
804 target = self._target
805 if not isinstance(self._target, Category) and target.isProtected():
806 if target.getAccessKey() != "" or target.getConference() and \
807 target.getConference().getAccessKey() != "":
808 raise KeyAccessError()
809 elif target.getModifKey() != "" or target.getConference() and \
810 target.getConference().getModifKey() != "":
811 raise ModificationError()
812 if self._getUser() is None:
813 self._checkSessionUser()
814 else:
815 raise AccessError()
818 class RHModificationBaseProtected(RHProtected):
820 _allowClosed = False
822 def _checkProtection(self):
823 if not self._target.canModify( self.getAW() ):
824 if self._target.getModifKey() != "":
825 raise ModificationError()
826 if self._getUser() is None:
827 self._checkSessionUser()
828 else:
829 raise ModificationError()
830 if hasattr(self._target, "getConference") and not self._allowClosed:
831 if self._target.getConference().isClosed():
832 raise ConferenceClosedError(self._target.getConference())