##// END OF EJS Templates
py3: fix non-ASCII URLs - decode unicode correctly before passing them to controllers as unicode strings...
Mads Kiilerich -
r8082:6c381371 default
parent child Browse files
Show More
@@ -1,790 +1,810 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Routes configuration
15 Routes configuration
16
16
17 The more specific and detailed routes should be defined first so they
17 The more specific and detailed routes should be defined first so they
18 may take precedent over the more generic routes. For more information
18 may take precedent over the more generic routes. For more information
19 refer to the routes manual at http://routes.groovie.org/docs/
19 refer to the routes manual at http://routes.groovie.org/docs/
20 """
20 """
21
21
22 from routes import Mapper
22 import routes
23 from tg import request
23 from tg import request
24
24
25 from kallithea.lib.utils2 import safe_str
26
25
27
26 # prefix for non repository related links needs to be prefixed with `/`
28 # prefix for non repository related links needs to be prefixed with `/`
27 ADMIN_PREFIX = '/_admin'
29 ADMIN_PREFIX = '/_admin'
28
30
29
31
32 class Mapper(routes.Mapper):
33 """
34 Subclassed Mapper with routematch patched to decode "unicode" str url to
35 *real* unicode str before applying matches and invoking controller methods.
36 """
37
38 def routematch(self, url=None, environ=None):
39 """
40 routematch that also decode url from "fake bytes" to real unicode
41 string before matching and invoking controllers.
42 """
43 # Process url like get_path_info does ... but PATH_INFO has already
44 # been retrieved from environ and is passed, so - let's just use that
45 # instead.
46 url = safe_str(url.encode('latin1'))
47 return super().routematch(url=url, environ=environ)
48
49
30 def make_map(config):
50 def make_map(config):
31 """Create, configure and return the routes Mapper"""
51 """Create, configure and return the routes Mapper"""
32 rmap = Mapper(directory=config['paths']['controllers'],
52 rmap = Mapper(directory=config['paths']['controllers'],
33 always_scan=config['debug'])
53 always_scan=config['debug'])
34 rmap.minimization = False
54 rmap.minimization = False
35 rmap.explicit = False
55 rmap.explicit = False
36
56
37 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
57 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
38
58
39 def check_repo(environ, match_dict):
59 def check_repo(environ, match_dict):
40 """
60 """
41 Check for valid repository for proper 404 handling.
61 Check for valid repository for proper 404 handling.
42 Also, a bit of side effect modifying match_dict ...
62 Also, a bit of side effect modifying match_dict ...
43 """
63 """
44 if match_dict.get('f_path'):
64 if match_dict.get('f_path'):
45 # fix for multiple initial slashes that causes errors
65 # fix for multiple initial slashes that causes errors
46 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
66 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
47
67
48 return is_valid_repo(match_dict['repo_name'], config['base_path'])
68 return is_valid_repo(match_dict['repo_name'], config['base_path'])
49
69
50 def check_group(environ, match_dict):
70 def check_group(environ, match_dict):
51 """
71 """
52 check for valid repository group for proper 404 handling
72 check for valid repository group for proper 404 handling
53
73
54 :param environ:
74 :param environ:
55 :param match_dict:
75 :param match_dict:
56 """
76 """
57 repo_group_name = match_dict.get('group_name')
77 repo_group_name = match_dict.get('group_name')
58 return is_valid_repo_group(repo_group_name, config['base_path'])
78 return is_valid_repo_group(repo_group_name, config['base_path'])
59
79
60 def check_group_skip_path(environ, match_dict):
80 def check_group_skip_path(environ, match_dict):
61 """
81 """
62 check for valid repository group for proper 404 handling, but skips
82 check for valid repository group for proper 404 handling, but skips
63 verification of existing path
83 verification of existing path
64
84
65 :param environ:
85 :param environ:
66 :param match_dict:
86 :param match_dict:
67 """
87 """
68 repo_group_name = match_dict.get('group_name')
88 repo_group_name = match_dict.get('group_name')
69 return is_valid_repo_group(repo_group_name, config['base_path'],
89 return is_valid_repo_group(repo_group_name, config['base_path'],
70 skip_path_check=True)
90 skip_path_check=True)
71
91
72 def check_user_group(environ, match_dict):
92 def check_user_group(environ, match_dict):
73 """
93 """
74 check for valid user group for proper 404 handling
94 check for valid user group for proper 404 handling
75
95
76 :param environ:
96 :param environ:
77 :param match_dict:
97 :param match_dict:
78 """
98 """
79 return True
99 return True
80
100
81 def check_int(environ, match_dict):
101 def check_int(environ, match_dict):
82 return match_dict.get('id').isdigit()
102 return match_dict.get('id').isdigit()
83
103
84 #==========================================================================
104 #==========================================================================
85 # CUSTOM ROUTES HERE
105 # CUSTOM ROUTES HERE
86 #==========================================================================
106 #==========================================================================
87
107
88 # MAIN PAGE
108 # MAIN PAGE
89 rmap.connect('home', '/', controller='home')
109 rmap.connect('home', '/', controller='home')
90 rmap.connect('about', '/about', controller='home', action='about')
110 rmap.connect('about', '/about', controller='home', action='about')
91 rmap.redirect('/favicon.ico', '/images/favicon.ico')
111 rmap.redirect('/favicon.ico', '/images/favicon.ico')
92 rmap.connect('repo_switcher_data', '/_repos', controller='home',
112 rmap.connect('repo_switcher_data', '/_repos', controller='home',
93 action='repo_switcher_data')
113 action='repo_switcher_data')
94 rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home',
114 rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home',
95 action='users_and_groups_data')
115 action='users_and_groups_data')
96
116
97 rmap.connect('rst_help',
117 rmap.connect('rst_help',
98 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
118 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
99 _static=True)
119 _static=True)
100 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
120 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
101 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
121 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
102
122
103 # ADMIN REPOSITORY ROUTES
123 # ADMIN REPOSITORY ROUTES
104 with rmap.submapper(path_prefix=ADMIN_PREFIX,
124 with rmap.submapper(path_prefix=ADMIN_PREFIX,
105 controller='admin/repos') as m:
125 controller='admin/repos') as m:
106 m.connect("repos", "/repos",
126 m.connect("repos", "/repos",
107 action="create", conditions=dict(method=["POST"]))
127 action="create", conditions=dict(method=["POST"]))
108 m.connect("repos", "/repos",
128 m.connect("repos", "/repos",
109 conditions=dict(method=["GET"]))
129 conditions=dict(method=["GET"]))
110 m.connect("new_repo", "/create_repository",
130 m.connect("new_repo", "/create_repository",
111 action="create_repository", conditions=dict(method=["GET"]))
131 action="create_repository", conditions=dict(method=["GET"]))
112 m.connect("update_repo", "/repos/{repo_name:.*?}",
132 m.connect("update_repo", "/repos/{repo_name:.*?}",
113 action="update", conditions=dict(method=["POST"],
133 action="update", conditions=dict(method=["POST"],
114 function=check_repo))
134 function=check_repo))
115 m.connect("delete_repo", "/repos/{repo_name:.*?}/delete",
135 m.connect("delete_repo", "/repos/{repo_name:.*?}/delete",
116 action="delete", conditions=dict(method=["POST"]))
136 action="delete", conditions=dict(method=["POST"]))
117
137
118 # ADMIN REPOSITORY GROUPS ROUTES
138 # ADMIN REPOSITORY GROUPS ROUTES
119 with rmap.submapper(path_prefix=ADMIN_PREFIX,
139 with rmap.submapper(path_prefix=ADMIN_PREFIX,
120 controller='admin/repo_groups') as m:
140 controller='admin/repo_groups') as m:
121 m.connect("repos_groups", "/repo_groups",
141 m.connect("repos_groups", "/repo_groups",
122 action="create", conditions=dict(method=["POST"]))
142 action="create", conditions=dict(method=["POST"]))
123 m.connect("repos_groups", "/repo_groups",
143 m.connect("repos_groups", "/repo_groups",
124 conditions=dict(method=["GET"]))
144 conditions=dict(method=["GET"]))
125 m.connect("new_repos_group", "/repo_groups/new",
145 m.connect("new_repos_group", "/repo_groups/new",
126 action="new", conditions=dict(method=["GET"]))
146 action="new", conditions=dict(method=["GET"]))
127 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
147 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
128 action="update", conditions=dict(method=["POST"],
148 action="update", conditions=dict(method=["POST"],
129 function=check_group))
149 function=check_group))
130
150
131 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
151 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
132 action="show", conditions=dict(method=["GET"],
152 action="show", conditions=dict(method=["GET"],
133 function=check_group))
153 function=check_group))
134
154
135 # EXTRAS REPO GROUP ROUTES
155 # EXTRAS REPO GROUP ROUTES
136 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
156 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
137 action="edit",
157 action="edit",
138 conditions=dict(method=["GET"], function=check_group))
158 conditions=dict(method=["GET"], function=check_group))
139
159
140 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
160 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
141 action="edit_repo_group_advanced",
161 action="edit_repo_group_advanced",
142 conditions=dict(method=["GET"], function=check_group))
162 conditions=dict(method=["GET"], function=check_group))
143
163
144 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
164 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
145 action="edit_repo_group_perms",
165 action="edit_repo_group_perms",
146 conditions=dict(method=["GET"], function=check_group))
166 conditions=dict(method=["GET"], function=check_group))
147 m.connect("edit_repo_group_perms_update", "/repo_groups/{group_name:.*?}/edit/permissions",
167 m.connect("edit_repo_group_perms_update", "/repo_groups/{group_name:.*?}/edit/permissions",
148 action="update_perms",
168 action="update_perms",
149 conditions=dict(method=["POST"], function=check_group))
169 conditions=dict(method=["POST"], function=check_group))
150 m.connect("edit_repo_group_perms_delete", "/repo_groups/{group_name:.*?}/edit/permissions/delete",
170 m.connect("edit_repo_group_perms_delete", "/repo_groups/{group_name:.*?}/edit/permissions/delete",
151 action="delete_perms",
171 action="delete_perms",
152 conditions=dict(method=["POST"], function=check_group))
172 conditions=dict(method=["POST"], function=check_group))
153
173
154 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}/delete",
174 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}/delete",
155 action="delete", conditions=dict(method=["POST"],
175 action="delete", conditions=dict(method=["POST"],
156 function=check_group_skip_path))
176 function=check_group_skip_path))
157
177
158 # ADMIN USER ROUTES
178 # ADMIN USER ROUTES
159 with rmap.submapper(path_prefix=ADMIN_PREFIX,
179 with rmap.submapper(path_prefix=ADMIN_PREFIX,
160 controller='admin/users') as m:
180 controller='admin/users') as m:
161 m.connect("new_user", "/users/new",
181 m.connect("new_user", "/users/new",
162 action="create", conditions=dict(method=["POST"]))
182 action="create", conditions=dict(method=["POST"]))
163 m.connect("users", "/users",
183 m.connect("users", "/users",
164 conditions=dict(method=["GET"]))
184 conditions=dict(method=["GET"]))
165 m.connect("formatted_users", "/users.{format}",
185 m.connect("formatted_users", "/users.{format}",
166 conditions=dict(method=["GET"]))
186 conditions=dict(method=["GET"]))
167 m.connect("new_user", "/users/new",
187 m.connect("new_user", "/users/new",
168 action="new", conditions=dict(method=["GET"]))
188 action="new", conditions=dict(method=["GET"]))
169 m.connect("update_user", "/users/{id}",
189 m.connect("update_user", "/users/{id}",
170 action="update", conditions=dict(method=["POST"]))
190 action="update", conditions=dict(method=["POST"]))
171 m.connect("delete_user", "/users/{id}/delete",
191 m.connect("delete_user", "/users/{id}/delete",
172 action="delete", conditions=dict(method=["POST"]))
192 action="delete", conditions=dict(method=["POST"]))
173 m.connect("edit_user", "/users/{id}/edit",
193 m.connect("edit_user", "/users/{id}/edit",
174 action="edit", conditions=dict(method=["GET"]))
194 action="edit", conditions=dict(method=["GET"]))
175
195
176 # EXTRAS USER ROUTES
196 # EXTRAS USER ROUTES
177 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
197 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
178 action="edit_advanced", conditions=dict(method=["GET"]))
198 action="edit_advanced", conditions=dict(method=["GET"]))
179
199
180 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
200 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
181 action="edit_api_keys", conditions=dict(method=["GET"]))
201 action="edit_api_keys", conditions=dict(method=["GET"]))
182 m.connect("edit_user_api_keys_update", "/users/{id}/edit/api_keys",
202 m.connect("edit_user_api_keys_update", "/users/{id}/edit/api_keys",
183 action="add_api_key", conditions=dict(method=["POST"]))
203 action="add_api_key", conditions=dict(method=["POST"]))
184 m.connect("edit_user_api_keys_delete", "/users/{id}/edit/api_keys/delete",
204 m.connect("edit_user_api_keys_delete", "/users/{id}/edit/api_keys/delete",
185 action="delete_api_key", conditions=dict(method=["POST"]))
205 action="delete_api_key", conditions=dict(method=["POST"]))
186
206
187 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
207 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
188 action="edit_ssh_keys", conditions=dict(method=["GET"]))
208 action="edit_ssh_keys", conditions=dict(method=["GET"]))
189 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
209 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
190 action="ssh_keys_add", conditions=dict(method=["POST"]))
210 action="ssh_keys_add", conditions=dict(method=["POST"]))
191 m.connect("edit_user_ssh_keys_delete", "/users/{id}/edit/ssh_keys/delete",
211 m.connect("edit_user_ssh_keys_delete", "/users/{id}/edit/ssh_keys/delete",
192 action="ssh_keys_delete", conditions=dict(method=["POST"]))
212 action="ssh_keys_delete", conditions=dict(method=["POST"]))
193
213
194 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
214 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
195 action="edit_perms", conditions=dict(method=["GET"]))
215 action="edit_perms", conditions=dict(method=["GET"]))
196 m.connect("edit_user_perms_update", "/users/{id}/edit/permissions",
216 m.connect("edit_user_perms_update", "/users/{id}/edit/permissions",
197 action="update_perms", conditions=dict(method=["POST"]))
217 action="update_perms", conditions=dict(method=["POST"]))
198
218
199 m.connect("edit_user_emails", "/users/{id}/edit/emails",
219 m.connect("edit_user_emails", "/users/{id}/edit/emails",
200 action="edit_emails", conditions=dict(method=["GET"]))
220 action="edit_emails", conditions=dict(method=["GET"]))
201 m.connect("edit_user_emails_update", "/users/{id}/edit/emails",
221 m.connect("edit_user_emails_update", "/users/{id}/edit/emails",
202 action="add_email", conditions=dict(method=["POST"]))
222 action="add_email", conditions=dict(method=["POST"]))
203 m.connect("edit_user_emails_delete", "/users/{id}/edit/emails/delete",
223 m.connect("edit_user_emails_delete", "/users/{id}/edit/emails/delete",
204 action="delete_email", conditions=dict(method=["POST"]))
224 action="delete_email", conditions=dict(method=["POST"]))
205
225
206 m.connect("edit_user_ips", "/users/{id}/edit/ips",
226 m.connect("edit_user_ips", "/users/{id}/edit/ips",
207 action="edit_ips", conditions=dict(method=["GET"]))
227 action="edit_ips", conditions=dict(method=["GET"]))
208 m.connect("edit_user_ips_update", "/users/{id}/edit/ips",
228 m.connect("edit_user_ips_update", "/users/{id}/edit/ips",
209 action="add_ip", conditions=dict(method=["POST"]))
229 action="add_ip", conditions=dict(method=["POST"]))
210 m.connect("edit_user_ips_delete", "/users/{id}/edit/ips/delete",
230 m.connect("edit_user_ips_delete", "/users/{id}/edit/ips/delete",
211 action="delete_ip", conditions=dict(method=["POST"]))
231 action="delete_ip", conditions=dict(method=["POST"]))
212
232
213 # ADMIN USER GROUPS REST ROUTES
233 # ADMIN USER GROUPS REST ROUTES
214 with rmap.submapper(path_prefix=ADMIN_PREFIX,
234 with rmap.submapper(path_prefix=ADMIN_PREFIX,
215 controller='admin/user_groups') as m:
235 controller='admin/user_groups') as m:
216 m.connect("users_groups", "/user_groups",
236 m.connect("users_groups", "/user_groups",
217 action="create", conditions=dict(method=["POST"]))
237 action="create", conditions=dict(method=["POST"]))
218 m.connect("users_groups", "/user_groups",
238 m.connect("users_groups", "/user_groups",
219 conditions=dict(method=["GET"]))
239 conditions=dict(method=["GET"]))
220 m.connect("new_users_group", "/user_groups/new",
240 m.connect("new_users_group", "/user_groups/new",
221 action="new", conditions=dict(method=["GET"]))
241 action="new", conditions=dict(method=["GET"]))
222 m.connect("update_users_group", "/user_groups/{id}",
242 m.connect("update_users_group", "/user_groups/{id}",
223 action="update", conditions=dict(method=["POST"]))
243 action="update", conditions=dict(method=["POST"]))
224 m.connect("delete_users_group", "/user_groups/{id}/delete",
244 m.connect("delete_users_group", "/user_groups/{id}/delete",
225 action="delete", conditions=dict(method=["POST"]))
245 action="delete", conditions=dict(method=["POST"]))
226 m.connect("edit_users_group", "/user_groups/{id}/edit",
246 m.connect("edit_users_group", "/user_groups/{id}/edit",
227 action="edit", conditions=dict(method=["GET"]),
247 action="edit", conditions=dict(method=["GET"]),
228 function=check_user_group)
248 function=check_user_group)
229
249
230 # EXTRAS USER GROUP ROUTES
250 # EXTRAS USER GROUP ROUTES
231 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
251 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
232 action="edit_default_perms", conditions=dict(method=["GET"]))
252 action="edit_default_perms", conditions=dict(method=["GET"]))
233 m.connect("edit_user_group_default_perms_update", "/user_groups/{id}/edit/default_perms",
253 m.connect("edit_user_group_default_perms_update", "/user_groups/{id}/edit/default_perms",
234 action="update_default_perms", conditions=dict(method=["POST"]))
254 action="update_default_perms", conditions=dict(method=["POST"]))
235
255
236 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
256 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
237 action="edit_perms", conditions=dict(method=["GET"]))
257 action="edit_perms", conditions=dict(method=["GET"]))
238 m.connect("edit_user_group_perms_update", "/user_groups/{id}/edit/perms",
258 m.connect("edit_user_group_perms_update", "/user_groups/{id}/edit/perms",
239 action="update_perms", conditions=dict(method=["POST"]))
259 action="update_perms", conditions=dict(method=["POST"]))
240 m.connect("edit_user_group_perms_delete", "/user_groups/{id}/edit/perms/delete",
260 m.connect("edit_user_group_perms_delete", "/user_groups/{id}/edit/perms/delete",
241 action="delete_perms", conditions=dict(method=["POST"]))
261 action="delete_perms", conditions=dict(method=["POST"]))
242
262
243 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
263 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
244 action="edit_advanced", conditions=dict(method=["GET"]))
264 action="edit_advanced", conditions=dict(method=["GET"]))
245
265
246 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
266 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
247 action="edit_members", conditions=dict(method=["GET"]))
267 action="edit_members", conditions=dict(method=["GET"]))
248
268
249 # ADMIN PERMISSIONS ROUTES
269 # ADMIN PERMISSIONS ROUTES
250 with rmap.submapper(path_prefix=ADMIN_PREFIX,
270 with rmap.submapper(path_prefix=ADMIN_PREFIX,
251 controller='admin/permissions') as m:
271 controller='admin/permissions') as m:
252 m.connect("admin_permissions", "/permissions",
272 m.connect("admin_permissions", "/permissions",
253 action="permission_globals", conditions=dict(method=["POST"]))
273 action="permission_globals", conditions=dict(method=["POST"]))
254 m.connect("admin_permissions", "/permissions",
274 m.connect("admin_permissions", "/permissions",
255 action="permission_globals", conditions=dict(method=["GET"]))
275 action="permission_globals", conditions=dict(method=["GET"]))
256
276
257 m.connect("admin_permissions_ips", "/permissions/ips",
277 m.connect("admin_permissions_ips", "/permissions/ips",
258 action="permission_ips", conditions=dict(method=["GET"]))
278 action="permission_ips", conditions=dict(method=["GET"]))
259
279
260 m.connect("admin_permissions_perms", "/permissions/perms",
280 m.connect("admin_permissions_perms", "/permissions/perms",
261 action="permission_perms", conditions=dict(method=["GET"]))
281 action="permission_perms", conditions=dict(method=["GET"]))
262
282
263 # ADMIN DEFAULTS ROUTES
283 # ADMIN DEFAULTS ROUTES
264 with rmap.submapper(path_prefix=ADMIN_PREFIX,
284 with rmap.submapper(path_prefix=ADMIN_PREFIX,
265 controller='admin/defaults') as m:
285 controller='admin/defaults') as m:
266 m.connect('defaults', '/defaults')
286 m.connect('defaults', '/defaults')
267 m.connect('defaults_update', 'defaults/{id}/update',
287 m.connect('defaults_update', 'defaults/{id}/update',
268 action="update", conditions=dict(method=["POST"]))
288 action="update", conditions=dict(method=["POST"]))
269
289
270 # ADMIN AUTH SETTINGS
290 # ADMIN AUTH SETTINGS
271 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
291 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
272 controller='admin/auth_settings', action='auth_settings',
292 controller='admin/auth_settings', action='auth_settings',
273 conditions=dict(method=["POST"]))
293 conditions=dict(method=["POST"]))
274 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
294 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
275 controller='admin/auth_settings')
295 controller='admin/auth_settings')
276
296
277 # ADMIN SETTINGS ROUTES
297 # ADMIN SETTINGS ROUTES
278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
298 with rmap.submapper(path_prefix=ADMIN_PREFIX,
279 controller='admin/settings') as m:
299 controller='admin/settings') as m:
280 m.connect("admin_settings", "/settings",
300 m.connect("admin_settings", "/settings",
281 action="settings_vcs", conditions=dict(method=["POST"]))
301 action="settings_vcs", conditions=dict(method=["POST"]))
282 m.connect("admin_settings", "/settings",
302 m.connect("admin_settings", "/settings",
283 action="settings_vcs", conditions=dict(method=["GET"]))
303 action="settings_vcs", conditions=dict(method=["GET"]))
284
304
285 m.connect("admin_settings_mapping", "/settings/mapping",
305 m.connect("admin_settings_mapping", "/settings/mapping",
286 action="settings_mapping", conditions=dict(method=["POST"]))
306 action="settings_mapping", conditions=dict(method=["POST"]))
287 m.connect("admin_settings_mapping", "/settings/mapping",
307 m.connect("admin_settings_mapping", "/settings/mapping",
288 action="settings_mapping", conditions=dict(method=["GET"]))
308 action="settings_mapping", conditions=dict(method=["GET"]))
289
309
290 m.connect("admin_settings_global", "/settings/global",
310 m.connect("admin_settings_global", "/settings/global",
291 action="settings_global", conditions=dict(method=["POST"]))
311 action="settings_global", conditions=dict(method=["POST"]))
292 m.connect("admin_settings_global", "/settings/global",
312 m.connect("admin_settings_global", "/settings/global",
293 action="settings_global", conditions=dict(method=["GET"]))
313 action="settings_global", conditions=dict(method=["GET"]))
294
314
295 m.connect("admin_settings_visual", "/settings/visual",
315 m.connect("admin_settings_visual", "/settings/visual",
296 action="settings_visual", conditions=dict(method=["POST"]))
316 action="settings_visual", conditions=dict(method=["POST"]))
297 m.connect("admin_settings_visual", "/settings/visual",
317 m.connect("admin_settings_visual", "/settings/visual",
298 action="settings_visual", conditions=dict(method=["GET"]))
318 action="settings_visual", conditions=dict(method=["GET"]))
299
319
300 m.connect("admin_settings_email", "/settings/email",
320 m.connect("admin_settings_email", "/settings/email",
301 action="settings_email", conditions=dict(method=["POST"]))
321 action="settings_email", conditions=dict(method=["POST"]))
302 m.connect("admin_settings_email", "/settings/email",
322 m.connect("admin_settings_email", "/settings/email",
303 action="settings_email", conditions=dict(method=["GET"]))
323 action="settings_email", conditions=dict(method=["GET"]))
304
324
305 m.connect("admin_settings_hooks", "/settings/hooks",
325 m.connect("admin_settings_hooks", "/settings/hooks",
306 action="settings_hooks", conditions=dict(method=["POST"]))
326 action="settings_hooks", conditions=dict(method=["POST"]))
307 m.connect("admin_settings_hooks_delete", "/settings/hooks/delete",
327 m.connect("admin_settings_hooks_delete", "/settings/hooks/delete",
308 action="settings_hooks", conditions=dict(method=["POST"]))
328 action="settings_hooks", conditions=dict(method=["POST"]))
309 m.connect("admin_settings_hooks", "/settings/hooks",
329 m.connect("admin_settings_hooks", "/settings/hooks",
310 action="settings_hooks", conditions=dict(method=["GET"]))
330 action="settings_hooks", conditions=dict(method=["GET"]))
311
331
312 m.connect("admin_settings_search", "/settings/search",
332 m.connect("admin_settings_search", "/settings/search",
313 action="settings_search", conditions=dict(method=["POST"]))
333 action="settings_search", conditions=dict(method=["POST"]))
314 m.connect("admin_settings_search", "/settings/search",
334 m.connect("admin_settings_search", "/settings/search",
315 action="settings_search", conditions=dict(method=["GET"]))
335 action="settings_search", conditions=dict(method=["GET"]))
316
336
317 m.connect("admin_settings_system", "/settings/system",
337 m.connect("admin_settings_system", "/settings/system",
318 action="settings_system", conditions=dict(method=["POST"]))
338 action="settings_system", conditions=dict(method=["POST"]))
319 m.connect("admin_settings_system", "/settings/system",
339 m.connect("admin_settings_system", "/settings/system",
320 action="settings_system", conditions=dict(method=["GET"]))
340 action="settings_system", conditions=dict(method=["GET"]))
321
341
322 # ADMIN MY ACCOUNT
342 # ADMIN MY ACCOUNT
323 with rmap.submapper(path_prefix=ADMIN_PREFIX,
343 with rmap.submapper(path_prefix=ADMIN_PREFIX,
324 controller='admin/my_account') as m:
344 controller='admin/my_account') as m:
325
345
326 m.connect("my_account", "/my_account",
346 m.connect("my_account", "/my_account",
327 action="my_account", conditions=dict(method=["GET"]))
347 action="my_account", conditions=dict(method=["GET"]))
328 m.connect("my_account", "/my_account",
348 m.connect("my_account", "/my_account",
329 action="my_account", conditions=dict(method=["POST"]))
349 action="my_account", conditions=dict(method=["POST"]))
330
350
331 m.connect("my_account_password", "/my_account/password",
351 m.connect("my_account_password", "/my_account/password",
332 action="my_account_password", conditions=dict(method=["GET"]))
352 action="my_account_password", conditions=dict(method=["GET"]))
333 m.connect("my_account_password", "/my_account/password",
353 m.connect("my_account_password", "/my_account/password",
334 action="my_account_password", conditions=dict(method=["POST"]))
354 action="my_account_password", conditions=dict(method=["POST"]))
335
355
336 m.connect("my_account_repos", "/my_account/repos",
356 m.connect("my_account_repos", "/my_account/repos",
337 action="my_account_repos", conditions=dict(method=["GET"]))
357 action="my_account_repos", conditions=dict(method=["GET"]))
338
358
339 m.connect("my_account_watched", "/my_account/watched",
359 m.connect("my_account_watched", "/my_account/watched",
340 action="my_account_watched", conditions=dict(method=["GET"]))
360 action="my_account_watched", conditions=dict(method=["GET"]))
341
361
342 m.connect("my_account_perms", "/my_account/perms",
362 m.connect("my_account_perms", "/my_account/perms",
343 action="my_account_perms", conditions=dict(method=["GET"]))
363 action="my_account_perms", conditions=dict(method=["GET"]))
344
364
345 m.connect("my_account_emails", "/my_account/emails",
365 m.connect("my_account_emails", "/my_account/emails",
346 action="my_account_emails", conditions=dict(method=["GET"]))
366 action="my_account_emails", conditions=dict(method=["GET"]))
347 m.connect("my_account_emails", "/my_account/emails",
367 m.connect("my_account_emails", "/my_account/emails",
348 action="my_account_emails_add", conditions=dict(method=["POST"]))
368 action="my_account_emails_add", conditions=dict(method=["POST"]))
349 m.connect("my_account_emails_delete", "/my_account/emails/delete",
369 m.connect("my_account_emails_delete", "/my_account/emails/delete",
350 action="my_account_emails_delete", conditions=dict(method=["POST"]))
370 action="my_account_emails_delete", conditions=dict(method=["POST"]))
351
371
352 m.connect("my_account_api_keys", "/my_account/api_keys",
372 m.connect("my_account_api_keys", "/my_account/api_keys",
353 action="my_account_api_keys", conditions=dict(method=["GET"]))
373 action="my_account_api_keys", conditions=dict(method=["GET"]))
354 m.connect("my_account_api_keys", "/my_account/api_keys",
374 m.connect("my_account_api_keys", "/my_account/api_keys",
355 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
375 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
356 m.connect("my_account_api_keys_delete", "/my_account/api_keys/delete",
376 m.connect("my_account_api_keys_delete", "/my_account/api_keys/delete",
357 action="my_account_api_keys_delete", conditions=dict(method=["POST"]))
377 action="my_account_api_keys_delete", conditions=dict(method=["POST"]))
358
378
359 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
379 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
360 action="my_account_ssh_keys", conditions=dict(method=["GET"]))
380 action="my_account_ssh_keys", conditions=dict(method=["GET"]))
361 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
381 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
362 action="my_account_ssh_keys_add", conditions=dict(method=["POST"]))
382 action="my_account_ssh_keys_add", conditions=dict(method=["POST"]))
363 m.connect("my_account_ssh_keys_delete", "/my_account/ssh_keys/delete",
383 m.connect("my_account_ssh_keys_delete", "/my_account/ssh_keys/delete",
364 action="my_account_ssh_keys_delete", conditions=dict(method=["POST"]))
384 action="my_account_ssh_keys_delete", conditions=dict(method=["POST"]))
365
385
366 # ADMIN GIST
386 # ADMIN GIST
367 with rmap.submapper(path_prefix=ADMIN_PREFIX,
387 with rmap.submapper(path_prefix=ADMIN_PREFIX,
368 controller='admin/gists') as m:
388 controller='admin/gists') as m:
369 m.connect("gists", "/gists",
389 m.connect("gists", "/gists",
370 action="create", conditions=dict(method=["POST"]))
390 action="create", conditions=dict(method=["POST"]))
371 m.connect("gists", "/gists",
391 m.connect("gists", "/gists",
372 conditions=dict(method=["GET"]))
392 conditions=dict(method=["GET"]))
373 m.connect("new_gist", "/gists/new",
393 m.connect("new_gist", "/gists/new",
374 action="new", conditions=dict(method=["GET"]))
394 action="new", conditions=dict(method=["GET"]))
375
395
376 m.connect("gist_delete", "/gists/{gist_id}/delete",
396 m.connect("gist_delete", "/gists/{gist_id}/delete",
377 action="delete", conditions=dict(method=["POST"]))
397 action="delete", conditions=dict(method=["POST"]))
378 m.connect("edit_gist", "/gists/{gist_id}/edit",
398 m.connect("edit_gist", "/gists/{gist_id}/edit",
379 action="edit", conditions=dict(method=["GET", "POST"]))
399 action="edit", conditions=dict(method=["GET", "POST"]))
380 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
400 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
381 action="check_revision", conditions=dict(method=["POST"]))
401 action="check_revision", conditions=dict(method=["POST"]))
382
402
383 m.connect("gist", "/gists/{gist_id}",
403 m.connect("gist", "/gists/{gist_id}",
384 action="show", conditions=dict(method=["GET"]))
404 action="show", conditions=dict(method=["GET"]))
385 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
405 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
386 revision="tip",
406 revision="tip",
387 action="show", conditions=dict(method=["GET"]))
407 action="show", conditions=dict(method=["GET"]))
388 m.connect("formatted_gist", "/gists/{gist_id}/{revision}/{format}",
408 m.connect("formatted_gist", "/gists/{gist_id}/{revision}/{format}",
389 revision="tip",
409 revision="tip",
390 action="show", conditions=dict(method=["GET"]))
410 action="show", conditions=dict(method=["GET"]))
391 m.connect("formatted_gist_file", "/gists/{gist_id}/{revision}/{format}/{f_path:.*}",
411 m.connect("formatted_gist_file", "/gists/{gist_id}/{revision}/{format}/{f_path:.*}",
392 revision='tip',
412 revision='tip',
393 action="show", conditions=dict(method=["GET"]))
413 action="show", conditions=dict(method=["GET"]))
394
414
395 # ADMIN MAIN PAGES
415 # ADMIN MAIN PAGES
396 with rmap.submapper(path_prefix=ADMIN_PREFIX,
416 with rmap.submapper(path_prefix=ADMIN_PREFIX,
397 controller='admin/admin') as m:
417 controller='admin/admin') as m:
398 m.connect('admin_home', '')
418 m.connect('admin_home', '')
399 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9. _-]*}',
419 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9. _-]*}',
400 action='add_repo')
420 action='add_repo')
401 #==========================================================================
421 #==========================================================================
402 # API V2
422 # API V2
403 #==========================================================================
423 #==========================================================================
404 with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
424 with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
405 action='_dispatch') as m:
425 action='_dispatch') as m:
406 m.connect('api', '/api')
426 m.connect('api', '/api')
407
427
408 # USER JOURNAL
428 # USER JOURNAL
409 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
429 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
410 controller='journal')
430 controller='journal')
411 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
431 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
412 controller='journal', action='journal_rss')
432 controller='journal', action='journal_rss')
413 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
433 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
414 controller='journal', action='journal_atom')
434 controller='journal', action='journal_atom')
415
435
416 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
436 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
417 controller='journal', action="public_journal")
437 controller='journal', action="public_journal")
418
438
419 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
439 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
420 controller='journal', action="public_journal_rss")
440 controller='journal', action="public_journal_rss")
421
441
422 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
442 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
423 controller='journal', action="public_journal_rss")
443 controller='journal', action="public_journal_rss")
424
444
425 rmap.connect('public_journal_atom',
445 rmap.connect('public_journal_atom',
426 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
446 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
427 action="public_journal_atom")
447 action="public_journal_atom")
428
448
429 rmap.connect('public_journal_atom_old',
449 rmap.connect('public_journal_atom_old',
430 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
450 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
431 action="public_journal_atom")
451 action="public_journal_atom")
432
452
433 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
453 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
434 controller='journal', action='toggle_following',
454 controller='journal', action='toggle_following',
435 conditions=dict(method=["POST"]))
455 conditions=dict(method=["POST"]))
436
456
437 # SEARCH
457 # SEARCH
438 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
458 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
439 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
459 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
440 controller='search',
460 controller='search',
441 conditions=dict(function=check_repo))
461 conditions=dict(function=check_repo))
442 rmap.connect('search_repo', '/{repo_name:.*?}/search',
462 rmap.connect('search_repo', '/{repo_name:.*?}/search',
443 controller='search',
463 controller='search',
444 conditions=dict(function=check_repo),
464 conditions=dict(function=check_repo),
445 )
465 )
446
466
447 # LOGIN/LOGOUT/REGISTER/SIGN IN
467 # LOGIN/LOGOUT/REGISTER/SIGN IN
448 rmap.connect('session_csrf_secret_token', '%s/session_csrf_secret_token' % ADMIN_PREFIX, controller='login', action='session_csrf_secret_token')
468 rmap.connect('session_csrf_secret_token', '%s/session_csrf_secret_token' % ADMIN_PREFIX, controller='login', action='session_csrf_secret_token')
449 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
469 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
450 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
470 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
451 action='logout')
471 action='logout')
452
472
453 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
473 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
454 action='register')
474 action='register')
455
475
456 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
476 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
457 controller='login', action='password_reset')
477 controller='login', action='password_reset')
458
478
459 rmap.connect('reset_password_confirmation',
479 rmap.connect('reset_password_confirmation',
460 '%s/password_reset_confirmation' % ADMIN_PREFIX,
480 '%s/password_reset_confirmation' % ADMIN_PREFIX,
461 controller='login', action='password_reset_confirmation')
481 controller='login', action='password_reset_confirmation')
462
482
463 # FEEDS
483 # FEEDS
464 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
484 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
465 controller='feed', action='rss',
485 controller='feed', action='rss',
466 conditions=dict(function=check_repo))
486 conditions=dict(function=check_repo))
467
487
468 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
488 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
469 controller='feed', action='atom',
489 controller='feed', action='atom',
470 conditions=dict(function=check_repo))
490 conditions=dict(function=check_repo))
471
491
472 #==========================================================================
492 #==========================================================================
473 # REPOSITORY ROUTES
493 # REPOSITORY ROUTES
474 #==========================================================================
494 #==========================================================================
475 rmap.connect('repo_creating_home', '/{repo_name:.*?}/repo_creating',
495 rmap.connect('repo_creating_home', '/{repo_name:.*?}/repo_creating',
476 controller='admin/repos', action='repo_creating')
496 controller='admin/repos', action='repo_creating')
477 rmap.connect('repo_check_home', '/{repo_name:.*?}/repo_check_creating',
497 rmap.connect('repo_check_home', '/{repo_name:.*?}/repo_check_creating',
478 controller='admin/repos', action='repo_check')
498 controller='admin/repos', action='repo_check')
479
499
480 rmap.connect('summary_home', '/{repo_name:.*?}',
500 rmap.connect('summary_home', '/{repo_name:.*?}',
481 controller='summary',
501 controller='summary',
482 conditions=dict(function=check_repo))
502 conditions=dict(function=check_repo))
483
503
484 # must be here for proper group/repo catching
504 # must be here for proper group/repo catching
485 rmap.connect('repos_group_home', '/{group_name:.*}',
505 rmap.connect('repos_group_home', '/{group_name:.*}',
486 controller='admin/repo_groups', action="show_by_name",
506 controller='admin/repo_groups', action="show_by_name",
487 conditions=dict(function=check_group))
507 conditions=dict(function=check_group))
488 rmap.connect('repo_stats_home', '/{repo_name:.*?}/statistics',
508 rmap.connect('repo_stats_home', '/{repo_name:.*?}/statistics',
489 controller='summary', action='statistics',
509 controller='summary', action='statistics',
490 conditions=dict(function=check_repo))
510 conditions=dict(function=check_repo))
491
511
492 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
512 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
493 controller='summary', action='repo_size',
513 controller='summary', action='repo_size',
494 conditions=dict(function=check_repo))
514 conditions=dict(function=check_repo))
495
515
496 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
516 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
497 controller='home', action='repo_refs_data')
517 controller='home', action='repo_refs_data')
498
518
499 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision:.*}',
519 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision:.*}',
500 controller='changeset', revision='tip',
520 controller='changeset', revision='tip',
501 conditions=dict(function=check_repo))
521 conditions=dict(function=check_repo))
502 rmap.connect('changeset_children', '/{repo_name:.*?}/changeset_children/{revision}',
522 rmap.connect('changeset_children', '/{repo_name:.*?}/changeset_children/{revision}',
503 controller='changeset', revision='tip', action="changeset_children",
523 controller='changeset', revision='tip', action="changeset_children",
504 conditions=dict(function=check_repo))
524 conditions=dict(function=check_repo))
505 rmap.connect('changeset_parents', '/{repo_name:.*?}/changeset_parents/{revision}',
525 rmap.connect('changeset_parents', '/{repo_name:.*?}/changeset_parents/{revision}',
506 controller='changeset', revision='tip', action="changeset_parents",
526 controller='changeset', revision='tip', action="changeset_parents",
507 conditions=dict(function=check_repo))
527 conditions=dict(function=check_repo))
508
528
509 # repo edit options
529 # repo edit options
510 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
530 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
511 controller='admin/repos', action="edit",
531 controller='admin/repos', action="edit",
512 conditions=dict(method=["GET"], function=check_repo))
532 conditions=dict(method=["GET"], function=check_repo))
513
533
514 rmap.connect("edit_repo_perms", "/{repo_name:.*?}/settings/permissions",
534 rmap.connect("edit_repo_perms", "/{repo_name:.*?}/settings/permissions",
515 controller='admin/repos', action="edit_permissions",
535 controller='admin/repos', action="edit_permissions",
516 conditions=dict(method=["GET"], function=check_repo))
536 conditions=dict(method=["GET"], function=check_repo))
517 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
537 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
518 controller='admin/repos', action="edit_permissions_update",
538 controller='admin/repos', action="edit_permissions_update",
519 conditions=dict(method=["POST"], function=check_repo))
539 conditions=dict(method=["POST"], function=check_repo))
520 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions/delete",
540 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions/delete",
521 controller='admin/repos', action="edit_permissions_revoke",
541 controller='admin/repos', action="edit_permissions_revoke",
522 conditions=dict(method=["POST"], function=check_repo))
542 conditions=dict(method=["POST"], function=check_repo))
523
543
524 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
544 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
525 controller='admin/repos', action="edit_fields",
545 controller='admin/repos', action="edit_fields",
526 conditions=dict(method=["GET"], function=check_repo))
546 conditions=dict(method=["GET"], function=check_repo))
527 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
547 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
528 controller='admin/repos', action="create_repo_field",
548 controller='admin/repos', action="create_repo_field",
529 conditions=dict(method=["POST"], function=check_repo))
549 conditions=dict(method=["POST"], function=check_repo))
530 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}/delete",
550 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}/delete",
531 controller='admin/repos', action="delete_repo_field",
551 controller='admin/repos', action="delete_repo_field",
532 conditions=dict(method=["POST"], function=check_repo))
552 conditions=dict(method=["POST"], function=check_repo))
533
553
534 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
554 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
535 controller='admin/repos', action="edit_advanced",
555 controller='admin/repos', action="edit_advanced",
536 conditions=dict(method=["GET"], function=check_repo))
556 conditions=dict(method=["GET"], function=check_repo))
537
557
538 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
558 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
539 controller='admin/repos', action="edit_advanced_journal",
559 controller='admin/repos', action="edit_advanced_journal",
540 conditions=dict(method=["POST"], function=check_repo))
560 conditions=dict(method=["POST"], function=check_repo))
541
561
542 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
562 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
543 controller='admin/repos', action="edit_advanced_fork",
563 controller='admin/repos', action="edit_advanced_fork",
544 conditions=dict(method=["POST"], function=check_repo))
564 conditions=dict(method=["POST"], function=check_repo))
545
565
546 rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
566 rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
547 controller='admin/repos', action="edit_caches",
567 controller='admin/repos', action="edit_caches",
548 conditions=dict(method=["GET"], function=check_repo))
568 conditions=dict(method=["GET"], function=check_repo))
549 rmap.connect("update_repo_caches", "/{repo_name:.*?}/settings/caches",
569 rmap.connect("update_repo_caches", "/{repo_name:.*?}/settings/caches",
550 controller='admin/repos', action="edit_caches",
570 controller='admin/repos', action="edit_caches",
551 conditions=dict(method=["POST"], function=check_repo))
571 conditions=dict(method=["POST"], function=check_repo))
552
572
553 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
573 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
554 controller='admin/repos', action="edit_remote",
574 controller='admin/repos', action="edit_remote",
555 conditions=dict(method=["GET"], function=check_repo))
575 conditions=dict(method=["GET"], function=check_repo))
556 rmap.connect("edit_repo_remote_update", "/{repo_name:.*?}/settings/remote",
576 rmap.connect("edit_repo_remote_update", "/{repo_name:.*?}/settings/remote",
557 controller='admin/repos', action="edit_remote",
577 controller='admin/repos', action="edit_remote",
558 conditions=dict(method=["POST"], function=check_repo))
578 conditions=dict(method=["POST"], function=check_repo))
559
579
560 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
580 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
561 controller='admin/repos', action="edit_statistics",
581 controller='admin/repos', action="edit_statistics",
562 conditions=dict(method=["GET"], function=check_repo))
582 conditions=dict(method=["GET"], function=check_repo))
563 rmap.connect("edit_repo_statistics_update", "/{repo_name:.*?}/settings/statistics",
583 rmap.connect("edit_repo_statistics_update", "/{repo_name:.*?}/settings/statistics",
564 controller='admin/repos', action="edit_statistics",
584 controller='admin/repos', action="edit_statistics",
565 conditions=dict(method=["POST"], function=check_repo))
585 conditions=dict(method=["POST"], function=check_repo))
566
586
567 # still working url for backward compat.
587 # still working url for backward compat.
568 rmap.connect('raw_changeset_home_depraced',
588 rmap.connect('raw_changeset_home_depraced',
569 '/{repo_name:.*?}/raw-changeset/{revision}',
589 '/{repo_name:.*?}/raw-changeset/{revision}',
570 controller='changeset', action='changeset_raw',
590 controller='changeset', action='changeset_raw',
571 revision='tip', conditions=dict(function=check_repo))
591 revision='tip', conditions=dict(function=check_repo))
572
592
573 ## new URLs
593 ## new URLs
574 rmap.connect('changeset_raw_home',
594 rmap.connect('changeset_raw_home',
575 '/{repo_name:.*?}/changeset-diff/{revision}',
595 '/{repo_name:.*?}/changeset-diff/{revision}',
576 controller='changeset', action='changeset_raw',
596 controller='changeset', action='changeset_raw',
577 revision='tip', conditions=dict(function=check_repo))
597 revision='tip', conditions=dict(function=check_repo))
578
598
579 rmap.connect('changeset_patch_home',
599 rmap.connect('changeset_patch_home',
580 '/{repo_name:.*?}/changeset-patch/{revision}',
600 '/{repo_name:.*?}/changeset-patch/{revision}',
581 controller='changeset', action='changeset_patch',
601 controller='changeset', action='changeset_patch',
582 revision='tip', conditions=dict(function=check_repo))
602 revision='tip', conditions=dict(function=check_repo))
583
603
584 rmap.connect('changeset_download_home',
604 rmap.connect('changeset_download_home',
585 '/{repo_name:.*?}/changeset-download/{revision}',
605 '/{repo_name:.*?}/changeset-download/{revision}',
586 controller='changeset', action='changeset_download',
606 controller='changeset', action='changeset_download',
587 revision='tip', conditions=dict(function=check_repo))
607 revision='tip', conditions=dict(function=check_repo))
588
608
589 rmap.connect('changeset_comment',
609 rmap.connect('changeset_comment',
590 '/{repo_name:.*?}/changeset-comment/{revision}',
610 '/{repo_name:.*?}/changeset-comment/{revision}',
591 controller='changeset', revision='tip', action='comment',
611 controller='changeset', revision='tip', action='comment',
592 conditions=dict(function=check_repo))
612 conditions=dict(function=check_repo))
593
613
594 rmap.connect('changeset_comment_delete',
614 rmap.connect('changeset_comment_delete',
595 '/{repo_name:.*?}/changeset-comment/{comment_id}/delete',
615 '/{repo_name:.*?}/changeset-comment/{comment_id}/delete',
596 controller='changeset', action='delete_comment',
616 controller='changeset', action='delete_comment',
597 conditions=dict(function=check_repo, method=["POST"]))
617 conditions=dict(function=check_repo, method=["POST"]))
598
618
599 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
619 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
600 controller='changeset', action='changeset_info')
620 controller='changeset', action='changeset_info')
601
621
602 rmap.connect('compare_home',
622 rmap.connect('compare_home',
603 '/{repo_name:.*?}/compare',
623 '/{repo_name:.*?}/compare',
604 controller='compare',
624 controller='compare',
605 conditions=dict(function=check_repo))
625 conditions=dict(function=check_repo))
606
626
607 rmap.connect('compare_url',
627 rmap.connect('compare_url',
608 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref_name:.*?}...{other_ref_type}@{other_ref_name:.*?}',
628 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref_name:.*?}...{other_ref_type}@{other_ref_name:.*?}',
609 controller='compare', action='compare',
629 controller='compare', action='compare',
610 conditions=dict(function=check_repo),
630 conditions=dict(function=check_repo),
611 requirements=dict(
631 requirements=dict(
612 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
632 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
613 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
633 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
614 )
634 )
615
635
616 rmap.connect('pullrequest_home',
636 rmap.connect('pullrequest_home',
617 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
637 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
618 conditions=dict(function=check_repo,
638 conditions=dict(function=check_repo,
619 method=["GET"]))
639 method=["GET"]))
620
640
621 rmap.connect('pullrequest_repo_info',
641 rmap.connect('pullrequest_repo_info',
622 '/{repo_name:.*?}/pull-request-repo-info',
642 '/{repo_name:.*?}/pull-request-repo-info',
623 controller='pullrequests', action='repo_info',
643 controller='pullrequests', action='repo_info',
624 conditions=dict(function=check_repo, method=["GET"]))
644 conditions=dict(function=check_repo, method=["GET"]))
625
645
626 rmap.connect('pullrequest',
646 rmap.connect('pullrequest',
627 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
647 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
628 action='create', conditions=dict(function=check_repo,
648 action='create', conditions=dict(function=check_repo,
629 method=["POST"]))
649 method=["POST"]))
630
650
631 rmap.connect('pullrequest_show',
651 rmap.connect('pullrequest_show',
632 '/{repo_name:.*?}/pull-request/{pull_request_id:\\d+}{extra:(/.*)?}', extra='',
652 '/{repo_name:.*?}/pull-request/{pull_request_id:\\d+}{extra:(/.*)?}', extra='',
633 controller='pullrequests',
653 controller='pullrequests',
634 action='show', conditions=dict(function=check_repo,
654 action='show', conditions=dict(function=check_repo,
635 method=["GET"]))
655 method=["GET"]))
636 rmap.connect('pullrequest_post',
656 rmap.connect('pullrequest_post',
637 '/{repo_name:.*?}/pull-request/{pull_request_id}',
657 '/{repo_name:.*?}/pull-request/{pull_request_id}',
638 controller='pullrequests',
658 controller='pullrequests',
639 action='post', conditions=dict(function=check_repo,
659 action='post', conditions=dict(function=check_repo,
640 method=["POST"]))
660 method=["POST"]))
641 rmap.connect('pullrequest_delete',
661 rmap.connect('pullrequest_delete',
642 '/{repo_name:.*?}/pull-request/{pull_request_id}/delete',
662 '/{repo_name:.*?}/pull-request/{pull_request_id}/delete',
643 controller='pullrequests',
663 controller='pullrequests',
644 action='delete', conditions=dict(function=check_repo,
664 action='delete', conditions=dict(function=check_repo,
645 method=["POST"]))
665 method=["POST"]))
646
666
647 rmap.connect('pullrequest_show_all',
667 rmap.connect('pullrequest_show_all',
648 '/{repo_name:.*?}/pull-request',
668 '/{repo_name:.*?}/pull-request',
649 controller='pullrequests',
669 controller='pullrequests',
650 action='show_all', conditions=dict(function=check_repo,
670 action='show_all', conditions=dict(function=check_repo,
651 method=["GET"]))
671 method=["GET"]))
652
672
653 rmap.connect('my_pullrequests',
673 rmap.connect('my_pullrequests',
654 '/my_pullrequests',
674 '/my_pullrequests',
655 controller='pullrequests',
675 controller='pullrequests',
656 action='show_my', conditions=dict(method=["GET"]))
676 action='show_my', conditions=dict(method=["GET"]))
657
677
658 rmap.connect('pullrequest_comment',
678 rmap.connect('pullrequest_comment',
659 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
679 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
660 controller='pullrequests',
680 controller='pullrequests',
661 action='comment', conditions=dict(function=check_repo,
681 action='comment', conditions=dict(function=check_repo,
662 method=["POST"]))
682 method=["POST"]))
663
683
664 rmap.connect('pullrequest_comment_delete',
684 rmap.connect('pullrequest_comment_delete',
665 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
685 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
666 controller='pullrequests', action='delete_comment',
686 controller='pullrequests', action='delete_comment',
667 conditions=dict(function=check_repo, method=["POST"]))
687 conditions=dict(function=check_repo, method=["POST"]))
668
688
669 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
689 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
670 controller='summary', conditions=dict(function=check_repo))
690 controller='summary', conditions=dict(function=check_repo))
671
691
672 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
692 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
673 controller='changelog', conditions=dict(function=check_repo))
693 controller='changelog', conditions=dict(function=check_repo))
674
694
675 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
695 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
676 controller='changelog',
696 controller='changelog',
677 conditions=dict(function=check_repo))
697 conditions=dict(function=check_repo))
678
698
679 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
699 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
680 controller='changelog', action='changelog_details',
700 controller='changelog', action='changelog_details',
681 conditions=dict(function=check_repo))
701 conditions=dict(function=check_repo))
682
702
683 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
703 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
684 controller='files', revision='tip', f_path='',
704 controller='files', revision='tip', f_path='',
685 conditions=dict(function=check_repo))
705 conditions=dict(function=check_repo))
686
706
687 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
707 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
688 controller='files', revision='tip', f_path='',
708 controller='files', revision='tip', f_path='',
689 conditions=dict(function=check_repo))
709 conditions=dict(function=check_repo))
690
710
691 rmap.connect('files_history_home',
711 rmap.connect('files_history_home',
692 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
712 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
693 controller='files', action='history', revision='tip', f_path='',
713 controller='files', action='history', revision='tip', f_path='',
694 conditions=dict(function=check_repo))
714 conditions=dict(function=check_repo))
695
715
696 rmap.connect('files_authors_home',
716 rmap.connect('files_authors_home',
697 '/{repo_name:.*?}/authors/{revision}/{f_path:.*}',
717 '/{repo_name:.*?}/authors/{revision}/{f_path:.*}',
698 controller='files', action='authors', revision='tip', f_path='',
718 controller='files', action='authors', revision='tip', f_path='',
699 conditions=dict(function=check_repo))
719 conditions=dict(function=check_repo))
700
720
701 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
721 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
702 controller='files', action='diff', revision='tip', f_path='',
722 controller='files', action='diff', revision='tip', f_path='',
703 conditions=dict(function=check_repo))
723 conditions=dict(function=check_repo))
704
724
705 rmap.connect('files_diff_2way_home', '/{repo_name:.*?}/diff-2way/{f_path:.+}',
725 rmap.connect('files_diff_2way_home', '/{repo_name:.*?}/diff-2way/{f_path:.+}',
706 controller='files', action='diff_2way', revision='tip', f_path='',
726 controller='files', action='diff_2way', revision='tip', f_path='',
707 conditions=dict(function=check_repo))
727 conditions=dict(function=check_repo))
708
728
709 rmap.connect('files_rawfile_home',
729 rmap.connect('files_rawfile_home',
710 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
730 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
711 controller='files', action='rawfile', revision='tip',
731 controller='files', action='rawfile', revision='tip',
712 f_path='', conditions=dict(function=check_repo))
732 f_path='', conditions=dict(function=check_repo))
713
733
714 rmap.connect('files_raw_home',
734 rmap.connect('files_raw_home',
715 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
735 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
716 controller='files', action='raw', revision='tip', f_path='',
736 controller='files', action='raw', revision='tip', f_path='',
717 conditions=dict(function=check_repo))
737 conditions=dict(function=check_repo))
718
738
719 rmap.connect('files_annotate_home',
739 rmap.connect('files_annotate_home',
720 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
740 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
721 controller='files', revision='tip',
741 controller='files', revision='tip',
722 f_path='', annotate='1', conditions=dict(function=check_repo))
742 f_path='', annotate='1', conditions=dict(function=check_repo))
723
743
724 rmap.connect('files_edit_home',
744 rmap.connect('files_edit_home',
725 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
745 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
726 controller='files', action='edit', revision='tip',
746 controller='files', action='edit', revision='tip',
727 f_path='', conditions=dict(function=check_repo))
747 f_path='', conditions=dict(function=check_repo))
728
748
729 rmap.connect('files_add_home',
749 rmap.connect('files_add_home',
730 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
750 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
731 controller='files', action='add', revision='tip',
751 controller='files', action='add', revision='tip',
732 f_path='', conditions=dict(function=check_repo))
752 f_path='', conditions=dict(function=check_repo))
733
753
734 rmap.connect('files_delete_home',
754 rmap.connect('files_delete_home',
735 '/{repo_name:.*?}/delete/{revision}/{f_path:.*}',
755 '/{repo_name:.*?}/delete/{revision}/{f_path:.*}',
736 controller='files', action='delete', revision='tip',
756 controller='files', action='delete', revision='tip',
737 f_path='', conditions=dict(function=check_repo))
757 f_path='', conditions=dict(function=check_repo))
738
758
739 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
759 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
740 controller='files', action='archivefile',
760 controller='files', action='archivefile',
741 conditions=dict(function=check_repo))
761 conditions=dict(function=check_repo))
742
762
743 rmap.connect('files_nodelist_home',
763 rmap.connect('files_nodelist_home',
744 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
764 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
745 controller='files', action='nodelist',
765 controller='files', action='nodelist',
746 conditions=dict(function=check_repo))
766 conditions=dict(function=check_repo))
747
767
748 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
768 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
749 controller='forks', action='fork_create',
769 controller='forks', action='fork_create',
750 conditions=dict(function=check_repo, method=["POST"]))
770 conditions=dict(function=check_repo, method=["POST"]))
751
771
752 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
772 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
753 controller='forks', action='fork',
773 controller='forks', action='fork',
754 conditions=dict(function=check_repo))
774 conditions=dict(function=check_repo))
755
775
756 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
776 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
757 controller='forks', action='forks',
777 controller='forks', action='forks',
758 conditions=dict(function=check_repo))
778 conditions=dict(function=check_repo))
759
779
760 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
780 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
761 controller='followers', action='followers',
781 controller='followers', action='followers',
762 conditions=dict(function=check_repo))
782 conditions=dict(function=check_repo))
763
783
764 return rmap
784 return rmap
765
785
766
786
767 class UrlGenerator(object):
787 class UrlGenerator(object):
768 """Emulate pylons.url in providing a wrapper around routes.url
788 """Emulate pylons.url in providing a wrapper around routes.url
769
789
770 This code was added during migration from Pylons to Turbogears2. Pylons
790 This code was added during migration from Pylons to Turbogears2. Pylons
771 already provided a wrapper like this, but Turbogears2 does not.
791 already provided a wrapper like this, but Turbogears2 does not.
772
792
773 When the routing of Kallithea is changed to use less Routes and more
793 When the routing of Kallithea is changed to use less Routes and more
774 Turbogears2-style routing, this class may disappear or change.
794 Turbogears2-style routing, this class may disappear or change.
775
795
776 url() (the __call__ method) returns the URL based on a route name and
796 url() (the __call__ method) returns the URL based on a route name and
777 arguments.
797 arguments.
778 url.current() returns the URL of the current page with arguments applied.
798 url.current() returns the URL of the current page with arguments applied.
779
799
780 Refer to documentation of Routes for details:
800 Refer to documentation of Routes for details:
781 https://routes.readthedocs.io/en/latest/generating.html#generation
801 https://routes.readthedocs.io/en/latest/generating.html#generation
782 """
802 """
783 def __call__(self, *args, **kwargs):
803 def __call__(self, *args, **kwargs):
784 return request.environ['routes.url'](*args, **kwargs)
804 return request.environ['routes.url'](*args, **kwargs)
785
805
786 def current(self, *args, **kwargs):
806 def current(self, *args, **kwargs):
787 return request.environ['routes.url'].current(*args, **kwargs)
807 return request.environ['routes.url'].current(*args, **kwargs)
788
808
789
809
790 url = UrlGenerator()
810 url = UrlGenerator()
@@ -1,644 +1,649 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 """
15 """
16 kallithea.lib.base
16 kallithea.lib.base
17 ~~~~~~~~~~~~~~~~~~
17 ~~~~~~~~~~~~~~~~~~
18
18
19 The base Controller API
19 The base Controller API
20 Provides the BaseController class for subclassing. And usage in different
20 Provides the BaseController class for subclassing. And usage in different
21 controllers
21 controllers
22
22
23 This file was forked by the Kallithea project in July 2014.
23 This file was forked by the Kallithea project in July 2014.
24 Original author and date, and relevant copyright and licensing information is below:
24 Original author and date, and relevant copyright and licensing information is below:
25 :created_on: Oct 06, 2010
25 :created_on: Oct 06, 2010
26 :author: marcink
26 :author: marcink
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 :license: GPLv3, see LICENSE.md for more details.
28 :license: GPLv3, see LICENSE.md for more details.
29 """
29 """
30
30
31 import base64
31 import base64
32 import datetime
32 import datetime
33 import logging
33 import logging
34 import traceback
34 import traceback
35 import warnings
35 import warnings
36
36
37 import decorator
37 import decorator
38 import paste.auth.basic
38 import paste.auth.basic
39 import paste.httpexceptions
39 import paste.httpexceptions
40 import paste.httpheaders
40 import paste.httpheaders
41 import webob.exc
41 import webob.exc
42 from tg import TGController, config, render_template, request, response, session
42 from tg import TGController, config, render_template, request, response, session
43 from tg import tmpl_context as c
43 from tg import tmpl_context as c
44 from tg.i18n import ugettext as _
44 from tg.i18n import ugettext as _
45
45
46 from kallithea import BACKENDS, __version__
46 from kallithea import BACKENDS, __version__
47 from kallithea.config.routing import url
47 from kallithea.config.routing import url
48 from kallithea.lib import auth_modules, ext_json
48 from kallithea.lib import auth_modules, ext_json
49 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
49 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
50 from kallithea.lib.exceptions import UserCreationError
50 from kallithea.lib.exceptions import UserCreationError
51 from kallithea.lib.utils import get_repo_slug, is_valid_repo
51 from kallithea.lib.utils import get_repo_slug, is_valid_repo
52 from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool
52 from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool
53 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
53 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
54 from kallithea.model import meta
54 from kallithea.model import meta
55 from kallithea.model.db import PullRequest, Repository, Setting, User
55 from kallithea.model.db import PullRequest, Repository, Setting, User
56 from kallithea.model.scm import ScmModel
56 from kallithea.model.scm import ScmModel
57
57
58
58
59 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
60
60
61
61
62 def render(template_path):
62 def render(template_path):
63 return render_template({'url': url}, 'mako', template_path)
63 return render_template({'url': url}, 'mako', template_path)
64
64
65
65
66 def _filter_proxy(ip):
66 def _filter_proxy(ip):
67 """
67 """
68 HEADERS can have multiple ips inside the left-most being the original
68 HEADERS can have multiple ips inside the left-most being the original
69 client, and each successive proxy that passed the request adding the IP
69 client, and each successive proxy that passed the request adding the IP
70 address where it received the request from.
70 address where it received the request from.
71
71
72 :param ip:
72 :param ip:
73 """
73 """
74 if ',' in ip:
74 if ',' in ip:
75 _ips = ip.split(',')
75 _ips = ip.split(',')
76 _first_ip = _ips[0].strip()
76 _first_ip = _ips[0].strip()
77 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
77 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
78 return _first_ip
78 return _first_ip
79 return ip
79 return ip
80
80
81
81
82 def _get_ip_addr(environ):
82 def _get_ip_addr(environ):
83 proxy_key = 'HTTP_X_REAL_IP'
83 proxy_key = 'HTTP_X_REAL_IP'
84 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
84 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
85 def_key = 'REMOTE_ADDR'
85 def_key = 'REMOTE_ADDR'
86
86
87 ip = environ.get(proxy_key)
87 ip = environ.get(proxy_key)
88 if ip:
88 if ip:
89 return _filter_proxy(ip)
89 return _filter_proxy(ip)
90
90
91 ip = environ.get(proxy_key2)
91 ip = environ.get(proxy_key2)
92 if ip:
92 if ip:
93 return _filter_proxy(ip)
93 return _filter_proxy(ip)
94
94
95 ip = environ.get(def_key, '0.0.0.0')
95 ip = environ.get(def_key, '0.0.0.0')
96 return _filter_proxy(ip)
96 return _filter_proxy(ip)
97
97
98
98
99 def get_path_info(environ):
99 def get_path_info(environ):
100 """Return unicode PATH_INFO from environ ... using tg.original_request if available.
100 """Return PATH_INFO from environ ... using tg.original_request if available.
101
102 In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
103 bytes. The code points are guaranteed to only use the lower 8 bit bits, and
104 encoding the string with the 1:1 encoding latin1 will give the
105 corresponding byte string ... which then can be decoded to proper unicode.
101 """
106 """
102 org_req = environ.get('tg.original_request')
107 org_req = environ.get('tg.original_request')
103 if org_req is not None:
108 if org_req is not None:
104 environ = org_req.environ
109 environ = org_req.environ
105 return safe_str(environ['PATH_INFO'])
110 return safe_str(environ['PATH_INFO'].encode('latin1'))
106
111
107
112
108 def log_in_user(user, remember, is_external_auth, ip_addr):
113 def log_in_user(user, remember, is_external_auth, ip_addr):
109 """
114 """
110 Log a `User` in and update session and cookies. If `remember` is True,
115 Log a `User` in and update session and cookies. If `remember` is True,
111 the session cookie is set to expire in a year; otherwise, it expires at
116 the session cookie is set to expire in a year; otherwise, it expires at
112 the end of the browser session.
117 the end of the browser session.
113
118
114 Returns populated `AuthUser` object.
119 Returns populated `AuthUser` object.
115 """
120 """
116 # It should not be possible to explicitly log in as the default user.
121 # It should not be possible to explicitly log in as the default user.
117 assert not user.is_default_user, user
122 assert not user.is_default_user, user
118
123
119 auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
124 auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
120 if auth_user is None:
125 if auth_user is None:
121 return None
126 return None
122
127
123 user.update_lastlogin()
128 user.update_lastlogin()
124 meta.Session().commit()
129 meta.Session().commit()
125
130
126 # Start new session to prevent session fixation attacks.
131 # Start new session to prevent session fixation attacks.
127 session.invalidate()
132 session.invalidate()
128 session['authuser'] = cookie = auth_user.to_cookie()
133 session['authuser'] = cookie = auth_user.to_cookie()
129
134
130 # If they want to be remembered, update the cookie.
135 # If they want to be remembered, update the cookie.
131 # NOTE: Assumes that beaker defaults to browser session cookie.
136 # NOTE: Assumes that beaker defaults to browser session cookie.
132 if remember:
137 if remember:
133 t = datetime.datetime.now() + datetime.timedelta(days=365)
138 t = datetime.datetime.now() + datetime.timedelta(days=365)
134 session._set_cookie_expires(t)
139 session._set_cookie_expires(t)
135
140
136 session.save()
141 session.save()
137
142
138 log.info('user %s is now authenticated and stored in '
143 log.info('user %s is now authenticated and stored in '
139 'session, session attrs %s', user.username, cookie)
144 'session, session attrs %s', user.username, cookie)
140
145
141 # dumps session attrs back to cookie
146 # dumps session attrs back to cookie
142 session._update_cookie_out()
147 session._update_cookie_out()
143
148
144 return auth_user
149 return auth_user
145
150
146
151
147 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
152 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
148
153
149 def __init__(self, realm, authfunc, auth_http_code=None):
154 def __init__(self, realm, authfunc, auth_http_code=None):
150 self.realm = realm
155 self.realm = realm
151 self.authfunc = authfunc
156 self.authfunc = authfunc
152 self._rc_auth_http_code = auth_http_code
157 self._rc_auth_http_code = auth_http_code
153
158
154 def build_authentication(self, environ):
159 def build_authentication(self, environ):
155 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
160 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
156 # Consume the whole body before sending a response
161 # Consume the whole body before sending a response
157 try:
162 try:
158 request_body_size = int(environ.get('CONTENT_LENGTH', 0))
163 request_body_size = int(environ.get('CONTENT_LENGTH', 0))
159 except (ValueError):
164 except (ValueError):
160 request_body_size = 0
165 request_body_size = 0
161 environ['wsgi.input'].read(request_body_size)
166 environ['wsgi.input'].read(request_body_size)
162 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
167 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
163 # return 403 if alternative http return code is specified in
168 # return 403 if alternative http return code is specified in
164 # Kallithea config
169 # Kallithea config
165 return paste.httpexceptions.HTTPForbidden(headers=head)
170 return paste.httpexceptions.HTTPForbidden(headers=head)
166 return paste.httpexceptions.HTTPUnauthorized(headers=head)
171 return paste.httpexceptions.HTTPUnauthorized(headers=head)
167
172
168 def authenticate(self, environ):
173 def authenticate(self, environ):
169 authorization = paste.httpheaders.AUTHORIZATION(environ)
174 authorization = paste.httpheaders.AUTHORIZATION(environ)
170 if not authorization:
175 if not authorization:
171 return self.build_authentication(environ)
176 return self.build_authentication(environ)
172 (authmeth, auth) = authorization.split(' ', 1)
177 (authmeth, auth) = authorization.split(' ', 1)
173 if 'basic' != authmeth.lower():
178 if 'basic' != authmeth.lower():
174 return self.build_authentication(environ)
179 return self.build_authentication(environ)
175 auth = safe_str(base64.b64decode(auth.strip()))
180 auth = safe_str(base64.b64decode(auth.strip()))
176 _parts = auth.split(':', 1)
181 _parts = auth.split(':', 1)
177 if len(_parts) == 2:
182 if len(_parts) == 2:
178 username, password = _parts
183 username, password = _parts
179 if self.authfunc(username, password, environ) is not None:
184 if self.authfunc(username, password, environ) is not None:
180 return username
185 return username
181 return self.build_authentication(environ)
186 return self.build_authentication(environ)
182
187
183 __call__ = authenticate
188 __call__ = authenticate
184
189
185
190
186 class BaseVCSController(object):
191 class BaseVCSController(object):
187 """Base controller for handling Mercurial/Git protocol requests
192 """Base controller for handling Mercurial/Git protocol requests
188 (coming from a VCS client, and not a browser).
193 (coming from a VCS client, and not a browser).
189 """
194 """
190
195
191 scm_alias = None # 'hg' / 'git'
196 scm_alias = None # 'hg' / 'git'
192
197
193 def __init__(self, application, config):
198 def __init__(self, application, config):
194 self.application = application
199 self.application = application
195 self.config = config
200 self.config = config
196 # base path of repo locations
201 # base path of repo locations
197 self.basepath = self.config['base_path']
202 self.basepath = self.config['base_path']
198 # authenticate this VCS request using the authentication modules
203 # authenticate this VCS request using the authentication modules
199 self.authenticate = BasicAuth('', auth_modules.authenticate,
204 self.authenticate = BasicAuth('', auth_modules.authenticate,
200 config.get('auth_ret_code'))
205 config.get('auth_ret_code'))
201
206
202 @classmethod
207 @classmethod
203 def parse_request(cls, environ):
208 def parse_request(cls, environ):
204 """If request is parsed as a request for this VCS, return a namespace with the parsed request.
209 """If request is parsed as a request for this VCS, return a namespace with the parsed request.
205 If the request is unknown, return None.
210 If the request is unknown, return None.
206 """
211 """
207 raise NotImplementedError()
212 raise NotImplementedError()
208
213
209 def _authorize(self, environ, action, repo_name, ip_addr):
214 def _authorize(self, environ, action, repo_name, ip_addr):
210 """Authenticate and authorize user.
215 """Authenticate and authorize user.
211
216
212 Since we're dealing with a VCS client and not a browser, we only
217 Since we're dealing with a VCS client and not a browser, we only
213 support HTTP basic authentication, either directly via raw header
218 support HTTP basic authentication, either directly via raw header
214 inspection, or by using container authentication to delegate the
219 inspection, or by using container authentication to delegate the
215 authentication to the web server.
220 authentication to the web server.
216
221
217 Returns (user, None) on successful authentication and authorization.
222 Returns (user, None) on successful authentication and authorization.
218 Returns (None, wsgi_app) to send the wsgi_app response to the client.
223 Returns (None, wsgi_app) to send the wsgi_app response to the client.
219 """
224 """
220 # Use anonymous access if allowed for action on repo.
225 # Use anonymous access if allowed for action on repo.
221 default_user = User.get_default_user(cache=True)
226 default_user = User.get_default_user(cache=True)
222 default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
227 default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
223 if default_authuser is None:
228 if default_authuser is None:
224 log.debug('No anonymous access at all') # move on to proper user auth
229 log.debug('No anonymous access at all') # move on to proper user auth
225 else:
230 else:
226 if self._check_permission(action, default_authuser, repo_name):
231 if self._check_permission(action, default_authuser, repo_name):
227 return default_authuser, None
232 return default_authuser, None
228 log.debug('Not authorized to access this repository as anonymous user')
233 log.debug('Not authorized to access this repository as anonymous user')
229
234
230 username = None
235 username = None
231 #==============================================================
236 #==============================================================
232 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
237 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
233 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
238 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
234 #==============================================================
239 #==============================================================
235
240
236 # try to auth based on environ, container auth methods
241 # try to auth based on environ, container auth methods
237 log.debug('Running PRE-AUTH for container based authentication')
242 log.debug('Running PRE-AUTH for container based authentication')
238 pre_auth = auth_modules.authenticate('', '', environ)
243 pre_auth = auth_modules.authenticate('', '', environ)
239 if pre_auth is not None and pre_auth.get('username'):
244 if pre_auth is not None and pre_auth.get('username'):
240 username = pre_auth['username']
245 username = pre_auth['username']
241 log.debug('PRE-AUTH got %s as username', username)
246 log.debug('PRE-AUTH got %s as username', username)
242
247
243 # If not authenticated by the container, running basic auth
248 # If not authenticated by the container, running basic auth
244 if not username:
249 if not username:
245 self.authenticate.realm = self.config['realm']
250 self.authenticate.realm = self.config['realm']
246 result = self.authenticate(environ)
251 result = self.authenticate(environ)
247 if isinstance(result, str):
252 if isinstance(result, str):
248 paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
253 paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
249 paste.httpheaders.REMOTE_USER.update(environ, result)
254 paste.httpheaders.REMOTE_USER.update(environ, result)
250 username = result
255 username = result
251 else:
256 else:
252 return None, result.wsgi_application
257 return None, result.wsgi_application
253
258
254 #==============================================================
259 #==============================================================
255 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
260 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
256 #==============================================================
261 #==============================================================
257 try:
262 try:
258 user = User.get_by_username_or_email(username)
263 user = User.get_by_username_or_email(username)
259 except Exception:
264 except Exception:
260 log.error(traceback.format_exc())
265 log.error(traceback.format_exc())
261 return None, webob.exc.HTTPInternalServerError()
266 return None, webob.exc.HTTPInternalServerError()
262
267
263 authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
268 authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
264 if authuser is None:
269 if authuser is None:
265 return None, webob.exc.HTTPForbidden()
270 return None, webob.exc.HTTPForbidden()
266 if not self._check_permission(action, authuser, repo_name):
271 if not self._check_permission(action, authuser, repo_name):
267 return None, webob.exc.HTTPForbidden()
272 return None, webob.exc.HTTPForbidden()
268
273
269 return user, None
274 return user, None
270
275
271 def _handle_request(self, environ, start_response):
276 def _handle_request(self, environ, start_response):
272 raise NotImplementedError()
277 raise NotImplementedError()
273
278
274 def _check_permission(self, action, authuser, repo_name):
279 def _check_permission(self, action, authuser, repo_name):
275 """
280 """
276 Checks permissions using action (push/pull) user and repository
281 Checks permissions using action (push/pull) user and repository
277 name
282 name
278
283
279 :param action: 'push' or 'pull' action
284 :param action: 'push' or 'pull' action
280 :param user: `User` instance
285 :param user: `User` instance
281 :param repo_name: repository name
286 :param repo_name: repository name
282 """
287 """
283 if action == 'push':
288 if action == 'push':
284 if not HasPermissionAnyMiddleware('repository.write',
289 if not HasPermissionAnyMiddleware('repository.write',
285 'repository.admin')(authuser,
290 'repository.admin')(authuser,
286 repo_name):
291 repo_name):
287 return False
292 return False
288
293
289 else:
294 else:
290 #any other action need at least read permission
295 #any other action need at least read permission
291 if not HasPermissionAnyMiddleware('repository.read',
296 if not HasPermissionAnyMiddleware('repository.read',
292 'repository.write',
297 'repository.write',
293 'repository.admin')(authuser,
298 'repository.admin')(authuser,
294 repo_name):
299 repo_name):
295 return False
300 return False
296
301
297 return True
302 return True
298
303
299 def _get_ip_addr(self, environ):
304 def _get_ip_addr(self, environ):
300 return _get_ip_addr(environ)
305 return _get_ip_addr(environ)
301
306
302 def __call__(self, environ, start_response):
307 def __call__(self, environ, start_response):
303 try:
308 try:
304 # try parsing a request for this VCS - if it fails, call the wrapped app
309 # try parsing a request for this VCS - if it fails, call the wrapped app
305 parsed_request = self.parse_request(environ)
310 parsed_request = self.parse_request(environ)
306 if parsed_request is None:
311 if parsed_request is None:
307 return self.application(environ, start_response)
312 return self.application(environ, start_response)
308
313
309 # skip passing error to error controller
314 # skip passing error to error controller
310 environ['pylons.status_code_redirect'] = True
315 environ['pylons.status_code_redirect'] = True
311
316
312 # quick check if repo exists...
317 # quick check if repo exists...
313 if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
318 if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
314 raise webob.exc.HTTPNotFound()
319 raise webob.exc.HTTPNotFound()
315
320
316 if parsed_request.action is None:
321 if parsed_request.action is None:
317 # Note: the client doesn't get the helpful error message
322 # Note: the client doesn't get the helpful error message
318 raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
323 raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
319
324
320 #======================================================================
325 #======================================================================
321 # CHECK PERMISSIONS
326 # CHECK PERMISSIONS
322 #======================================================================
327 #======================================================================
323 ip_addr = self._get_ip_addr(environ)
328 ip_addr = self._get_ip_addr(environ)
324 user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
329 user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
325 if response_app is not None:
330 if response_app is not None:
326 return response_app(environ, start_response)
331 return response_app(environ, start_response)
327
332
328 #======================================================================
333 #======================================================================
329 # REQUEST HANDLING
334 # REQUEST HANDLING
330 #======================================================================
335 #======================================================================
331 set_hook_environment(user.username, ip_addr,
336 set_hook_environment(user.username, ip_addr,
332 parsed_request.repo_name, self.scm_alias, parsed_request.action)
337 parsed_request.repo_name, self.scm_alias, parsed_request.action)
333
338
334 try:
339 try:
335 log.info('%s action on %s repo "%s" by "%s" from %s',
340 log.info('%s action on %s repo "%s" by "%s" from %s',
336 parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
341 parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
337 app = self._make_app(parsed_request)
342 app = self._make_app(parsed_request)
338 return app(environ, start_response)
343 return app(environ, start_response)
339 except Exception:
344 except Exception:
340 log.error(traceback.format_exc())
345 log.error(traceback.format_exc())
341 raise webob.exc.HTTPInternalServerError()
346 raise webob.exc.HTTPInternalServerError()
342
347
343 except webob.exc.HTTPException as e:
348 except webob.exc.HTTPException as e:
344 return e(environ, start_response)
349 return e(environ, start_response)
345
350
346
351
347 class BaseController(TGController):
352 class BaseController(TGController):
348
353
349 def _before(self, *args, **kwargs):
354 def _before(self, *args, **kwargs):
350 """
355 """
351 _before is called before controller methods and after __call__
356 _before is called before controller methods and after __call__
352 """
357 """
353 if request.needs_csrf_check:
358 if request.needs_csrf_check:
354 # CSRF protection: Whenever a request has ambient authority (whether
359 # CSRF protection: Whenever a request has ambient authority (whether
355 # through a session cookie or its origin IP address), it must include
360 # through a session cookie or its origin IP address), it must include
356 # the correct token, unless the HTTP method is GET or HEAD (and thus
361 # the correct token, unless the HTTP method is GET or HEAD (and thus
357 # guaranteed to be side effect free. In practice, the only situation
362 # guaranteed to be side effect free. In practice, the only situation
358 # where we allow side effects without ambient authority is when the
363 # where we allow side effects without ambient authority is when the
359 # authority comes from an API key; and that is handled above.
364 # authority comes from an API key; and that is handled above.
360 from kallithea.lib import helpers as h
365 from kallithea.lib import helpers as h
361 token = request.POST.get(h.session_csrf_secret_name)
366 token = request.POST.get(h.session_csrf_secret_name)
362 if not token or token != h.session_csrf_secret_token():
367 if not token or token != h.session_csrf_secret_token():
363 log.error('CSRF check failed')
368 log.error('CSRF check failed')
364 raise webob.exc.HTTPForbidden()
369 raise webob.exc.HTTPForbidden()
365
370
366 c.kallithea_version = __version__
371 c.kallithea_version = __version__
367 rc_config = Setting.get_app_settings()
372 rc_config = Setting.get_app_settings()
368
373
369 # Visual options
374 # Visual options
370 c.visual = AttributeDict({})
375 c.visual = AttributeDict({})
371
376
372 ## DB stored
377 ## DB stored
373 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
378 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
374 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
379 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
375 c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels'))
380 c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels'))
376 c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100))
381 c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100))
377 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
382 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
378 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
383 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
379 c.visual.show_version = str2bool(rc_config.get('show_version'))
384 c.visual.show_version = str2bool(rc_config.get('show_version'))
380 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
385 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
381 c.visual.gravatar_url = rc_config.get('gravatar_url')
386 c.visual.gravatar_url = rc_config.get('gravatar_url')
382
387
383 c.ga_code = rc_config.get('ga_code')
388 c.ga_code = rc_config.get('ga_code')
384 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
389 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
385 if c.ga_code and '<' not in c.ga_code:
390 if c.ga_code and '<' not in c.ga_code:
386 c.ga_code = '''<script type="text/javascript">
391 c.ga_code = '''<script type="text/javascript">
387 var _gaq = _gaq || [];
392 var _gaq = _gaq || [];
388 _gaq.push(['_setAccount', '%s']);
393 _gaq.push(['_setAccount', '%s']);
389 _gaq.push(['_trackPageview']);
394 _gaq.push(['_trackPageview']);
390
395
391 (function() {
396 (function() {
392 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
397 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
393 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
398 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
394 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
399 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
395 })();
400 })();
396 </script>''' % c.ga_code
401 </script>''' % c.ga_code
397 c.site_name = rc_config.get('title')
402 c.site_name = rc_config.get('title')
398 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
403 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
399 c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH
404 c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH
400
405
401 ## INI stored
406 ## INI stored
402 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
407 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
403 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
408 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
404 c.ssh_enabled = str2bool(config.get('ssh_enabled', False))
409 c.ssh_enabled = str2bool(config.get('ssh_enabled', False))
405
410
406 c.instance_id = config.get('instance_id')
411 c.instance_id = config.get('instance_id')
407 c.issues_url = config.get('bugtracker', url('issues_url'))
412 c.issues_url = config.get('bugtracker', url('issues_url'))
408 # END CONFIG VARS
413 # END CONFIG VARS
409
414
410 c.repo_name = get_repo_slug(request) # can be empty
415 c.repo_name = get_repo_slug(request) # can be empty
411 c.backends = list(BACKENDS)
416 c.backends = list(BACKENDS)
412
417
413 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
418 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
414
419
415 c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
420 c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
416
421
417 self.scm_model = ScmModel()
422 self.scm_model = ScmModel()
418
423
419 @staticmethod
424 @staticmethod
420 def _determine_auth_user(session_authuser, ip_addr):
425 def _determine_auth_user(session_authuser, ip_addr):
421 """
426 """
422 Create an `AuthUser` object given the API key/bearer token
427 Create an `AuthUser` object given the API key/bearer token
423 (if any) and the value of the authuser session cookie.
428 (if any) and the value of the authuser session cookie.
424 Returns None if no valid user is found (like not active or no access for IP).
429 Returns None if no valid user is found (like not active or no access for IP).
425 """
430 """
426
431
427 # Authenticate by session cookie
432 # Authenticate by session cookie
428 # In ancient login sessions, 'authuser' may not be a dict.
433 # In ancient login sessions, 'authuser' may not be a dict.
429 # In that case, the user will have to log in again.
434 # In that case, the user will have to log in again.
430 # v0.3 and earlier included an 'is_authenticated' key; if present,
435 # v0.3 and earlier included an 'is_authenticated' key; if present,
431 # this must be True.
436 # this must be True.
432 if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
437 if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
433 return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
438 return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
434
439
435 # Authenticate by auth_container plugin (if enabled)
440 # Authenticate by auth_container plugin (if enabled)
436 if any(
441 if any(
437 plugin.is_container_auth
442 plugin.is_container_auth
438 for plugin in auth_modules.get_auth_plugins()
443 for plugin in auth_modules.get_auth_plugins()
439 ):
444 ):
440 try:
445 try:
441 user_info = auth_modules.authenticate('', '', request.environ)
446 user_info = auth_modules.authenticate('', '', request.environ)
442 except UserCreationError as e:
447 except UserCreationError as e:
443 from kallithea.lib import helpers as h
448 from kallithea.lib import helpers as h
444 h.flash(e, 'error', logf=log.error)
449 h.flash(e, 'error', logf=log.error)
445 else:
450 else:
446 if user_info is not None:
451 if user_info is not None:
447 username = user_info['username']
452 username = user_info['username']
448 user = User.get_by_username(username, case_insensitive=True)
453 user = User.get_by_username(username, case_insensitive=True)
449 return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
454 return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
450
455
451 # User is default user (if active) or anonymous
456 # User is default user (if active) or anonymous
452 default_user = User.get_default_user(cache=True)
457 default_user = User.get_default_user(cache=True)
453 authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
458 authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
454 if authuser is None: # fall back to anonymous
459 if authuser is None: # fall back to anonymous
455 authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
460 authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
456 return authuser
461 return authuser
457
462
458 @staticmethod
463 @staticmethod
459 def _basic_security_checks():
464 def _basic_security_checks():
460 """Perform basic security/sanity checks before processing the request."""
465 """Perform basic security/sanity checks before processing the request."""
461
466
462 # Only allow the following HTTP request methods.
467 # Only allow the following HTTP request methods.
463 if request.method not in ['GET', 'HEAD', 'POST']:
468 if request.method not in ['GET', 'HEAD', 'POST']:
464 raise webob.exc.HTTPMethodNotAllowed()
469 raise webob.exc.HTTPMethodNotAllowed()
465
470
466 # Also verify the _method override - no longer allowed.
471 # Also verify the _method override - no longer allowed.
467 if request.params.get('_method') is None:
472 if request.params.get('_method') is None:
468 pass # no override, no problem
473 pass # no override, no problem
469 else:
474 else:
470 raise webob.exc.HTTPMethodNotAllowed()
475 raise webob.exc.HTTPMethodNotAllowed()
471
476
472 # Make sure CSRF token never appears in the URL. If so, invalidate it.
477 # Make sure CSRF token never appears in the URL. If so, invalidate it.
473 from kallithea.lib import helpers as h
478 from kallithea.lib import helpers as h
474 if h.session_csrf_secret_name in request.GET:
479 if h.session_csrf_secret_name in request.GET:
475 log.error('CSRF key leak detected')
480 log.error('CSRF key leak detected')
476 session.pop(h.session_csrf_secret_name, None)
481 session.pop(h.session_csrf_secret_name, None)
477 session.save()
482 session.save()
478 h.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
483 h.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
479 category='error')
484 category='error')
480
485
481 # WebOb already ignores request payload parameters for anything other
486 # WebOb already ignores request payload parameters for anything other
482 # than POST/PUT, but double-check since other Kallithea code relies on
487 # than POST/PUT, but double-check since other Kallithea code relies on
483 # this assumption.
488 # this assumption.
484 if request.method not in ['POST', 'PUT'] and request.POST:
489 if request.method not in ['POST', 'PUT'] and request.POST:
485 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
490 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
486 raise webob.exc.HTTPBadRequest()
491 raise webob.exc.HTTPBadRequest()
487
492
488 def __call__(self, environ, context):
493 def __call__(self, environ, context):
489 try:
494 try:
490 ip_addr = _get_ip_addr(environ)
495 ip_addr = _get_ip_addr(environ)
491 self._basic_security_checks()
496 self._basic_security_checks()
492
497
493 api_key = request.GET.get('api_key')
498 api_key = request.GET.get('api_key')
494 try:
499 try:
495 # Request.authorization may raise ValueError on invalid input
500 # Request.authorization may raise ValueError on invalid input
496 type, params = request.authorization
501 type, params = request.authorization
497 except (ValueError, TypeError):
502 except (ValueError, TypeError):
498 pass
503 pass
499 else:
504 else:
500 if type.lower() == 'bearer':
505 if type.lower() == 'bearer':
501 api_key = params # bearer token is an api key too
506 api_key = params # bearer token is an api key too
502
507
503 if api_key is None:
508 if api_key is None:
504 authuser = self._determine_auth_user(
509 authuser = self._determine_auth_user(
505 session.get('authuser'),
510 session.get('authuser'),
506 ip_addr=ip_addr,
511 ip_addr=ip_addr,
507 )
512 )
508 needs_csrf_check = request.method not in ['GET', 'HEAD']
513 needs_csrf_check = request.method not in ['GET', 'HEAD']
509
514
510 else:
515 else:
511 dbuser = User.get_by_api_key(api_key)
516 dbuser = User.get_by_api_key(api_key)
512 if dbuser is None:
517 if dbuser is None:
513 log.info('No db user found for authentication with API key ****%s from %s',
518 log.info('No db user found for authentication with API key ****%s from %s',
514 api_key[-4:], ip_addr)
519 api_key[-4:], ip_addr)
515 authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
520 authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
516 needs_csrf_check = False # API key provides CSRF protection
521 needs_csrf_check = False # API key provides CSRF protection
517
522
518 if authuser is None:
523 if authuser is None:
519 log.info('No valid user found')
524 log.info('No valid user found')
520 raise webob.exc.HTTPForbidden()
525 raise webob.exc.HTTPForbidden()
521
526
522 # set globals for auth user
527 # set globals for auth user
523 request.authuser = authuser
528 request.authuser = authuser
524 request.ip_addr = ip_addr
529 request.ip_addr = ip_addr
525 request.needs_csrf_check = needs_csrf_check
530 request.needs_csrf_check = needs_csrf_check
526
531
527 log.info('IP: %s User: %s accessed %s',
532 log.info('IP: %s User: %s accessed %s',
528 request.ip_addr, request.authuser,
533 request.ip_addr, request.authuser,
529 get_path_info(environ),
534 get_path_info(environ),
530 )
535 )
531 return super(BaseController, self).__call__(environ, context)
536 return super(BaseController, self).__call__(environ, context)
532 except webob.exc.HTTPException as e:
537 except webob.exc.HTTPException as e:
533 return e
538 return e
534
539
535
540
536 class BaseRepoController(BaseController):
541 class BaseRepoController(BaseController):
537 """
542 """
538 Base class for controllers responsible for loading all needed data for
543 Base class for controllers responsible for loading all needed data for
539 repository loaded items are
544 repository loaded items are
540
545
541 c.db_repo_scm_instance: instance of scm repository
546 c.db_repo_scm_instance: instance of scm repository
542 c.db_repo: instance of db
547 c.db_repo: instance of db
543 c.repository_followers: number of followers
548 c.repository_followers: number of followers
544 c.repository_forks: number of forks
549 c.repository_forks: number of forks
545 c.repository_following: weather the current user is following the current repo
550 c.repository_following: weather the current user is following the current repo
546 """
551 """
547
552
548 def _before(self, *args, **kwargs):
553 def _before(self, *args, **kwargs):
549 super(BaseRepoController, self)._before(*args, **kwargs)
554 super(BaseRepoController, self)._before(*args, **kwargs)
550 if c.repo_name: # extracted from routes
555 if c.repo_name: # extracted from routes
551 _dbr = Repository.get_by_repo_name(c.repo_name)
556 _dbr = Repository.get_by_repo_name(c.repo_name)
552 if not _dbr:
557 if not _dbr:
553 return
558 return
554
559
555 log.debug('Found repository in database %s with state `%s`',
560 log.debug('Found repository in database %s with state `%s`',
556 _dbr, _dbr.repo_state)
561 _dbr, _dbr.repo_state)
557 route = getattr(request.environ.get('routes.route'), 'name', '')
562 route = getattr(request.environ.get('routes.route'), 'name', '')
558
563
559 # allow to delete repos that are somehow damages in filesystem
564 # allow to delete repos that are somehow damages in filesystem
560 if route in ['delete_repo']:
565 if route in ['delete_repo']:
561 return
566 return
562
567
563 if _dbr.repo_state in [Repository.STATE_PENDING]:
568 if _dbr.repo_state in [Repository.STATE_PENDING]:
564 if route in ['repo_creating_home']:
569 if route in ['repo_creating_home']:
565 return
570 return
566 check_url = url('repo_creating_home', repo_name=c.repo_name)
571 check_url = url('repo_creating_home', repo_name=c.repo_name)
567 raise webob.exc.HTTPFound(location=check_url)
572 raise webob.exc.HTTPFound(location=check_url)
568
573
569 dbr = c.db_repo = _dbr
574 dbr = c.db_repo = _dbr
570 c.db_repo_scm_instance = c.db_repo.scm_instance
575 c.db_repo_scm_instance = c.db_repo.scm_instance
571 if c.db_repo_scm_instance is None:
576 if c.db_repo_scm_instance is None:
572 log.error('%s this repository is present in database but it '
577 log.error('%s this repository is present in database but it '
573 'cannot be created as an scm instance', c.repo_name)
578 'cannot be created as an scm instance', c.repo_name)
574 from kallithea.lib import helpers as h
579 from kallithea.lib import helpers as h
575 h.flash(_('Repository not found in the filesystem'),
580 h.flash(_('Repository not found in the filesystem'),
576 category='error')
581 category='error')
577 raise webob.exc.HTTPNotFound()
582 raise webob.exc.HTTPNotFound()
578
583
579 # some globals counter for menu
584 # some globals counter for menu
580 c.repository_followers = self.scm_model.get_followers(dbr)
585 c.repository_followers = self.scm_model.get_followers(dbr)
581 c.repository_forks = self.scm_model.get_forks(dbr)
586 c.repository_forks = self.scm_model.get_forks(dbr)
582 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
587 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
583 c.repository_following = self.scm_model.is_following_repo(
588 c.repository_following = self.scm_model.is_following_repo(
584 c.repo_name, request.authuser.user_id)
589 c.repo_name, request.authuser.user_id)
585
590
586 @staticmethod
591 @staticmethod
587 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
592 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
588 """
593 """
589 Safe way to get changeset. If error occurs show error.
594 Safe way to get changeset. If error occurs show error.
590 """
595 """
591 from kallithea.lib import helpers as h
596 from kallithea.lib import helpers as h
592 try:
597 try:
593 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
598 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
594 except EmptyRepositoryError as e:
599 except EmptyRepositoryError as e:
595 if returnempty:
600 if returnempty:
596 return repo.scm_instance.EMPTY_CHANGESET
601 return repo.scm_instance.EMPTY_CHANGESET
597 h.flash(_('There are no changesets yet'), category='error')
602 h.flash(_('There are no changesets yet'), category='error')
598 raise webob.exc.HTTPNotFound()
603 raise webob.exc.HTTPNotFound()
599 except ChangesetDoesNotExistError as e:
604 except ChangesetDoesNotExistError as e:
600 h.flash(_('Changeset for %s %s not found in %s') %
605 h.flash(_('Changeset for %s %s not found in %s') %
601 (ref_type, ref_name, repo.repo_name),
606 (ref_type, ref_name, repo.repo_name),
602 category='error')
607 category='error')
603 raise webob.exc.HTTPNotFound()
608 raise webob.exc.HTTPNotFound()
604 except RepositoryError as e:
609 except RepositoryError as e:
605 log.error(traceback.format_exc())
610 log.error(traceback.format_exc())
606 h.flash(e, category='error')
611 h.flash(e, category='error')
607 raise webob.exc.HTTPBadRequest()
612 raise webob.exc.HTTPBadRequest()
608
613
609
614
610 @decorator.decorator
615 @decorator.decorator
611 def jsonify(func, *args, **kwargs):
616 def jsonify(func, *args, **kwargs):
612 """Action decorator that formats output for JSON
617 """Action decorator that formats output for JSON
613
618
614 Given a function that will return content, this decorator will turn
619 Given a function that will return content, this decorator will turn
615 the result into JSON, with a content-type of 'application/json' and
620 the result into JSON, with a content-type of 'application/json' and
616 output it.
621 output it.
617 """
622 """
618 response.headers['Content-Type'] = 'application/json; charset=utf-8'
623 response.headers['Content-Type'] = 'application/json; charset=utf-8'
619 data = func(*args, **kwargs)
624 data = func(*args, **kwargs)
620 if isinstance(data, (list, tuple)):
625 if isinstance(data, (list, tuple)):
621 # A JSON list response is syntactically valid JavaScript and can be
626 # A JSON list response is syntactically valid JavaScript and can be
622 # loaded and executed as JavaScript by a malicious third-party site
627 # loaded and executed as JavaScript by a malicious third-party site
623 # using <script>, which can lead to cross-site data leaks.
628 # using <script>, which can lead to cross-site data leaks.
624 # JSON responses should therefore be scalars or objects (i.e. Python
629 # JSON responses should therefore be scalars or objects (i.e. Python
625 # dicts), because a JSON object is a syntax error if intepreted as JS.
630 # dicts), because a JSON object is a syntax error if intepreted as JS.
626 msg = "JSON responses with Array envelopes are susceptible to " \
631 msg = "JSON responses with Array envelopes are susceptible to " \
627 "cross-site data leak attacks, see " \
632 "cross-site data leak attacks, see " \
628 "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
633 "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
629 warnings.warn(msg, Warning, 2)
634 warnings.warn(msg, Warning, 2)
630 log.warning(msg)
635 log.warning(msg)
631 log.debug("Returning JSON wrapped action output")
636 log.debug("Returning JSON wrapped action output")
632 return ascii_bytes(ext_json.dumps(data))
637 return ascii_bytes(ext_json.dumps(data))
633
638
634 @decorator.decorator
639 @decorator.decorator
635 def IfSshEnabled(func, *args, **kwargs):
640 def IfSshEnabled(func, *args, **kwargs):
636 """Decorator for functions that can only be called if SSH access is enabled.
641 """Decorator for functions that can only be called if SSH access is enabled.
637
642
638 If SSH access is disabled in the configuration file, HTTPNotFound is raised.
643 If SSH access is disabled in the configuration file, HTTPNotFound is raised.
639 """
644 """
640 if not c.ssh_enabled:
645 if not c.ssh_enabled:
641 from kallithea.lib import helpers as h
646 from kallithea.lib import helpers as h
642 h.flash(_("SSH access is disabled."), category='warning')
647 h.flash(_("SSH access is disabled."), category='warning')
643 raise webob.exc.HTTPNotFound()
648 raise webob.exc.HTTPNotFound()
644 return func(*args, **kwargs)
649 return func(*args, **kwargs)
@@ -1,41 +1,41 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.middleware.permanent_repo_url
15 kallithea.lib.middleware.permanent_repo_url
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with
18 middleware to handle permanent repo URLs, replacing PATH_INFO '/_123/yada' with
19 '/name/of/repo/yada' after looking 123 up in the database.
19 '/name/of/repo/yada' after looking 123 up in the database.
20 """
20 """
21
21
22
22
23 from kallithea.lib.utils import fix_repo_id_name
23 from kallithea.lib.utils import fix_repo_id_name
24 from kallithea.lib.utils2 import safe_bytes, safe_str
24 from kallithea.lib.utils2 import safe_bytes, safe_str
25
25
26
26
27 class PermanentRepoUrl(object):
27 class PermanentRepoUrl(object):
28
28
29 def __init__(self, app, config):
29 def __init__(self, app, config):
30 self.application = app
30 self.application = app
31 self.config = config
31 self.config = config
32
32
33 def __call__(self, environ, start_response):
33 def __call__(self, environ, start_response):
34 # Extract path_info as get_path_info does, but do it explicitly because
34 # Extract path_info as get_path_info does, but do it explicitly because
35 # we also have to do the reverse operation when patching it back in
35 # we also have to do the reverse operation when patching it back in
36 path_info = safe_str(environ['PATH_INFO'])
36 path_info = safe_str(environ['PATH_INFO'].encode('latin1'))
37 if path_info.startswith('/'): # it must
37 if path_info.startswith('/'): # it must
38 path_info = '/' + fix_repo_id_name(path_info[1:])
38 path_info = '/' + fix_repo_id_name(path_info[1:])
39 environ['PATH_INFO'] = safe_bytes(path_info)
39 environ['PATH_INFO'] = safe_bytes(path_info).decode('latin1')
40
40
41 return self.application(environ, start_response)
41 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now