# HG changeset patch # User Julien Cristau # Date 1413562618 -7200 # Node ID 4352b7ccde04c0c510975d0fc5edca6a11342134 # Parent c84ad981fc4a3279c88110a046ef88003cdd4e38# Parent b77419a02e17dca7ff95016afc5d1d603265716a merge 3.19.5 into 3.20 branch diff -r c84ad981fc4a -r 4352b7ccde04 .hgtags --- a/.hgtags Thu Sep 25 15:49:13 2014 +0200 +++ b/.hgtags Fri Oct 17 18:16:58 2014 +0200 @@ -371,3 +371,9 @@ 37f7c60f89f13dfcf326a4ea0a98ca20d959f7bd cubicweb-version-3.19.3 37f7c60f89f13dfcf326a4ea0a98ca20d959f7bd cubicweb-debian-version-3.19.3-1 37f7c60f89f13dfcf326a4ea0a98ca20d959f7bd cubicweb-centos-version-3.19.3-1 +c4e740e50fc7d371d14df17d26bc42d1f8060261 cubicweb-version-3.19.4 +c4e740e50fc7d371d14df17d26bc42d1f8060261 cubicweb-debian-version-3.19.4-1 +c4e740e50fc7d371d14df17d26bc42d1f8060261 cubicweb-centos-version-3.19.4-1 +3ac86df519af2a1194cb3fc882d30d0e1bf44e3b cubicweb-version-3.19.5 +3ac86df519af2a1194cb3fc882d30d0e1bf44e3b cubicweb-debian-version-3.19.5-1 +3ac86df519af2a1194cb3fc882d30d0e1bf44e3b cubicweb-centos-version-3.19.5-1 diff -r c84ad981fc4a -r 4352b7ccde04 __pkginfo__.py --- a/__pkginfo__.py Thu Sep 25 15:49:13 2014 +0200 +++ b/__pkginfo__.py Fri Oct 17 18:16:58 2014 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 19, 4) +numversion = (3, 19, 5) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -56,7 +56,7 @@ __recommends__ = { 'docutils': '>= 0.6', 'Pyro': '>= 3.9.1, < 4.0.0', - 'PIL': '', # for captcha + 'Pillow': '', # for captcha 'pycrypto': '', # for crypto extensions 'fyzz': '>= 0.1.0', # for sparql 'vobject': '>= 0.6.0', # for ical view diff -r c84ad981fc4a -r 4352b7ccde04 cubicweb.spec --- a/cubicweb.spec Thu Sep 25 15:49:13 2014 +0200 +++ b/cubicweb.spec Fri Oct 17 18:16:58 2014 +0200 @@ -7,7 +7,7 @@ %endif Name: cubicweb -Version: 3.19.4 +Version: 3.19.5 Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz diff -r c84ad981fc4a -r 4352b7ccde04 debian/changelog --- a/debian/changelog Thu Sep 25 15:49:13 2014 +0200 +++ b/debian/changelog Fri Oct 17 18:16:58 2014 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.19.5-1) unstable; urgency=low + + * new upstream release + + -- Julien Cristau Mon, 06 Oct 2014 17:32:28 +0200 + cubicweb (3.19.4-1) unstable; urgency=low * new upstream release diff -r c84ad981fc4a -r 4352b7ccde04 devtools/qunit.py --- a/devtools/qunit.py Thu Sep 25 15:49:13 2014 +0200 +++ b/devtools/qunit.py Fri Oct 17 18:16:58 2014 +0200 @@ -62,34 +62,18 @@ def __init__(self, url=None): self._process = None - self._tmp_dir = mkdtemp(prefix='cwtest-ffxprof-') - self._profile_data = {'uid': uuid4()} - self._profile_name = self.profile_name_mask % self._profile_data - stdout = TemporaryFile() - stderr = TemporaryFile() + self._profile_dir = mkdtemp(prefix='cwtest-ffxprof-') self.firefox_cmd = ['firefox', '-no-remote'] if os.name == 'posix': self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'), '-a', '-s', '-noreset -screen 0 640x480x8'] + self.firefox_cmd - try: - home = osp.expanduser('~') - user = getlogin() - assert os.access(home, os.W_OK), \ - 'No write access to your home directory, Firefox will crash.'\ - ' Are you sure "%s" is a valid home for user "%s"' % (home, user) - check_call(self.firefox_cmd + ['-CreateProfile', - '%s %s' % (self._profile_name, self._tmp_dir)], - stdout=stdout, stderr=stderr) - except CalledProcessError as cpe: - stdout.seek(0) - stderr.seek(0) - raise VerboseCalledProcessError(cpe.returncode, cpe.cmd, stdout.read(), stderr.read()) def start(self, url): self.stop() - fnull = open(os.devnull, 'w') - self._process = Popen(self.firefox_cmd + ['-P', self._profile_name, url], - stdout=fnull, stderr=fnull) + cmd = self.firefox_cmd + ['-silent', '--profile', self._profile_dir, + '-url', url] + with open(os.devnull, 'w') as fnull: + self._process = Popen(cmd, stdout=fnull, stderr=fnull) def stop(self): if self._process is not None: @@ -100,7 +84,6 @@ def __del__(self): self.stop() - rmtree(self._tmp_dir) class QUnitTestCase(CubicWebServerTC): @@ -111,6 +94,7 @@ all_js_tests = () def setUp(self): + self.config.global_set_option('access-control-allow-origin', '*') super(QUnitTestCase, self).setUp() self.test_queue = Queue() class MyQUnitResultController(QUnitResultController): @@ -155,7 +139,7 @@ # generate html test file jquery_dir = 'file://' + self.config.locate_resource('jquery.js')[0] - html_test_file = NamedTemporaryFile(suffix='.html') + html_test_file = NamedTemporaryFile(suffix='.html', delete=False) html_test_file.write(make_qunit_html(test_file, depends, base_url=self.config['base-url'], web_data_path=jquery_dir)) @@ -168,6 +152,12 @@ self.test_queue.get(False) browser = FirefoxHelper() + # start firefox once to let it init the profile (and run system-wide + # add-ons post setup, blegh), and then kill it ... + browser.start('about:blank') + import time; time.sleep(5) + browser.stop() + # ... then actually run the test file browser.start(html_test_file.name) test_count = 0 error = False @@ -290,8 +280,10 @@ 'web_test': cw_path('devtools', 'data'), } - html = [''' + html = [''' + + @@ -317,7 +309,7 @@

