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 = ' |
|
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: |
|
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 == " |
|
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_ |
|
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_ |
|
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_ |
|
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( |
|
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 |
|
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 |
|
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 |
|
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 |
|
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