##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r3352:ac6efde5 merge default
parent child Browse files
Show More
@@ -0,0 +1,50 b''
1 |RCE| 4.15.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2019-01-01
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - Downloads: properly encode " in the filenames, and add RFC 5987 header for non-ascii files.
19 - Documentation: updated configuration for Nginx and reverse proxy.
20 - VCS: streaming will use now 100kb chunks for faster network throughput.
21
22
23 Security
24 ^^^^^^^^
25
26 - Diffs: fixed xss in context diff menu.
27 - Downloads: properly encode " in the filenames, prevents from hiding executable
28 files disguised in another type of file using crafted file names.
29
30 Performance
31 ^^^^^^^^^^^
32
33
34
35 Fixes
36 ^^^^^
37
38 - VCS: handle excessive slashes in from of the repo name path, fixes #5522.
39 This prevents 500 errors when excessive slashes are used
40 - SVN: support proxy-prefix properly, fixes #5521.
41 - Pull requests: validate ref types on API calls for pull request so users cannot
42 provide wrongs ones.
43 - Scheduler: fix url generation with proxy prefix.
44 - Celery: add DB connection ping to validate DB connection is working at worker startup.
45
46
47 Upgrade notes
48 ^^^^^^^^^^^^^
49
50 - Scheduled release addressing reported problems in 4.15.X releases.
@@ -47,3 +47,4 b' 7dc62c090881fb5d03268141e71e0940d7c3295d'
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
@@ -9,6 +9,7 b' Release Notes'
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.15.1.rst
12 release-notes-4.15.0.rst
13 release-notes-4.15.0.rst
13 release-notes-4.14.1.rst
14 release-notes-4.14.1.rst
14 release-notes-4.14.0.rst
15 release-notes-4.14.0.rst
@@ -56,6 +56,25 b' class TestCreatePullRequestApi(object):'
56 assert_error(id_, expected, given=response.body)
56 assert_error(id_, expected, given=response.body)
57
57
58 @pytest.mark.backends("git", "hg")
58 @pytest.mark.backends("git", "hg")
59 @pytest.mark.parametrize('source_ref', [
60 'bookmarg:default:initial'
61 ])
62 def test_create_with_wrong_refs_data(self, backend, source_ref):
63
64 data = self._prepare_data(backend)
65 data['source_ref'] = source_ref
66
67 id_, params = build_data(
68 self.apikey_regular, 'create_pull_request', **data)
69
70 response = api_call(self.app, params)
71
72 expected = "Ref `{}` type is not allowed. " \
73 "Only:['bookmark', 'book', 'tag', 'branch'] " \
74 "are possible.".format(source_ref)
75 assert_error(id_, expected, given=response.body)
76
77 @pytest.mark.backends("git", "hg")
59 def test_create_with_correct_data(self, backend):
78 def test_create_with_correct_data(self, backend):
60 data = self._prepare_data(backend)
79 data = self._prepare_data(backend)
61 RepoModel().revoke_user_permission(
80 RepoModel().revoke_user_permission(
@@ -84,11 +84,11 b' class TestResolveRefOrError(object):'
84
84
85 def test_non_supported_refs(self):
85 def test_non_supported_refs(self):
86 repo = Mock()
86 repo = Mock()
87 ref = 'ancestor:ref'
87 ref = 'bookmark:ref'
88 with pytest.raises(JSONRPCError) as excinfo:
88 with pytest.raises(JSONRPCError) as excinfo:
89 utils.resolve_ref_or_error(ref, repo)
89 utils.resolve_ref_or_error(ref, repo)
90 expected_message = (
90 expected_message = (
91 'The specified value:ancestor:`ref` does not exist, or is not allowed.')
91 'The specified value:bookmark:`ref` does not exist, or is not allowed.')
92 assert excinfo.value.message == expected_message
92 assert excinfo.value.message == expected_message
93
93
94 def test_branch_is_not_found(self):
94 def test_branch_is_not_found(self):
@@ -388,7 +388,19 b' def get_commit_or_error(ref, repo):'
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
389
389
390
390
391 def resolve_ref_or_error(ref, repo):
391 def _get_ref_hash(repo, type_, name):
392 vcs_repo = repo.scm_instance()
393 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
394 return vcs_repo.branches[name]
395 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
396 return vcs_repo.bookmarks[name]
397 else:
398 raise ValueError()
399
400
401 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
402 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
403
392 def _parse_ref(type_, name, hash_=None):
404 def _parse_ref(type_, name, hash_=None):
393 return type_, name, hash_
405 return type_, name, hash_
394
406
@@ -399,6 +411,12 b' def resolve_ref_or_error(ref, repo):'
399 'Ref `{ref}` given in a wrong format. Please check the API'
411 'Ref `{ref}` given in a wrong format. Please check the API'
400 ' documentation for more details'.format(ref=ref))
412 ' documentation for more details'.format(ref=ref))
401
413
414 if ref_type not in allowed_ref_types:
415 raise JSONRPCError(
416 'Ref `{ref}` type is not allowed. '
417 'Only:{allowed_refs} are possible.'.format(
418 ref=ref, allowed_refs=allowed_ref_types))
419
402 try:
420 try:
403 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
421 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
404 except (KeyError, ValueError):
422 except (KeyError, ValueError):
@@ -429,13 +447,3 b' def _get_commit_dict('
429 "raw_diff": raw_diff,
447 "raw_diff": raw_diff,
430 "stats": stats
448 "stats": stats
431 }
449 }
432
433
434 def _get_ref_hash(repo, type_, name):
435 vcs_repo = repo.scm_instance()
436 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
437 return vcs_repo.branches[name]
438 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
439 return vcs_repo.bookmarks[name]
440 else:
441 raise ValueError()
@@ -428,7 +428,7 b' class TestRawFileHandling(object):'
428 repo_name=backend.repo_name,
428 repo_name=backend.repo_name,
429 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
429 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
430
430
431 assert response.content_disposition == "attachment; filename=nodes.py"
431 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
432 assert response.content_type == "text/x-python"
432 assert response.content_type == "text/x-python"
433
433
434 def test_download_file_wrong_cs(self, backend):
434 def test_download_file_wrong_cs(self, backend):
@@ -24,6 +24,7 b' import os'
24 import shutil
24 import shutil
25 import tempfile
25 import tempfile
26 import collections
26 import collections
27 import urllib
27
28
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.view import view_config
30 from pyramid.view import view_config
@@ -709,9 +710,14 b' class RepoFilesView(RepoAppView):'
709
710
710 return Response(html)
711 return Response(html)
711
712
712 def _get_attachement_disposition(self, f_path):
713 def _get_attachement_headers(self, f_path):
713 return 'attachment; filename=%s' % \
714 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
714 safe_str(f_path.split(Repository.NAME_SEP)[-1])
715 safe_path = f_name.replace('"', '\\"')
716 encoded_path = urllib.quote(f_name)
717
718 return "attachment; " \
719 "filename=\"{}\"; " \
720 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
715
721
716 @LoginRequired()
722 @LoginRequired()
717 @HasRepoPermissionAnyDecorator(
723 @HasRepoPermissionAnyDecorator(
@@ -766,7 +772,7 b' class RepoFilesView(RepoAppView):'
766 mimetype, disposition = 'text/plain', 'inline'
772 mimetype, disposition = 'text/plain', 'inline'
767
773
768 if disposition == 'attachment':
774 if disposition == 'attachment':
769 disposition = self._get_attachement_disposition(f_path)
775 disposition = self._get_attachement_headers(f_path)
770
776
771 def stream_node():
777 def stream_node():
772 yield file_node.raw_bytes
778 yield file_node.raw_bytes
@@ -805,7 +811,7 b' class RepoFilesView(RepoAppView):'
805 # overwrite our pointer with the REAL large-file
811 # overwrite our pointer with the REAL large-file
806 file_node = lf_node
812 file_node = lf_node
807
813
808 disposition = self._get_attachement_disposition(f_path)
814 disposition = self._get_attachement_headers(f_path)
809
815
810 def stream_node():
816 def stream_node():
811 yield file_node.raw_bytes
817 yield file_node.raw_bytes
@@ -51,7 +51,8 b' class SimpleSvnApp(object):'
51 data = environ['wsgi.input']
51 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
52 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
53 has_content_length = 'CONTENT_LENGTH' in environ
54 path_info = self._get_url(environ['PATH_INFO'])
54 path_info = self._get_url(
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
55 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 log.debug('Handling: %s method via `%s`', req_method, path_info)
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
57
58
@@ -117,9 +118,9 b' class SimpleSvnApp(object):'
117 response_headers)
118 response_headers)
118 return response.iter_content(chunk_size=1024)
119 return response.iter_content(chunk_size=1024)
119
120
120 def _get_url(self, path):
121 def _get_url(self, svn_http_server, path):
121 url_path = urlparse.urljoin(
122 svn_http_server_url = (svn_http_server or '').rstrip('/')
122 self.config.get('subversion_http_server_url', ''), path)
123 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
123 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
124 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
124 return url_path
125 return url_path
125
126
@@ -43,6 +43,7 b' import sqlalchemy.exc'
43 import sqlalchemy.sql
43 import sqlalchemy.sql
44 import webob
44 import webob
45 import pyramid.threadlocal
45 import pyramid.threadlocal
46 from pyramid.settings import asbool
46
47
47 import rhodecode
48 import rhodecode
48 from rhodecode.translation import _, _pluralize
49 from rhodecode.translation import _, _pluralize
@@ -361,7 +362,8 b' def ping_connection(connection, branch):'
361 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
362 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
362 """Custom engine_from_config functions."""
363 """Custom engine_from_config functions."""
363 log = logging.getLogger('sqlalchemy.engine')
364 log = logging.getLogger('sqlalchemy.engine')
364 _ping_connection = configuration.pop('sqlalchemy.db1.ping_connection', None)
365 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
366 debug = asbool(configuration.get('debug'))
365
367
366 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
368 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
367
369
@@ -370,12 +372,12 b' def engine_from_config(configuration, pr'
370 normal = '\x1b[0m'
372 normal = '\x1b[0m'
371 return ''.join([color_seq, sql, normal])
373 return ''.join([color_seq, sql, normal])
372
374
373 if configuration['debug'] or _ping_connection:
375 if use_ping_connection:
376 log.debug('Adding ping_connection on the engine config.')
374 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
377 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
375
378
376 if configuration['debug']:
379 if debug:
377 # attach events only for debug configuration
380 # attach events only for debug configuration
378
379 def before_cursor_execute(conn, cursor, statement,
381 def before_cursor_execute(conn, cursor, statement,
380 parameters, context, executemany):
382 parameters, context, executemany):
381 setattr(conn, 'query_start_time', time.time())
383 setattr(conn, 'query_start_time', time.time())
@@ -394,10 +396,8 b' def engine_from_config(configuration, pr'
394 parameters, context, executemany):
396 parameters, context, executemany):
395 delattr(conn, 'query_start_time')
397 delattr(conn, 'query_start_time')
396
398
397 sqlalchemy.event.listen(engine, "before_cursor_execute",
399 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
398 before_cursor_execute)
400 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
399 sqlalchemy.event.listen(engine, "after_cursor_execute",
400 after_cursor_execute)
401
401
402 return engine
402 return engine
403
403
@@ -92,6 +92,8 b' class PullRequestModel(BaseModel):'
92 'This pull request cannot be updated because the source '
92 'This pull request cannot be updated because the source '
93 'reference is missing.'),
93 'reference is missing.'),
94 }
94 }
95 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
95
97
96 def __get_pull_request(self, pull_request):
98 def __get_pull_request(self, pull_request):
97 return self._get_instance((
99 return self._get_instance((
@@ -633,7 +635,7 b' class PullRequestModel(BaseModel):'
633
635
634 def has_valid_update_type(self, pull_request):
636 def has_valid_update_type(self, pull_request):
635 source_ref_type = pull_request.source_ref_parts.type
637 source_ref_type = pull_request.source_ref_parts.type
636 return source_ref_type in ['book', 'branch', 'tag']
638 return source_ref_type in self.REF_TYPES
637
639
638 def update_commits(self, pull_request):
640 def update_commits(self, pull_request):
639 """
641 """
@@ -713,7 +715,7 b' class PullRequestModel(BaseModel):'
713 pull_request_version = pull_request
715 pull_request_version = pull_request
714
716
715 try:
717 try:
716 if target_ref_type in ('tag', 'branch', 'book'):
718 if target_ref_type in self.REF_TYPES:
717 target_commit = target_repo.get_commit(target_ref_name)
719 target_commit = target_repo.get_commit(target_ref_name)
718 else:
720 else:
719 target_commit = target_repo.get_commit(target_ref_id)
721 target_commit = target_repo.get_commit(target_ref_id)
@@ -1289,7 +1291,7 b' class PullRequestModel(BaseModel):'
1289 return merge_state
1291 return merge_state
1290
1292
1291 def _refresh_reference(self, reference, vcs_repository):
1293 def _refresh_reference(self, reference, vcs_repository):
1292 if reference.type in ('branch', 'book'):
1294 if reference.type in self.UPDATABLE_REF_TYPES:
1293 name_or_id = reference.name
1295 name_or_id = reference.name
1294 else:
1296 else:
1295 name_or_id = reference.commit_id
1297 name_or_id = reference.commit_id
@@ -909,6 +909,8 b' def get_comments_for(diff_type, comments'
909 };
909 };
910
910
911 var animateText = $.debounce(100, function(fPath, anchorId) {
911 var animateText = $.debounce(100, function(fPath, anchorId) {
912 fPath = Select2.util.escapeMarkup(fPath);
913
912 // animate setting the text
914 // animate setting the text
913 var callback = function () {
915 var callback = function () {
914 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
916 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
@@ -161,9 +161,17 b' class TestSimpleSvnApp(object):'
161 response_headers = self.app._get_response_headers(headers)
161 response_headers = self.app._get_response_headers(headers)
162 assert sorted(response_headers) == sorted(expected_headers)
162 assert sorted(response_headers) == sorted(expected_headers)
163
163
164 def test_get_url(self):
164 @pytest.mark.parametrize('svn_http_url, path_info, expected_url', [
165 url = self.app._get_url(self.path)
165 ('http://localhost:8200', '/repo_name', 'http://localhost:8200/repo_name'),
166 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
166 ('http://localhost:8200///', '/repo_name', 'http://localhost:8200/repo_name'),
167 ('http://localhost:8200', '/group/repo_name', 'http://localhost:8200/group/repo_name'),
168 ('http://localhost:8200/', '/group/repo_name', 'http://localhost:8200/group/repo_name'),
169 ('http://localhost:8200/prefix', '/repo_name', 'http://localhost:8200/prefix/repo_name'),
170 ('http://localhost:8200/prefix', 'repo_name', 'http://localhost:8200/prefix/repo_name'),
171 ('http://localhost:8200/prefix', '/group/repo_name', 'http://localhost:8200/prefix/group/repo_name')
172 ])
173 def test_get_url(self, svn_http_url, path_info, expected_url):
174 url = self.app._get_url(svn_http_url, path_info)
167 assert url == expected_url
175 assert url == expected_url
168
176
169 def test_call(self):
177 def test_call(self):
General Comments 0
You need to be logged in to leave comments. Login now