More verbose error when AJAX request to RH fails
[cds-indico.git] / indico / MaKaC / services / implementation / base.py
blob1bd828a5910d7e7728054dd2ac48b5aaf6808dec
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
164 def __init__(self, params):
165 if not self.UNICODE_PARAMS:
166 params = unicode_struct_to_utf8(params)
167 self._reqParams = self._params = params
168 self._requestStarted = False
169 # Fill in the aw instance with the current information
170 self._aw = AccessWrapper()
171 self._aw.setUser(session.avatar)
172 self._target = None
173 self._startTime = None
174 self._tohttps = request.is_secure
175 self._endTime = None
176 self._doProcess = True #Flag which indicates whether the RH process
177 # must be carried out; this is useful for
178 # the checkProtection methods
179 self._tempFilesToDelete = []
180 self._redisPipeline = None
182 # Methods =============================================================
184 def _checkParams(self):
186 Checks the request parameters (normally overloaded)
188 pass
190 def _checkProtection( self ):
192 Checks protection when accessing resources (normally overloaded)
194 pass
196 def _processError(self):
198 Treats errors occured during the process of a RH, returning an error string.
199 @param e: the exception
200 @type e: An Exception-derived type
203 trace = traceback.format_exception(*sys.exc_info())
205 return ''.join(trace)
207 def _deleteTempFiles( self ):
208 if len(self._tempFilesToDelete) > 0:
209 for file in self._tempFilesToDelete:
210 os.remove(file)
212 def process(self):
214 Processes the request, analyzing the parameters, and feeding them to the
215 _getAnswer() method (implemented by derived classes)
218 ContextManager.set('currentRH', self)
220 self._checkParams()
221 self._checkProtection()
223 try:
224 security.Sanitization.sanitizationCheck(self._target, self._params, self._aw, ['requestInfo'])
225 except HtmlForbiddenTag as e:
226 raise HTMLSecurityError('ERR-X0','HTML Security problem. %s ' % str(e))
228 if self._doProcess:
229 if Config.getInstance().getProfile():
230 import profile, pstats, random
231 proffilename = os.path.join(Config.getInstance().getTempDir(), "service%s.prof" % random.random())
232 result = [None]
233 profile.runctx("result[0] = self._getAnswer()", globals(), locals(), proffilename)
234 answer = result[0]
235 rep = Config.getInstance().getTempDir()
236 stats = pstats.Stats(proffilename)
237 stats.strip_dirs()
238 stats.sort_stats('cumulative', 'time', 'calls')
239 stats.dump_stats(os.path.join(rep, "IndicoServiceRequestProfile.log"))
240 os.remove(proffilename)
241 else:
242 answer = self._getAnswer()
243 self._deleteTempFiles()
245 return answer
247 def _getAnswer(self):
249 To be overloaded. It should contain the code that does the actual
250 business logic and returns a result (python JSON-serializable object).
251 If this method is not overloaded, an exception will occur.
252 If you don't want to return an answer, you should still implement this method with 'pass'.
254 # This exception will happen if the _getAnswer method is not implemented in a derived class
255 raise MaKaCError("No answer was returned")
258 class ProtectedService(ServiceBase):
260 ProtectedService is a parent class for ProtectedDisplayService and ProtectedModificationService
263 def _checkSessionUser(self):
265 Checks that the current user exists (is authenticated)
268 if self._getUser() == None:
269 self._doProcess = False
270 raise ServiceAccessError("You are currently not authenticated. Please log in again.")
273 class ProtectedDisplayService(ProtectedService):
275 A ProtectedDisplayService can only be accessed by users that
276 are authorized to "see" the target resource
279 def _checkProtection( self ):
281 Overloads ProtectedService._checkProtection, assuring that
282 the user is authorized to view the target resource
284 if not self._target.canAccess( self.getAW() ):
286 from MaKaC.conference import Link, LocalFile
288 # in some cases, the target does not directly have an owner
289 if (isinstance(self._target, Link) or
290 isinstance(self._target, LocalFile)):
291 target = self._target.getOwner()
292 else:
293 target = self._target
294 if not isinstance(target, Category):
295 if target.getAccessKey() != "" or target.getConference().getAccessKey() != "":
296 raise ServiceAccessError("You are currently not authenticated or cannot access this service. Please log in again if necessary.")
297 if self._getUser() == None:
298 self._checkSessionUser()
299 else:
300 raise ServiceAccessError("You cannot access this service. Please log in again if necessary.")
303 class LoggedOnlyService(ProtectedService):
305 Only accessible to users who are logged in (access keys not allowed)
308 def _checkProtection( self ):
309 self._checkSessionUser()
312 class ProtectedModificationService(ProtectedService):
314 A ProtectedModificationService can only be accessed by users that
315 are authorized to modify the target resource
317 def _checkProtection( self ):
319 Overloads ProtectedService._checkProtection, so that it is
320 verified if the user has modification access to the resource
323 target = self._target
324 if (type(target) == conference.SessionSlot):
325 target = target.getSession()
327 if not target.canModify( self.getAW() ):
328 if target.getModifKey() != "":
329 raise ServiceAccessError("You don't have the rights to modify this object")
330 if self._getUser() == None:
331 self._checkSessionUser()
332 else:
333 raise ServiceAccessError("You don't have the rights to modify this object")
334 if hasattr(self._target, "getConference") and hasattr(self._target, "isClosed"):
335 if target.getConference().isClosed():
336 raise ServiceAccessError("Conference %s is closed"%target.getConference().getId())
338 class AdminService(LoggedOnlyService):
340 A AdminService can only be accessed by administrators
342 def _checkProtection(self):
343 LoggedOnlyService._checkProtection(self)
344 if not session.user.is_admin:
345 raise ServiceAccessError(_("Only administrators can perform this operation"))
347 class TextModificationBase:
349 Base class for text field modification
352 def _getAnswer( self ):
353 """ Calls _handleGet() or _handleSet() on the derived classes, in order to make it happen. Provides
354 them with self._value.
356 When calling _handleGet(), it will return the value to return.
357 When calling _handleSet(), it will return:
358 -either self._value if there were no problems
359 -either a FieldModificationWarning object (pickled) if there are warnings to give to the user
362 # fetch the 'value' parameter (default for text)
363 if self._params.has_key('value'):
364 self._value = self._params['value']
365 else:
366 # None if not passed
367 self._value = None
369 if self._value == None:
370 return self._handleGet()
371 else:
372 setResult = self._handleSet()
373 if isinstance(setResult, Warning):
374 return ResultWithWarning(self._value, setResult).fossilize()
375 else:
376 return self._value
378 class HTMLModificationBase:
380 Base class for HTML field modification
382 def _getAnswer( self ):
384 Calls _handle() on the derived classes, in order to make it happen. Provides
385 them with self._value.
388 if self._params.has_key('value'):
389 self._value = self._params['value']
390 else:
391 self._value = None
393 if self._value == None:
394 return self._handleGet()
395 else:
396 self._handleSet()
398 return self._value
401 class DateTimeModificationBase( TextModificationBase ):
402 """ Date and time modification base class
403 Its _handleSet method is called by TextModificationBase's _getAnswer method.
404 DateTimeModificationBase's _handletSet method will call the _setParam method
405 from the classes that inherits from DateTimeModificationBase.
406 _handleSet will return whatever _setParam returns (usually None if there were no problems,
407 or a FieldModificationWarning object with information about a problem / warning to give to the user)
409 def _handleSet(self):
410 try:
411 naiveDate = parseDateTime(self._value)
412 except ValueError:
413 raise ServiceError("ERR-E2",
414 "Date/time is not in the correct format")
416 try:
417 self._pTime = setAdjustedDate(naiveDate, self._conf)
418 return self._setParam()
419 except TimingError,e:
420 raise ServiceError("ERR-E2", e.getMessage())
422 class ListModificationBase:
423 """ Base class for a list modification.
424 The class that inherits from this must have:
425 -a _handleGet() method that returns a list.
426 -a _handleSet() method which can use self._value to process the input. self._value will be a list.
429 def _getAnswer(self):
430 if self._params.has_key('value'):
431 pm = ParameterManager(self._params)
432 self._value = pm.extract("value", pType=list, allowEmpty=True)
433 else:
434 self._value = None
436 if self._value == None:
437 return self._handleGet()
438 else:
439 self._handleSet()
441 return self._value
443 class TwoListModificationBase:
444 """ Base class for two lists modification.
445 The class that inherits from this must have:
446 -a _handleGet() method that returns a list, given self._destination
447 -a _handleSet() method which can use self._value and self._destination to process the input.
448 self._value will be a list. self._destination will be 'left' or 'right'
451 def _getAnswer(self):
452 self._destination = self._params.get('destination', None)
453 if self._destination == None or (self._destination != 'right' and self._destination != 'left'):
454 #TODO: add this error to the wiki
455 raise ServiceError("ERR-E4", 'Destination list not set to "right" or "left"')
457 if self._params.has_key('value'):
458 pm = ParameterManager(self._params)
459 self._value = pm.extract("value", pType=list, allowEmpty=False)
460 else:
461 self._value = None
463 if self._value == None:
464 return self._handleGet()
465 else:
466 self._handleSet()
468 return self._value
471 class ExportToICalBase(object):
473 def _checkParams(self):
474 user = self._getUser()
475 if not user:
476 raise ServiceAccessError("User is not logged in!")
477 apiKey = user.api_key
478 if not apiKey:
479 raise ServiceAccessError("User has no API key!")
480 elif apiKey.is_blocked:
481 raise ServiceAccessError("This API key is blocked!")
482 self._apiKey = apiKey