Disable HTML security checks for error reports
[cds-indico.git] / indico / MaKaC / services / implementation / base.py
blobce514a50ffc974ffe826c8e3b7dfc0748695c146
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/>.
18 import distutils
19 import os
20 import sys
21 import traceback
22 from datetime import datetime, date
24 from flask import request, session
25 from pytz import timezone
28 from MaKaC.conference import Category
29 from MaKaC import conference
30 from MaKaC.common.timezoneUtils import setAdjustedDate
31 from MaKaC.common import security
32 from MaKaC.common.utils import parseDateTime
34 from MaKaC.errors import MaKaCError, HtmlForbiddenTag, TimingError
35 from MaKaC.services.interface.rpc.common import ServiceError, ServiceAccessError, HTMLSecurityError, Warning,\
36 ResultWithWarning
38 from MaKaC.webinterface.rh.base import RequestHandlerBase
40 from MaKaC.accessControl import AccessWrapper
42 from MaKaC.i18n import _
43 from MaKaC.common.contextManager import ContextManager
44 import MaKaC.common.info as info
46 from indico.core.config import Config
47 from indico.util.string import unicode_struct_to_utf8
49 # base module for asynchronous server requests
52 class ExpectedParameterException(ServiceError):
53 """
54 Represents an exception that occurs when a type of parameter was expected
55 but another one was obtained
56 """
58 def __init__(self, paramName, expected, got):
59 ServiceError.__init__(self, "ERR-P2","'%s': Expected '%s', got instead '%s'" % (paramName, expected, got))
62 class EmptyParameterException(ServiceError):
63 """
64 Thrown when a parameter that should have a value is empty
65 """
66 def __init__(self, paramName=""):
67 ServiceError.__init__(self, "ERR-P3","Expected parameter '%s' is empty"%paramName)
70 class DateTimeParameterException(ServiceError):
71 """
72 Thrown when a parameter that should have a value is empty
73 """
74 def __init__(self, paramName, value):
75 ServiceError.__init__(self, "ERR-P4","Date/Time %s = '%s' is not valid " % (paramName, value))
78 class ParameterManager(object):
80 """
81 The ParameterManager makes parameter processing a bit easier, by providing
82 some default transformations
83 """
85 def __init__(self, paramList, allowEmpty=False, timezone=None):
86 self._paramList = paramList
87 self._allowEmpty = allowEmpty
88 self._timezone = timezone
90 def extract(self, paramName, pType=None, allowEmpty=None, defaultValue=None):
91 """
92 Extracts a parameter, given a parameter name, and optional type
93 """
95 # "global" policy applies if allowEmpty not set
96 if allowEmpty is None:
97 allowEmpty = self._allowEmpty
99 value = self._paramList.get(paramName)
100 if value is None:
101 if allowEmpty:
102 value = defaultValue
103 else:
104 raise EmptyParameterException(paramName)
106 if value is not None:
107 if pType == str:
108 value = str(value)
109 elif pType == int:
110 value = int(value)
111 elif pType == float:
112 value = float(value)
113 elif pType == bool:
114 if not type(value) == bool:
115 try:
116 value = distutils.util.strtobool(str(value))
117 except ValueError:
118 raise ExpectedParameterException(paramName, bool, type(value))
119 elif pType == dict:
120 if not type(value) == dict:
121 raise ExpectedParameterException(paramName, dict, type(value))
122 elif pType == list:
123 if not type(value) == list:
124 raise ExpectedParameterException(paramName, list, type(value))
125 elif pType == date:
126 # format will possibly be accomodated to different standards,
127 # in the future
128 value = datetime.strptime(value, '%Y/%m/%d').date()
129 elif pType == datetime:
130 # format will possibly be accomodated to different standards,
131 # in the future
132 try:
133 # both strings and objects are accepted
134 if isinstance(value, basestring):
135 naiveDate = datetime.strptime(value, '%Y/%m/%d %H:%M')
136 elif value:
137 naiveDate = datetime.strptime(value['date']+' '+value['time'][:5], '%Y/%m/%d %H:%M')
138 else:
139 naiveDate = None
140 except ValueError:
141 raise DateTimeParameterException(paramName, value)
143 if self._timezone and naiveDate:
144 value = timezone(self._timezone).localize(naiveDate).astimezone(timezone('utc'))
145 else:
146 value = naiveDate
148 if (value is None or value == "") and not allowEmpty:
149 EmptyParameterException(paramName)
151 return value
153 def setTimezone(self, tz):
154 self._timezone = tz
157 class ServiceBase(RequestHandlerBase):
159 The ServiceBase class is the basic class for services.
162 UNICODE_PARAMS = False
163 CHECK_HTML = True
165 def __init__(self, params):
166 if not self.UNICODE_PARAMS:
167 params = unicode_struct_to_utf8(params)
168 self._reqParams = self._params = params
169 self._requestStarted = False
170 # Fill in the aw instance with the current information
171 self._aw = AccessWrapper()
172 self._aw.setUser(session.avatar)
173 self._target = None
174 self._startTime = None
175 self._tohttps = request.is_secure
176 self._endTime = None
177 self._doProcess = True #Flag which indicates whether the RH process
178 # must be carried out; this is useful for
179 # the checkProtection methods
180 self._tempFilesToDelete = []
181 self._redisPipeline = None
183 # Methods =============================================================
185 def _checkParams(self):
187 Checks the request parameters (normally overloaded)
189 pass
191 def _checkProtection( self ):
193 Checks protection when accessing resources (normally overloaded)
195 pass
197 def _processError(self):
199 Treats errors occured during the process of a RH, returning an error string.
200 @param e: the exception
201 @type e: An Exception-derived type
204 trace = traceback.format_exception(*sys.exc_info())
206 return ''.join(trace)
208 def _deleteTempFiles( self ):
209 if len(self._tempFilesToDelete) > 0:
210 for file in self._tempFilesToDelete:
211 os.remove(file)
213 def process(self):
215 Processes the request, analyzing the parameters, and feeding them to the
216 _getAnswer() method (implemented by derived classes)
219 ContextManager.set('currentRH', self)
221 self._checkParams()
222 self._checkProtection()
224 if self.CHECK_HTML:
225 try:
226 security.Sanitization.sanitizationCheck(self._target, self._params, self._aw, ['requestInfo'])
227 except HtmlForbiddenTag as e:
228 raise HTMLSecurityError('ERR-X0', 'HTML Security problem. {}'.format(e))
230 if self._doProcess:
231 if Config.getInstance().getProfile():
232 import profile, pstats, random
233 proffilename = os.path.join(Config.getInstance().getTempDir(), "service%s.prof" % random.random())
234 result = [None]
235 profile.runctx("result[0] = self._getAnswer()", globals(), locals(), proffilename)
236 answer = result[0]
237 rep = Config.getInstance().getTempDir()
238 stats = pstats.Stats(proffilename)
239 stats.strip_dirs()
240 stats.sort_stats('cumulative', 'time', 'calls')
241 stats.dump_stats(os.path.join(rep, "IndicoServiceRequestProfile.log"))
242 os.remove(proffilename)
243 else:
244 answer = self._getAnswer()
245 self._deleteTempFiles()
247 return answer
249 def _getAnswer(self):
251 To be overloaded. It should contain the code that does the actual
252 business logic and returns a result (python JSON-serializable object).
253 If this method is not overloaded, an exception will occur.
254 If you don't want to return an answer, you should still implement this method with 'pass'.
256 # This exception will happen if the _getAnswer method is not implemented in a derived class
257 raise MaKaCError("No answer was returned")
260 class ProtectedService(ServiceBase):
262 ProtectedService is a parent class for ProtectedDisplayService and ProtectedModificationService
265 def _checkSessionUser(self):
267 Checks that the current user exists (is authenticated)
270 if self._getUser() == None:
271 self._doProcess = False
272 raise ServiceAccessError("You are currently not authenticated. Please log in again.")
275 class ProtectedDisplayService(ProtectedService):
277 A ProtectedDisplayService can only be accessed by users that
278 are authorized to "see" the target resource
281 def _checkProtection( self ):
283 Overloads ProtectedService._checkProtection, assuring that
284 the user is authorized to view the target resource
286 if not self._target.canAccess( self.getAW() ):
288 from MaKaC.conference import Link, LocalFile
290 # in some cases, the target does not directly have an owner
291 if (isinstance(self._target, Link) or
292 isinstance(self._target, LocalFile)):
293 target = self._target.getOwner()
294 else:
295 target = self._target
296 if not isinstance(target, Category):
297 if target.getAccessKey() != "" or target.getConference().getAccessKey() != "":
298 raise ServiceAccessError("You are currently not authenticated or cannot access this service. Please log in again if necessary.")
299 if self._getUser() == None:
300 self._checkSessionUser()
301 else:
302 raise ServiceAccessError("You cannot access this service. Please log in again if necessary.")
305 class LoggedOnlyService(ProtectedService):
307 Only accessible to users who are logged in (access keys not allowed)
310 def _checkProtection( self ):
311 self._checkSessionUser()
314 class ProtectedModificationService(ProtectedService):
316 A ProtectedModificationService can only be accessed by users that
317 are authorized to modify the target resource
319 def _checkProtection( self ):
321 Overloads ProtectedService._checkProtection, so that it is
322 verified if the user has modification access to the resource
325 target = self._target
326 if (type(target) == conference.SessionSlot):
327 target = target.getSession()
329 if not target.canModify( self.getAW() ):
330 if target.getModifKey() != "":
331 raise ServiceAccessError("You don't have the rights to modify this object")
332 if self._getUser() == None:
333 self._checkSessionUser()
334 else:
335 raise ServiceAccessError("You don't have the rights to modify this object")
336 if hasattr(self._target, "getConference") and hasattr(self._target, "isClosed"):
337 if target.getConference().isClosed():
338 raise ServiceAccessError("Conference %s is closed"%target.getConference().getId())
340 class AdminService(LoggedOnlyService):
342 A AdminService can only be accessed by administrators
344 def _checkProtection(self):
345 LoggedOnlyService._checkProtection(self)
346 if not session.user.is_admin:
347 raise ServiceAccessError(_("Only administrators can perform this operation"))
349 class TextModificationBase:
351 Base class for text field modification
354 def _getAnswer( self ):
355 """ Calls _handleGet() or _handleSet() on the derived classes, in order to make it happen. Provides
356 them with self._value.
358 When calling _handleGet(), it will return the value to return.
359 When calling _handleSet(), it will return:
360 -either self._value if there were no problems
361 -either a FieldModificationWarning object (pickled) if there are warnings to give to the user
364 # fetch the 'value' parameter (default for text)
365 if self._params.has_key('value'):
366 self._value = self._params['value']
367 else:
368 # None if not passed
369 self._value = None
371 if self._value == None:
372 return self._handleGet()
373 else:
374 setResult = self._handleSet()
375 if isinstance(setResult, Warning):
376 return ResultWithWarning(self._value, setResult).fossilize()
377 else:
378 return self._value
380 class HTMLModificationBase:
382 Base class for HTML field modification
384 def _getAnswer( self ):
386 Calls _handle() on the derived classes, in order to make it happen. Provides
387 them with self._value.
390 if self._params.has_key('value'):
391 self._value = self._params['value']
392 else:
393 self._value = None
395 if self._value == None:
396 return self._handleGet()
397 else:
398 self._handleSet()
400 return self._value
403 class DateTimeModificationBase( TextModificationBase ):
404 """ Date and time modification base class
405 Its _handleSet method is called by TextModificationBase's _getAnswer method.
406 DateTimeModificationBase's _handletSet method will call the _setParam method
407 from the classes that inherits from DateTimeModificationBase.
408 _handleSet will return whatever _setParam returns (usually None if there were no problems,
409 or a FieldModificationWarning object with information about a problem / warning to give to the user)
411 def _handleSet(self):
412 try:
413 naiveDate = parseDateTime(self._value)
414 except ValueError:
415 raise ServiceError("ERR-E2",
416 "Date/time is not in the correct format")
418 try:
419 self._pTime = setAdjustedDate(naiveDate, self._conf)
420 return self._setParam()
421 except TimingError,e:
422 raise ServiceError("ERR-E2", e.getMessage())
424 class ListModificationBase:
425 """ Base class for a list modification.
426 The class that inherits from this must have:
427 -a _handleGet() method that returns a list.
428 -a _handleSet() method which can use self._value to process the input. self._value will be a list.
431 def _getAnswer(self):
432 if self._params.has_key('value'):
433 pm = ParameterManager(self._params)
434 self._value = pm.extract("value", pType=list, allowEmpty=True)
435 else:
436 self._value = None
438 if self._value == None:
439 return self._handleGet()
440 else:
441 self._handleSet()
443 return self._value
445 class TwoListModificationBase:
446 """ Base class for two lists modification.
447 The class that inherits from this must have:
448 -a _handleGet() method that returns a list, given self._destination
449 -a _handleSet() method which can use self._value and self._destination to process the input.
450 self._value will be a list. self._destination will be 'left' or 'right'
453 def _getAnswer(self):
454 self._destination = self._params.get('destination', None)
455 if self._destination == None or (self._destination != 'right' and self._destination != 'left'):
456 #TODO: add this error to the wiki
457 raise ServiceError("ERR-E4", 'Destination list not set to "right" or "left"')
459 if self._params.has_key('value'):
460 pm = ParameterManager(self._params)
461 self._value = pm.extract("value", pType=list, allowEmpty=False)
462 else:
463 self._value = None
465 if self._value == None:
466 return self._handleGet()
467 else:
468 self._handleSet()
470 return self._value
473 class ExportToICalBase(object):
475 def _checkParams(self):
476 user = self._getUser()
477 if not user:
478 raise ServiceAccessError("User is not logged in!")
479 apiKey = user.api_key
480 if not apiKey:
481 raise ServiceAccessError("User has no API key!")
482 elif apiKey.is_blocked:
483 raise ServiceAccessError("This API key is blocked!")
484 self._apiKey = apiKey