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
26 from datetime
import datetime
, timedelta
27 from functools
import wraps
, partial
28 from urlparse
import urljoin
29 from xml
.sax
.saxutils
import escape
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 (
48 ConferenceClosedError
,
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():
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.
89 Returns True if current user is a user or has either a modification key in their session.
91 return session
.get('modifKeys') or self
._getUser
()
95 Returns the access wrapper related to this session/user
99 accessWrapper
= property(getAW
)
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 ''
119 return urljoin(Config
.getInstance().getBaseSecureURL(), request
.path
) + query_string
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
):
132 def _getTruncatedParams(self
):
133 """Truncates 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)
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
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).
161 _uh - (URLHandler) Associated URLHandler which points to the
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
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
201 normalize_url_spec
= {
204 'preserved_args': set()
208 self
._responseUtil
= ResponseUtil()
209 self
._requestStarted
= False
210 self
._aw
= AccessWrapper() # Fill in the aw instance with the current information
213 self
._startTime
= 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
223 # Methods =============================================================
229 return self
._isMobile
231 def _setSessionUser(self
):
232 self
._aw
.setUser(session
.avatar
)
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"
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']
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))
278 def _normaliseListParam(self
, param
):
279 if not isinstance(param
, list):
283 def _processError(self
, e
):
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()):
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']:
319 expected
= value
.locator
320 except AttributeError:
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
)
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
)))
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).
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)
363 valid_methods
= [m
for m
in HTTP_VERBS
if hasattr(self
, '_process_' + m
)]
364 raise MethodNotAllowed(valid_methods
)
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':
377 referer
= request
.referrer
378 # allow empty - otherwise we might lock out paranoid users blocking referers
382 if referer
.startswith(Config
.getInstance().getBaseURL()):
384 # valid https referer - if https is enabled
385 base_secure
= Config
.getInstance().getBaseSecureURL()
386 if base_secure
and referer
.startswith(base_secure
):
388 raise BadRefererError('This operation is not allowed from an external referer.')
391 def _processGeneralError(self
, e
):
392 """Treats general errors occured during the process of a RH."""
394 if Config
.getInstance().getPropagateAllExceptions():
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():
405 return errors
.WPUnexpectedError(self
).display()
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.")
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()
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.
445 if self
._checkHttpsRedirect
():
447 return errors
.WPKeyAccessError(self
).display()
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
454 if self
._checkHttpsRedirect
():
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()
465 def _processConferenceClosedError(self
, e
):
466 """Treats access to modification pages for conferences when they are closed."""
468 return WPConferenceModificationClosed(self
, e
._conf
).display()
471 def _processTimingError(self
, e
):
472 """Treats timing errors occured during the process of a RH."""
474 return errors
.WPTimingError(self
, e
).display()
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.")
489 explanation
= e
.description
491 message
= e
.getMessage()
492 explanation
= e
.getExplanation()
493 return WErrorWSGI((message
, explanation
)).getHTML()
496 def _processParentTimingError(self
, e
):
497 """Treats timing errors occured during the process of a RH."""
499 return errors
.WPParentTimingError(self
, e
).display()
502 def _processEntryTimingError(self
, e
):
503 """Treats timing errors occured during the process of a RH."""
505 return errors
.WPEntryTimingError(self
, e
).display()
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()
514 def _processLaTeXError(self
, e
):
515 """Treats access errors occured during the process of a RH."""
517 return errors
.WPLaTeXError(self
, e
).display()
520 def _processRestrictedHTML(self
, e
):
522 return errors
.WPRestrictedHTML(self
, escape(str(e
))).display()
525 def _processHtmlScriptError(self
, e
):
527 return errors
.WPHtmlScriptError(self
, escape(str(e
))).display()
530 def _processHtmlForbiddenTag(self
, e
):
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
()
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():
557 if self
._checkHttpsRedirect
():
558 return self
._responseUtil
.make_redirect()
561 self
._reqParams
= copy
.copy(params
)
563 def _process_retry_do(self
, profile
):
564 profile_name
, res
= '', ''
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
()
573 cp_result
= self
._checkParams
(self
._reqParams
)
575 if isinstance(cp_result
, (current_app
.response_class
, Response
)):
578 func
= getattr(self
, '_checkParams_' + request
.method
, None)
581 if isinstance(cp_result
, (current_app
.response_class
, Response
)):
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()
591 self
._checkProtection
()
592 func
= getattr(self
, '_checkProtection_' + request
.method
, None)
596 security
.Sanitization
.sanitizationCheck(self
._target
,
599 self
._doNotSanitizeFields
)
603 profile_name
= os
.path
.join(Config
.getInstance().getTempDir(), 'stone{}.prof'.format(random
.random()))
605 profiler
.runctx('result[0] = self._process()', globals(), locals(), profile_name
)
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
621 flush_after_commit_queue(True)
622 GenericMailer
.flushQueue(True) # send emails
623 self
._deleteTempFiles
()
625 Logger
.get('mail').exception('Mail sending operation failed')
626 # execute redis pipeline if we have one
627 if self
._redisPipeline
:
629 self
._redisPipeline
.execute()
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
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()
645 ContextManager
.destroy()
646 ContextManager
.set('currentRH', 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
))
659 for i
, retry
in enumerate(transaction
.attempts(max_retries
)):
662 signals
.before_retry
.send()
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
671 DBMgr
.getInstance().endRequest(commit
=False)
673 except (ConflictError
, POSKeyError
):
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
:
681 Logger
.get('requestHandler').warning('Client Disconnected! (Request {})'.format(request
))
683 self
._process
_success
()
684 except Exception as e
:
686 res
= self
._getMethodByExceptionName
(e
)(e
)
688 totalTime
= (datetime
.now() - self
._startTime
)
689 textLog
.append('{} : Request ended'.format(totalTime
))
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
)
696 stats
.sort_stats('cumulative', 'time', 'calls')
697 stats
.dump_stats(os
.path
.join(rep
, 'IndicoRequestProfile.log'))
698 output
= StringIO
.StringIO()
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
))
710 f
.write('retried : {}\n'.format(10-retry
))
712 f
.write('--------------------------------\n\n')
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
):
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
:
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
):
770 def wrap_function(cls
, func
):
771 """Decorates a function to run within the RH's framework"""
773 def wrapper(*args
, **kwargs
):
774 return cls(partial(func
, *args
, **kwargs
)).process({})
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.")
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()
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
()
818 class RHModificationBaseProtected(RHProtected
):
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
()
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())