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
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
)
173 self
._startTime
= None
174 self
._tohttps
= request
.is_secure
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)
190 def _checkProtection( self
):
192 Checks protection when accessing resources (normally overloaded)
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
:
214 Processes the request, analyzing the parameters, and feeding them to the
215 _getAnswer() method (implemented by derived classes)
218 ContextManager
.set('currentRH', self
)
221 self
._checkProtection
()
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
))
229 if Config
.getInstance().getProfile():
230 import profile
, pstats
, random
231 proffilename
= os
.path
.join(Config
.getInstance().getTempDir(), "service%s.prof" % random
.random())
233 profile
.runctx("result[0] = self._getAnswer()", globals(), locals(), proffilename
)
235 rep
= Config
.getInstance().getTempDir()
236 stats
= pstats
.Stats(proffilename
)
238 stats
.sort_stats('cumulative', 'time', 'calls')
239 stats
.dump_stats(os
.path
.join(rep
, "IndicoServiceRequestProfile.log"))
240 os
.remove(proffilename
)
242 answer
= self
._getAnswer
()
243 self
._deleteTempFiles
()
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()
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
()
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
()
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']
369 if self
._value
== None:
370 return self
._handleGet
()
372 setResult
= self
._handleSet
()
373 if isinstance(setResult
, Warning):
374 return ResultWithWarning(self
._value
, setResult
).fossilize()
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']
393 if self
._value
== None:
394 return self
._handleGet
()
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
):
411 naiveDate
= parseDateTime(self
._value
)
413 raise ServiceError("ERR-E2",
414 "Date/time is not in the correct format")
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)
436 if self
._value
== None:
437 return self
._handleGet
()
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)
463 if self
._value
== None:
464 return self
._handleGet
()
471 class ExportToICalBase(object):
473 def _checkParams(self
):
474 user
= self
._getUser
()
476 raise ServiceAccessError("User is not logged in!")
477 apiKey
= user
.api_key
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