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/>.
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,\
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
):
54 Represents an exception that occurs when a type of parameter was expected
55 but another one was obtained
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
):
64 Thrown when a parameter that should have a value is empty
66 def __init__(self
, paramName
=""):
67 ServiceError
.__init
__(self
, "ERR-P3","Expected parameter '%s' is empty"%paramName
)
70 class DateTimeParameterException(ServiceError
):
72 Thrown when a parameter that should have a value is empty
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):
81 The ParameterManager makes parameter processing a bit easier, by providing
82 some default transformations
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):
92 Extracts a parameter, given a parameter name, and optional type
95 # "global" policy applies if allowEmpty not set
96 if allowEmpty
is None:
97 allowEmpty
= self
._allowEmpty
99 value
= self
._paramList
.get(paramName
)
104 raise EmptyParameterException(paramName
)
106 if value
is not None:
114 if not type(value
) == bool:
116 value
= distutils
.util
.strtobool(str(value
))
118 raise ExpectedParameterException(paramName
, bool, type(value
))
120 if not type(value
) == dict:
121 raise ExpectedParameterException(paramName
, dict, type(value
))
123 if not type(value
) == list:
124 raise ExpectedParameterException(paramName
, list, type(value
))
126 # format will possibly be accomodated to different standards,
128 value
= datetime
.strptime(value
, '%Y/%m/%d').date()
129 elif pType
== datetime
:
130 # format will possibly be accomodated to different standards,
133 # both strings and objects are accepted
134 if isinstance(value
, basestring
):
135 naiveDate
= datetime
.strptime(value
, '%Y/%m/%d %H:%M')
137 naiveDate
= datetime
.strptime(value
['date']+' '+value
['time'][:5], '%Y/%m/%d %H:%M')
141 raise DateTimeParameterException(paramName
, value
)
143 if self
._timezone
and naiveDate
:
144 value
= timezone(self
._timezone
).localize(naiveDate
).astimezone(timezone('utc'))
148 if (value
is None or value
== "") and not allowEmpty
:
149 EmptyParameterException(paramName
)
153 def setTimezone(self
, tz
):
157 class ServiceBase(RequestHandlerBase
):
159 The ServiceBase class is the basic class for services.
162 UNICODE_PARAMS
= False
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
)
174 self
._startTime
= None
175 self
._tohttps
= request
.is_secure
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)
191 def _checkProtection( self
):
193 Checks protection when accessing resources (normally overloaded)
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
:
215 Processes the request, analyzing the parameters, and feeding them to the
216 _getAnswer() method (implemented by derived classes)
219 ContextManager
.set('currentRH', self
)
222 self
._checkProtection
()
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
))
231 if Config
.getInstance().getProfile():
232 import profile
, pstats
, random
233 proffilename
= os
.path
.join(Config
.getInstance().getTempDir(), "service%s.prof" % random
.random())
235 profile
.runctx("result[0] = self._getAnswer()", globals(), locals(), proffilename
)
237 rep
= Config
.getInstance().getTempDir()
238 stats
= pstats
.Stats(proffilename
)
240 stats
.sort_stats('cumulative', 'time', 'calls')
241 stats
.dump_stats(os
.path
.join(rep
, "IndicoServiceRequestProfile.log"))
242 os
.remove(proffilename
)
244 answer
= self
._getAnswer
()
245 self
._deleteTempFiles
()
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()
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
()
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
()
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']
371 if self
._value
== None:
372 return self
._handleGet
()
374 setResult
= self
._handleSet
()
375 if isinstance(setResult
, Warning):
376 return ResultWithWarning(self
._value
, setResult
).fossilize()
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']
395 if self
._value
== None:
396 return self
._handleGet
()
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
):
413 naiveDate
= parseDateTime(self
._value
)
415 raise ServiceError("ERR-E2",
416 "Date/time is not in the correct format")
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)
438 if self
._value
== None:
439 return self
._handleGet
()
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)
465 if self
._value
== None:
466 return self
._handleGet
()
473 class ExportToICalBase(object):
475 def _checkParams(self
):
476 user
= self
._getUser
()
478 raise ServiceAccessError("User is not logged in!")
479 apiKey
= user
.api_key
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