1 # -*- coding: utf-8 -*-
4 ## This file is part of CDS Indico.
5 ## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 CERN.
7 ## CDS Indico is free software; you can redistribute it and/or
8 ## modify it under the terms of the GNU General Public License as
9 ## published by the Free Software Foundation; either version 2 of the
10 ## License, or (at your option) any later version.
12 ## CDS Indico is distributed in the hope that it will be useful, but
13 ## WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 ## General Public License for more details.
17 ## You should have received a copy of the GNU General Public License
18 ## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
19 ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
25 # python stdlib imports
31 from urlparse
import parse_qs
32 from ZODB
.POSException
import ConflictError
36 from indico
.web
.http_api
import ExportInterface
, LimitExceededException
37 from indico
.web
.http_api
.auth
import APIKeyHolder
38 from indico
.web
.http_api
.cache
import RequestCache
39 from indico
.web
.http_api
.responses
import HTTPAPIResult
, HTTPAPIError
40 from indico
.web
.http_api
.util
import remove_lists
, get_query_parameter
41 from indico
.web
.http_api
import API_MODE_KEY
, API_MODE_ONLYKEY
, API_MODE_SIGNED
, API_MODE_ONLYKEY_SIGNED
, API_MODE_ALL_SIGNED
42 from indico
.web
.wsgi
import webinterface_handler_config
as apache
43 from indico
.util
.metadata
.serializer
import Serializer
45 # indico legacy imports
46 from MaKaC
.common
import DBMgr
47 from MaKaC
.common
.fossilize
import fossilizes
, fossilize
, Fossilizable
48 from MaKaC
.accessControl
import AccessWrapper
49 from MaKaC
.common
.info
import HelperMaKaCInfo
51 # Maximum number of records that will get exported for each detail level
55 'subcontributions': 500,
59 # Valid URLs for export handlers. the last group has to be the response type
61 r
'/export/(event|categ)/(\w+(?:-\w+)*)\.(\w+)$': 'handler_event_categ'
65 EXPORT_URL_MAP
= dict((re
.compile(pathRe
), handlerFunc
) for pathRe
, handlerFunc
in EXPORT_URL_MAP
.iteritems())
66 # Remove the extension at the end or before the querystring
67 RE_REMOVE_EXTENSION
= re
.compile(r
'\.(\w+)(?:$|(?=\?))')
70 def normalizeQuery(path
, query
, ts
=None, remove
=('timestamp', 'signature')):
71 """Normalize request path and query so it can be used for caching and signing
73 Returns a string consisting of path and sorted query string.
74 Dynamic arguments like signature and timestamp are removed from the query string.
76 qdata
= remove_lists(parse_qs(query
))
81 qdata
['timestamp'] = ts
82 sortedQuery
= sorted(qdata
.items(), key
=lambda x
: x
[0].lower())
84 return '%s?%s' % (path
, urllib
.urlencode(sortedQuery
))
89 def validateSignature(key
, signature
, path
, query
, timestamp
=None):
91 timestamp
= int(time
.time())
94 for i
in xrange(-1, 2):
95 h
= hmac
.new(key
, normalizeQuery(path
, query
, ts
+ i
), hashlib
.sha1
)
96 candidates
.append(h
.hexdigest())
97 if signature
not in candidates
:
98 raise HTTPAPIError('Signature invalid (check system clock)', apache
.HTTP_FORBIDDEN
)
101 def getAK(apiKey
, signature
, path
, query
):
102 minfo
= HelperMaKaCInfo
.getMaKaCInfoInstance()
103 apiMode
= minfo
.getAPIMode()
105 if apiMode
in (API_MODE_ONLYKEY
, API_MODE_ONLYKEY_SIGNED
, API_MODE_ALL_SIGNED
):
106 raise HTTPAPIError('API key is missing', apache
.HTTP_FORBIDDEN
)
109 if not akh
.hasKey(apiKey
):
110 raise HTTPAPIError('Invalid API key', apache
.HTTP_FORBIDDEN
)
111 ak
= akh
.getById(apiKey
)
113 raise HTTPAPIError('API key is blocked', apache
.HTTP_FORBIDDEN
)
114 # Signature validation
117 validateSignature(ak
.getSignKey(), signature
, path
, query
)
118 elif apiMode
in (API_MODE_SIGNED
, API_MODE_ALL_SIGNED
):
119 raise HTTPAPIError('Signature missing', apache
.HTTP_FORBIDDEN
)
120 elif apiMode
== API_MODE_ONLYKEY_SIGNED
:
122 return ak
, onlyPublic
125 def buildAW(ak
, req
, onlyPublic
=False):
127 if ak
and not onlyPublic
:
128 # If we have an authenticated request, require HTTPS
129 minfo
= HelperMaKaCInfo
.getMaKaCInfoInstance()
130 if not req
.is_https() and minfo
.isAPIHTTPSRequired():
131 raise HTTPAPIError('HTTPS is required', apache
.HTTP_FORBIDDEN
)
132 aw
.setUser(ak
.getUser())
136 def getExportHandler(path
):
137 """Get the export handler, handler args and return type from a path"""
140 for pathRe
, handlerFunc
in EXPORT_URL_MAP
.iteritems():
141 match
= pathRe
.match(path
)
146 groups
= match
and match
.groups()
147 if not match
or groups
[-1] not in ExportInterface
.getAllowedFormats():
148 return None, None, None
149 return globals()[func
], groups
[:-1], groups
[-1]
152 def handler_event_categ(dbi
, aw
, qdata
, dtype
, idlist
):
153 idlist
= idlist
.split('-')
155 expInt
= ExportInterface(dbi
, aw
)
156 tzName
= get_query_parameter(qdata
, ['tz'], None)
157 detail
= get_query_parameter(qdata
, ['d', 'detail'], 'events')
158 userLimit
= get_query_parameter(qdata
, ['n', 'limit'], 0, integer
=True)
159 offset
= get_query_parameter(qdata
, ['O', 'offset'], 0, integer
=True)
160 orderBy
= get_query_parameter(qdata
, ['o', 'order'], 'start')
161 descending
= get_query_parameter(qdata
, ['c', 'descending'], False)
164 info
= HelperMaKaCInfo
.getMaKaCInfoInstance()
165 tzName
= info
.getTimezone()
167 tz
= pytz
.timezone(tzName
)
169 max = MAX_RECORDS
.get(detail
, 10000)
171 raise HTTPAPIError("You can only request up to %d records per request with the detail level '%s" %
172 (max, detail
), apache
.HTTP_BAD_REQUEST
)
174 # impose a hard limit
175 limit
= userLimit
if userLimit
> 0 else max
178 iterator
= expInt
.category(idlist
, tz
, offset
, limit
, detail
, orderBy
, descending
, qdata
)
179 elif dtype
== 'event':
180 iterator
= expInt
.event(idlist
, tz
, offset
, limit
, detail
, orderBy
, descending
, qdata
)
187 resultList
.append(obj
)
188 except LimitExceededException
:
189 complete
= (limit
== userLimit
)
191 return resultList
, complete
193 def handler(req
, **params
):
194 path
, query
= req
.URLFields
['PATH_INFO'], req
.URLFields
['QUERY_STRING']
195 # Parse the actual query string
196 qdata
= parse_qs(query
)
198 dbi
= DBMgr
.getInstance()
201 cache
= RequestCache(HelperMaKaCInfo
.getMaKaCInfoInstance().getAPICacheTTL())
203 apiKey
= get_query_parameter(qdata
, ['ak', 'apikey'], None)
204 signature
= get_query_parameter(qdata
, ['signature'])
205 no_cache
= get_query_parameter(qdata
, ['nc', 'nocache'], 'no') == 'yes'
206 pretty
= get_query_parameter(qdata
, ['p', 'pretty'], 'no') == 'yes'
207 onlyPublic
= get_query_parameter(qdata
, ['op', 'onlypublic'], 'no') == 'yes'
209 # Get our handler function and its argument and response type
210 func
, args
, dformat
= getExportHandler(path
)
211 if func
is None or dformat
is None:
212 raise apache
.SERVER_RETURN
, apache
.HTTP_NOT_FOUND
214 ak
= error
= result
= None
215 ts
= int(time
.time())
217 # Validate the API key (and its signature)
218 ak
, enforceOnlyPublic
= getAK(apiKey
, signature
, path
, query
)
219 if enforceOnlyPublic
:
221 # Create an access wrapper for the API key's user
222 aw
= buildAW(ak
, req
, onlyPublic
)
223 # Get rid of API key in cache key if we did not impersonate a user
224 if ak
and aw
.getUser() is None:
225 cache_key
= normalizeQuery(path
, query
, remove
=('ak', 'apiKey', 'signature', 'timestamp'))
227 cache_key
= normalizeQuery(path
, query
, remove
=('signature', 'timestamp'))
231 cache_key
= RE_REMOVE_EXTENSION
.sub('', cache_key
)
233 obj
= cache
.loadObject(cache_key
)
235 result
, complete
= obj
.getContent()
239 # Perform the actual exporting
240 result
, complete
= func(dbi
, aw
, qdata
, *args
)
241 if result
is not None and add_to_cache
:
242 cache
.cacheObject(cache_key
, (result
, complete
))
243 except HTTPAPIError
, e
:
246 req
.status
= e
.getCode()
248 if result
is None and error
is None:
250 raise apache
.SERVER_RETURN
, apache
.HTTP_NOT_FOUND
252 if ak
and error
is None:
253 # Commit only if there was an API key and no error
254 for _retry
in xrange(10):
256 ak
.used(req
.remote_ip
, path
, query
, not onlyPublic
)
259 except ConflictError
:
264 # No need to commit stuff if we didn't use an API key
265 # (nothing was written)
266 dbi
.endRequest(False)
268 serializer
= Serializer
.create(dformat
, pretty
=pretty
,
269 **remove_lists(qdata
))
272 resultFossil
= fossilize(error
)
274 resultFossil
= fossilize(HTTPAPIResult(result
, path
, query
, ts
, complete
))
275 del resultFossil
['_fossil']
277 req
.headers_out
['Content-Type'] = serializer
.getMIMEType()
278 return serializer(resultFossil
)