|
1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """Some utilities for CubicWeb server/clients.""" |
|
19 |
|
20 from __future__ import division |
|
21 |
|
22 __docformat__ = "restructuredtext en" |
|
23 |
|
24 import decimal |
|
25 import datetime |
|
26 import random |
|
27 import re |
|
28 import json |
|
29 |
|
30 from operator import itemgetter |
|
31 from inspect import getargspec |
|
32 from itertools import repeat |
|
33 from uuid import uuid4 |
|
34 from warnings import warn |
|
35 from threading import Lock |
|
36 from logging import getLogger |
|
37 |
|
38 from six import text_type |
|
39 from six.moves.urllib.parse import urlparse |
|
40 |
|
41 from logilab.mtconverter import xml_escape |
|
42 from logilab.common.deprecation import deprecated |
|
43 from logilab.common.date import ustrftime |
|
44 |
|
45 _MARKER = object() |
|
46 |
|
47 # initialize random seed from current time |
|
48 random.seed() |
|
49 |
|
50 def admincnx(appid): |
|
51 from cubicweb.cwconfig import CubicWebConfiguration |
|
52 from cubicweb.server.repository import Repository |
|
53 from cubicweb.server.utils import TasksManager |
|
54 config = CubicWebConfiguration.config_for(appid) |
|
55 |
|
56 login = config.default_admin_config['login'] |
|
57 password = config.default_admin_config['password'] |
|
58 |
|
59 repo = Repository(config, TasksManager()) |
|
60 session = repo.new_session(login, password=password) |
|
61 return session.new_cnx() |
|
62 |
|
63 |
|
64 def make_uid(key=None): |
|
65 """Return a unique identifier string. |
|
66 |
|
67 if specified, `key` is used to prefix the generated uid so it can be used |
|
68 for instance as a DOM id or as sql table name. |
|
69 |
|
70 See uuid.uuid4 documentation for the shape of the generated identifier, but |
|
71 this is basically a 32 bits hexadecimal string. |
|
72 """ |
|
73 if key is None: |
|
74 return uuid4().hex |
|
75 return str(key) + uuid4().hex |
|
76 |
|
77 |
|
78 def support_args(callable, *argnames): |
|
79 """return true if the callable support given argument names""" |
|
80 if isinstance(callable, type): |
|
81 callable = callable.__init__ |
|
82 argspec = getargspec(callable) |
|
83 if argspec[2]: |
|
84 return True |
|
85 for argname in argnames: |
|
86 if argname not in argspec[0]: |
|
87 return False |
|
88 return True |
|
89 |
|
90 |
|
91 class wrap_on_write(object): |
|
92 """ Sometimes it is convenient to NOT write some container element |
|
93 if it happens that there is nothing to be written within, |
|
94 but this cannot be known beforehand. |
|
95 Hence one can do this: |
|
96 |
|
97 .. sourcecode:: python |
|
98 |
|
99 with wrap_on_write(w, '<div class="foo">', '</div>') as wow: |
|
100 component.render_stuff(wow) |
|
101 """ |
|
102 def __init__(self, w, tag, closetag=None): |
|
103 self.written = False |
|
104 self.tag = text_type(tag) |
|
105 self.closetag = closetag |
|
106 self.w = w |
|
107 |
|
108 def __enter__(self): |
|
109 return self |
|
110 |
|
111 def __call__(self, data): |
|
112 if self.written is False: |
|
113 self.w(self.tag) |
|
114 self.written = True |
|
115 self.w(data) |
|
116 |
|
117 def __exit__(self, exctype, value, traceback): |
|
118 if self.written is True: |
|
119 if self.closetag: |
|
120 self.w(text_type(self.closetag)) |
|
121 else: |
|
122 self.w(self.tag.replace('<', '</', 1)) |
|
123 |
|
124 |
|
125 # use networkX instead ? |
|
126 # http://networkx.lanl.gov/reference/algorithms.traversal.html#module-networkx.algorithms.traversal.astar |
|
127 def transitive_closure_of(entity, rtype, _seen=None): |
|
128 """return transitive closure *for the subgraph starting from the given |
|
129 entity* (eg 'parent' entities are not included in the results) |
|
130 """ |
|
131 if _seen is None: |
|
132 _seen = set() |
|
133 _seen.add(entity.eid) |
|
134 yield entity |
|
135 for child in getattr(entity, rtype): |
|
136 if child.eid in _seen: |
|
137 continue |
|
138 for subchild in transitive_closure_of(child, rtype, _seen): |
|
139 yield subchild |
|
140 |
|
141 |
|
142 class RepeatList(object): |
|
143 """fake a list with the same element in each row""" |
|
144 __slots__ = ('_size', '_item') |
|
145 def __init__(self, size, item): |
|
146 self._size = size |
|
147 self._item = item |
|
148 def __repr__(self): |
|
149 return '<cubicweb.utils.RepeatList at %s item=%s size=%s>' % ( |
|
150 id(self), self._item, self._size) |
|
151 def __len__(self): |
|
152 return self._size |
|
153 def __iter__(self): |
|
154 return repeat(self._item, self._size) |
|
155 def __getitem__(self, index): |
|
156 if isinstance(index, slice): |
|
157 # XXX could be more efficient, but do we bother? |
|
158 return ([self._item] * self._size)[index] |
|
159 return self._item |
|
160 def __delitem__(self, idc): |
|
161 assert self._size > 0 |
|
162 self._size -= 1 |
|
163 def __add__(self, other): |
|
164 if isinstance(other, RepeatList): |
|
165 if other._item == self._item: |
|
166 return RepeatList(self._size + other._size, self._item) |
|
167 return ([self._item] * self._size) + other[:] |
|
168 return ([self._item] * self._size) + other |
|
169 def __radd__(self, other): |
|
170 if isinstance(other, RepeatList): |
|
171 if other._item == self._item: |
|
172 return RepeatList(self._size + other._size, self._item) |
|
173 return other[:] + ([self._item] * self._size) |
|
174 return other[:] + ([self._item] * self._size) |
|
175 def __eq__(self, other): |
|
176 if isinstance(other, RepeatList): |
|
177 return other._size == self._size and other._item == self._item |
|
178 return self[:] == other |
|
179 def __ne__(self, other): |
|
180 return not (self == other) |
|
181 def __hash__(self): |
|
182 raise NotImplementedError |
|
183 def pop(self, i): |
|
184 self._size -= 1 |
|
185 |
|
186 |
|
187 class UStringIO(list): |
|
188 """a file wrapper which automatically encode unicode string to an encoding |
|
189 specifed in the constructor |
|
190 """ |
|
191 |
|
192 def __init__(self, tracewrites=False, *args, **kwargs): |
|
193 self.tracewrites = tracewrites |
|
194 super(UStringIO, self).__init__(*args, **kwargs) |
|
195 |
|
196 def __bool__(self): |
|
197 return True |
|
198 |
|
199 __nonzero__ = __bool__ |
|
200 |
|
201 def write(self, value): |
|
202 assert isinstance(value, text_type), u"unicode required not %s : %s"\ |
|
203 % (type(value).__name__, repr(value)) |
|
204 if self.tracewrites: |
|
205 from traceback import format_stack |
|
206 stack = format_stack(None)[:-1] |
|
207 escaped_stack = xml_escape(json_dumps(u'\n'.join(stack))) |
|
208 escaped_html = xml_escape(value).replace('\n', '<br/>\n') |
|
209 tpl = u'<span onclick="alert(%s)">%s</span>' |
|
210 value = tpl % (escaped_stack, escaped_html) |
|
211 self.append(value) |
|
212 |
|
213 def getvalue(self): |
|
214 return u''.join(self) |
|
215 |
|
216 def __repr__(self): |
|
217 return '<%s at %#x>' % (self.__class__.__name__, id(self)) |
|
218 |
|
219 |
|
220 class HTMLHead(UStringIO): |
|
221 """wraps HTML header's stream |
|
222 |
|
223 Request objects use a HTMLHead instance to ease adding of |
|
224 javascripts and stylesheets |
|
225 """ |
|
226 js_unload_code = u'''if (typeof(pageDataUnloaded) == 'undefined') { |
|
227 jQuery(window).unload(unloadPageData); |
|
228 pageDataUnloaded = true; |
|
229 }''' |
|
230 script_opening = u'<script type="text/javascript">\n' |
|
231 script_closing = u'\n</script>' |
|
232 |
|
233 def __init__(self, req, *args, **kwargs): |
|
234 super(HTMLHead, self).__init__(*args, **kwargs) |
|
235 self.jsvars = [] |
|
236 self.jsfiles = [] |
|
237 self.cssfiles = [] |
|
238 self.ie_cssfiles = [] |
|
239 self.post_inlined_scripts = [] |
|
240 self.pagedata_unload = False |
|
241 self._cw = req |
|
242 self.datadir_url = req.datadir_url |
|
243 |
|
244 def add_raw(self, rawheader): |
|
245 self.write(rawheader) |
|
246 |
|
247 def define_var(self, var, value, override=True): |
|
248 """adds a javascript var declaration / assginment in the header |
|
249 |
|
250 :param var: the variable name |
|
251 :param value: the variable value (as a raw python value, |
|
252 it will be jsonized later) |
|
253 :param override: if False, don't set the variable value if the variable |
|
254 is already defined. Default is True. |
|
255 """ |
|
256 self.jsvars.append( (var, value, override) ) |
|
257 |
|
258 def add_post_inline_script(self, content): |
|
259 self.post_inlined_scripts.append(content) |
|
260 |
|
261 def add_onload(self, jscode): |
|
262 self.add_post_inline_script(u"""$(cw).one('server-response', function(event) { |
|
263 %s});""" % jscode) |
|
264 |
|
265 |
|
266 def add_js(self, jsfile): |
|
267 """adds `jsfile` to the list of javascripts used in the webpage |
|
268 |
|
269 This function checks if the file has already been added |
|
270 :param jsfile: the script's URL |
|
271 """ |
|
272 if jsfile not in self.jsfiles: |
|
273 self.jsfiles.append(jsfile) |
|
274 |
|
275 def add_css(self, cssfile, media='all'): |
|
276 """adds `cssfile` to the list of javascripts used in the webpage |
|
277 |
|
278 This function checks if the file has already been added |
|
279 :param cssfile: the stylesheet's URL |
|
280 """ |
|
281 if (cssfile, media) not in self.cssfiles: |
|
282 self.cssfiles.append( (cssfile, media) ) |
|
283 |
|
284 def add_ie_css(self, cssfile, media='all', iespec=u'[if lt IE 8]'): |
|
285 """registers some IE specific CSS""" |
|
286 if (cssfile, media, iespec) not in self.ie_cssfiles: |
|
287 self.ie_cssfiles.append( (cssfile, media, iespec) ) |
|
288 |
|
289 def add_unload_pagedata(self): |
|
290 """registers onunload callback to clean page data on server""" |
|
291 if not self.pagedata_unload: |
|
292 self.post_inlined_scripts.append(self.js_unload_code) |
|
293 self.pagedata_unload = True |
|
294 |
|
295 def concat_urls(self, urls): |
|
296 """concatenates urls into one url usable by Apache mod_concat |
|
297 |
|
298 This method returns the url without modifying it if there is only |
|
299 one element in the list |
|
300 :param urls: list of local urls/filenames to concatenate |
|
301 """ |
|
302 if len(urls) == 1: |
|
303 return urls[0] |
|
304 len_prefix = len(self.datadir_url) |
|
305 concated = u','.join(url[len_prefix:] for url in urls) |
|
306 return (u'%s??%s' % (self.datadir_url, concated)) |
|
307 |
|
308 def group_urls(self, urls_spec): |
|
309 """parses urls_spec in order to generate concatenated urls |
|
310 for js and css includes |
|
311 |
|
312 This method checks if the file is local and if it shares options |
|
313 with direct neighbors |
|
314 :param urls_spec: entire list of urls/filenames to inspect |
|
315 """ |
|
316 concatable = [] |
|
317 prev_islocal = False |
|
318 prev_key = None |
|
319 for url, key in urls_spec: |
|
320 islocal = url.startswith(self.datadir_url) |
|
321 if concatable and (islocal != prev_islocal or key != prev_key): |
|
322 yield (self.concat_urls(concatable), prev_key) |
|
323 del concatable[:] |
|
324 if not islocal: |
|
325 yield (url, key) |
|
326 else: |
|
327 concatable.append(url) |
|
328 prev_islocal = islocal |
|
329 prev_key = key |
|
330 if concatable: |
|
331 yield (self.concat_urls(concatable), prev_key) |
|
332 |
|
333 |
|
334 def getvalue(self, skiphead=False): |
|
335 """reimplement getvalue to provide a consistent (and somewhat browser |
|
336 optimzed cf. http://stevesouders.com/cuzillion) order in external |
|
337 resources declaration |
|
338 """ |
|
339 w = self.write |
|
340 # 1/ variable declaration if any |
|
341 if self.jsvars: |
|
342 if skiphead: |
|
343 w(u'<cubicweb:script>') |
|
344 else: |
|
345 w(self.script_opening) |
|
346 for var, value, override in self.jsvars: |
|
347 vardecl = u'%s = %s;' % (var, json.dumps(value)) |
|
348 if not override: |
|
349 vardecl = (u'if (typeof %s == "undefined") {%s}' % |
|
350 (var, vardecl)) |
|
351 w(vardecl + u'\n') |
|
352 if skiphead: |
|
353 w(u'</cubicweb:script>') |
|
354 else: |
|
355 w(self.script_closing) |
|
356 # 2/ css files |
|
357 ie_cssfiles = ((x, (y, z)) for x, y, z in self.ie_cssfiles) |
|
358 if self.datadir_url and self._cw.vreg.config['concat-resources']: |
|
359 cssfiles = self.group_urls(self.cssfiles) |
|
360 ie_cssfiles = self.group_urls(ie_cssfiles) |
|
361 jsfiles = (x for x, _ in self.group_urls((x, None) for x in self.jsfiles)) |
|
362 else: |
|
363 cssfiles = self.cssfiles |
|
364 jsfiles = self.jsfiles |
|
365 for cssfile, media in cssfiles: |
|
366 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' % |
|
367 (media, xml_escape(cssfile))) |
|
368 # 3/ ie css if necessary |
|
369 if self.ie_cssfiles: # use self.ie_cssfiles because `ie_cssfiles` is a genexp |
|
370 for cssfile, (media, iespec) in ie_cssfiles: |
|
371 w(u'<!--%s>\n' % iespec) |
|
372 w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' % |
|
373 (media, xml_escape(cssfile))) |
|
374 w(u'<![endif]--> \n') |
|
375 # 4/ js files |
|
376 for jsfile in jsfiles: |
|
377 if skiphead: |
|
378 # Don't insert <script> tags directly as they would be |
|
379 # interpreted directly by some browsers (e.g. IE). |
|
380 # Use <cubicweb:script> tags instead and let |
|
381 # `loadAjaxHtmlHead` handle the script insertion / execution. |
|
382 w(u'<cubicweb:script src="%s"></cubicweb:script>\n' % |
|
383 xml_escape(jsfile)) |
|
384 # FIXME: a probably better implementation might be to add |
|
385 # JS or CSS urls in a JS list that loadAjaxHtmlHead |
|
386 # would iterate on and postprocess: |
|
387 # cw._ajax_js_scripts.push('myscript.js') |
|
388 # Then, in loadAjaxHtmlHead, do something like: |
|
389 # jQuery.each(cw._ajax_js_script, jQuery.getScript) |
|
390 else: |
|
391 w(u'<script type="text/javascript" src="%s"></script>\n' % |
|
392 xml_escape(jsfile)) |
|
393 # 5/ post inlined scripts (i.e. scripts depending on other JS files) |
|
394 if self.post_inlined_scripts: |
|
395 if skiphead: |
|
396 for script in self.post_inlined_scripts: |
|
397 w(u'<cubicweb:script>') |
|
398 w(xml_escape(script)) |
|
399 w(u'</cubicweb:script>') |
|
400 else: |
|
401 w(self.script_opening) |
|
402 w(u'\n\n'.join(self.post_inlined_scripts)) |
|
403 w(self.script_closing) |
|
404 # at the start of this function, the parent UStringIO may already have |
|
405 # data in it, so we can't w(u'<head>\n') at the top. Instead, we create |
|
406 # a temporary UStringIO to get the same debugging output formatting |
|
407 # if debugging is enabled. |
|
408 headtag = UStringIO(tracewrites=self.tracewrites) |
|
409 if not skiphead: |
|
410 headtag.write(u'<head>\n') |
|
411 w(u'</head>\n') |
|
412 return headtag.getvalue() + super(HTMLHead, self).getvalue() |
|
413 |
|
414 |
|
415 class HTMLStream(object): |
|
416 """represents a HTML page. |
|
417 |
|
418 This is used my main templates so that HTML headers can be added |
|
419 at any time during the page generation. |
|
420 |
|
421 HTMLStream uses the (U)StringIO interface to be compliant with |
|
422 existing code. |
|
423 """ |
|
424 |
|
425 def __init__(self, req): |
|
426 self.tracehtml = req.tracehtml |
|
427 # stream for <head> |
|
428 self.head = req.html_headers |
|
429 # main stream |
|
430 self.body = UStringIO(tracewrites=req.tracehtml) |
|
431 # this method will be assigned to self.w in views |
|
432 self.write = self.body.write |
|
433 self.doctype = u'' |
|
434 self._htmlattrs = [('lang', req.lang)] |
|
435 # keep main_stream's reference on req for easier text/html demoting |
|
436 req.main_stream = self |
|
437 |
|
438 @deprecated('[3.17] there are no namespaces in html, xhtml is not served any longer') |
|
439 def add_namespace(self, prefix, uri): |
|
440 pass |
|
441 |
|
442 @deprecated('[3.17] there are no namespaces in html, xhtml is not served any longer') |
|
443 def set_namespaces(self, namespaces): |
|
444 pass |
|
445 |
|
446 def add_htmlattr(self, attrname, attrvalue): |
|
447 self._htmlattrs.append( (attrname, attrvalue) ) |
|
448 |
|
449 def set_htmlattrs(self, attrs): |
|
450 self._htmlattrs = attrs |
|
451 |
|
452 def set_doctype(self, doctype, reset_xmldecl=None): |
|
453 self.doctype = doctype |
|
454 if reset_xmldecl is not None: |
|
455 warn('[3.17] xhtml is no more supported', |
|
456 DeprecationWarning, stacklevel=2) |
|
457 |
|
458 @property |
|
459 def htmltag(self): |
|
460 attrs = ' '.join('%s="%s"' % (attr, xml_escape(value)) |
|
461 for attr, value in self._htmlattrs) |
|
462 if attrs: |
|
463 return '<html xmlns:cubicweb="http://www.cubicweb.org" %s>' % attrs |
|
464 return '<html xmlns:cubicweb="http://www.cubicweb.org">' |
|
465 |
|
466 def getvalue(self): |
|
467 """writes HTML headers, closes </head> tag and writes HTML body""" |
|
468 if self.tracehtml: |
|
469 css = u'\n'.join((u'span {', |
|
470 u' font-family: monospace;', |
|
471 u' word-break: break-all;', |
|
472 u' word-wrap: break-word;', |
|
473 u'}', |
|
474 u'span:hover {', |
|
475 u' color: red;', |
|
476 u' text-decoration: underline;', |
|
477 u'}')) |
|
478 style = u'<style type="text/css">\n%s\n</style>\n' % css |
|
479 return (u'<!DOCTYPE html>\n' |
|
480 + u'<html>\n<head>\n%s\n</head>\n' % style |
|
481 + u'<body>\n' |
|
482 + u'<span>' + xml_escape(self.doctype) + u'</span><br/>' |
|
483 + u'<span>' + xml_escape(self.htmltag) + u'</span><br/>' |
|
484 + self.head.getvalue() |
|
485 + self.body.getvalue() |
|
486 + u'<span>' + xml_escape(u'</html>') + u'</span>' |
|
487 + u'</body>\n</html>') |
|
488 return u'%s\n%s\n%s\n%s\n</html>' % (self.doctype, |
|
489 self.htmltag, |
|
490 self.head.getvalue(), |
|
491 self.body.getvalue()) |
|
492 |
|
493 |
|
494 class CubicWebJsonEncoder(json.JSONEncoder): |
|
495 """define a json encoder to be able to encode yams std types""" |
|
496 |
|
497 def default(self, obj): |
|
498 if hasattr(obj, '__json_encode__'): |
|
499 return obj.__json_encode__() |
|
500 if isinstance(obj, datetime.datetime): |
|
501 return ustrftime(obj, '%Y/%m/%d %H:%M:%S') |
|
502 elif isinstance(obj, datetime.date): |
|
503 return ustrftime(obj, '%Y/%m/%d') |
|
504 elif isinstance(obj, datetime.time): |
|
505 return obj.strftime('%H:%M:%S') |
|
506 elif isinstance(obj, datetime.timedelta): |
|
507 return (obj.days * 24 * 60 * 60) + obj.seconds |
|
508 elif isinstance(obj, decimal.Decimal): |
|
509 return float(obj) |
|
510 try: |
|
511 return json.JSONEncoder.default(self, obj) |
|
512 except TypeError: |
|
513 # we never ever want to fail because of an unknown type, |
|
514 # just return None in those cases. |
|
515 return None |
|
516 |
|
517 def json_dumps(value, **kwargs): |
|
518 return json.dumps(value, cls=CubicWebJsonEncoder, **kwargs) |
|
519 |
|
520 |
|
521 class JSString(str): |
|
522 """use this string sub class in values given to :func:`js_dumps` to |
|
523 insert raw javascript chain in some JSON string |
|
524 """ |
|
525 |
|
526 def _dict2js(d, predictable=False): |
|
527 if predictable: |
|
528 it = sorted(d.items()) |
|
529 else: |
|
530 it = d.items() |
|
531 res = [key + ': ' + js_dumps(val, predictable) |
|
532 for key, val in it] |
|
533 return '{%s}' % ', '.join(res) |
|
534 |
|
535 def _list2js(l, predictable=False): |
|
536 return '[%s]' % ', '.join([js_dumps(val, predictable) for val in l]) |
|
537 |
|
538 def js_dumps(something, predictable=False): |
|
539 """similar as :func:`json_dumps`, except values which are instances of |
|
540 :class:`JSString` are expected to be valid javascript and will be output |
|
541 as is |
|
542 |
|
543 >>> js_dumps({'hop': JSString('$.hop'), 'bar': None}, predictable=True) |
|
544 '{bar: null, hop: $.hop}' |
|
545 >>> js_dumps({'hop': '$.hop'}) |
|
546 '{hop: "$.hop"}' |
|
547 >>> js_dumps({'hip': {'hop': JSString('momo')}}) |
|
548 '{hip: {hop: momo}}' |
|
549 """ |
|
550 if isinstance(something, dict): |
|
551 return _dict2js(something, predictable) |
|
552 if isinstance(something, list): |
|
553 return _list2js(something, predictable) |
|
554 if isinstance(something, JSString): |
|
555 return something |
|
556 return json_dumps(something, sort_keys=predictable) |
|
557 |
|
558 PERCENT_IN_URLQUOTE_RE = re.compile(r'%(?=[0-9a-fA-F]{2})') |
|
559 def js_href(javascript_code): |
|
560 """Generate a "javascript: ..." string for an href attribute. |
|
561 |
|
562 Some % which may be interpreted in a href context will be escaped. |
|
563 |
|
564 In an href attribute, url-quotes-looking fragments are interpreted before |
|
565 being given to the javascript engine. Valid url quotes are in the form |
|
566 ``%xx`` with xx being a byte in hexadecimal form. This means that ``%toto`` |
|
567 will be unaltered but ``%babar`` will be mangled because ``ba`` is the |
|
568 hexadecimal representation of 186. |
|
569 |
|
570 >>> js_href('alert("babar");') |
|
571 'javascript: alert("babar");' |
|
572 >>> js_href('alert("%babar");') |
|
573 'javascript: alert("%25babar");' |
|
574 >>> js_href('alert("%toto %babar");') |
|
575 'javascript: alert("%toto %25babar");' |
|
576 >>> js_href('alert("%1337%");') |
|
577 'javascript: alert("%251337%");' |
|
578 """ |
|
579 return 'javascript: ' + PERCENT_IN_URLQUOTE_RE.sub(r'%25', javascript_code) |
|
580 |
|
581 |
|
582 def parse_repo_uri(uri): |
|
583 """ transform a command line uri into a (protocol, hostport, appid), e.g: |
|
584 <myapp> -> 'inmemory', None, '<myapp>' |
|
585 inmemory://<myapp> -> 'inmemory', None, '<myapp>' |
|
586 """ |
|
587 parseduri = urlparse(uri) |
|
588 scheme = parseduri.scheme |
|
589 if scheme == '': |
|
590 return ('inmemory', None, parseduri.path) |
|
591 if scheme == 'inmemory': |
|
592 return (scheme, None, parseduri.netloc) |
|
593 raise NotImplementedError('URI protocol not implemented for `%s`' % uri) |
|
594 |
|
595 |
|
596 |
|
597 logger = getLogger('cubicweb.utils') |
|
598 |
|
599 class QueryCache(object): |
|
600 """ a minimalist dict-like object to be used by the querier |
|
601 and native source (replaces lgc.cache for this very usage) |
|
602 |
|
603 To be efficient it must be properly used. The usage patterns are |
|
604 quite specific to its current clients. |
|
605 |
|
606 The ceiling value should be sufficiently high, else it will be |
|
607 ruthlessly inefficient (there will be warnings when this happens). |
|
608 A good (high enough) value can only be set on a per-application |
|
609 value. A default, reasonnably high value is provided but tuning |
|
610 e.g `rql-cache-size` can certainly help. |
|
611 |
|
612 There are two kinds of elements to put in this cache: |
|
613 * frequently used elements |
|
614 * occasional elements |
|
615 |
|
616 The former should finish in the _permanent structure after some |
|
617 warmup. |
|
618 |
|
619 Occasional elements can be buggy requests (server-side) or |
|
620 end-user (web-ui provided) requests. These have to be cleaned up |
|
621 when they fill the cache, without evicting the useful, frequently |
|
622 used entries. |
|
623 """ |
|
624 # quite arbitrary, but we want to never |
|
625 # immortalize some use-a-little query |
|
626 _maxlevel = 15 |
|
627 |
|
628 def __init__(self, ceiling=3000): |
|
629 self._max = ceiling |
|
630 # keys belonging forever to this cache |
|
631 self._permanent = set() |
|
632 # mapping of key (that can get wiped) to getitem count |
|
633 self._transient = {} |
|
634 self._data = {} |
|
635 self._lock = Lock() |
|
636 |
|
637 def __len__(self): |
|
638 with self._lock: |
|
639 return len(self._data) |
|
640 |
|
641 def __getitem__(self, k): |
|
642 with self._lock: |
|
643 if k in self._permanent: |
|
644 return self._data[k] |
|
645 v = self._transient.get(k, _MARKER) |
|
646 if v is _MARKER: |
|
647 self._transient[k] = 1 |
|
648 return self._data[k] |
|
649 if v > self._maxlevel: |
|
650 self._permanent.add(k) |
|
651 self._transient.pop(k, None) |
|
652 else: |
|
653 self._transient[k] += 1 |
|
654 return self._data[k] |
|
655 |
|
656 def __setitem__(self, k, v): |
|
657 with self._lock: |
|
658 if len(self._data) >= self._max: |
|
659 self._try_to_make_room() |
|
660 self._data[k] = v |
|
661 |
|
662 def pop(self, key, default=_MARKER): |
|
663 with self._lock: |
|
664 try: |
|
665 if default is _MARKER: |
|
666 return self._data.pop(key) |
|
667 return self._data.pop(key, default) |
|
668 finally: |
|
669 if key in self._permanent: |
|
670 self._permanent.remove(key) |
|
671 else: |
|
672 self._transient.pop(key, None) |
|
673 |
|
674 def clear(self): |
|
675 with self._lock: |
|
676 self._clear() |
|
677 |
|
678 def _clear(self): |
|
679 self._permanent = set() |
|
680 self._transient = {} |
|
681 self._data = {} |
|
682 |
|
683 def _try_to_make_room(self): |
|
684 current_size = len(self._data) |
|
685 items = sorted(self._transient.items(), key=itemgetter(1)) |
|
686 level = 0 |
|
687 for k, v in items: |
|
688 self._data.pop(k, None) |
|
689 self._transient.pop(k, None) |
|
690 if v > level: |
|
691 datalen = len(self._data) |
|
692 if datalen == 0: |
|
693 return |
|
694 if (current_size - datalen) / datalen > .1: |
|
695 break |
|
696 level = v |
|
697 else: |
|
698 # we removed cruft but everything is permanent |
|
699 if len(self._data) >= self._max: |
|
700 logger.warning('Cache %s is full.' % id(self)) |
|
701 self._clear() |
|
702 |
|
703 def _usage_report(self): |
|
704 with self._lock: |
|
705 return {'itemcount': len(self._data), |
|
706 'transientcount': len(self._transient), |
|
707 'permanentcount': len(self._permanent)} |
|
708 |
|
709 def popitem(self): |
|
710 raise NotImplementedError() |
|
711 |
|
712 def setdefault(self, key, default=None): |
|
713 raise NotImplementedError() |
|
714 |
|
715 def update(self, other): |
|
716 raise NotImplementedError() |