QUnit example

-
    +
      ''') return u'\n'.join(html) diff -r c84ad981fc4a -r 4352b7ccde04 i18n/de.po --- a/i18n/de.po Thu Sep 25 15:49:13 2014 +0200 +++ b/i18n/de.po Fri Oct 17 18:16:58 2014 +0200 @@ -222,9 +222,6 @@ msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" msgstr "" -msgid "Any" -msgstr "irgendein" - msgid "Attributes permissions:" msgstr "Rechte der Attribute" @@ -1495,12 +1492,6 @@ msgid "components" msgstr "Komponenten" -msgid "components_etypenavigation" -msgstr "nach Typ filtern" - -msgid "components_etypenavigation_description" -msgstr "Erlaubt die Sortierung von Suchergebnissen nach Entitätstyp" - msgid "components_navigation" msgstr "Seitennavigation" @@ -4459,9 +4450,6 @@ msgid "we are not yet ready to handle this query" msgstr "Momentan können wir diese sparql-Anfrage noch nicht ausführen." -msgid "web sessions without CNX" -msgstr "" - msgid "wednesday" msgstr "Mittwoch" @@ -4553,9 +4541,18 @@ msgid "you should probably delete that property" msgstr "Sie sollten diese Eigenschaft wahrscheinlich löschen." +#~ msgid "Any" +#~ msgstr "irgendein" + #~ msgid "can't connect to source %s, some data may be missing" #~ msgstr "Keine Verbindung zu der Quelle %s, einige Daten könnten fehlen" +#~ msgid "components_etypenavigation" +#~ msgstr "nach Typ filtern" + +#~ msgid "components_etypenavigation_description" +#~ msgstr "Erlaubt die Sortierung von Suchergebnissen nach Entitätstyp" + #~ msgid "error while querying source %s, some data may be missing" #~ msgstr "" #~ "Fehler beim Zugriff auf Quelle %s, möglicherweise sind die Daten " diff -r c84ad981fc4a -r 4352b7ccde04 i18n/en.po --- a/i18n/en.po Thu Sep 25 15:49:13 2014 +0200 +++ b/i18n/en.po Fri Oct 17 18:16:58 2014 +0200 @@ -211,9 +211,6 @@ msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" msgstr "" -msgid "Any" -msgstr "" - msgid "Attributes permissions:" msgstr "" @@ -1451,12 +1448,6 @@ msgid "components" msgstr "" -msgid "components_etypenavigation" -msgstr "filtering by type" - -msgid "components_etypenavigation_description" -msgstr "permit to filter search results by entity type" - msgid "components_navigation" msgstr "page navigation" @@ -4350,9 +4341,6 @@ msgid "we are not yet ready to handle this query" msgstr "" -msgid "web sessions without CNX" -msgstr "" - msgid "wednesday" msgstr "" @@ -4441,3 +4429,9 @@ msgid "you should probably delete that property" msgstr "" + +#~ msgid "components_etypenavigation" +#~ msgstr "filtering by type" + +#~ msgid "components_etypenavigation_description" +#~ msgstr "permit to filter search results by entity type" diff -r c84ad981fc4a -r 4352b7ccde04 i18n/es.po --- a/i18n/es.po Thu Sep 25 15:49:13 2014 +0200 +++ b/i18n/es.po Fri Oct 17 18:16:58 2014 +0200 @@ -230,9 +230,6 @@ msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" msgstr "Relación agregada : %(entity_from)s %(rtype)s %(entity_to)s" -msgid "Any" -msgstr "Cualquiera" - msgid "Attributes permissions:" msgstr "Permisos de atributos:" @@ -1521,12 +1518,6 @@ msgid "components" msgstr "Componentes" -msgid "components_etypenavigation" -msgstr "Filtar por tipo" - -msgid "components_etypenavigation_description" -msgstr "Permite filtrar por tipo de entidad los resultados de una búsqueda" - msgid "components_navigation" msgstr "Navigación por página" @@ -4521,9 +4512,6 @@ msgid "we are not yet ready to handle this query" msgstr "Aún no podemos manejar este tipo de consulta Sparql" -msgid "web sessions without CNX" -msgstr "sesiones web sin conexión asociada" - msgid "wednesday" msgstr "Miércoles" @@ -4619,6 +4607,9 @@ #~ msgid "%s relation should not be in mapped" #~ msgstr "la relación %s no debería estar mapeada" +#~ msgid "Any" +#~ msgstr "Cualquiera" + #~ msgid "attribute/relation can't be mapped, only entity and relation types" #~ msgstr "" #~ "los atributos y las relaciones no pueden ser mapeados, solamente los " @@ -4633,6 +4624,12 @@ #~ msgid "can't mix dontcross and write options" #~ msgstr "no puede mezclar las opciones dontcross y write" +#~ msgid "components_etypenavigation" +#~ msgstr "Filtar por tipo" + +#~ msgid "components_etypenavigation_description" +#~ msgstr "Permite filtrar por tipo de entidad los resultados de una búsqueda" + #~ msgid "error while querying source %s, some data may be missing" #~ msgstr "" #~ "Un error ha ocurrido al interrogar %s, es posible que los \n" @@ -4649,6 +4646,9 @@ #~ msgid "unknown option(s): %s" #~ msgstr "opcion(es) desconocida(s): %s" +#~ msgid "web sessions without CNX" +#~ msgstr "sesiones web sin conexión asociada" + #~ msgid "you may want to specify something for %s" #~ msgstr "usted desea quizás especificar algo para la relación %s" diff -r c84ad981fc4a -r 4352b7ccde04 i18n/fr.po --- a/i18n/fr.po Thu Sep 25 15:49:13 2014 +0200 +++ b/i18n/fr.po Fri Oct 17 18:16:58 2014 +0200 @@ -224,9 +224,6 @@ msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s" msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s" -msgid "Any" -msgstr "Tous" - msgid "Attributes permissions:" msgstr "Permissions des attributs" @@ -1516,12 +1513,6 @@ msgid "components" msgstr "composants" -msgid "components_etypenavigation" -msgstr "filtrage par type" - -msgid "components_etypenavigation_description" -msgstr "permet de filtrer par type d'entité les résultats d'une recherche" - msgid "components_navigation" msgstr "navigation par page" @@ -4523,9 +4514,6 @@ msgstr "" "nous ne sommes pas capable de gérer ce type de requête sparql pour le moment" -msgid "web sessions without CNX" -msgstr "sessions web sans connexion associée" - msgid "wednesday" msgstr "mercredi" @@ -4621,6 +4609,9 @@ #~ msgid "%s relation should not be in mapped" #~ msgstr "la relation %s ne devrait pas ếtre mappé" +#~ msgid "Any" +#~ msgstr "Tous" + #~ msgid "attribute/relation can't be mapped, only entity and relation types" #~ msgstr "" #~ "les attributs et relations ne peuvent être mappés, uniquement les types " @@ -4635,6 +4626,12 @@ #~ msgid "can't mix dontcross and write options" #~ msgstr "ne peut mélanger dontcross et write options" +#~ msgid "components_etypenavigation" +#~ msgstr "filtrage par type" + +#~ msgid "components_etypenavigation_description" +#~ msgstr "permet de filtrer par type d'entité les résultats d'une recherche" + #~ msgid "error while querying source %s, some data may be missing" #~ msgstr "" #~ "une erreur est survenue en interrogeant %s, il est possible que les\n" @@ -4651,6 +4648,9 @@ #~ msgid "unknown option(s): %s" #~ msgstr "option(s) inconnue(s) : %s" +#~ msgid "web sessions without CNX" +#~ msgstr "sessions web sans connexion associée" + #~ msgid "you may want to specify something for %s" #~ msgstr "vous désirez peut-être spécifié quelque chose pour la relation %s" diff -r c84ad981fc4a -r 4352b7ccde04 server/sources/native.py --- a/server/sources/native.py Thu Sep 25 15:49:13 2014 +0200 +++ b/server/sources/native.py Fri Oct 17 18:16:58 2014 +0200 @@ -1023,14 +1023,16 @@ sql = self.sqlgen.select('tx_entity_actions', restr, ('txa_action', 'txa_public', 'txa_order', 'etype', 'eid', 'changes')) - cu = self.doexec(cnx, sql, restr) - actions = [tx.EntityAction(a,p,o,et,e,c and loads(self.binary_to_str(c))) - for a,p,o,et,e,c in cu.fetchall()] + with cnx.ensure_cnx_set: + cu = self.doexec(cnx, sql, restr) + actions = [tx.EntityAction(a,p,o,et,e,c and loads(self.binary_to_str(c))) + for a,p,o,et,e,c in cu.fetchall()] sql = self.sqlgen.select('tx_relation_actions', restr, ('txa_action', 'txa_public', 'txa_order', 'rtype', 'eid_from', 'eid_to')) - cu = self.doexec(cnx, sql, restr) - actions += [tx.RelationAction(*args) for args in cu.fetchall()] + with cnx.ensure_cnx_set: + cu = self.doexec(cnx, sql, restr) + actions += [tx.RelationAction(*args) for args in cu.fetchall()] return sorted(actions, key=lambda x: x.order) def undo_transaction(self, cnx, txuuid): diff -r c84ad981fc4a -r 4352b7ccde04 server/test/unittest_undo.py --- a/server/test/unittest_undo.py Thu Sep 25 15:49:13 2014 +0200 +++ b/server/test/unittest_undo.py Fri Oct 17 18:16:58 2014 +0200 @@ -75,6 +75,8 @@ self.assertTrue(self.txuuid) # test transaction api with self.admin_access.client_cnx() as cnx: + tx_actions = cnx.transaction_actions(self.txuuid) + self.assertEqual(len(tx_actions), 2, tx_actions) self.assertRaises(NoSuchTransaction, cnx.transaction_info, 'hop') self.assertRaises(NoSuchTransaction, diff -r c84ad981fc4a -r 4352b7ccde04 web/http_headers.py --- a/web/http_headers.py Thu Sep 25 15:49:13 2014 +0200 +++ b/web/http_headers.py Fri Oct 17 18:16:58 2014 +0200 @@ -445,6 +445,21 @@ l.append('%s=%s' % (k, v)) return ";".join(l) +def generateTrueFalse(value): + """ + Return 'true' or 'false' depending on the value. + + * 'true' values are `True`, `1`, `"true"` + * 'false' values are `False`, `0`, `"false"` + + """ + if (value in (True, 1) or + isinstance(value, basestring) and value.lower() == 'true'): + return 'true' + if (value in (False, 0) or + isinstance(value, basestring) and value.lower() == 'false'): + return 'false' + raise ValueError("Invalid true/false header value: %s" % value) class MimeType(object): def fromString(klass, mimeTypeString): @@ -1489,12 +1504,8 @@ 'Accept-Charset': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultCharset), 'Accept-Encoding': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultEncoding), 'Accept-Language': (tokenize, listParser(parseAcceptQvalue), dict), - 'Access-Control-Allow-Origin': (last, parseAllowOrigin,), - 'Access-Control-Allow-Credentials': (last, parseAllowCreds,), - 'Access-Control-Allow-Methods': (tokenize, listParser(parseHTTPMethod), list), 'Access-Control-Request-Method': (parseHTTPMethod, ), 'Access-Control-Request-Headers': (filterTokens, ), - 'Access-Control-Expose-Headers': (filterTokens, ), 'Authorization': (last, parseAuthorization), 'Cookie': (parseCookie,), 'Expect': (tokenize, listParser(parseExpect), dict), @@ -1521,8 +1532,6 @@ listGenerator(generateAcceptQvalue), singleHeader), 'Accept-Language': (iteritems, listGenerator(generateAcceptQvalue), singleHeader), 'Access-Control-Request-Method': (unique, str, singleHeader, ), - 'Access-Control-Expose-Headers': (listGenerator(str), ), - 'Access-Control-Allow-Headers': (listGenerator(str), ), 'Authorization': (generateAuthorization,), # what is "credentials" 'Cookie': (generateCookie, singleHeader), 'Expect': (iteritems, listGenerator(generateExpect), singleHeader), @@ -1544,6 +1553,11 @@ parser_response_headers = { 'Accept-Ranges': (tokenize, filterTokens), + 'Access-Control-Allow-Origin': (last, parseAllowOrigin,), + 'Access-Control-Allow-Credentials': (last, parseAllowCreds,), + 'Access-Control-Allow-Methods': (tokenize, listParser(parseHTTPMethod), list), + 'Access-Control-Allow-Headers': (listGenerator(str), ), + 'Access-Control-Expose-Headers': (filterTokens, ), 'Age': (last, int), 'ETag': (tokenize, ETag.parse), 'Location': (last,), # TODO: URI object? @@ -1559,6 +1573,11 @@ generator_response_headers = { 'Accept-Ranges': (generateList, singleHeader), + 'Access-Control-Allow-Origin': (unique, str, singleHeader), + 'Access-Control-Allow-Credentials': (generateTrueFalse, singleHeader), + 'Access-Control-Allow-Headers': (set, generateList, singleHeader), + 'Access-Control-Allow-Methods': (set, generateList, singleHeader), + 'Access-Control-Expose-Headers': (set, generateList, singleHeader), 'Age': (unique, str, singleHeader), 'ETag': (ETag.generate, singleHeader), 'Location': (unique, str, singleHeader), diff -r c84ad981fc4a -r 4352b7ccde04 web/test/unittest_http_headers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_http_headers.py Fri Oct 17 18:16:58 2014 +0200 @@ -0,0 +1,14 @@ +import unittest + +from cubicweb.web import http_headers + + +class TestGenerators(unittest.TestCase): + def test_generate_true_false(self): + for v in (True, 1, 'true', 'True', 'TRUE'): + self.assertEqual('true', http_headers.generateTrueFalse(v)) + for v in (False, 0, 'false', 'False', 'FALSE'): + self.assertEqual('false', http_headers.generateTrueFalse(v)) + + with self.assertRaises(ValueError): + http_headers.generateTrueFalse('any value') diff -r c84ad981fc4a -r 4352b7ccde04 wsgi/request.py --- a/wsgi/request.py Thu Sep 25 15:49:13 2014 +0200 +++ b/wsgi/request.py Fri Oct 17 18:16:58 2014 +0200 @@ -32,7 +32,9 @@ from urlparse import parse_qs from warnings import warn -from cubicweb.multipart import copy_file, parse_form_data +from cubicweb.multipart import ( + copy_file, parse_form_data, MultipartError, parse_options_header) +from cubicweb.web import RequestError from cubicweb.web.request import CubicWebRequestBase from cubicweb.wsgi import pformat, normalize_header @@ -81,10 +83,7 @@ self.content = environ['wsgi.input'] if files is not None: for key, part in files.iteritems(): - name = None - if part.filename is not None: - name = unicode(part.filename, self.encoding) - self.form[key] = (name, part.file) + self.form[key] = (part.filename, part.file) def __repr__(self): # Since this is called as part of error handling, we need to be very @@ -127,9 +126,18 @@ post = parse_qs(self.environ.get('QUERY_STRING', '')) files = None if self.method == 'POST': - forms, files = parse_form_data(self.environ, strict=True, - mem_limit=self.vreg.config['max-post-length']) - post.update(forms.dict) + content_type = self.environ.get('CONTENT_TYPE') + if not content_type: + raise RequestError("Missing Content-Type") + content_type, options = parse_options_header(content_type) + if content_type in ( + 'multipart/form-data', + 'application/x-www-form-urlencoded', + 'application/x-url-encoded'): + forms, files = parse_form_data( + self.environ, strict=True, + mem_limit=self.vreg.config['max-post-length']) + post.update(forms.dict) self.content.seek(0, 0) return post, files diff -r c84ad981fc4a -r 4352b7ccde04 wsgi/test/unittest_wsgi.py --- a/wsgi/test/unittest_wsgi.py Thu Sep 25 15:49:13 2014 +0200 +++ b/wsgi/test/unittest_wsgi.py Fri Oct 17 18:16:58 2014 +0200 @@ -6,6 +6,7 @@ from cubicweb.devtools.webtest import CubicWebTestTC from cubicweb.wsgi.request import CubicWebWsgiRequest +from cubicweb.multipart import MultipartError class WSGIAppTC(CubicWebTestTC): @@ -66,6 +67,19 @@ '/', params={'__login': self.admlogin, '__password': self.admpassword}) + def test_post_bad_form(self): + with self.assertRaises(MultipartError): + self.webapp.post( + '/', + params='badcontent', + headers={'Content-Type': 'multipart/form-data'}) + + def test_post_non_form(self): + self.webapp.post( + '/', + params='{}', + headers={'Content-Type': 'application/json'}) + def test_get_multiple_variables(self): r = webtest.app.TestRequest.blank('/?arg=1&arg=2') req = CubicWebWsgiRequest(r.environ, self.vreg) @@ -78,6 +92,17 @@ self.assertEqual([u'1', u'2'], req.form['arg']) + def test_post_files(self): + content_type, params = self.webapp.encode_multipart( + (), (('filefield', 'aname', 'acontent'),)) + r = webtest.app.TestRequest.blank( + '/', POST=params, content_type=content_type) + req = CubicWebWsgiRequest(r.environ, self.vreg) + self.assertIn('filefield', req.form) + fieldvalue = req.form['filefield'] + self.assertEqual(u'aname', fieldvalue[0]) + self.assertEqual('acontent', fieldvalue[1].read()) + def test_post_unicode_urlencoded(self): params = 'arg=%C3%A9' r = webtest.app.TestRequest.blank(