Show warning on POST to RHs without CSRF check
[cds-indico.git] / indico / MaKaC / webinterface / rh / base.py
blob57798a35c6708e2ee93b2d579b808b44f02b0f2f
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 import warnings
27 from datetime import datetime, timedelta
28 from functools import wraps, partial
29 from urlparse import urljoin
30 from xml.sax.saxutils import escape
32 import transaction
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 (
47 AccessError,
48 BadRefererError,
49 ConferenceClosedError,
50 KeyAccessError,
51 MaKaCError,
52 ModificationError,
53 NotLoggedError,
54 NotFoundError)
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():
77 _uh = None
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.
85 """
86 pass
88 def _getAuth(self):
89 """
90 Returns True if current user is a user or has either a modification key in their session.
91 """
92 return session.get('modifKeys') or self._getUser()
94 def getAW(self):
95 """
96 Returns the access wrapper related to this session/user
97 """
98 return self._aw
100 accessWrapper = property(getAW)
102 def _getUser(self):
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 ''
119 if secure:
120 return urljoin(Config.getInstance().getBaseSecureURL(), request.path) + query_string
121 else:
122 return request.url
124 def use_https(self):
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):
131 return self._params
133 def _getTruncatedParams(self):
134 """Truncates params"""
135 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)
141 else:
142 params[key] = value
143 return params
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
156 interface).
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).
161 Attributes:
162 _uh - (URLHandler) Associated URLHandler which points to the
163 current rh.
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
172 the rh.
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
201 #: checks.
202 normalize_url_spec = {
203 'args': {},
204 'locators': set(),
205 'preserved_args': set()
208 def __init__(self):
209 self._responseUtil = ResponseUtil()
210 self._requestStarted = False
211 self._aw = AccessWrapper() # Fill in the aw instance with the current information
212 self._target = None
213 self._reqParams = {}
214 self._startTime = None
215 self._endTime = 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
222 # needed
224 # Methods =============================================================
226 def getTarget(self):
227 return self._target
229 def isMobile(self):
230 return self._isMobile
232 def _setSessionUser(self):
233 self._aw.setUser(session.avatar)
235 @property
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"
253 else:
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']
261 else:
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))
275 return True
276 else:
277 return False
279 def _normaliseListParam(self, param):
280 if not isinstance(param, list):
281 return [param]
282 return param
284 def _processError(self, e):
285 raise
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()):
304 return
305 spec = {
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']:
318 value = getter(self)
319 try:
320 expected = value.locator
321 except AttributeError:
322 try:
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)
330 if r.defaults))
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)))
335 else:
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).
354 pass
356 def _process(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)
363 if method is None:
364 valid_methods = [m for m in HTTP_VERBS if hasattr(self, '_process_' + m)]
365 raise MethodNotAllowed(valid_methods)
366 return method()
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':
384 return
385 referer = request.referrer
386 # allow empty - otherwise we might lock out paranoid users blocking referers
387 if not referer:
388 return
389 # valid http referer
390 if referer.startswith(Config.getInstance().getBaseURL()):
391 return
392 # valid https referer - if https is enabled
393 base_secure = Config.getInstance().getBaseSecureURL()
394 if base_secure and referer.startswith(base_secure):
395 return
396 raise BadRefererError('This operation is not allowed from an external referer.')
398 @jsonify_error
399 def _processGeneralError(self, e):
400 """Treats general errors occured during the process of a RH."""
402 if Config.getInstance().getPropagateAllExceptions():
403 raise
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():
412 raise
413 return errors.WPUnexpectedError(self).display()
415 @jsonify_error
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.")
426 else:
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()
445 @jsonify_error
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.
452 self._tohttps = True
453 if self._checkHttpsRedirect():
454 return
455 return errors.WPKeyAccessError(self).display()
457 @jsonify_error
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
461 self._tohttps = True
462 if self._checkHttpsRedirect():
463 return
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()
472 @jsonify_error
473 def _processConferenceClosedError(self, e):
474 """Treats access to modification pages for conferences when they are closed."""
476 return WPConferenceModificationClosed(self, e._conf).display()
478 @jsonify_error
479 def _processTimingError(self, e):
480 """Treats timing errors occured during the process of a RH."""
482 return errors.WPTimingError(self, e).display()
484 @jsonify_error
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.")
496 else:
497 explanation = e.description
498 else:
499 message = e.getMessage()
500 explanation = e.getExplanation()
501 return WErrorWSGI((message, explanation)).getHTML()
503 @jsonify_error
504 def _processParentTimingError(self, e):
505 """Treats timing errors occured during the process of a RH."""
507 return errors.WPParentTimingError(self, e).display()
509 @jsonify_error
510 def _processEntryTimingError(self, e):
511 """Treats timing errors occured during the process of a RH."""
513 return errors.WPEntryTimingError(self, e).display()
515 @jsonify_error
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()
521 @jsonify_error
522 def _processLaTeXError(self, e):
523 """Treats access errors occured during the process of a RH."""
525 return errors.WPLaTeXError(self, e).display()
527 @jsonify_error
528 def _processRestrictedHTML(self, e):
530 return errors.WPRestrictedHTML(self, escape(str(e))).display()
532 @jsonify_error
533 def _processHtmlScriptError(self, e):
534 """ TODO """
535 return errors.WPHtmlScriptError(self, escape(str(e))).display()
537 @jsonify_error
538 def _processHtmlForbiddenTag(self, e):
539 """ TODO """
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()
559 if self._getAuth():
560 if self._getUser():
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():
564 self._tohttps = True
565 if self._checkHttpsRedirect():
566 return self._responseUtil.make_redirect()
568 self._checkCSRF()
569 self._reqParams = copy.copy(params)
571 def _process_retry_do(self, profile):
572 profile_name, res = '', ''
573 try:
574 self._legacy_check()
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()
580 else:
581 cp_result = self._checkParams(self._reqParams)
583 if isinstance(cp_result, (current_app.response_class, Response)):
584 return '', cp_result
586 func = getattr(self, '_checkParams_' + request.method, None)
587 if func:
588 cp_result = func()
589 if isinstance(cp_result, (current_app.response_class, Response)):
590 return '', cp_result
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()
596 if rv is not None:
597 return '', rv
599 self._checkProtection()
600 func = getattr(self, '_checkProtection_' + request.method, None)
601 if func:
602 func()
604 security.Sanitization.sanitizationCheck(self._target,
605 self._reqParams,
606 self._aw,
607 self._doNotSanitizeFields)
609 if self._doProcess:
610 if profile:
611 profile_name = os.path.join(Config.getInstance().getTempDir(), 'stone{}.prof'.format(random.random()))
612 result = [None]
613 profiler.runctx('result[0] = self._process()', globals(), locals(), profile_name)
614 res = result[0]
615 else:
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
628 try:
629 flush_after_commit_queue(True)
630 GenericMailer.flushQueue(True) # send emails
631 self._deleteTempFiles()
632 except:
633 Logger.get('mail').exception('Mail sending operation failed')
634 # execute redis pipeline if we have one
635 if self._redisPipeline:
636 try:
637 self._redisPipeline.execute()
638 except RedisError:
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
644 raise BadRequest
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()
652 # clear the context
653 ContextManager.destroy()
654 ContextManager.set('currentRH', self)
655 g.rh = 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))
666 try:
667 for i, retry in enumerate(transaction.attempts(max_retries)):
668 with retry:
669 if i > 0:
670 signals.before_retry.send()
672 try:
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
677 raise ConflictError
678 transaction.commit()
679 DBMgr.getInstance().endRequest(commit=False)
680 break
681 except (ConflictError, POSKeyError):
682 transaction.abort()
683 import traceback
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:
688 transaction.abort()
689 Logger.get('requestHandler').warning('Client Disconnected! (Request {})'.format(request))
690 time.sleep(i)
691 self._process_success()
692 except Exception as e:
693 transaction.abort()
694 res = self._getMethodByExceptionName(e)(e)
696 totalTime = (datetime.now() - self._startTime)
697 textLog.append('{} : Request ended'.format(totalTime))
699 # log request timing
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)
703 stats.strip_dirs()
704 stats.sort_stats('cumulative', 'time', 'calls')
705 stats.dump_stats(os.path.join(rep, 'IndicoRequestProfile.log'))
706 output = StringIO.StringIO()
707 sys.stdout = output
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))
717 f.write('\n')
718 f.write('retried : {}\n'.format(10-retry))
719 f.write(s)
720 f.write('--------------------------------\n\n')
721 f.close()
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):
736 exception_name = {
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:
751 if f is not None:
752 os.remove(f)
754 relativeURL = None
757 class RHSimple(RH):
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):
770 RH.__init__(self)
771 self.func = func
773 def _process(self):
774 rv = self.func()
775 return rv
777 @classmethod
778 def wrap_function(cls, func):
779 """Decorates a function to run within the RH's framework"""
780 @wraps(func)
781 def wrapper(*args, **kwargs):
782 return cls(partial(func, *args, **kwargs)).process({})
784 return wrapper
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.")
796 else:
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()
811 else:
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()
822 else:
823 raise AccessError()
826 class RHModificationBaseProtected(RHProtected):
828 _allowClosed = False
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()
836 else:
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())