##// END OF EJS Templates
cleanup: replace redirect with WebOb exceptions...
Søren Løvborg -
r5543:d9b78d8f default
parent child Browse files
Show More
@@ -1,149 +1,149 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.auth_settings
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 pluggable authentication controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Nov 26, 2010
23 23 :author: akesterson
24 24 """
25 25
26 26 import logging
27 27 import formencode.htmlfill
28 28 import traceback
29 29
30 30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
32 31 from pylons.i18n.translation import _
32 from webob.exc import HTTPFound
33 33
34 34 from kallithea.lib import helpers as h
35 35 from kallithea.lib.compat import formatted_json
36 36 from kallithea.lib.base import BaseController, render
37 37 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
38 38 from kallithea.lib import auth_modules
39 39 from kallithea.model.forms import AuthSettingsForm
40 40 from kallithea.model.db import Setting
41 41 from kallithea.model.meta import Session
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class AuthSettingsController(BaseController):
47 47
48 48 @LoginRequired()
49 49 @HasPermissionAllDecorator('hg.admin')
50 50 def __before__(self):
51 51 super(AuthSettingsController, self).__before__()
52 52
53 53 def __load_defaults(self):
54 54 c.available_plugins = [
55 55 'kallithea.lib.auth_modules.auth_internal',
56 56 'kallithea.lib.auth_modules.auth_container',
57 57 'kallithea.lib.auth_modules.auth_ldap',
58 58 'kallithea.lib.auth_modules.auth_crowd',
59 59 'kallithea.lib.auth_modules.auth_pam'
60 60 ]
61 61 c.enabled_plugins = Setting.get_auth_plugins()
62 62
63 63 def __render(self, defaults, errors):
64 64 c.defaults = {}
65 65 c.plugin_settings = {}
66 66 c.plugin_shortnames = {}
67 67
68 68 for module in c.enabled_plugins:
69 69 plugin = auth_modules.loadplugin(module)
70 70 plugin_name = plugin.name
71 71 c.plugin_shortnames[module] = plugin_name
72 72 c.plugin_settings[module] = plugin.plugin_settings()
73 73 for v in c.plugin_settings[module]:
74 74 fullname = ("auth_" + plugin_name + "_" + v["name"])
75 75 if "default" in v:
76 76 c.defaults[fullname] = v["default"]
77 77 # Current values will be the default on the form, if there are any
78 78 setting = Setting.get_by_name(fullname)
79 79 if setting is not None:
80 80 c.defaults[fullname] = setting.app_settings_value
81 81 # we want to show , separated list of enabled plugins
82 82 c.defaults['auth_plugins'] = ','.join(c.enabled_plugins)
83 83
84 84 if defaults:
85 85 c.defaults.update(defaults)
86 86
87 87 log.debug(formatted_json(defaults))
88 88 return formencode.htmlfill.render(
89 89 render('admin/auth/auth_settings.html'),
90 90 defaults=c.defaults,
91 91 errors=errors,
92 92 prefix_error=False,
93 93 encoding="UTF-8",
94 94 force_defaults=False)
95 95
96 96 def index(self):
97 97 self.__load_defaults()
98 98 return self.__render(defaults=None, errors=None)
99 99
100 100 def auth_settings(self):
101 101 """POST create and store auth settings"""
102 102 self.__load_defaults()
103 103 log.debug("POST Result: %s", formatted_json(dict(request.POST)))
104 104
105 105 # First, parse only the plugin list (not the plugin settings).
106 106 _auth_plugins_validator = AuthSettingsForm([]).fields['auth_plugins']
107 107 try:
108 108 new_enabled_plugins = _auth_plugins_validator.to_python(request.POST.get('auth_plugins'))
109 109 except formencode.Invalid:
110 110 # User provided an invalid plugin list. Just fall back to
111 111 # the list of currently enabled plugins. (We'll re-validate
112 112 # and show an error message to the user, below.)
113 113 pass
114 114 else:
115 115 # Hide plugins that the user has asked to be disabled, but
116 116 # do not show plugins that the user has asked to be enabled
117 117 # (yet), since that'll cause validation errors and/or wrong
118 118 # settings being applied (e.g. checkboxes being cleared),
119 119 # since the plugin settings will not be in the POST data.
120 120 c.enabled_plugins = [ p for p in c.enabled_plugins if p in new_enabled_plugins ]
121 121
122 122 # Next, parse everything including plugin settings.
123 123 _form = AuthSettingsForm(c.enabled_plugins)()
124 124
125 125 try:
126 126 form_result = _form.to_python(dict(request.POST))
127 127 for k, v in form_result.items():
128 128 if k == 'auth_plugins':
129 129 # we want to store it comma separated inside our settings
130 130 v = ','.join(v)
131 131 log.debug("%s = %s", k, str(v))
132 132 setting = Setting.create_or_update(k, v)
133 133 Session().add(setting)
134 134 Session().commit()
135 135 h.flash(_('Auth settings updated successfully'),
136 136 category='success')
137 137 except formencode.Invalid as errors:
138 138 log.error(traceback.format_exc())
139 139 e = errors.error_dict or {}
140 140 return self.__render(
141 141 defaults=errors.value,
142 142 errors=e,
143 143 )
144 144 except Exception:
145 145 log.error(traceback.format_exc())
146 146 h.flash(_('error occurred during update of auth settings'),
147 147 category='error')
148 148
149 return redirect(url('auth_home'))
149 raise HTTPFound(location=url('auth_home'))
@@ -1,132 +1,132 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.defaults
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 default settings controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 27, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 from formencode import htmlfill
32 32
33 33 from pylons import request, tmpl_context as c, url
34 from pylons.controllers.util import redirect
35 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36 36
37 37 from kallithea.lib import helpers as h
38 38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 39 from kallithea.lib.base import BaseController, render
40 40 from kallithea.model.forms import DefaultsForm
41 41 from kallithea.model.meta import Session
42 42 from kallithea import BACKENDS
43 43 from kallithea.model.db import Setting
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class DefaultsController(BaseController):
49 49 """REST Controller styled on the Atom Publishing Protocol"""
50 50 # To properly map this controller, ensure your config/routing.py
51 51 # file has a resource setup:
52 52 # map.resource('default', 'defaults')
53 53
54 54 @LoginRequired()
55 55 @HasPermissionAllDecorator('hg.admin')
56 56 def __before__(self):
57 57 super(DefaultsController, self).__before__()
58 58
59 59 def index(self, format='html'):
60 60 """GET /defaults: All items in the collection"""
61 61 # url('defaults')
62 62 c.backends = BACKENDS.keys()
63 63 defaults = Setting.get_default_repo_settings()
64 64
65 65 return htmlfill.render(
66 66 render('admin/defaults/defaults.html'),
67 67 defaults=defaults,
68 68 encoding="UTF-8",
69 69 force_defaults=False
70 70 )
71 71
72 72 def create(self):
73 73 """POST /defaults: Create a new item"""
74 74 # url('defaults')
75 75
76 76 def new(self, format='html'):
77 77 """GET /defaults/new: Form to create a new item"""
78 78 # url('new_default')
79 79
80 80 def update(self, id):
81 81 """PUT /defaults/id: Update an existing item"""
82 82 # Forms posted to this method should contain a hidden field:
83 83 # <input type="hidden" name="_method" value="PUT" />
84 84 # Or using helpers:
85 85 # h.form(url('default', id=ID),
86 86 # method='put')
87 87 # url('default', id=ID)
88 88
89 89 _form = DefaultsForm()()
90 90
91 91 try:
92 92 form_result = _form.to_python(dict(request.POST))
93 93 for k, v in form_result.iteritems():
94 94 setting = Setting.create_or_update(k, v)
95 95 Session().add(setting)
96 96 Session().commit()
97 97 h.flash(_('Default settings updated successfully'),
98 98 category='success')
99 99
100 100 except formencode.Invalid as errors:
101 101 defaults = errors.value
102 102
103 103 return htmlfill.render(
104 104 render('admin/defaults/defaults.html'),
105 105 defaults=defaults,
106 106 errors=errors.error_dict or {},
107 107 prefix_error=False,
108 108 encoding="UTF-8",
109 109 force_defaults=False)
110 110 except Exception:
111 111 log.error(traceback.format_exc())
112 112 h.flash(_('Error occurred during update of defaults'),
113 113 category='error')
114 114
115 return redirect(url('defaults'))
115 raise HTTPFound(location=url('defaults'))
116 116
117 117 def delete(self, id):
118 118 """DELETE /defaults/id: Delete an existing item"""
119 119 # Forms posted to this method should contain a hidden field:
120 120 # <input type="hidden" name="_method" value="DELETE" />
121 121 # Or using helpers:
122 122 # h.form(url('default', id=ID),
123 123 # method='delete')
124 124 # url('default', id=ID)
125 125
126 126 def show(self, id, format='html'):
127 127 """GET /defaults/id: Show a specific item"""
128 128 # url('default', id=ID)
129 129
130 130 def edit(self, id, format='html'):
131 131 """GET /defaults/id/edit: Form to edit an existing item"""
132 132 # url('edit_default', id=ID)
@@ -1,293 +1,292 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.gists
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 gist controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 9, 2013
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import time
29 29 import logging
30 30 import traceback
31 31 import formencode.htmlfill
32 32
33 33 from pylons import request, response, tmpl_context as c, url
34 from pylons.controllers.util import redirect
35 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden
36 36
37 37 from kallithea.model.forms import GistForm
38 38 from kallithea.model.gist import GistModel
39 39 from kallithea.model.meta import Session
40 40 from kallithea.model.db import Gist, User
41 41 from kallithea.lib import helpers as h
42 42 from kallithea.lib.base import BaseController, render
43 43 from kallithea.lib.auth import LoginRequired, NotAnonymous
44 44 from kallithea.lib.utils import jsonify
45 45 from kallithea.lib.utils2 import safe_int, time_to_datetime
46 46 from kallithea.lib.helpers import Page
47 from webob.exc import HTTPNotFound, HTTPForbidden
48 47 from sqlalchemy.sql.expression import or_
49 48 from kallithea.lib.vcs.exceptions import VCSError, NodeNotChangedError
50 49
51 50 log = logging.getLogger(__name__)
52 51
53 52
54 53 class GistsController(BaseController):
55 54 """REST Controller styled on the Atom Publishing Protocol"""
56 55
57 56 def __load_defaults(self, extra_values=None):
58 57 c.lifetime_values = [
59 58 (str(-1), _('Forever')),
60 59 (str(5), _('5 minutes')),
61 60 (str(60), _('1 hour')),
62 61 (str(60 * 24), _('1 day')),
63 62 (str(60 * 24 * 30), _('1 month')),
64 63 ]
65 64 if extra_values:
66 65 c.lifetime_values.append(extra_values)
67 66 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
68 67
69 68 @LoginRequired()
70 69 def index(self):
71 70 """GET /admin/gists: All items in the collection"""
72 71 # url('gists')
73 72 not_default_user = c.authuser.username != User.DEFAULT_USER
74 73 c.show_private = request.GET.get('private') and not_default_user
75 74 c.show_public = request.GET.get('public') and not_default_user
76 75
77 76 gists = Gist().query()\
78 77 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
79 78 .order_by(Gist.created_on.desc())
80 79
81 80 # MY private
82 81 if c.show_private and not c.show_public:
83 82 gists = gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
84 83 .filter(Gist.gist_owner == c.authuser.user_id)
85 84 # MY public
86 85 elif c.show_public and not c.show_private:
87 86 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
88 87 .filter(Gist.gist_owner == c.authuser.user_id)
89 88
90 89 # MY public+private
91 90 elif c.show_private and c.show_public:
92 91 gists = gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
93 92 Gist.gist_type == Gist.GIST_PRIVATE))\
94 93 .filter(Gist.gist_owner == c.authuser.user_id)
95 94
96 95 # default show ALL public gists
97 96 if not c.show_public and not c.show_private:
98 97 gists = gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
99 98
100 99 c.gists = gists
101 100 p = safe_int(request.GET.get('page', 1), 1)
102 101 c.gists_pager = Page(c.gists, page=p, items_per_page=10)
103 102 return render('admin/gists/index.html')
104 103
105 104 @LoginRequired()
106 105 @NotAnonymous()
107 106 def create(self):
108 107 """POST /admin/gists: Create a new item"""
109 108 # url('gists')
110 109 self.__load_defaults()
111 110 gist_form = GistForm([x[0] for x in c.lifetime_values])()
112 111 try:
113 112 form_result = gist_form.to_python(dict(request.POST))
114 113 #TODO: multiple files support, from the form
115 114 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
116 115 nodes = {
117 116 filename: {
118 117 'content': form_result['content'],
119 118 'lexer': form_result['mimetype'] # None is autodetect
120 119 }
121 120 }
122 121 _public = form_result['public']
123 122 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
124 123 gist = GistModel().create(
125 124 description=form_result['description'],
126 125 owner=c.authuser.user_id,
127 126 gist_mapping=nodes,
128 127 gist_type=gist_type,
129 128 lifetime=form_result['lifetime']
130 129 )
131 130 Session().commit()
132 131 new_gist_id = gist.gist_access_id
133 132 except formencode.Invalid as errors:
134 133 defaults = errors.value
135 134
136 135 return formencode.htmlfill.render(
137 136 render('admin/gists/new.html'),
138 137 defaults=defaults,
139 138 errors=errors.error_dict or {},
140 139 prefix_error=False,
141 140 encoding="UTF-8",
142 141 force_defaults=False)
143 142
144 143 except Exception as e:
145 144 log.error(traceback.format_exc())
146 145 h.flash(_('Error occurred during gist creation'), category='error')
147 return redirect(url('new_gist'))
148 return redirect(url('gist', gist_id=new_gist_id))
146 raise HTTPFound(location=url('new_gist'))
147 raise HTTPFound(location=url('gist', gist_id=new_gist_id))
149 148
150 149 @LoginRequired()
151 150 @NotAnonymous()
152 151 def new(self, format='html'):
153 152 """GET /admin/gists/new: Form to create a new item"""
154 153 # url('new_gist')
155 154 self.__load_defaults()
156 155 return render('admin/gists/new.html')
157 156
158 157 @LoginRequired()
159 158 @NotAnonymous()
160 159 def update(self, gist_id):
161 160 """PUT /admin/gists/gist_id: Update an existing item"""
162 161 # Forms posted to this method should contain a hidden field:
163 162 # <input type="hidden" name="_method" value="PUT" />
164 163 # Or using helpers:
165 164 # h.form(url('gist', gist_id=ID),
166 165 # method='put')
167 166 # url('gist', gist_id=ID)
168 167
169 168 @LoginRequired()
170 169 @NotAnonymous()
171 170 def delete(self, gist_id):
172 171 """DELETE /admin/gists/gist_id: Delete an existing item"""
173 172 # Forms posted to this method should contain a hidden field:
174 173 # <input type="hidden" name="_method" value="DELETE" />
175 174 # Or using helpers:
176 175 # h.form(url('gist', gist_id=ID),
177 176 # method='delete')
178 177 # url('gist', gist_id=ID)
179 178 gist = GistModel().get_gist(gist_id)
180 179 owner = gist.gist_owner == c.authuser.user_id
181 180 if h.HasPermissionAny('hg.admin')() or owner:
182 181 GistModel().delete(gist)
183 182 Session().commit()
184 183 h.flash(_('Deleted gist %s') % gist.gist_access_id, category='success')
185 184 else:
186 185 raise HTTPForbidden()
187 186
188 return redirect(url('gists'))
187 raise HTTPFound(location=url('gists'))
189 188
190 189 @LoginRequired()
191 190 def show(self, gist_id, revision='tip', format='html', f_path=None):
192 191 """GET /admin/gists/gist_id: Show a specific item"""
193 192 # url('gist', gist_id=ID)
194 193 c.gist = Gist.get_or_404(gist_id)
195 194
196 195 #check if this gist is not expired
197 196 if c.gist.gist_expires != -1:
198 197 if time.time() > c.gist.gist_expires:
199 198 log.error('Gist expired at %s',
200 199 time_to_datetime(c.gist.gist_expires))
201 200 raise HTTPNotFound()
202 201 try:
203 202 c.file_changeset, c.files = GistModel().get_gist_files(gist_id,
204 203 revision=revision)
205 204 except VCSError:
206 205 log.error(traceback.format_exc())
207 206 raise HTTPNotFound()
208 207 if format == 'raw':
209 208 content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)])
210 209 response.content_type = 'text/plain'
211 210 return content
212 211 return render('admin/gists/show.html')
213 212
214 213 @LoginRequired()
215 214 @NotAnonymous()
216 215 def edit(self, gist_id, format='html'):
217 216 """GET /admin/gists/gist_id/edit: Form to edit an existing item"""
218 217 # url('edit_gist', gist_id=ID)
219 218 c.gist = Gist.get_or_404(gist_id)
220 219
221 220 #check if this gist is not expired
222 221 if c.gist.gist_expires != -1:
223 222 if time.time() > c.gist.gist_expires:
224 223 log.error('Gist expired at %s',
225 224 time_to_datetime(c.gist.gist_expires))
226 225 raise HTTPNotFound()
227 226 try:
228 227 c.file_changeset, c.files = GistModel().get_gist_files(gist_id)
229 228 except VCSError:
230 229 log.error(traceback.format_exc())
231 230 raise HTTPNotFound()
232 231
233 232 self.__load_defaults(extra_values=('0', _('Unmodified')))
234 233 rendered = render('admin/gists/edit.html')
235 234
236 235 if request.POST:
237 236 rpost = request.POST
238 237 nodes = {}
239 238 for org_filename, filename, mimetype, content in zip(
240 239 rpost.getall('org_files'),
241 240 rpost.getall('files'),
242 241 rpost.getall('mimetypes'),
243 242 rpost.getall('contents')):
244 243
245 244 nodes[org_filename] = {
246 245 'org_filename': org_filename,
247 246 'filename': filename,
248 247 'content': content,
249 248 'lexer': mimetype,
250 249 }
251 250 try:
252 251 GistModel().update(
253 252 gist=c.gist,
254 253 description=rpost['description'],
255 254 owner=c.gist.owner,
256 255 gist_mapping=nodes,
257 256 gist_type=c.gist.gist_type,
258 257 lifetime=rpost['lifetime']
259 258 )
260 259
261 260 Session().commit()
262 261 h.flash(_('Successfully updated gist content'), category='success')
263 262 except NodeNotChangedError:
264 263 # raised if nothing was changed in repo itself. We anyway then
265 264 # store only DB stuff for gist
266 265 Session().commit()
267 266 h.flash(_('Successfully updated gist data'), category='success')
268 267 except Exception:
269 268 log.error(traceback.format_exc())
270 269 h.flash(_('Error occurred during update of gist %s') % gist_id,
271 270 category='error')
272 271
273 return redirect(url('gist', gist_id=gist_id))
272 raise HTTPFound(location=url('gist', gist_id=gist_id))
274 273
275 274 return rendered
276 275
277 276 @LoginRequired()
278 277 @NotAnonymous()
279 278 @jsonify
280 279 def check_revision(self, gist_id):
281 280 c.gist = Gist.get_or_404(gist_id)
282 281 last_rev = c.gist.scm_instance.get_changeset()
283 282 success = True
284 283 revision = request.POST.get('revision')
285 284
286 285 ##TODO: maybe move this to model ?
287 286 if revision != last_rev.raw_id:
288 287 log.error('Last revision %s is different than submitted %s',
289 288 revision, last_rev)
290 289 # our gist has newer version than we
291 290 success = False
292 291
293 292 return {'success': success}
@@ -1,277 +1,277 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.my_account
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 my account controller for Kallithea admin
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: August 20, 2013
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from sqlalchemy import func
33 33 from formencode import htmlfill
34 34 from pylons import request, tmpl_context as c, url
35 from pylons.controllers.util import redirect
36 35 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
37 37
38 38 from kallithea import EXTERN_TYPE_INTERNAL
39 39 from kallithea.lib import helpers as h
40 40 from kallithea.lib import auth_modules
41 41 from kallithea.lib.auth import LoginRequired, NotAnonymous, AuthUser
42 42 from kallithea.lib.base import BaseController, render
43 43 from kallithea.lib.utils2 import generate_api_key, safe_int
44 44 from kallithea.lib.compat import json
45 45 from kallithea.model.db import Repository, UserEmailMap, User, UserFollowing
46 46 from kallithea.model.forms import UserForm, PasswordChangeForm
47 47 from kallithea.model.user import UserModel
48 48 from kallithea.model.repo import RepoModel
49 49 from kallithea.model.api_key import ApiKeyModel
50 50 from kallithea.model.meta import Session
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountController(BaseController):
56 56 """REST Controller styled on the Atom Publishing Protocol"""
57 57 # To properly map this controller, ensure your config/routing.py
58 58 # file has a resource setup:
59 59 # map.resource('setting', 'settings', controller='admin/settings',
60 60 # path_prefix='/admin', name_prefix='admin_')
61 61
62 62 @LoginRequired()
63 63 @NotAnonymous()
64 64 def __before__(self):
65 65 super(MyAccountController, self).__before__()
66 66
67 67 def __load_data(self):
68 68 c.user = User.get(self.authuser.user_id)
69 69 if c.user.username == User.DEFAULT_USER:
70 70 h.flash(_("You can't edit this user since it's"
71 71 " crucial for entire application"), category='warning')
72 return redirect(url('users'))
72 raise HTTPFound(location=url('users'))
73 73 c.EXTERN_TYPE_INTERNAL = EXTERN_TYPE_INTERNAL
74 74
75 75 def _load_my_repos_data(self, watched=False):
76 76 if watched:
77 77 admin = False
78 78 repos_list = [x.follows_repository for x in
79 79 Session().query(UserFollowing).filter(
80 80 UserFollowing.user_id ==
81 81 self.authuser.user_id).all()]
82 82 else:
83 83 admin = True
84 84 repos_list = Session().query(Repository)\
85 85 .filter(Repository.user_id ==
86 86 self.authuser.user_id)\
87 87 .order_by(func.lower(Repository.repo_name)).all()
88 88
89 89 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
90 90 admin=admin)
91 91 #json used to render the grid
92 92 return json.dumps(repos_data)
93 93
94 94 def my_account(self):
95 95 """
96 96 GET /_admin/my_account Displays info about my account
97 97 """
98 98 # url('my_account')
99 99 c.active = 'profile'
100 100 self.__load_data()
101 101 c.perm_user = AuthUser(user_id=self.authuser.user_id)
102 102 c.ip_addr = self.ip_addr
103 103 managed_fields = auth_modules.get_managed_fields(c.user)
104 104 def_user_perms = User.get_default_user().AuthUser.permissions['global']
105 105 if 'hg.register.none' in def_user_perms:
106 106 managed_fields.extend(['username', 'firstname', 'lastname', 'email'])
107 107
108 108 c.readonly = lambda n: 'readonly' if n in managed_fields else None
109 109
110 110 defaults = c.user.get_dict()
111 111 update = False
112 112 if request.POST:
113 113 _form = UserForm(edit=True,
114 114 old_data={'user_id': self.authuser.user_id,
115 115 'email': self.authuser.email})()
116 116 form_result = {}
117 117 try:
118 118 post_data = dict(request.POST)
119 119 post_data['new_password'] = ''
120 120 post_data['password_confirmation'] = ''
121 121 form_result = _form.to_python(post_data)
122 122 # skip updating those attrs for my account
123 123 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
124 124 'new_password', 'password_confirmation',
125 125 ] + managed_fields
126 126
127 127 UserModel().update(self.authuser.user_id, form_result,
128 128 skip_attrs=skip_attrs)
129 129 h.flash(_('Your account was updated successfully'),
130 130 category='success')
131 131 Session().commit()
132 132 update = True
133 133
134 134 except formencode.Invalid as errors:
135 135 return htmlfill.render(
136 136 render('admin/my_account/my_account.html'),
137 137 defaults=errors.value,
138 138 errors=errors.error_dict or {},
139 139 prefix_error=False,
140 140 encoding="UTF-8",
141 141 force_defaults=False)
142 142 except Exception:
143 143 log.error(traceback.format_exc())
144 144 h.flash(_('Error occurred during update of user %s') \
145 145 % form_result.get('username'), category='error')
146 146 if update:
147 return redirect('my_account')
147 raise HTTPFound(location='my_account')
148 148 return htmlfill.render(
149 149 render('admin/my_account/my_account.html'),
150 150 defaults=defaults,
151 151 encoding="UTF-8",
152 152 force_defaults=False)
153 153
154 154 def my_account_password(self):
155 155 c.active = 'password'
156 156 self.__load_data()
157 157
158 158 managed_fields = auth_modules.get_managed_fields(c.user)
159 159 c.can_change_password = 'password' not in managed_fields
160 160
161 161 if request.POST and c.can_change_password:
162 162 _form = PasswordChangeForm(self.authuser.username)()
163 163 try:
164 164 form_result = _form.to_python(request.POST)
165 165 UserModel().update(self.authuser.user_id, form_result)
166 166 Session().commit()
167 167 h.flash(_("Successfully updated password"), category='success')
168 168 except formencode.Invalid as errors:
169 169 return htmlfill.render(
170 170 render('admin/my_account/my_account.html'),
171 171 defaults=errors.value,
172 172 errors=errors.error_dict or {},
173 173 prefix_error=False,
174 174 encoding="UTF-8",
175 175 force_defaults=False)
176 176 except Exception:
177 177 log.error(traceback.format_exc())
178 178 h.flash(_('Error occurred during update of user password'),
179 179 category='error')
180 180 return render('admin/my_account/my_account.html')
181 181
182 182 def my_account_repos(self):
183 183 c.active = 'repos'
184 184 self.__load_data()
185 185
186 186 #json used to render the grid
187 187 c.data = self._load_my_repos_data()
188 188 return render('admin/my_account/my_account.html')
189 189
190 190 def my_account_watched(self):
191 191 c.active = 'watched'
192 192 self.__load_data()
193 193
194 194 #json used to render the grid
195 195 c.data = self._load_my_repos_data(watched=True)
196 196 return render('admin/my_account/my_account.html')
197 197
198 198 def my_account_perms(self):
199 199 c.active = 'perms'
200 200 self.__load_data()
201 201 c.perm_user = AuthUser(user_id=self.authuser.user_id)
202 202 c.ip_addr = self.ip_addr
203 203
204 204 return render('admin/my_account/my_account.html')
205 205
206 206 def my_account_emails(self):
207 207 c.active = 'emails'
208 208 self.__load_data()
209 209
210 210 c.user_email_map = UserEmailMap.query()\
211 211 .filter(UserEmailMap.user == c.user).all()
212 212 return render('admin/my_account/my_account.html')
213 213
214 214 def my_account_emails_add(self):
215 215 email = request.POST.get('new_email')
216 216
217 217 try:
218 218 UserModel().add_extra_email(self.authuser.user_id, email)
219 219 Session().commit()
220 220 h.flash(_("Added email %s to user") % email, category='success')
221 221 except formencode.Invalid as error:
222 222 msg = error.error_dict['email']
223 223 h.flash(msg, category='error')
224 224 except Exception:
225 225 log.error(traceback.format_exc())
226 226 h.flash(_('An error occurred during email saving'),
227 227 category='error')
228 return redirect(url('my_account_emails'))
228 raise HTTPFound(location=url('my_account_emails'))
229 229
230 230 def my_account_emails_delete(self):
231 231 email_id = request.POST.get('del_email_id')
232 232 user_model = UserModel()
233 233 user_model.delete_extra_email(self.authuser.user_id, email_id)
234 234 Session().commit()
235 235 h.flash(_("Removed email from user"), category='success')
236 return redirect(url('my_account_emails'))
236 raise HTTPFound(location=url('my_account_emails'))
237 237
238 238 def my_account_api_keys(self):
239 239 c.active = 'api_keys'
240 240 self.__load_data()
241 241 show_expired = True
242 242 c.lifetime_values = [
243 243 (str(-1), _('Forever')),
244 244 (str(5), _('5 minutes')),
245 245 (str(60), _('1 hour')),
246 246 (str(60 * 24), _('1 day')),
247 247 (str(60 * 24 * 30), _('1 month')),
248 248 ]
249 249 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
250 250 c.user_api_keys = ApiKeyModel().get_api_keys(self.authuser.user_id,
251 251 show_expired=show_expired)
252 252 return render('admin/my_account/my_account.html')
253 253
254 254 def my_account_api_keys_add(self):
255 255 lifetime = safe_int(request.POST.get('lifetime'), -1)
256 256 description = request.POST.get('description')
257 257 ApiKeyModel().create(self.authuser.user_id, description, lifetime)
258 258 Session().commit()
259 259 h.flash(_("API key successfully created"), category='success')
260 return redirect(url('my_account_api_keys'))
260 raise HTTPFound(location=url('my_account_api_keys'))
261 261
262 262 def my_account_api_keys_delete(self):
263 263 api_key = request.POST.get('del_api_key')
264 264 user_id = self.authuser.user_id
265 265 if request.POST.get('del_api_key_builtin'):
266 266 user = User.get(user_id)
267 267 if user is not None:
268 268 user.api_key = generate_api_key()
269 269 Session().add(user)
270 270 Session().commit()
271 271 h.flash(_("API key successfully reset"), category='success')
272 272 elif api_key:
273 273 ApiKeyModel().delete(api_key, self.authuser.user_id)
274 274 Session().commit()
275 275 h.flash(_("API key successfully deleted"), category='success')
276 276
277 return redirect(url('my_account_api_keys'))
277 raise HTTPFound(location=url('my_account_api_keys'))
@@ -1,196 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.permissions
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 permissions controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 27, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import logging
30 30 import traceback
31 31 import formencode
32 32 from formencode import htmlfill
33 33
34 34 from pylons import request, tmpl_context as c, url
35 from pylons.controllers.util import redirect
36 35 from pylons.i18n.translation import _
36 from webob.exc import HTTPFound
37 37
38 38 from kallithea.lib import helpers as h
39 39 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
40 40 from kallithea.lib.base import BaseController, render
41 41 from kallithea.model.forms import DefaultPermissionsForm
42 42 from kallithea.model.permission import PermissionModel
43 43 from kallithea.model.db import User, UserIpMap
44 44 from kallithea.model.meta import Session
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class PermissionsController(BaseController):
50 50 """REST Controller styled on the Atom Publishing Protocol"""
51 51 # To properly map this controller, ensure your config/routing.py
52 52 # file has a resource setup:
53 53 # map.resource('permission', 'permissions')
54 54
55 55 @LoginRequired()
56 56 @HasPermissionAllDecorator('hg.admin')
57 57 def __before__(self):
58 58 super(PermissionsController, self).__before__()
59 59
60 60 def __load_data(self):
61 61 c.repo_perms_choices = [('repository.none', _('None'),),
62 62 ('repository.read', _('Read'),),
63 63 ('repository.write', _('Write'),),
64 64 ('repository.admin', _('Admin'),)]
65 65 c.group_perms_choices = [('group.none', _('None'),),
66 66 ('group.read', _('Read'),),
67 67 ('group.write', _('Write'),),
68 68 ('group.admin', _('Admin'),)]
69 69 c.user_group_perms_choices = [('usergroup.none', _('None'),),
70 70 ('usergroup.read', _('Read'),),
71 71 ('usergroup.write', _('Write'),),
72 72 ('usergroup.admin', _('Admin'),)]
73 73 c.register_choices = [
74 74 ('hg.register.none',
75 75 _('Disabled')),
76 76 ('hg.register.manual_activate',
77 77 _('Allowed with manual account activation')),
78 78 ('hg.register.auto_activate',
79 79 _('Allowed with automatic account activation')), ]
80 80
81 81 c.extern_activate_choices = [
82 82 ('hg.extern_activate.manual', _('Manual activation of external account')),
83 83 ('hg.extern_activate.auto', _('Automatic activation of external account')),
84 84 ]
85 85
86 86 c.repo_create_choices = [('hg.create.none', _('Disabled')),
87 87 ('hg.create.repository', _('Enabled'))]
88 88
89 89 c.repo_create_on_write_choices = [
90 90 ('hg.create.write_on_repogroup.true', _('Enabled')),
91 91 ('hg.create.write_on_repogroup.false', _('Disabled')),
92 92 ]
93 93
94 94 c.user_group_create_choices = [('hg.usergroup.create.false', _('Disabled')),
95 95 ('hg.usergroup.create.true', _('Enabled'))]
96 96
97 97 c.repo_group_create_choices = [('hg.repogroup.create.false', _('Disabled')),
98 98 ('hg.repogroup.create.true', _('Enabled'))]
99 99
100 100 c.fork_choices = [('hg.fork.none', _('Disabled')),
101 101 ('hg.fork.repository', _('Enabled'))]
102 102
103 103 def permission_globals(self):
104 104 c.active = 'globals'
105 105 self.__load_data()
106 106 if request.POST:
107 107 _form = DefaultPermissionsForm(
108 108 [x[0] for x in c.repo_perms_choices],
109 109 [x[0] for x in c.group_perms_choices],
110 110 [x[0] for x in c.user_group_perms_choices],
111 111 [x[0] for x in c.repo_create_choices],
112 112 [x[0] for x in c.repo_create_on_write_choices],
113 113 [x[0] for x in c.repo_group_create_choices],
114 114 [x[0] for x in c.user_group_create_choices],
115 115 [x[0] for x in c.fork_choices],
116 116 [x[0] for x in c.register_choices],
117 117 [x[0] for x in c.extern_activate_choices])()
118 118
119 119 try:
120 120 form_result = _form.to_python(dict(request.POST))
121 121 form_result.update({'perm_user_name': 'default'})
122 122 PermissionModel().update(form_result)
123 123 Session().commit()
124 124 h.flash(_('Global permissions updated successfully'),
125 125 category='success')
126 126
127 127 except formencode.Invalid as errors:
128 128 defaults = errors.value
129 129
130 130 return htmlfill.render(
131 131 render('admin/permissions/permissions.html'),
132 132 defaults=defaults,
133 133 errors=errors.error_dict or {},
134 134 prefix_error=False,
135 135 encoding="UTF-8",
136 136 force_defaults=False)
137 137 except Exception:
138 138 log.error(traceback.format_exc())
139 139 h.flash(_('Error occurred during update of permissions'),
140 140 category='error')
141 141
142 return redirect(url('admin_permissions'))
142 raise HTTPFound(location=url('admin_permissions'))
143 143
144 144 c.user = User.get_default_user()
145 145 defaults = {'anonymous': c.user.active}
146 146
147 147 for p in c.user.user_perms:
148 148 if p.permission.permission_name.startswith('repository.'):
149 149 defaults['default_repo_perm'] = p.permission.permission_name
150 150
151 151 if p.permission.permission_name.startswith('group.'):
152 152 defaults['default_group_perm'] = p.permission.permission_name
153 153
154 154 if p.permission.permission_name.startswith('usergroup.'):
155 155 defaults['default_user_group_perm'] = p.permission.permission_name
156 156
157 157 if p.permission.permission_name.startswith('hg.create.write_on_repogroup'):
158 158 defaults['create_on_write'] = p.permission.permission_name
159 159
160 160 elif p.permission.permission_name.startswith('hg.create.'):
161 161 defaults['default_repo_create'] = p.permission.permission_name
162 162
163 163 if p.permission.permission_name.startswith('hg.repogroup.'):
164 164 defaults['default_repo_group_create'] = p.permission.permission_name
165 165
166 166 if p.permission.permission_name.startswith('hg.usergroup.'):
167 167 defaults['default_user_group_create'] = p.permission.permission_name
168 168
169 169 if p.permission.permission_name.startswith('hg.register.'):
170 170 defaults['default_register'] = p.permission.permission_name
171 171
172 172 if p.permission.permission_name.startswith('hg.extern_activate.'):
173 173 defaults['default_extern_activate'] = p.permission.permission_name
174 174
175 175 if p.permission.permission_name.startswith('hg.fork.'):
176 176 defaults['default_fork'] = p.permission.permission_name
177 177
178 178 return htmlfill.render(
179 179 render('admin/permissions/permissions.html'),
180 180 defaults=defaults,
181 181 encoding="UTF-8",
182 182 force_defaults=False)
183 183
184 184 def permission_ips(self):
185 185 c.active = 'ips'
186 186 c.user = User.get_default_user()
187 187 c.user_ip_map = UserIpMap.query()\
188 188 .filter(UserIpMap.user == c.user).all()
189 189
190 190 return render('admin/permissions/permissions.html')
191 191
192 192 def permission_perms(self):
193 193 c.active = 'perms'
194 194 c.user = User.get_default_user()
195 195 c.perm_user = c.user.AuthUser
196 196 return render('admin/permissions/permissions.html')
@@ -1,459 +1,458 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.repo_groups
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Repository groups controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Mar 23, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 import itertools
32 32
33 33 from formencode import htmlfill
34 34
35 35 from pylons import request, tmpl_context as c, url
36 from pylons.controllers.util import redirect
37 36 from pylons.i18n.translation import _, ungettext
38 from webob.exc import HTTPForbidden, HTTPNotFound, HTTPInternalServerError
37 from webob.exc import HTTPFound, HTTPForbidden, HTTPNotFound, HTTPInternalServerError
39 38
40 39 import kallithea
41 40 from kallithea.lib import helpers as h
42 41 from kallithea.lib.compat import json
43 42 from kallithea.lib.auth import LoginRequired, \
44 43 HasRepoGroupPermissionAnyDecorator, HasRepoGroupPermissionAll, \
45 44 HasPermissionAll
46 45 from kallithea.lib.base import BaseController, render
47 46 from kallithea.model.db import RepoGroup, Repository
48 47 from kallithea.model.scm import RepoGroupList, AvailableRepoGroupChoices
49 48 from kallithea.model.repo_group import RepoGroupModel
50 49 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
51 50 from kallithea.model.meta import Session
52 51 from kallithea.model.repo import RepoModel
53 52 from kallithea.lib.utils2 import safe_int
54 53 from sqlalchemy.sql.expression import func
55 54
56 55
57 56 log = logging.getLogger(__name__)
58 57
59 58
60 59 class RepoGroupsController(BaseController):
61 60
62 61 @LoginRequired()
63 62 def __before__(self):
64 63 super(RepoGroupsController, self).__before__()
65 64
66 65 def __load_defaults(self, extras=(), exclude=()):
67 66 """extras is used for keeping current parent ignoring permissions
68 67 exclude is used for not moving group to itself TODO: also exclude descendants
69 68 Note: only admin can create top level groups
70 69 """
71 70 repo_groups = AvailableRepoGroupChoices([], ['group.admin'], extras)
72 71 exclude_group_ids = set(rg.group_id for rg in exclude)
73 72 c.repo_groups = [rg for rg in repo_groups
74 73 if rg[0] not in exclude_group_ids]
75 74
76 75 repo_model = RepoModel()
77 76 c.users_array = repo_model.get_users_js()
78 77 c.user_groups_array = repo_model.get_user_groups_js()
79 78
80 79 def __load_data(self, group_id):
81 80 """
82 81 Load defaults settings for edit, and update
83 82
84 83 :param group_id:
85 84 """
86 85 repo_group = RepoGroup.get_or_404(group_id)
87 86 data = repo_group.get_dict()
88 87 data['group_name'] = repo_group.name
89 88
90 89 # fill repository group users
91 90 for p in repo_group.repo_group_to_perm:
92 91 data.update({'u_perm_%s' % p.user.username:
93 92 p.permission.permission_name})
94 93
95 94 # fill repository group groups
96 95 for p in repo_group.users_group_to_perm:
97 96 data.update({'g_perm_%s' % p.users_group.users_group_name:
98 97 p.permission.permission_name})
99 98
100 99 return data
101 100
102 101 def _revoke_perms_on_yourself(self, form_result):
103 102 _up = filter(lambda u: c.authuser.username == u[0],
104 103 form_result['perms_updates'])
105 104 _new = filter(lambda u: c.authuser.username == u[0],
106 105 form_result['perms_new'])
107 106 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
108 107 return True
109 108 return False
110 109
111 110 def index(self, format='html'):
112 111 """GET /repo_groups: All items in the collection"""
113 112 # url('repos_groups')
114 113 _list = RepoGroup.query()\
115 114 .order_by(func.lower(RepoGroup.group_name))\
116 115 .all()
117 116 group_iter = RepoGroupList(_list, perm_set=['group.admin'])
118 117 repo_groups_data = []
119 118 total_records = len(group_iter)
120 119 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
121 120 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
122 121
123 122 repo_group_name = lambda repo_group_name, children_groups: (
124 123 template.get_def("repo_group_name")
125 124 .render(repo_group_name, children_groups, _=_, h=h, c=c)
126 125 )
127 126 repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
128 127 template.get_def("repo_group_actions")
129 128 .render(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
130 129 ungettext=ungettext)
131 130 )
132 131
133 132 for repo_gr in group_iter:
134 133 children_groups = map(h.safe_unicode,
135 134 itertools.chain((g.name for g in repo_gr.parents),
136 135 (x.name for x in [repo_gr])))
137 136 repo_count = repo_gr.repositories.count()
138 137 repo_groups_data.append({
139 138 "raw_name": repo_gr.group_name,
140 139 "group_name": repo_group_name(repo_gr.group_name, children_groups),
141 140 "desc": h.escape(repo_gr.group_description),
142 141 "repos": repo_count,
143 142 "owner": h.person(repo_gr.user),
144 143 "action": repo_group_actions(repo_gr.group_id, repo_gr.group_name,
145 144 repo_count)
146 145 })
147 146
148 147 c.data = json.dumps({
149 148 "totalRecords": total_records,
150 149 "startIndex": 0,
151 150 "sort": None,
152 151 "dir": "asc",
153 152 "records": repo_groups_data
154 153 })
155 154
156 155 return render('admin/repo_groups/repo_groups.html')
157 156
158 157 def create(self):
159 158 """POST /repo_groups: Create a new item"""
160 159 # url('repos_groups')
161 160
162 161 self.__load_defaults()
163 162
164 163 # permissions for can create group based on parent_id are checked
165 164 # here in the Form
166 165 repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
167 166 try:
168 167 form_result = repo_group_form.to_python(dict(request.POST))
169 168 gr = RepoGroupModel().create(
170 169 group_name=form_result['group_name'],
171 170 group_description=form_result['group_description'],
172 171 parent=form_result['group_parent_id'],
173 172 owner=self.authuser.user_id, # TODO: make editable
174 173 copy_permissions=form_result['group_copy_permissions']
175 174 )
176 175 Session().commit()
177 176 #TODO: in futureaction_logger(, '', '', '', self.sa)
178 177 except formencode.Invalid as errors:
179 178 return htmlfill.render(
180 179 render('admin/repo_groups/repo_group_add.html'),
181 180 defaults=errors.value,
182 181 errors=errors.error_dict or {},
183 182 prefix_error=False,
184 183 encoding="UTF-8",
185 184 force_defaults=False)
186 185 except Exception:
187 186 log.error(traceback.format_exc())
188 187 h.flash(_('Error occurred during creation of repository group %s') \
189 188 % request.POST.get('group_name'), category='error')
190 189 parent_group_id = form_result['group_parent_id']
191 190 #TODO: maybe we should get back to the main view, not the admin one
192 return redirect(url('repos_groups', parent_group=parent_group_id))
191 raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
193 192 h.flash(_('Created repository group %s') % gr.group_name,
194 193 category='success')
195 return redirect(url('repos_group_home', group_name=gr.group_name))
194 raise HTTPFound(location=url('repos_group_home', group_name=gr.group_name))
196 195
197 196 def new(self):
198 197 """GET /repo_groups/new: Form to create a new item"""
199 198 # url('new_repos_group')
200 199 if HasPermissionAll('hg.admin')('group create'):
201 200 #we're global admin, we're ok and we can create TOP level groups
202 201 pass
203 202 else:
204 203 # we pass in parent group into creation form, thus we know
205 204 # what would be the group, we can check perms here !
206 205 group_id = safe_int(request.GET.get('parent_group'))
207 206 group = RepoGroup.get(group_id) if group_id else None
208 207 group_name = group.group_name if group else None
209 208 if HasRepoGroupPermissionAll('group.admin')(group_name, 'group create'):
210 209 pass
211 210 else:
212 211 raise HTTPForbidden()
213 212
214 213 self.__load_defaults()
215 214 return render('admin/repo_groups/repo_group_add.html')
216 215
217 216 @HasRepoGroupPermissionAnyDecorator('group.admin')
218 217 def update(self, group_name):
219 218 """PUT /repo_groups/group_name: Update an existing item"""
220 219 # Forms posted to this method should contain a hidden field:
221 220 # <input type="hidden" name="_method" value="PUT" />
222 221 # Or using helpers:
223 222 # h.form(url('repos_group', group_name=GROUP_NAME),
224 223 # method='put')
225 224 # url('repos_group', group_name=GROUP_NAME)
226 225
227 226 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
228 227 self.__load_defaults(extras=[c.repo_group.parent_group],
229 228 exclude=[c.repo_group])
230 229
231 230 # TODO: kill allow_empty_group - it is only used for redundant form validation!
232 231 if HasPermissionAll('hg.admin')('group edit'):
233 232 #we're global admin, we're ok and we can create TOP level groups
234 233 allow_empty_group = True
235 234 elif not c.repo_group.parent_group:
236 235 allow_empty_group = True
237 236 else:
238 237 allow_empty_group = False
239 238 repo_group_form = RepoGroupForm(
240 239 edit=True,
241 240 old_data=c.repo_group.get_dict(),
242 241 repo_groups=c.repo_groups,
243 242 can_create_in_root=allow_empty_group,
244 243 )()
245 244 try:
246 245 form_result = repo_group_form.to_python(dict(request.POST))
247 246
248 247 new_gr = RepoGroupModel().update(group_name, form_result)
249 248 Session().commit()
250 249 h.flash(_('Updated repository group %s') \
251 250 % form_result['group_name'], category='success')
252 251 # we now have new name !
253 252 group_name = new_gr.group_name
254 253 #TODO: in future action_logger(, '', '', '', self.sa)
255 254 except formencode.Invalid as errors:
256 255
257 256 return htmlfill.render(
258 257 render('admin/repo_groups/repo_group_edit.html'),
259 258 defaults=errors.value,
260 259 errors=errors.error_dict or {},
261 260 prefix_error=False,
262 261 encoding="UTF-8",
263 262 force_defaults=False)
264 263 except Exception:
265 264 log.error(traceback.format_exc())
266 265 h.flash(_('Error occurred during update of repository group %s') \
267 266 % request.POST.get('group_name'), category='error')
268 267
269 return redirect(url('edit_repo_group', group_name=group_name))
268 raise HTTPFound(location=url('edit_repo_group', group_name=group_name))
270 269
271 270 @HasRepoGroupPermissionAnyDecorator('group.admin')
272 271 def delete(self, group_name):
273 272 """DELETE /repo_groups/group_name: Delete an existing item"""
274 273 # Forms posted to this method should contain a hidden field:
275 274 # <input type="hidden" name="_method" value="DELETE" />
276 275 # Or using helpers:
277 276 # h.form(url('repos_group', group_name=GROUP_NAME),
278 277 # method='delete')
279 278 # url('repos_group', group_name=GROUP_NAME)
280 279
281 280 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
282 281 repos = gr.repositories.all()
283 282 if repos:
284 283 h.flash(_('This group contains %s repositories and cannot be '
285 284 'deleted') % len(repos), category='warning')
286 return redirect(url('repos_groups'))
285 raise HTTPFound(location=url('repos_groups'))
287 286
288 287 children = gr.children.all()
289 288 if children:
290 289 h.flash(_('This group contains %s subgroups and cannot be deleted'
291 290 % (len(children))), category='warning')
292 return redirect(url('repos_groups'))
291 raise HTTPFound(location=url('repos_groups'))
293 292
294 293 try:
295 294 RepoGroupModel().delete(group_name)
296 295 Session().commit()
297 296 h.flash(_('Removed repository group %s') % group_name,
298 297 category='success')
299 298 #TODO: in future action_logger(, '', '', '', self.sa)
300 299 except Exception:
301 300 log.error(traceback.format_exc())
302 301 h.flash(_('Error occurred during deletion of repository group %s')
303 302 % group_name, category='error')
304 303
305 304 if gr.parent_group:
306 return redirect(url('repos_group_home', group_name=gr.parent_group.group_name))
307 return redirect(url('repos_groups'))
305 raise HTTPFound(location=url('repos_group_home', group_name=gr.parent_group.group_name))
306 raise HTTPFound(location=url('repos_groups'))
308 307
309 308 def show_by_name(self, group_name):
310 309 """
311 310 This is a proxy that does a lookup group_name -> id, and shows
312 311 the group by id view instead
313 312 """
314 313 group_name = group_name.rstrip('/')
315 314 id_ = RepoGroup.get_by_group_name(group_name)
316 315 if id_:
317 316 return self.show(group_name)
318 317 raise HTTPNotFound
319 318
320 319 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
321 320 'group.admin')
322 321 def show(self, group_name):
323 322 """GET /repo_groups/group_name: Show a specific item"""
324 323 # url('repos_group', group_name=GROUP_NAME)
325 324 c.active = 'settings'
326 325
327 326 c.group = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
328 327 c.group_repos = c.group.repositories.all()
329 328
330 329 #overwrite our cached list with current filter
331 330 c.repo_cnt = 0
332 331
333 332 groups = RepoGroup.query().order_by(RepoGroup.group_name)\
334 333 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
335 334 c.groups = self.scm_model.get_repo_groups(groups)
336 335
337 336 c.repos_list = Repository.query()\
338 337 .filter(Repository.group_id == c.group.group_id)\
339 338 .order_by(func.lower(Repository.repo_name))\
340 339 .all()
341 340
342 341 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
343 342 admin=False)
344 343 #json used to render the grid
345 344 c.data = json.dumps(repos_data)
346 345
347 346 return render('admin/repo_groups/repo_group_show.html')
348 347
349 348 @HasRepoGroupPermissionAnyDecorator('group.admin')
350 349 def edit(self, group_name):
351 350 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
352 351 # url('edit_repo_group', group_name=GROUP_NAME)
353 352 c.active = 'settings'
354 353
355 354 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
356 355 self.__load_defaults(extras=[c.repo_group.parent_group],
357 356 exclude=[c.repo_group])
358 357 defaults = self.__load_data(c.repo_group.group_id)
359 358
360 359 return htmlfill.render(
361 360 render('admin/repo_groups/repo_group_edit.html'),
362 361 defaults=defaults,
363 362 encoding="UTF-8",
364 363 force_defaults=False
365 364 )
366 365
367 366 @HasRepoGroupPermissionAnyDecorator('group.admin')
368 367 def edit_repo_group_advanced(self, group_name):
369 368 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
370 369 # url('edit_repo_group', group_name=GROUP_NAME)
371 370 c.active = 'advanced'
372 371 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
373 372
374 373 return render('admin/repo_groups/repo_group_edit.html')
375 374
376 375 @HasRepoGroupPermissionAnyDecorator('group.admin')
377 376 def edit_repo_group_perms(self, group_name):
378 377 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
379 378 # url('edit_repo_group', group_name=GROUP_NAME)
380 379 c.active = 'perms'
381 380 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
382 381 self.__load_defaults()
383 382 defaults = self.__load_data(c.repo_group.group_id)
384 383
385 384 return htmlfill.render(
386 385 render('admin/repo_groups/repo_group_edit.html'),
387 386 defaults=defaults,
388 387 encoding="UTF-8",
389 388 force_defaults=False
390 389 )
391 390
392 391 @HasRepoGroupPermissionAnyDecorator('group.admin')
393 392 def update_perms(self, group_name):
394 393 """
395 394 Update permissions for given repository group
396 395
397 396 :param group_name:
398 397 """
399 398
400 399 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
401 400 valid_recursive_choices = ['none', 'repos', 'groups', 'all']
402 401 form_result = RepoGroupPermsForm(valid_recursive_choices)().to_python(request.POST)
403 402 if not c.authuser.is_admin:
404 403 if self._revoke_perms_on_yourself(form_result):
405 404 msg = _('Cannot revoke permission for yourself as admin')
406 405 h.flash(msg, category='warning')
407 return redirect(url('edit_repo_group_perms', group_name=group_name))
406 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
408 407 recursive = form_result['recursive']
409 408 # iterate over all members(if in recursive mode) of this groups and
410 409 # set the permissions !
411 410 # this can be potentially heavy operation
412 411 RepoGroupModel()._update_permissions(c.repo_group,
413 412 form_result['perms_new'],
414 413 form_result['perms_updates'],
415 414 recursive)
416 415 #TODO: implement this
417 416 #action_logger(self.authuser, 'admin_changed_repo_permissions',
418 417 # repo_name, self.ip_addr, self.sa)
419 418 Session().commit()
420 419 h.flash(_('Repository group permissions updated'), category='success')
421 return redirect(url('edit_repo_group_perms', group_name=group_name))
420 raise HTTPFound(location=url('edit_repo_group_perms', group_name=group_name))
422 421
423 422 @HasRepoGroupPermissionAnyDecorator('group.admin')
424 423 def delete_perms(self, group_name):
425 424 """
426 425 DELETE an existing repository group permission user
427 426
428 427 :param group_name:
429 428 """
430 429 try:
431 430 obj_type = request.POST.get('obj_type')
432 431 obj_id = None
433 432 if obj_type == 'user':
434 433 obj_id = safe_int(request.POST.get('user_id'))
435 434 elif obj_type == 'user_group':
436 435 obj_id = safe_int(request.POST.get('user_group_id'))
437 436
438 437 if not c.authuser.is_admin:
439 438 if obj_type == 'user' and c.authuser.user_id == obj_id:
440 439 msg = _('Cannot revoke permission for yourself as admin')
441 440 h.flash(msg, category='warning')
442 441 raise Exception('revoke admin permission on self')
443 442 recursive = request.POST.get('recursive', 'none')
444 443 if obj_type == 'user':
445 444 RepoGroupModel().delete_permission(repo_group=group_name,
446 445 obj=obj_id, obj_type='user',
447 446 recursive=recursive)
448 447 elif obj_type == 'user_group':
449 448 RepoGroupModel().delete_permission(repo_group=group_name,
450 449 obj=obj_id,
451 450 obj_type='user_group',
452 451 recursive=recursive)
453 452
454 453 Session().commit()
455 454 except Exception:
456 455 log.error(traceback.format_exc())
457 456 h.flash(_('An error occurred during revoking of permission'),
458 457 category='error')
459 458 raise HTTPInternalServerError()
@@ -1,641 +1,640 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.repos
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Repositories controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 7, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 from formencode import htmlfill
32 from webob.exc import HTTPInternalServerError, HTTPForbidden, HTTPNotFound
33 32 from pylons import request, tmpl_context as c, url
34 from pylons.controllers.util import redirect
35 33 from pylons.i18n.translation import _
36 34 from sqlalchemy.sql.expression import func
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
37 36
38 37 from kallithea.lib import helpers as h
39 38 from kallithea.lib.auth import LoginRequired, \
40 39 HasRepoPermissionAllDecorator, NotAnonymous, HasPermissionAny, \
41 40 HasRepoPermissionAnyDecorator
42 41 from kallithea.lib.base import BaseRepoController, render
43 42 from kallithea.lib.utils import action_logger, jsonify
44 43 from kallithea.lib.vcs import RepositoryError
45 44 from kallithea.model.meta import Session
46 45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup,\
47 46 Setting, RepositoryField
48 47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
49 48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
50 49 from kallithea.model.repo import RepoModel
51 50 from kallithea.lib.compat import json
52 51 from kallithea.lib.exceptions import AttachedForksError
53 52 from kallithea.lib.utils2 import safe_int
54 53
55 54 log = logging.getLogger(__name__)
56 55
57 56
58 57 class ReposController(BaseRepoController):
59 58 """
60 59 REST Controller styled on the Atom Publishing Protocol"""
61 60 # To properly map this controller, ensure your config/routing.py
62 61 # file has a resource setup:
63 62 # map.resource('repo', 'repos')
64 63
65 64 @LoginRequired()
66 65 def __before__(self):
67 66 super(ReposController, self).__before__()
68 67
69 68 def _load_repo(self, repo_name):
70 69 repo_obj = Repository.get_by_repo_name(repo_name)
71 70
72 71 if repo_obj is None:
73 72 h.not_mapped_error(repo_name)
74 return redirect(url('repos'))
73 raise HTTPFound(location=url('repos'))
75 74
76 75 return repo_obj
77 76
78 77 def __load_defaults(self, repo=None):
79 78 top_perms = ['hg.create.repository']
80 79 repo_group_perms = ['group.admin']
81 80 if HasPermissionAny('hg.create.write_on_repogroup.true')():
82 81 repo_group_perms.append('group.write')
83 82 extras = [] if repo is None else [repo.group]
84 83
85 84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
86 85
87 86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
88 87
89 88 def __load_data(self, repo_name=None):
90 89 """
91 90 Load defaults settings for edit, and update
92 91
93 92 :param repo_name:
94 93 """
95 94 c.repo_info = self._load_repo(repo_name)
96 95 self.__load_defaults(c.repo_info)
97 96
98 97 defaults = RepoModel()._get_defaults(repo_name)
99 98 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
100 99
101 100 return defaults
102 101
103 102 def index(self, format='html'):
104 103 """GET /repos: All items in the collection"""
105 104 # url('repos')
106 105 _list = Repository.query()\
107 106 .order_by(func.lower(Repository.repo_name))\
108 107 .all()
109 108
110 109 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
111 110 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
112 111 admin=True,
113 112 super_user_actions=True)
114 113 #json used to render the grid
115 114 c.data = json.dumps(repos_data)
116 115
117 116 return render('admin/repos/repos.html')
118 117
119 118 @NotAnonymous()
120 119 def create(self):
121 120 """
122 121 POST /repos: Create a new item"""
123 122 # url('repos')
124 123
125 124 self.__load_defaults()
126 125 form_result = {}
127 126 task_id = None
128 127 try:
129 128 # CanWriteGroup validators checks permissions of this POST
130 129 form_result = RepoForm(repo_groups=c.repo_groups,
131 130 landing_revs=c.landing_revs_choices)()\
132 131 .to_python(dict(request.POST))
133 132
134 133 # create is done sometimes async on celery, db transaction
135 134 # management is handled there.
136 135 task = RepoModel().create(form_result, self.authuser.user_id)
137 136 from celery.result import BaseAsyncResult
138 137 if isinstance(task, BaseAsyncResult):
139 138 task_id = task.task_id
140 139 except formencode.Invalid as errors:
141 140 log.info(errors)
142 141 return htmlfill.render(
143 142 render('admin/repos/repo_add.html'),
144 143 defaults=errors.value,
145 144 errors=errors.error_dict or {},
146 145 prefix_error=False,
147 146 force_defaults=False,
148 147 encoding="UTF-8")
149 148
150 149 except Exception:
151 150 log.error(traceback.format_exc())
152 151 msg = (_('Error creating repository %s')
153 152 % form_result.get('repo_name'))
154 153 h.flash(msg, category='error')
155 return redirect(url('home'))
154 raise HTTPFound(location=url('home'))
156 155
157 return redirect(h.url('repo_creating_home',
156 raise HTTPFound(location=h.url('repo_creating_home',
158 157 repo_name=form_result['repo_name_full'],
159 158 task_id=task_id))
160 159
161 160 @NotAnonymous()
162 161 def create_repository(self):
163 162 """GET /_admin/create_repository: Form to create a new item"""
164 163 self.__load_defaults()
165 164 if not c.repo_groups:
166 165 raise HTTPForbidden
167 166 parent_group = request.GET.get('parent_group')
168 167
169 168 ## apply the defaults from defaults page
170 169 defaults = Setting.get_default_repo_settings(strip_prefix=True)
171 170 if parent_group:
172 171 prg = RepoGroup.get(parent_group)
173 172 if prg is None or not any(rgc[0] == prg.group_id
174 173 for rgc in c.repo_groups):
175 174 raise HTTPForbidden
176 175 defaults.update({'repo_group': parent_group})
177 176
178 177 return htmlfill.render(
179 178 render('admin/repos/repo_add.html'),
180 179 defaults=defaults,
181 180 errors={},
182 181 prefix_error=False,
183 182 encoding="UTF-8",
184 183 force_defaults=False)
185 184
186 185 @LoginRequired()
187 186 @NotAnonymous()
188 187 def repo_creating(self, repo_name):
189 188 c.repo = repo_name
190 189 c.task_id = request.GET.get('task_id')
191 190 if not c.repo:
192 191 raise HTTPNotFound()
193 192 return render('admin/repos/repo_creating.html')
194 193
195 194 @LoginRequired()
196 195 @NotAnonymous()
197 196 @jsonify
198 197 def repo_check(self, repo_name):
199 198 c.repo = repo_name
200 199 task_id = request.GET.get('task_id')
201 200
202 201 if task_id and task_id not in ['None']:
203 202 from kallithea import CELERY_ON
204 203 from celery.result import AsyncResult
205 204 if CELERY_ON:
206 205 task = AsyncResult(task_id)
207 206 if task.failed():
208 207 raise HTTPInternalServerError(task.traceback)
209 208
210 209 repo = Repository.get_by_repo_name(repo_name)
211 210 if repo and repo.repo_state == Repository.STATE_CREATED:
212 211 if repo.clone_uri:
213 212 h.flash(_('Created repository %s from %s')
214 213 % (repo.repo_name, repo.clone_uri_hidden), category='success')
215 214 else:
216 215 repo_url = h.link_to(repo.repo_name,
217 216 h.url('summary_home',
218 217 repo_name=repo.repo_name))
219 218 fork = repo.fork
220 219 if fork is not None:
221 220 fork_name = fork.repo_name
222 221 h.flash(h.literal(_('Forked repository %s as %s')
223 222 % (fork_name, repo_url)), category='success')
224 223 else:
225 224 h.flash(h.literal(_('Created repository %s') % repo_url),
226 225 category='success')
227 226 return {'result': True}
228 227 return {'result': False}
229 228
230 229 @HasRepoPermissionAllDecorator('repository.admin')
231 230 def update(self, repo_name):
232 231 """
233 232 PUT /repos/repo_name: Update an existing item"""
234 233 # Forms posted to this method should contain a hidden field:
235 234 # <input type="hidden" name="_method" value="PUT" />
236 235 # Or using helpers:
237 236 # h.form(url('put_repo', repo_name=ID),
238 237 # method='put')
239 238 # url('put_repo', repo_name=ID)
240 239 c.repo_info = self._load_repo(repo_name)
241 240 self.__load_defaults(c.repo_info)
242 241 c.active = 'settings'
243 242 c.repo_fields = RepositoryField.query()\
244 243 .filter(RepositoryField.repository == c.repo_info).all()
245 244
246 245 repo_model = RepoModel()
247 246 changed_name = repo_name
248 247 repo = Repository.get_by_repo_name(repo_name)
249 248 old_data = {
250 249 'repo_name': repo_name,
251 250 'repo_group': repo.group.get_dict() if repo.group else {},
252 251 'repo_type': repo.repo_type,
253 252 }
254 253 _form = RepoForm(edit=True, old_data=old_data,
255 254 repo_groups=c.repo_groups,
256 255 landing_revs=c.landing_revs_choices)()
257 256
258 257 try:
259 258 form_result = _form.to_python(dict(request.POST))
260 259 repo = repo_model.update(repo_name, **form_result)
261 260 ScmModel().mark_for_invalidation(repo_name)
262 261 h.flash(_('Repository %s updated successfully') % repo_name,
263 262 category='success')
264 263 changed_name = repo.repo_name
265 264 action_logger(self.authuser, 'admin_updated_repo',
266 265 changed_name, self.ip_addr, self.sa)
267 266 Session().commit()
268 267 except formencode.Invalid as errors:
269 268 log.info(errors)
270 269 defaults = self.__load_data(repo_name)
271 270 defaults.update(errors.value)
272 271 c.users_array = repo_model.get_users_js()
273 272 return htmlfill.render(
274 273 render('admin/repos/repo_edit.html'),
275 274 defaults=defaults,
276 275 errors=errors.error_dict or {},
277 276 prefix_error=False,
278 277 encoding="UTF-8",
279 278 force_defaults=False)
280 279
281 280 except Exception:
282 281 log.error(traceback.format_exc())
283 282 h.flash(_('Error occurred during update of repository %s') \
284 283 % repo_name, category='error')
285 return redirect(url('edit_repo', repo_name=changed_name))
284 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
286 285
287 286 @HasRepoPermissionAllDecorator('repository.admin')
288 287 def delete(self, repo_name):
289 288 """
290 289 DELETE /repos/repo_name: Delete an existing item"""
291 290 # Forms posted to this method should contain a hidden field:
292 291 # <input type="hidden" name="_method" value="DELETE" />
293 292 # Or using helpers:
294 293 # h.form(url('delete_repo', repo_name=ID),
295 294 # method='delete')
296 295 # url('delete_repo', repo_name=ID)
297 296
298 297 repo_model = RepoModel()
299 298 repo = repo_model.get_by_repo_name(repo_name)
300 299 if not repo:
301 300 h.not_mapped_error(repo_name)
302 return redirect(url('repos'))
301 raise HTTPFound(location=url('repos'))
303 302 try:
304 303 _forks = repo.forks.count()
305 304 handle_forks = None
306 305 if _forks and request.POST.get('forks'):
307 306 do = request.POST['forks']
308 307 if do == 'detach_forks':
309 308 handle_forks = 'detach'
310 309 h.flash(_('Detached %s forks') % _forks, category='success')
311 310 elif do == 'delete_forks':
312 311 handle_forks = 'delete'
313 312 h.flash(_('Deleted %s forks') % _forks, category='success')
314 313 repo_model.delete(repo, forks=handle_forks)
315 314 action_logger(self.authuser, 'admin_deleted_repo',
316 315 repo_name, self.ip_addr, self.sa)
317 316 ScmModel().mark_for_invalidation(repo_name)
318 317 h.flash(_('Deleted repository %s') % repo_name, category='success')
319 318 Session().commit()
320 319 except AttachedForksError:
321 320 h.flash(_('Cannot delete repository %s which still has forks')
322 321 % repo_name, category='warning')
323 322
324 323 except Exception:
325 324 log.error(traceback.format_exc())
326 325 h.flash(_('An error occurred during deletion of %s') % repo_name,
327 326 category='error')
328 327
329 328 if repo.group:
330 return redirect(url('repos_group_home', group_name=repo.group.group_name))
331 return redirect(url('repos'))
329 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
330 raise HTTPFound(location=url('repos'))
332 331
333 332 @HasRepoPermissionAllDecorator('repository.admin')
334 333 def edit(self, repo_name):
335 334 """GET /repo_name/settings: Form to edit an existing item"""
336 335 # url('edit_repo', repo_name=ID)
337 336 defaults = self.__load_data(repo_name)
338 337 c.repo_fields = RepositoryField.query()\
339 338 .filter(RepositoryField.repository == c.repo_info).all()
340 339 repo_model = RepoModel()
341 340 c.users_array = repo_model.get_users_js()
342 341 c.active = 'settings'
343 342 return htmlfill.render(
344 343 render('admin/repos/repo_edit.html'),
345 344 defaults=defaults,
346 345 encoding="UTF-8",
347 346 force_defaults=False)
348 347
349 348 @HasRepoPermissionAllDecorator('repository.admin')
350 349 def edit_permissions(self, repo_name):
351 350 """GET /repo_name/settings: Form to edit an existing item"""
352 351 # url('edit_repo', repo_name=ID)
353 352 c.repo_info = self._load_repo(repo_name)
354 353 repo_model = RepoModel()
355 354 c.users_array = repo_model.get_users_js()
356 355 c.user_groups_array = repo_model.get_user_groups_js()
357 356 c.active = 'permissions'
358 357 defaults = RepoModel()._get_defaults(repo_name)
359 358
360 359 return htmlfill.render(
361 360 render('admin/repos/repo_edit.html'),
362 361 defaults=defaults,
363 362 encoding="UTF-8",
364 363 force_defaults=False)
365 364
366 365 def edit_permissions_update(self, repo_name):
367 366 form = RepoPermsForm()().to_python(request.POST)
368 367 RepoModel()._update_permissions(repo_name, form['perms_new'],
369 368 form['perms_updates'])
370 369 #TODO: implement this
371 370 #action_logger(self.authuser, 'admin_changed_repo_permissions',
372 371 # repo_name, self.ip_addr, self.sa)
373 372 Session().commit()
374 373 h.flash(_('Repository permissions updated'), category='success')
375 return redirect(url('edit_repo_perms', repo_name=repo_name))
374 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
376 375
377 376 def edit_permissions_revoke(self, repo_name):
378 377 try:
379 378 obj_type = request.POST.get('obj_type')
380 379 obj_id = None
381 380 if obj_type == 'user':
382 381 obj_id = safe_int(request.POST.get('user_id'))
383 382 elif obj_type == 'user_group':
384 383 obj_id = safe_int(request.POST.get('user_group_id'))
385 384
386 385 if obj_type == 'user':
387 386 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
388 387 elif obj_type == 'user_group':
389 388 RepoModel().revoke_user_group_permission(
390 389 repo=repo_name, group_name=obj_id
391 390 )
392 391 #TODO: implement this
393 392 #action_logger(self.authuser, 'admin_revoked_repo_permissions',
394 393 # repo_name, self.ip_addr, self.sa)
395 394 Session().commit()
396 395 except Exception:
397 396 log.error(traceback.format_exc())
398 397 h.flash(_('An error occurred during revoking of permission'),
399 398 category='error')
400 399 raise HTTPInternalServerError()
401 400
402 401 @HasRepoPermissionAllDecorator('repository.admin')
403 402 def edit_fields(self, repo_name):
404 403 """GET /repo_name/settings: Form to edit an existing item"""
405 404 # url('edit_repo', repo_name=ID)
406 405 c.repo_info = self._load_repo(repo_name)
407 406 c.repo_fields = RepositoryField.query()\
408 407 .filter(RepositoryField.repository == c.repo_info).all()
409 408 c.active = 'fields'
410 409 if request.POST:
411 410
412 return redirect(url('repo_edit_fields'))
411 raise HTTPFound(location=url('repo_edit_fields'))
413 412 return render('admin/repos/repo_edit.html')
414 413
415 414 @HasRepoPermissionAllDecorator('repository.admin')
416 415 def create_repo_field(self, repo_name):
417 416 try:
418 417 form_result = RepoFieldForm()().to_python(dict(request.POST))
419 418 new_field = RepositoryField()
420 419 new_field.repository = Repository.get_by_repo_name(repo_name)
421 420 new_field.field_key = form_result['new_field_key']
422 421 new_field.field_type = form_result['new_field_type'] # python type
423 422 new_field.field_value = form_result['new_field_value'] # set initial blank value
424 423 new_field.field_desc = form_result['new_field_desc']
425 424 new_field.field_label = form_result['new_field_label']
426 425 Session().add(new_field)
427 426 Session().commit()
428 427 except Exception as e:
429 428 log.error(traceback.format_exc())
430 429 msg = _('An error occurred during creation of field')
431 430 if isinstance(e, formencode.Invalid):
432 431 msg += ". " + e.msg
433 432 h.flash(msg, category='error')
434 return redirect(url('edit_repo_fields', repo_name=repo_name))
433 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
435 434
436 435 @HasRepoPermissionAllDecorator('repository.admin')
437 436 def delete_repo_field(self, repo_name, field_id):
438 437 field = RepositoryField.get_or_404(field_id)
439 438 try:
440 439 Session().delete(field)
441 440 Session().commit()
442 441 except Exception as e:
443 442 log.error(traceback.format_exc())
444 443 msg = _('An error occurred during removal of field')
445 444 h.flash(msg, category='error')
446 return redirect(url('edit_repo_fields', repo_name=repo_name))
445 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
447 446
448 447 @HasRepoPermissionAllDecorator('repository.admin')
449 448 def edit_advanced(self, repo_name):
450 449 """GET /repo_name/settings: Form to edit an existing item"""
451 450 # url('edit_repo', repo_name=ID)
452 451 c.repo_info = self._load_repo(repo_name)
453 452 c.default_user_id = User.get_default_user().user_id
454 453 c.in_public_journal = UserFollowing.query()\
455 454 .filter(UserFollowing.user_id == c.default_user_id)\
456 455 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
457 456
458 457 _repos = Repository.query().order_by(Repository.repo_name).all()
459 458 read_access_repos = RepoList(_repos)
460 459 c.repos_list = [(None, _('-- Not a fork --'))]
461 460 c.repos_list += [(x.repo_id, x.repo_name)
462 461 for x in read_access_repos
463 462 if x.repo_id != c.repo_info.repo_id]
464 463
465 464 defaults = {
466 465 'id_fork_of': c.repo_info.fork.repo_id if c.repo_info.fork else ''
467 466 }
468 467
469 468 c.active = 'advanced'
470 469 if request.POST:
471 return redirect(url('repo_edit_advanced'))
470 raise HTTPFound(location=url('repo_edit_advanced'))
472 471 return htmlfill.render(
473 472 render('admin/repos/repo_edit.html'),
474 473 defaults=defaults,
475 474 encoding="UTF-8",
476 475 force_defaults=False)
477 476
478 477 @HasRepoPermissionAllDecorator('repository.admin')
479 478 def edit_advanced_journal(self, repo_name):
480 479 """
481 480 Sets this repository to be visible in public journal,
482 481 in other words asking default user to follow this repo
483 482
484 483 :param repo_name:
485 484 """
486 485
487 486 try:
488 487 repo_id = Repository.get_by_repo_name(repo_name).repo_id
489 488 user_id = User.get_default_user().user_id
490 489 self.scm_model.toggle_following_repo(repo_id, user_id)
491 490 h.flash(_('Updated repository visibility in public journal'),
492 491 category='success')
493 492 Session().commit()
494 493 except Exception:
495 494 h.flash(_('An error occurred during setting this'
496 495 ' repository in public journal'),
497 496 category='error')
498 return redirect(url('edit_repo_advanced', repo_name=repo_name))
497 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
499 498
500 499
501 500 @HasRepoPermissionAllDecorator('repository.admin')
502 501 def edit_advanced_fork(self, repo_name):
503 502 """
504 503 Mark given repository as a fork of another
505 504
506 505 :param repo_name:
507 506 """
508 507 try:
509 508 fork_id = request.POST.get('id_fork_of')
510 509 repo = ScmModel().mark_as_fork(repo_name, fork_id,
511 510 self.authuser.username)
512 511 fork = repo.fork.repo_name if repo.fork else _('Nothing')
513 512 Session().commit()
514 513 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
515 514 category='success')
516 515 except RepositoryError as e:
517 516 log.error(traceback.format_exc())
518 517 h.flash(str(e), category='error')
519 518 except Exception as e:
520 519 log.error(traceback.format_exc())
521 520 h.flash(_('An error occurred during this operation'),
522 521 category='error')
523 522
524 return redirect(url('edit_repo_advanced', repo_name=repo_name))
523 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
525 524
526 525 @HasRepoPermissionAllDecorator('repository.admin')
527 526 def edit_advanced_locking(self, repo_name):
528 527 """
529 528 Unlock repository when it is locked !
530 529
531 530 :param repo_name:
532 531 """
533 532 try:
534 533 repo = Repository.get_by_repo_name(repo_name)
535 534 if request.POST.get('set_lock'):
536 535 Repository.lock(repo, c.authuser.user_id)
537 536 h.flash(_('Repository has been locked'), category='success')
538 537 elif request.POST.get('set_unlock'):
539 538 Repository.unlock(repo)
540 539 h.flash(_('Repository has been unlocked'), category='success')
541 540 except Exception as e:
542 541 log.error(traceback.format_exc())
543 542 h.flash(_('An error occurred during unlocking'),
544 543 category='error')
545 return redirect(url('edit_repo_advanced', repo_name=repo_name))
544 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
546 545
547 546 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
548 547 def toggle_locking(self, repo_name):
549 548 """
550 549 Toggle locking of repository by simple GET call to url
551 550
552 551 :param repo_name:
553 552 """
554 553
555 554 try:
556 555 repo = Repository.get_by_repo_name(repo_name)
557 556
558 557 if repo.enable_locking:
559 558 if repo.locked[0]:
560 559 Repository.unlock(repo)
561 560 h.flash(_('Repository has been unlocked'), category='success')
562 561 else:
563 562 Repository.lock(repo, c.authuser.user_id)
564 563 h.flash(_('Repository has been locked'), category='success')
565 564
566 565 except Exception as e:
567 566 log.error(traceback.format_exc())
568 567 h.flash(_('An error occurred during unlocking'),
569 568 category='error')
570 return redirect(url('summary_home', repo_name=repo_name))
569 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
571 570
572 571 @HasRepoPermissionAllDecorator('repository.admin')
573 572 def edit_caches(self, repo_name):
574 573 """GET /repo_name/settings: Form to edit an existing item"""
575 574 # url('edit_repo', repo_name=ID)
576 575 c.repo_info = self._load_repo(repo_name)
577 576 c.active = 'caches'
578 577 if request.POST:
579 578 try:
580 579 ScmModel().mark_for_invalidation(repo_name)
581 580 Session().commit()
582 581 h.flash(_('Cache invalidation successful'),
583 582 category='success')
584 583 except Exception as e:
585 584 log.error(traceback.format_exc())
586 585 h.flash(_('An error occurred during cache invalidation'),
587 586 category='error')
588 587
589 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
588 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
590 589 return render('admin/repos/repo_edit.html')
591 590
592 591 @HasRepoPermissionAllDecorator('repository.admin')
593 592 def edit_remote(self, repo_name):
594 593 """GET /repo_name/settings: Form to edit an existing item"""
595 594 # url('edit_repo', repo_name=ID)
596 595 c.repo_info = self._load_repo(repo_name)
597 596 c.active = 'remote'
598 597 if request.POST:
599 598 try:
600 599 ScmModel().pull_changes(repo_name, self.authuser.username)
601 600 h.flash(_('Pulled from remote location'), category='success')
602 601 except Exception as e:
603 602 log.error(traceback.format_exc())
604 603 h.flash(_('An error occurred during pull from remote location'),
605 604 category='error')
606 return redirect(url('edit_repo_remote', repo_name=c.repo_name))
605 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
607 606 return render('admin/repos/repo_edit.html')
608 607
609 608 @HasRepoPermissionAllDecorator('repository.admin')
610 609 def edit_statistics(self, repo_name):
611 610 """GET /repo_name/settings: Form to edit an existing item"""
612 611 # url('edit_repo', repo_name=ID)
613 612 c.repo_info = self._load_repo(repo_name)
614 613 repo = c.repo_info.scm_instance
615 614
616 615 if c.repo_info.stats:
617 616 # this is on what revision we ended up so we add +1 for count
618 617 last_rev = c.repo_info.stats.stat_on_revision + 1
619 618 else:
620 619 last_rev = 0
621 620 c.stats_revision = last_rev
622 621
623 622 c.repo_last_rev = repo.count() if repo.revisions else 0
624 623
625 624 if last_rev == 0 or c.repo_last_rev == 0:
626 625 c.stats_percentage = 0
627 626 else:
628 627 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
629 628
630 629 c.active = 'statistics'
631 630 if request.POST:
632 631 try:
633 632 RepoModel().delete_stats(repo_name)
634 633 Session().commit()
635 634 except Exception as e:
636 635 log.error(traceback.format_exc())
637 636 h.flash(_('An error occurred during deletion of repository stats'),
638 637 category='error')
639 return redirect(url('edit_repo_statistics', repo_name=c.repo_name))
638 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
640 639
641 640 return render('admin/repos/repo_edit.html')
@@ -1,528 +1,528 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.settings
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 settings controller for Kallithea admin
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Jul 14, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
35 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36 36
37 37 from kallithea.lib import helpers as h
38 38 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator
39 39 from kallithea.lib.base import BaseController, render
40 40 from kallithea.lib.celerylib import tasks, run_task
41 41 from kallithea.lib.exceptions import HgsubversionImportError
42 42 from kallithea.lib.utils import repo2db_mapper, set_app_settings
43 43 from kallithea.model.db import Ui, Repository, Setting
44 44 from kallithea.model.forms import ApplicationSettingsForm, \
45 45 ApplicationUiSettingsForm, ApplicationVisualisationForm
46 46 from kallithea.model.scm import ScmModel
47 47 from kallithea.model.notification import EmailNotificationModel
48 48 from kallithea.model.meta import Session
49 49 from kallithea.lib.utils2 import str2bool, safe_unicode
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class SettingsController(BaseController):
54 54 """REST Controller styled on the Atom Publishing Protocol"""
55 55 # To properly map this controller, ensure your config/routing.py
56 56 # file has a resource setup:
57 57 # map.resource('setting', 'settings', controller='admin/settings',
58 58 # path_prefix='/admin', name_prefix='admin_')
59 59
60 60 @LoginRequired()
61 61 def __before__(self):
62 62 super(SettingsController, self).__before__()
63 63
64 64 def _get_hg_ui_settings(self):
65 65 ret = Ui.query().all()
66 66
67 67 if not ret:
68 68 raise Exception('Could not get application ui settings !')
69 69 settings = {}
70 70 for each in ret:
71 71 k = each.ui_key
72 72 v = each.ui_value
73 73 if k == '/':
74 74 k = 'root_path'
75 75
76 76 if k == 'push_ssl':
77 77 v = str2bool(v)
78 78
79 79 if k.find('.') != -1:
80 80 k = k.replace('.', '_')
81 81
82 82 if each.ui_section in ['hooks', 'extensions']:
83 83 v = each.ui_active
84 84
85 85 settings[each.ui_section + '_' + k] = v
86 86 return settings
87 87
88 88 @HasPermissionAllDecorator('hg.admin')
89 89 def settings_vcs(self):
90 90 """GET /admin/settings: All items in the collection"""
91 91 # url('admin_settings')
92 92 c.active = 'vcs'
93 93 if request.POST:
94 94 application_form = ApplicationUiSettingsForm()()
95 95 try:
96 96 form_result = application_form.to_python(dict(request.POST))
97 97 except formencode.Invalid as errors:
98 98 return htmlfill.render(
99 99 render('admin/settings/settings.html'),
100 100 defaults=errors.value,
101 101 errors=errors.error_dict or {},
102 102 prefix_error=False,
103 103 encoding="UTF-8",
104 104 force_defaults=False)
105 105
106 106 try:
107 107 sett = Ui.get_by_key('push_ssl')
108 108 sett.ui_value = form_result['web_push_ssl']
109 109 Session().add(sett)
110 110 if c.visual.allow_repo_location_change:
111 111 sett = Ui.get_by_key('/')
112 112 sett.ui_value = form_result['paths_root_path']
113 113 Session().add(sett)
114 114
115 115 #HOOKS
116 116 sett = Ui.get_by_key(Ui.HOOK_UPDATE)
117 117 sett.ui_active = form_result['hooks_changegroup_update']
118 118 Session().add(sett)
119 119
120 120 sett = Ui.get_by_key(Ui.HOOK_REPO_SIZE)
121 121 sett.ui_active = form_result['hooks_changegroup_repo_size']
122 122 Session().add(sett)
123 123
124 124 sett = Ui.get_by_key(Ui.HOOK_PUSH)
125 125 sett.ui_active = form_result['hooks_changegroup_push_logger']
126 126 Session().add(sett)
127 127
128 128 sett = Ui.get_by_key(Ui.HOOK_PULL)
129 129 sett.ui_active = form_result['hooks_outgoing_pull_logger']
130 130
131 131 Session().add(sett)
132 132
133 133 ## EXTENSIONS
134 134 sett = Ui.get_by_key('largefiles')
135 135 if not sett:
136 136 #make one if it's not there !
137 137 sett = Ui()
138 138 sett.ui_key = 'largefiles'
139 139 sett.ui_section = 'extensions'
140 140 sett.ui_active = form_result['extensions_largefiles']
141 141 Session().add(sett)
142 142
143 143 sett = Ui.get_by_key('hgsubversion')
144 144 if not sett:
145 145 #make one if it's not there !
146 146 sett = Ui()
147 147 sett.ui_key = 'hgsubversion'
148 148 sett.ui_section = 'extensions'
149 149
150 150 sett.ui_active = form_result['extensions_hgsubversion']
151 151 if sett.ui_active:
152 152 try:
153 153 import hgsubversion # pragma: no cover
154 154 except ImportError:
155 155 raise HgsubversionImportError
156 156 Session().add(sett)
157 157
158 158 # sett = Ui.get_by_key('hggit')
159 159 # if not sett:
160 160 # #make one if it's not there !
161 161 # sett = Ui()
162 162 # sett.ui_key = 'hggit'
163 163 # sett.ui_section = 'extensions'
164 164 #
165 165 # sett.ui_active = form_result['extensions_hggit']
166 166 # Session().add(sett)
167 167
168 168 Session().commit()
169 169
170 170 h.flash(_('Updated VCS settings'), category='success')
171 171
172 172 except HgsubversionImportError:
173 173 log.error(traceback.format_exc())
174 174 h.flash(_('Unable to activate hgsubversion support. '
175 175 'The "hgsubversion" library is missing'),
176 176 category='error')
177 177
178 178 except Exception:
179 179 log.error(traceback.format_exc())
180 180 h.flash(_('Error occurred while updating '
181 181 'application settings'), category='error')
182 182
183 183 defaults = Setting.get_app_settings()
184 184 defaults.update(self._get_hg_ui_settings())
185 185
186 186 return htmlfill.render(
187 187 render('admin/settings/settings.html'),
188 188 defaults=defaults,
189 189 encoding="UTF-8",
190 190 force_defaults=False)
191 191
192 192 @HasPermissionAllDecorator('hg.admin')
193 193 def settings_mapping(self):
194 194 """GET /admin/settings/mapping: All items in the collection"""
195 195 # url('admin_settings_mapping')
196 196 c.active = 'mapping'
197 197 if request.POST:
198 198 rm_obsolete = request.POST.get('destroy', False)
199 199 install_git_hooks = request.POST.get('hooks', False)
200 200 overwrite_git_hooks = request.POST.get('hooks_overwrite', False);
201 201 invalidate_cache = request.POST.get('invalidate', False)
202 202 log.debug('rescanning repo location with destroy obsolete=%s, '
203 203 'install git hooks=%s and '
204 204 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
205 205
206 206 if invalidate_cache:
207 207 log.debug('invalidating all repositories cache')
208 208 for repo in Repository.get_all():
209 209 ScmModel().mark_for_invalidation(repo.repo_name)
210 210
211 211 filesystem_repos = ScmModel().repo_scan()
212 212 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
213 213 install_git_hooks=install_git_hooks,
214 214 user=c.authuser.username,
215 215 overwrite_git_hooks=overwrite_git_hooks)
216 216 h.flash(h.literal(_('Repositories successfully rescanned. Added: %s. Removed: %s.') %
217 217 (', '.join(h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name))
218 218 for repo_name in added) or '-',
219 219 ', '.join(h.escape(safe_unicode(repo_name)) for repo_name in removed) or '-')),
220 220 category='success')
221 return redirect(url('admin_settings_mapping'))
221 raise HTTPFound(location=url('admin_settings_mapping'))
222 222
223 223 defaults = Setting.get_app_settings()
224 224 defaults.update(self._get_hg_ui_settings())
225 225
226 226 return htmlfill.render(
227 227 render('admin/settings/settings.html'),
228 228 defaults=defaults,
229 229 encoding="UTF-8",
230 230 force_defaults=False)
231 231
232 232 @HasPermissionAllDecorator('hg.admin')
233 233 def settings_global(self):
234 234 """GET /admin/settings/global: All items in the collection"""
235 235 # url('admin_settings_global')
236 236 c.active = 'global'
237 237 if request.POST:
238 238 application_form = ApplicationSettingsForm()()
239 239 try:
240 240 form_result = application_form.to_python(dict(request.POST))
241 241 except formencode.Invalid as errors:
242 242 return htmlfill.render(
243 243 render('admin/settings/settings.html'),
244 244 defaults=errors.value,
245 245 errors=errors.error_dict or {},
246 246 prefix_error=False,
247 247 encoding="UTF-8",
248 248 force_defaults=False)
249 249
250 250 try:
251 251 sett1 = Setting.create_or_update('title',
252 252 form_result['title'])
253 253 Session().add(sett1)
254 254
255 255 sett2 = Setting.create_or_update('realm',
256 256 form_result['realm'])
257 257 Session().add(sett2)
258 258
259 259 sett3 = Setting.create_or_update('ga_code',
260 260 form_result['ga_code'])
261 261 Session().add(sett3)
262 262
263 263 sett4 = Setting.create_or_update('captcha_public_key',
264 264 form_result['captcha_public_key'])
265 265 Session().add(sett4)
266 266
267 267 sett5 = Setting.create_or_update('captcha_private_key',
268 268 form_result['captcha_private_key'])
269 269 Session().add(sett5)
270 270
271 271 Session().commit()
272 272 set_app_settings(config)
273 273 h.flash(_('Updated application settings'), category='success')
274 274
275 275 except Exception:
276 276 log.error(traceback.format_exc())
277 277 h.flash(_('Error occurred while updating '
278 278 'application settings'),
279 279 category='error')
280 280
281 return redirect(url('admin_settings_global'))
281 raise HTTPFound(location=url('admin_settings_global'))
282 282
283 283 defaults = Setting.get_app_settings()
284 284 defaults.update(self._get_hg_ui_settings())
285 285
286 286 return htmlfill.render(
287 287 render('admin/settings/settings.html'),
288 288 defaults=defaults,
289 289 encoding="UTF-8",
290 290 force_defaults=False)
291 291
292 292 @HasPermissionAllDecorator('hg.admin')
293 293 def settings_visual(self):
294 294 """GET /admin/settings/visual: All items in the collection"""
295 295 # url('admin_settings_visual')
296 296 c.active = 'visual'
297 297 if request.POST:
298 298 application_form = ApplicationVisualisationForm()()
299 299 try:
300 300 form_result = application_form.to_python(dict(request.POST))
301 301 except formencode.Invalid as errors:
302 302 return htmlfill.render(
303 303 render('admin/settings/settings.html'),
304 304 defaults=errors.value,
305 305 errors=errors.error_dict or {},
306 306 prefix_error=False,
307 307 encoding="UTF-8",
308 308 force_defaults=False)
309 309
310 310 try:
311 311 settings = [
312 312 ('show_public_icon', 'show_public_icon', 'bool'),
313 313 ('show_private_icon', 'show_private_icon', 'bool'),
314 314 ('stylify_metatags', 'stylify_metatags', 'bool'),
315 315 ('repository_fields', 'repository_fields', 'bool'),
316 316 ('dashboard_items', 'dashboard_items', 'int'),
317 317 ('admin_grid_items', 'admin_grid_items', 'int'),
318 318 ('show_version', 'show_version', 'bool'),
319 319 ('use_gravatar', 'use_gravatar', 'bool'),
320 320 ('gravatar_url', 'gravatar_url', 'unicode'),
321 321 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
322 322 ]
323 323 for setting, form_key, type_ in settings:
324 324 sett = Setting.create_or_update(setting,
325 325 form_result[form_key], type_)
326 326 Session().add(sett)
327 327
328 328 Session().commit()
329 329 set_app_settings(config)
330 330 h.flash(_('Updated visualisation settings'),
331 331 category='success')
332 332
333 333 except Exception:
334 334 log.error(traceback.format_exc())
335 335 h.flash(_('Error occurred during updating '
336 336 'visualisation settings'),
337 337 category='error')
338 338
339 return redirect(url('admin_settings_visual'))
339 raise HTTPFound(location=url('admin_settings_visual'))
340 340
341 341 defaults = Setting.get_app_settings()
342 342 defaults.update(self._get_hg_ui_settings())
343 343
344 344 return htmlfill.render(
345 345 render('admin/settings/settings.html'),
346 346 defaults=defaults,
347 347 encoding="UTF-8",
348 348 force_defaults=False)
349 349
350 350 @HasPermissionAllDecorator('hg.admin')
351 351 def settings_email(self):
352 352 """GET /admin/settings/email: All items in the collection"""
353 353 # url('admin_settings_email')
354 354 c.active = 'email'
355 355 if request.POST:
356 356 test_email = request.POST.get('test_email')
357 357 test_email_subj = 'Kallithea test email'
358 358 test_body = ('Kallithea Email test, '
359 359 'Kallithea version: %s' % c.kallithea_version)
360 360 if not test_email:
361 361 h.flash(_('Please enter email address'), category='error')
362 return redirect(url('admin_settings_email'))
362 raise HTTPFound(location=url('admin_settings_email'))
363 363
364 364 test_email_txt_body = EmailNotificationModel()\
365 365 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
366 366 'txt', body=test_body)
367 367 test_email_html_body = EmailNotificationModel()\
368 368 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
369 369 'html', body=test_body)
370 370
371 371 recipients = [test_email] if test_email else None
372 372
373 373 run_task(tasks.send_email, recipients, test_email_subj,
374 374 test_email_txt_body, test_email_html_body)
375 375
376 376 h.flash(_('Send email task created'), category='success')
377 return redirect(url('admin_settings_email'))
377 raise HTTPFound(location=url('admin_settings_email'))
378 378
379 379 defaults = Setting.get_app_settings()
380 380 defaults.update(self._get_hg_ui_settings())
381 381
382 382 import kallithea
383 383 c.ini = kallithea.CONFIG
384 384
385 385 return htmlfill.render(
386 386 render('admin/settings/settings.html'),
387 387 defaults=defaults,
388 388 encoding="UTF-8",
389 389 force_defaults=False)
390 390
391 391 @HasPermissionAllDecorator('hg.admin')
392 392 def settings_hooks(self):
393 393 """GET /admin/settings/hooks: All items in the collection"""
394 394 # url('admin_settings_hooks')
395 395 c.active = 'hooks'
396 396 if request.POST:
397 397 if c.visual.allow_custom_hooks_settings:
398 398 ui_key = request.POST.get('new_hook_ui_key')
399 399 ui_value = request.POST.get('new_hook_ui_value')
400 400
401 401 hook_id = request.POST.get('hook_id')
402 402
403 403 try:
404 404 ui_key = ui_key and ui_key.strip()
405 405 if ui_value and ui_key:
406 406 Ui.create_or_update_hook(ui_key, ui_value)
407 407 h.flash(_('Added new hook'), category='success')
408 408 elif hook_id:
409 409 Ui.delete(hook_id)
410 410 Session().commit()
411 411
412 412 # check for edits
413 413 update = False
414 414 _d = request.POST.dict_of_lists()
415 415 for k, v in zip(_d.get('hook_ui_key', []),
416 416 _d.get('hook_ui_value_new', [])):
417 417 Ui.create_or_update_hook(k, v)
418 418 update = True
419 419
420 420 if update:
421 421 h.flash(_('Updated hooks'), category='success')
422 422 Session().commit()
423 423 except Exception:
424 424 log.error(traceback.format_exc())
425 425 h.flash(_('Error occurred during hook creation'),
426 426 category='error')
427 427
428 return redirect(url('admin_settings_hooks'))
428 raise HTTPFound(location=url('admin_settings_hooks'))
429 429
430 430 defaults = Setting.get_app_settings()
431 431 defaults.update(self._get_hg_ui_settings())
432 432
433 433 c.hooks = Ui.get_builtin_hooks()
434 434 c.custom_hooks = Ui.get_custom_hooks()
435 435
436 436 return htmlfill.render(
437 437 render('admin/settings/settings.html'),
438 438 defaults=defaults,
439 439 encoding="UTF-8",
440 440 force_defaults=False)
441 441
442 442 @HasPermissionAllDecorator('hg.admin')
443 443 def settings_search(self):
444 444 """GET /admin/settings/search: All items in the collection"""
445 445 # url('admin_settings_search')
446 446 c.active = 'search'
447 447 if request.POST:
448 448 repo_location = self._get_hg_ui_settings()['paths_root_path']
449 449 full_index = request.POST.get('full_index', False)
450 450 run_task(tasks.whoosh_index, repo_location, full_index)
451 451 h.flash(_('Whoosh reindex task scheduled'), category='success')
452 return redirect(url('admin_settings_search'))
452 raise HTTPFound(location=url('admin_settings_search'))
453 453
454 454 defaults = Setting.get_app_settings()
455 455 defaults.update(self._get_hg_ui_settings())
456 456
457 457 return htmlfill.render(
458 458 render('admin/settings/settings.html'),
459 459 defaults=defaults,
460 460 encoding="UTF-8",
461 461 force_defaults=False)
462 462
463 463 @HasPermissionAllDecorator('hg.admin')
464 464 def settings_system(self):
465 465 """GET /admin/settings/system: All items in the collection"""
466 466 # url('admin_settings_system')
467 467 c.active = 'system'
468 468
469 469 defaults = Setting.get_app_settings()
470 470 defaults.update(self._get_hg_ui_settings())
471 471
472 472 import kallithea
473 473 c.ini = kallithea.CONFIG
474 474 c.update_url = defaults.get('update_url')
475 475 server_info = Setting.get_server_info()
476 476 for key, val in server_info.iteritems():
477 477 setattr(c, key, val)
478 478
479 479 return htmlfill.render(
480 480 render('admin/settings/settings.html'),
481 481 defaults=defaults,
482 482 encoding="UTF-8",
483 483 force_defaults=False)
484 484
485 485 @HasPermissionAllDecorator('hg.admin')
486 486 def settings_system_update(self):
487 487 """GET /admin/settings/system/updates: All items in the collection"""
488 488 # url('admin_settings_system_update')
489 489 import json
490 490 import urllib2
491 491 from kallithea.lib.verlib import NormalizedVersion
492 492 from kallithea import __version__
493 493
494 494 defaults = Setting.get_app_settings()
495 495 defaults.update(self._get_hg_ui_settings())
496 496 _update_url = defaults.get('update_url', '')
497 497 _update_url = "" # FIXME: disabled
498 498
499 499 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s)
500 500 try:
501 501 import kallithea
502 502 ver = kallithea.__version__
503 503 log.debug('Checking for upgrade on `%s` server', _update_url)
504 504 opener = urllib2.build_opener()
505 505 opener.addheaders = [('User-agent', 'Kallithea-SCM/%s' % ver)]
506 506 response = opener.open(_update_url)
507 507 response_data = response.read()
508 508 data = json.loads(response_data)
509 509 except urllib2.URLError as e:
510 510 log.error(traceback.format_exc())
511 511 return _err('Failed to contact upgrade server: %r' % e)
512 512 except ValueError as e:
513 513 log.error(traceback.format_exc())
514 514 return _err('Bad data sent from update server')
515 515
516 516 latest = data['versions'][0]
517 517
518 518 c.update_url = _update_url
519 519 c.latest_data = latest
520 520 c.latest_ver = latest['version']
521 521 c.cur_ver = __version__
522 522 c.should_upgrade = False
523 523
524 524 if NormalizedVersion(c.latest_ver) > NormalizedVersion(c.cur_ver):
525 525 c.should_upgrade = True
526 526 c.important_notices = latest['general']
527 527
528 528 return render('admin/settings/settings_system_update.html'),
@@ -1,466 +1,466 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.user_groups
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 User Groups crud controller for pylons
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Jan 25, 2011
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
35 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36 36
37 37 from sqlalchemy.orm import joinedload
38 38 from sqlalchemy.sql.expression import func
39 39 from webob.exc import HTTPInternalServerError
40 40
41 41 import kallithea
42 42 from kallithea.lib import helpers as h
43 43 from kallithea.lib.exceptions import UserGroupsAssignedException, \
44 44 RepoGroupAssignmentError
45 45 from kallithea.lib.utils2 import safe_unicode, safe_int
46 46 from kallithea.lib.auth import LoginRequired, \
47 47 HasUserGroupPermissionAnyDecorator, HasPermissionAnyDecorator
48 48 from kallithea.lib.base import BaseController, render
49 49 from kallithea.model.scm import UserGroupList
50 50 from kallithea.model.user_group import UserGroupModel
51 51 from kallithea.model.repo import RepoModel
52 52 from kallithea.model.db import User, UserGroup, UserGroupToPerm, \
53 53 UserGroupRepoToPerm, UserGroupRepoGroupToPerm
54 54 from kallithea.model.forms import UserGroupForm, UserGroupPermsForm, \
55 55 CustomDefaultPermissionsForm
56 56 from kallithea.model.meta import Session
57 57 from kallithea.lib.utils import action_logger
58 58 from kallithea.lib.compat import json
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class UserGroupsController(BaseController):
64 64 """REST Controller styled on the Atom Publishing Protocol"""
65 65
66 66 @LoginRequired()
67 67 def __before__(self):
68 68 super(UserGroupsController, self).__before__()
69 69 c.available_permissions = config['available_permissions']
70 70
71 71 def __load_data(self, user_group_id):
72 72 c.group_members_obj = sorted((x.user for x in c.user_group.members),
73 73 key=lambda u: u.username.lower())
74 74
75 75 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
76 76 c.available_members = sorted(((x.user_id, x.username) for x in
77 77 User.query().all()),
78 78 key=lambda u: u[1].lower())
79 79
80 80 def __load_defaults(self, user_group_id):
81 81 """
82 82 Load defaults settings for edit, and update
83 83
84 84 :param user_group_id:
85 85 """
86 86 user_group = UserGroup.get_or_404(user_group_id)
87 87 data = user_group.get_dict()
88 88 return data
89 89
90 90 def index(self, format='html'):
91 91 """GET /users_groups: All items in the collection"""
92 92 # url('users_groups')
93 93 _list = UserGroup.query()\
94 94 .order_by(func.lower(UserGroup.users_group_name))\
95 95 .all()
96 96 group_iter = UserGroupList(_list, perm_set=['usergroup.admin'])
97 97 user_groups_data = []
98 98 total_records = len(group_iter)
99 99 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
100 100 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
101 101
102 102 user_group_name = lambda user_group_id, user_group_name: (
103 103 template.get_def("user_group_name")
104 104 .render(user_group_id, user_group_name, _=_, h=h, c=c)
105 105 )
106 106 user_group_actions = lambda user_group_id, user_group_name: (
107 107 template.get_def("user_group_actions")
108 108 .render(user_group_id, user_group_name, _=_, h=h, c=c)
109 109 )
110 110 for user_gr in group_iter:
111 111
112 112 user_groups_data.append({
113 113 "raw_name": user_gr.users_group_name,
114 114 "group_name": user_group_name(user_gr.users_group_id,
115 115 user_gr.users_group_name),
116 116 "desc": h.escape(user_gr.user_group_description),
117 117 "members": len(user_gr.members),
118 118 "active": h.boolicon(user_gr.users_group_active),
119 119 "owner": h.person(user_gr.user.username),
120 120 "action": user_group_actions(user_gr.users_group_id, user_gr.users_group_name)
121 121 })
122 122
123 123 c.data = json.dumps({
124 124 "totalRecords": total_records,
125 125 "startIndex": 0,
126 126 "sort": None,
127 127 "dir": "asc",
128 128 "records": user_groups_data
129 129 })
130 130
131 131 return render('admin/user_groups/user_groups.html')
132 132
133 133 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
134 134 def create(self):
135 135 """POST /users_groups: Create a new item"""
136 136 # url('users_groups')
137 137
138 138 users_group_form = UserGroupForm()()
139 139 try:
140 140 form_result = users_group_form.to_python(dict(request.POST))
141 141 ug = UserGroupModel().create(name=form_result['users_group_name'],
142 142 description=form_result['user_group_description'],
143 143 owner=self.authuser.user_id,
144 144 active=form_result['users_group_active'])
145 145
146 146 gr = form_result['users_group_name']
147 147 action_logger(self.authuser,
148 148 'admin_created_users_group:%s' % gr,
149 149 None, self.ip_addr, self.sa)
150 150 h.flash(h.literal(_('Created user group %s') % h.link_to(h.escape(gr), url('edit_users_group', id=ug.users_group_id))),
151 151 category='success')
152 152 Session().commit()
153 153 except formencode.Invalid as errors:
154 154 return htmlfill.render(
155 155 render('admin/user_groups/user_group_add.html'),
156 156 defaults=errors.value,
157 157 errors=errors.error_dict or {},
158 158 prefix_error=False,
159 159 encoding="UTF-8",
160 160 force_defaults=False)
161 161 except Exception:
162 162 log.error(traceback.format_exc())
163 163 h.flash(_('Error occurred during creation of user group %s') \
164 164 % request.POST.get('users_group_name'), category='error')
165 165
166 return redirect(url('users_groups'))
166 raise HTTPFound(location=url('users_groups'))
167 167
168 168 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
169 169 def new(self, format='html'):
170 170 """GET /user_groups/new: Form to create a new item"""
171 171 # url('new_users_group')
172 172 return render('admin/user_groups/user_group_add.html')
173 173
174 174 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
175 175 def update(self, id):
176 176 """PUT /user_groups/id: Update an existing item"""
177 177 # Forms posted to this method should contain a hidden field:
178 178 # <input type="hidden" name="_method" value="PUT" />
179 179 # Or using helpers:
180 180 # h.form(url('users_group', id=ID),
181 181 # method='put')
182 182 # url('users_group', id=ID)
183 183
184 184 c.user_group = UserGroup.get_or_404(id)
185 185 c.active = 'settings'
186 186 self.__load_data(id)
187 187
188 188 available_members = [safe_unicode(x[0]) for x in c.available_members]
189 189
190 190 users_group_form = UserGroupForm(edit=True,
191 191 old_data=c.user_group.get_dict(),
192 192 available_members=available_members)()
193 193
194 194 try:
195 195 form_result = users_group_form.to_python(request.POST)
196 196 UserGroupModel().update(c.user_group, form_result)
197 197 gr = form_result['users_group_name']
198 198 action_logger(self.authuser,
199 199 'admin_updated_users_group:%s' % gr,
200 200 None, self.ip_addr, self.sa)
201 201 h.flash(_('Updated user group %s') % gr, category='success')
202 202 Session().commit()
203 203 except formencode.Invalid as errors:
204 204 ug_model = UserGroupModel()
205 205 defaults = errors.value
206 206 e = errors.error_dict or {}
207 207 defaults.update({
208 208 'create_repo_perm': ug_model.has_perm(id,
209 209 'hg.create.repository'),
210 210 'fork_repo_perm': ug_model.has_perm(id,
211 211 'hg.fork.repository'),
212 212 '_method': 'put'
213 213 })
214 214
215 215 return htmlfill.render(
216 216 render('admin/user_groups/user_group_edit.html'),
217 217 defaults=defaults,
218 218 errors=e,
219 219 prefix_error=False,
220 220 encoding="UTF-8",
221 221 force_defaults=False)
222 222 except Exception:
223 223 log.error(traceback.format_exc())
224 224 h.flash(_('Error occurred during update of user group %s') \
225 225 % request.POST.get('users_group_name'), category='error')
226 226
227 return redirect(url('edit_users_group', id=id))
227 raise HTTPFound(location=url('edit_users_group', id=id))
228 228
229 229 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
230 230 def delete(self, id):
231 231 """DELETE /user_groups/id: Delete an existing item"""
232 232 # Forms posted to this method should contain a hidden field:
233 233 # <input type="hidden" name="_method" value="DELETE" />
234 234 # Or using helpers:
235 235 # h.form(url('users_group', id=ID),
236 236 # method='delete')
237 237 # url('users_group', id=ID)
238 238 usr_gr = UserGroup.get_or_404(id)
239 239 try:
240 240 UserGroupModel().delete(usr_gr)
241 241 Session().commit()
242 242 h.flash(_('Successfully deleted user group'), category='success')
243 243 except UserGroupsAssignedException as e:
244 244 h.flash(e, category='error')
245 245 except Exception:
246 246 log.error(traceback.format_exc())
247 247 h.flash(_('An error occurred during deletion of user group'),
248 248 category='error')
249 return redirect(url('users_groups'))
249 raise HTTPFound(location=url('users_groups'))
250 250
251 251 def show(self, id, format='html'):
252 252 """GET /user_groups/id: Show a specific item"""
253 253 # url('users_group', id=ID)
254 254
255 255 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
256 256 def edit(self, id, format='html'):
257 257 """GET /user_groups/id/edit: Form to edit an existing item"""
258 258 # url('edit_users_group', id=ID)
259 259
260 260 c.user_group = UserGroup.get_or_404(id)
261 261 c.active = 'settings'
262 262 self.__load_data(id)
263 263
264 264 defaults = self.__load_defaults(id)
265 265
266 266 return htmlfill.render(
267 267 render('admin/user_groups/user_group_edit.html'),
268 268 defaults=defaults,
269 269 encoding="UTF-8",
270 270 force_defaults=False
271 271 )
272 272
273 273 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
274 274 def edit_perms(self, id):
275 275 c.user_group = UserGroup.get_or_404(id)
276 276 c.active = 'perms'
277 277
278 278 repo_model = RepoModel()
279 279 c.users_array = repo_model.get_users_js()
280 280 c.user_groups_array = repo_model.get_user_groups_js()
281 281
282 282 defaults = {}
283 283 # fill user group users
284 284 for p in c.user_group.user_user_group_to_perm:
285 285 defaults.update({'u_perm_%s' % p.user.username:
286 286 p.permission.permission_name})
287 287
288 288 for p in c.user_group.user_group_user_group_to_perm:
289 289 defaults.update({'g_perm_%s' % p.user_group.users_group_name:
290 290 p.permission.permission_name})
291 291
292 292 return htmlfill.render(
293 293 render('admin/user_groups/user_group_edit.html'),
294 294 defaults=defaults,
295 295 encoding="UTF-8",
296 296 force_defaults=False
297 297 )
298 298
299 299 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
300 300 def update_perms(self, id):
301 301 """
302 302 grant permission for given usergroup
303 303
304 304 :param id:
305 305 """
306 306 user_group = UserGroup.get_or_404(id)
307 307 form = UserGroupPermsForm()().to_python(request.POST)
308 308
309 309 # set the permissions !
310 310 try:
311 311 UserGroupModel()._update_permissions(user_group, form['perms_new'],
312 312 form['perms_updates'])
313 313 except RepoGroupAssignmentError:
314 314 h.flash(_('Target group cannot be the same'), category='error')
315 return redirect(url('edit_user_group_perms', id=id))
315 raise HTTPFound(location=url('edit_user_group_perms', id=id))
316 316 #TODO: implement this
317 317 #action_logger(self.authuser, 'admin_changed_repo_permissions',
318 318 # repo_name, self.ip_addr, self.sa)
319 319 Session().commit()
320 320 h.flash(_('User group permissions updated'), category='success')
321 return redirect(url('edit_user_group_perms', id=id))
321 raise HTTPFound(location=url('edit_user_group_perms', id=id))
322 322
323 323 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
324 324 def delete_perms(self, id):
325 325 """
326 326 DELETE an existing repository group permission user
327 327
328 328 :param group_name:
329 329 """
330 330 try:
331 331 obj_type = request.POST.get('obj_type')
332 332 obj_id = None
333 333 if obj_type == 'user':
334 334 obj_id = safe_int(request.POST.get('user_id'))
335 335 elif obj_type == 'user_group':
336 336 obj_id = safe_int(request.POST.get('user_group_id'))
337 337
338 338 if not c.authuser.is_admin:
339 339 if obj_type == 'user' and c.authuser.user_id == obj_id:
340 340 msg = _('Cannot revoke permission for yourself as admin')
341 341 h.flash(msg, category='warning')
342 342 raise Exception('revoke admin permission on self')
343 343 if obj_type == 'user':
344 344 UserGroupModel().revoke_user_permission(user_group=id,
345 345 user=obj_id)
346 346 elif obj_type == 'user_group':
347 347 UserGroupModel().revoke_user_group_permission(target_user_group=id,
348 348 user_group=obj_id)
349 349 Session().commit()
350 350 except Exception:
351 351 log.error(traceback.format_exc())
352 352 h.flash(_('An error occurred during revoking of permission'),
353 353 category='error')
354 354 raise HTTPInternalServerError()
355 355
356 356 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
357 357 def edit_default_perms(self, id):
358 358 c.user_group = UserGroup.get_or_404(id)
359 359 c.active = 'default_perms'
360 360
361 361 permissions = {
362 362 'repositories': {},
363 363 'repositories_groups': {}
364 364 }
365 365 ugroup_repo_perms = UserGroupRepoToPerm.query()\
366 366 .options(joinedload(UserGroupRepoToPerm.permission))\
367 367 .options(joinedload(UserGroupRepoToPerm.repository))\
368 368 .filter(UserGroupRepoToPerm.users_group_id == id)\
369 369 .all()
370 370
371 371 for gr in ugroup_repo_perms:
372 372 permissions['repositories'][gr.repository.repo_name] \
373 373 = gr.permission.permission_name
374 374
375 375 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
376 376 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
377 377 .options(joinedload(UserGroupRepoGroupToPerm.group))\
378 378 .filter(UserGroupRepoGroupToPerm.users_group_id == id)\
379 379 .all()
380 380
381 381 for gr in ugroup_group_perms:
382 382 permissions['repositories_groups'][gr.group.group_name] \
383 383 = gr.permission.permission_name
384 384 c.permissions = permissions
385 385
386 386 ug_model = UserGroupModel()
387 387
388 388 defaults = c.user_group.get_dict()
389 389 defaults.update({
390 390 'create_repo_perm': ug_model.has_perm(c.user_group,
391 391 'hg.create.repository'),
392 392 'create_user_group_perm': ug_model.has_perm(c.user_group,
393 393 'hg.usergroup.create.true'),
394 394 'fork_repo_perm': ug_model.has_perm(c.user_group,
395 395 'hg.fork.repository'),
396 396 })
397 397
398 398 return htmlfill.render(
399 399 render('admin/user_groups/user_group_edit.html'),
400 400 defaults=defaults,
401 401 encoding="UTF-8",
402 402 force_defaults=False
403 403 )
404 404
405 405 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
406 406 def update_default_perms(self, id):
407 407 """PUT /users_perm/id: Update an existing item"""
408 408 # url('users_group_perm', id=ID, method='put')
409 409
410 410 user_group = UserGroup.get_or_404(id)
411 411
412 412 try:
413 413 form = CustomDefaultPermissionsForm()()
414 414 form_result = form.to_python(request.POST)
415 415
416 416 inherit_perms = form_result['inherit_default_permissions']
417 417 user_group.inherit_default_permissions = inherit_perms
418 418 Session().add(user_group)
419 419 usergroup_model = UserGroupModel()
420 420
421 421 defs = UserGroupToPerm.query()\
422 422 .filter(UserGroupToPerm.users_group == user_group)\
423 423 .all()
424 424 for ug in defs:
425 425 Session().delete(ug)
426 426
427 427 if form_result['create_repo_perm']:
428 428 usergroup_model.grant_perm(id, 'hg.create.repository')
429 429 else:
430 430 usergroup_model.grant_perm(id, 'hg.create.none')
431 431 if form_result['create_user_group_perm']:
432 432 usergroup_model.grant_perm(id, 'hg.usergroup.create.true')
433 433 else:
434 434 usergroup_model.grant_perm(id, 'hg.usergroup.create.false')
435 435 if form_result['fork_repo_perm']:
436 436 usergroup_model.grant_perm(id, 'hg.fork.repository')
437 437 else:
438 438 usergroup_model.grant_perm(id, 'hg.fork.none')
439 439
440 440 h.flash(_("Updated permissions"), category='success')
441 441 Session().commit()
442 442 except Exception:
443 443 log.error(traceback.format_exc())
444 444 h.flash(_('An error occurred during permissions saving'),
445 445 category='error')
446 446
447 return redirect(url('edit_user_group_default_perms', id=id))
447 raise HTTPFound(location=url('edit_user_group_default_perms', id=id))
448 448
449 449 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
450 450 def edit_advanced(self, id):
451 451 c.user_group = UserGroup.get_or_404(id)
452 452 c.active = 'advanced'
453 453 c.group_members_obj = sorted((x.user for x in c.user_group.members),
454 454 key=lambda u: u.username.lower())
455 455 return render('admin/user_groups/user_group_edit.html')
456 456
457 457
458 458 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
459 459 def edit_members(self, id):
460 460 c.user_group = UserGroup.get_or_404(id)
461 461 c.active = 'members'
462 462 c.group_members_obj = sorted((x.user for x in c.user_group.members),
463 463 key=lambda u: u.username.lower())
464 464
465 465 c.group_members = [(x.user_id, x.username) for x in c.group_members_obj]
466 466 return render('admin/user_groups/user_group_edit.html')
@@ -1,487 +1,486 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.users
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Users crud controller for pylons
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
35 34 from pylons.i18n.translation import _
36 35 from sqlalchemy.sql.expression import func
37 from webob.exc import HTTPNotFound
36 from webob.exc import HTTPFound, HTTPNotFound
38 37
39 38 import kallithea
40 39 from kallithea.lib.exceptions import DefaultUserException, \
41 40 UserOwnsReposException, UserCreationError
42 41 from kallithea.lib import helpers as h
43 42 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
44 43 AuthUser
45 44 from kallithea.lib import auth_modules
46 45 from kallithea.lib.auth_modules import auth_internal
47 46 from kallithea.lib.base import BaseController, render
48 47 from kallithea.model.api_key import ApiKeyModel
49 48
50 49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
51 50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
52 51 from kallithea.model.user import UserModel
53 52 from kallithea.model.meta import Session
54 53 from kallithea.lib.utils import action_logger
55 54 from kallithea.lib.compat import json
56 55 from kallithea.lib.utils2 import datetime_to_time, safe_int, generate_api_key
57 56
58 57 log = logging.getLogger(__name__)
59 58
60 59
61 60 class UsersController(BaseController):
62 61 """REST Controller styled on the Atom Publishing Protocol"""
63 62
64 63 @LoginRequired()
65 64 @HasPermissionAllDecorator('hg.admin')
66 65 def __before__(self):
67 66 super(UsersController, self).__before__()
68 67 c.available_permissions = config['available_permissions']
69 68 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
70 69
71 70 def index(self, format='html'):
72 71 """GET /users: All items in the collection"""
73 72 # url('users')
74 73
75 74 c.users_list = User.query().order_by(User.username)\
76 75 .filter(User.username != User.DEFAULT_USER)\
77 76 .order_by(func.lower(User.username))\
78 77 .all()
79 78
80 79 users_data = []
81 80 total_records = len(c.users_list)
82 81 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
83 82 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
84 83
85 84 grav_tmpl = '<div class="gravatar">%s</div>'
86 85
87 86 username = lambda user_id, username: (
88 87 template.get_def("user_name")
89 88 .render(user_id, username, _=_, h=h, c=c))
90 89
91 90 user_actions = lambda user_id, username: (
92 91 template.get_def("user_actions")
93 92 .render(user_id, username, _=_, h=h, c=c))
94 93
95 94 for user in c.users_list:
96 95 users_data.append({
97 96 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
98 97 "raw_name": user.username,
99 98 "username": username(user.user_id, user.username),
100 99 "firstname": h.escape(user.name),
101 100 "lastname": h.escape(user.lastname),
102 101 "last_login": h.fmt_date(user.last_login),
103 102 "last_login_raw": datetime_to_time(user.last_login),
104 103 "active": h.boolicon(user.active),
105 104 "admin": h.boolicon(user.admin),
106 105 "extern_type": user.extern_type,
107 106 "extern_name": user.extern_name,
108 107 "action": user_actions(user.user_id, user.username),
109 108 })
110 109
111 110 c.data = json.dumps({
112 111 "totalRecords": total_records,
113 112 "startIndex": 0,
114 113 "sort": None,
115 114 "dir": "asc",
116 115 "records": users_data
117 116 })
118 117
119 118 return render('admin/users/users.html')
120 119
121 120 def create(self):
122 121 """POST /users: Create a new item"""
123 122 # url('users')
124 123 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
125 124 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
126 125 user_model = UserModel()
127 126 user_form = UserForm()()
128 127 try:
129 128 form_result = user_form.to_python(dict(request.POST))
130 129 user = user_model.create(form_result)
131 130 usr = form_result['username']
132 131 action_logger(self.authuser, 'admin_created_user:%s' % usr,
133 132 None, self.ip_addr, self.sa)
134 133 h.flash(h.literal(_('Created user %s') % h.link_to(h.escape(usr), url('edit_user', id=user.user_id))),
135 134 category='success')
136 135 Session().commit()
137 136 except formencode.Invalid as errors:
138 137 return htmlfill.render(
139 138 render('admin/users/user_add.html'),
140 139 defaults=errors.value,
141 140 errors=errors.error_dict or {},
142 141 prefix_error=False,
143 142 encoding="UTF-8",
144 143 force_defaults=False)
145 144 except UserCreationError as e:
146 145 h.flash(e, 'error')
147 146 except Exception:
148 147 log.error(traceback.format_exc())
149 148 h.flash(_('Error occurred during creation of user %s') \
150 149 % request.POST.get('username'), category='error')
151 return redirect(url('users'))
150 raise HTTPFound(location=url('users'))
152 151
153 152 def new(self, format='html'):
154 153 """GET /users/new: Form to create a new item"""
155 154 # url('new_user')
156 155 c.default_extern_type = auth_internal.KallitheaAuthPlugin.name
157 156 c.default_extern_name = auth_internal.KallitheaAuthPlugin.name
158 157 return render('admin/users/user_add.html')
159 158
160 159 def update(self, id):
161 160 """PUT /users/id: Update an existing item"""
162 161 # Forms posted to this method should contain a hidden field:
163 162 # <input type="hidden" name="_method" value="PUT" />
164 163 # Or using helpers:
165 164 # h.form(url('update_user', id=ID),
166 165 # method='put')
167 166 # url('user', id=ID)
168 167 user_model = UserModel()
169 168 user = user_model.get(id)
170 169 _form = UserForm(edit=True, old_data={'user_id': id,
171 170 'email': user.email})()
172 171 form_result = {}
173 172 try:
174 173 form_result = _form.to_python(dict(request.POST))
175 174 skip_attrs = ['extern_type', 'extern_name',
176 175 ] + auth_modules.get_managed_fields(user)
177 176
178 177 user_model.update(id, form_result, skip_attrs=skip_attrs)
179 178 usr = form_result['username']
180 179 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
181 180 None, self.ip_addr, self.sa)
182 181 h.flash(_('User updated successfully'), category='success')
183 182 Session().commit()
184 183 except formencode.Invalid as errors:
185 184 defaults = errors.value
186 185 e = errors.error_dict or {}
187 186 defaults.update({
188 187 'create_repo_perm': user_model.has_perm(id,
189 188 'hg.create.repository'),
190 189 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
191 190 '_method': 'put'
192 191 })
193 192 return htmlfill.render(
194 193 self._render_edit_profile(user),
195 194 defaults=defaults,
196 195 errors=e,
197 196 prefix_error=False,
198 197 encoding="UTF-8",
199 198 force_defaults=False)
200 199 except Exception:
201 200 log.error(traceback.format_exc())
202 201 h.flash(_('Error occurred during update of user %s') \
203 202 % form_result.get('username'), category='error')
204 return redirect(url('edit_user', id=id))
203 raise HTTPFound(location=url('edit_user', id=id))
205 204
206 205 def delete(self, id):
207 206 """DELETE /users/id: Delete an existing item"""
208 207 # Forms posted to this method should contain a hidden field:
209 208 # <input type="hidden" name="_method" value="DELETE" />
210 209 # Or using helpers:
211 210 # h.form(url('delete_user', id=ID),
212 211 # method='delete')
213 212 # url('user', id=ID)
214 213 usr = User.get_or_404(id)
215 214 try:
216 215 UserModel().delete(usr)
217 216 Session().commit()
218 217 h.flash(_('Successfully deleted user'), category='success')
219 218 except (UserOwnsReposException, DefaultUserException) as e:
220 219 h.flash(e, category='warning')
221 220 except Exception:
222 221 log.error(traceback.format_exc())
223 222 h.flash(_('An error occurred during deletion of user'),
224 223 category='error')
225 return redirect(url('users'))
224 raise HTTPFound(location=url('users'))
226 225
227 226 def show(self, id, format='html'):
228 227 """GET /users/id: Show a specific item"""
229 228 # url('user', id=ID)
230 229 User.get_or_404(-1)
231 230
232 231 def _get_user_or_raise_if_default(self, id):
233 232 try:
234 233 return User.get_or_404(id, allow_default=False)
235 234 except DefaultUserException:
236 235 h.flash(_("The default user cannot be edited"), category='warning')
237 236 raise HTTPNotFound
238 237
239 238 def _render_edit_profile(self, user):
240 239 c.user = user
241 240 c.active = 'profile'
242 241 c.perm_user = AuthUser(dbuser=user)
243 242 c.ip_addr = self.ip_addr
244 243 managed_fields = auth_modules.get_managed_fields(user)
245 244 c.readonly = lambda n: 'readonly' if n in managed_fields else None
246 245 return render('admin/users/user_edit.html')
247 246
248 247 def edit(self, id, format='html'):
249 248 """GET /users/id/edit: Form to edit an existing item"""
250 249 # url('edit_user', id=ID)
251 250 user = self._get_user_or_raise_if_default(id)
252 251 defaults = user.get_dict()
253 252
254 253 return htmlfill.render(
255 254 self._render_edit_profile(user),
256 255 defaults=defaults,
257 256 encoding="UTF-8",
258 257 force_defaults=False)
259 258
260 259 def edit_advanced(self, id):
261 260 c.user = self._get_user_or_raise_if_default(id)
262 261 c.active = 'advanced'
263 262 c.perm_user = AuthUser(user_id=id)
264 263 c.ip_addr = self.ip_addr
265 264
266 265 umodel = UserModel()
267 266 defaults = c.user.get_dict()
268 267 defaults.update({
269 268 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
270 269 'create_user_group_perm': umodel.has_perm(c.user,
271 270 'hg.usergroup.create.true'),
272 271 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
273 272 })
274 273 return htmlfill.render(
275 274 render('admin/users/user_edit.html'),
276 275 defaults=defaults,
277 276 encoding="UTF-8",
278 277 force_defaults=False)
279 278
280 279 def edit_api_keys(self, id):
281 280 c.user = self._get_user_or_raise_if_default(id)
282 281 c.active = 'api_keys'
283 282 show_expired = True
284 283 c.lifetime_values = [
285 284 (str(-1), _('Forever')),
286 285 (str(5), _('5 minutes')),
287 286 (str(60), _('1 hour')),
288 287 (str(60 * 24), _('1 day')),
289 288 (str(60 * 24 * 30), _('1 month')),
290 289 ]
291 290 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
292 291 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
293 292 show_expired=show_expired)
294 293 defaults = c.user.get_dict()
295 294 return htmlfill.render(
296 295 render('admin/users/user_edit.html'),
297 296 defaults=defaults,
298 297 encoding="UTF-8",
299 298 force_defaults=False)
300 299
301 300 def add_api_key(self, id):
302 301 c.user = self._get_user_or_raise_if_default(id)
303 302
304 303 lifetime = safe_int(request.POST.get('lifetime'), -1)
305 304 description = request.POST.get('description')
306 305 ApiKeyModel().create(c.user.user_id, description, lifetime)
307 306 Session().commit()
308 307 h.flash(_("API key successfully created"), category='success')
309 return redirect(url('edit_user_api_keys', id=c.user.user_id))
308 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
310 309
311 310 def delete_api_key(self, id):
312 311 c.user = self._get_user_or_raise_if_default(id)
313 312
314 313 api_key = request.POST.get('del_api_key')
315 314 if request.POST.get('del_api_key_builtin'):
316 315 user = User.get(c.user.user_id)
317 316 if user is not None:
318 317 user.api_key = generate_api_key()
319 318 Session().add(user)
320 319 Session().commit()
321 320 h.flash(_("API key successfully reset"), category='success')
322 321 elif api_key:
323 322 ApiKeyModel().delete(api_key, c.user.user_id)
324 323 Session().commit()
325 324 h.flash(_("API key successfully deleted"), category='success')
326 325
327 return redirect(url('edit_user_api_keys', id=c.user.user_id))
326 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
328 327
329 328 def update_account(self, id):
330 329 pass
331 330
332 331 def edit_perms(self, id):
333 332 c.user = self._get_user_or_raise_if_default(id)
334 333 c.active = 'perms'
335 334 c.perm_user = AuthUser(user_id=id)
336 335 c.ip_addr = self.ip_addr
337 336
338 337 umodel = UserModel()
339 338 defaults = c.user.get_dict()
340 339 defaults.update({
341 340 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
342 341 'create_user_group_perm': umodel.has_perm(c.user,
343 342 'hg.usergroup.create.true'),
344 343 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
345 344 })
346 345 return htmlfill.render(
347 346 render('admin/users/user_edit.html'),
348 347 defaults=defaults,
349 348 encoding="UTF-8",
350 349 force_defaults=False)
351 350
352 351 def update_perms(self, id):
353 352 """PUT /users_perm/id: Update an existing item"""
354 353 # url('user_perm', id=ID, method='put')
355 354 user = self._get_user_or_raise_if_default(id)
356 355
357 356 try:
358 357 form = CustomDefaultPermissionsForm()()
359 358 form_result = form.to_python(request.POST)
360 359
361 360 inherit_perms = form_result['inherit_default_permissions']
362 361 user.inherit_default_permissions = inherit_perms
363 362 Session().add(user)
364 363 user_model = UserModel()
365 364
366 365 defs = UserToPerm.query()\
367 366 .filter(UserToPerm.user == user)\
368 367 .all()
369 368 for ug in defs:
370 369 Session().delete(ug)
371 370
372 371 if form_result['create_repo_perm']:
373 372 user_model.grant_perm(id, 'hg.create.repository')
374 373 else:
375 374 user_model.grant_perm(id, 'hg.create.none')
376 375 if form_result['create_user_group_perm']:
377 376 user_model.grant_perm(id, 'hg.usergroup.create.true')
378 377 else:
379 378 user_model.grant_perm(id, 'hg.usergroup.create.false')
380 379 if form_result['fork_repo_perm']:
381 380 user_model.grant_perm(id, 'hg.fork.repository')
382 381 else:
383 382 user_model.grant_perm(id, 'hg.fork.none')
384 383 h.flash(_("Updated permissions"), category='success')
385 384 Session().commit()
386 385 except Exception:
387 386 log.error(traceback.format_exc())
388 387 h.flash(_('An error occurred during permissions saving'),
389 388 category='error')
390 return redirect(url('edit_user_perms', id=id))
389 raise HTTPFound(location=url('edit_user_perms', id=id))
391 390
392 391 def edit_emails(self, id):
393 392 c.user = self._get_user_or_raise_if_default(id)
394 393 c.active = 'emails'
395 394 c.user_email_map = UserEmailMap.query()\
396 395 .filter(UserEmailMap.user == c.user).all()
397 396
398 397 defaults = c.user.get_dict()
399 398 return htmlfill.render(
400 399 render('admin/users/user_edit.html'),
401 400 defaults=defaults,
402 401 encoding="UTF-8",
403 402 force_defaults=False)
404 403
405 404 def add_email(self, id):
406 405 """POST /user_emails:Add an existing item"""
407 406 # url('user_emails', id=ID, method='put')
408 407 user = self._get_user_or_raise_if_default(id)
409 408 email = request.POST.get('new_email')
410 409 user_model = UserModel()
411 410
412 411 try:
413 412 user_model.add_extra_email(id, email)
414 413 Session().commit()
415 414 h.flash(_("Added email %s to user") % email, category='success')
416 415 except formencode.Invalid as error:
417 416 msg = error.error_dict['email']
418 417 h.flash(msg, category='error')
419 418 except Exception:
420 419 log.error(traceback.format_exc())
421 420 h.flash(_('An error occurred during email saving'),
422 421 category='error')
423 return redirect(url('edit_user_emails', id=id))
422 raise HTTPFound(location=url('edit_user_emails', id=id))
424 423
425 424 def delete_email(self, id):
426 425 """DELETE /user_emails_delete/id: Delete an existing item"""
427 426 # url('user_emails_delete', id=ID, method='delete')
428 427 user = self._get_user_or_raise_if_default(id)
429 428 email_id = request.POST.get('del_email_id')
430 429 user_model = UserModel()
431 430 user_model.delete_extra_email(id, email_id)
432 431 Session().commit()
433 432 h.flash(_("Removed email from user"), category='success')
434 return redirect(url('edit_user_emails', id=id))
433 raise HTTPFound(location=url('edit_user_emails', id=id))
435 434
436 435 def edit_ips(self, id):
437 436 c.user = self._get_user_or_raise_if_default(id)
438 437 c.active = 'ips'
439 438 c.user_ip_map = UserIpMap.query()\
440 439 .filter(UserIpMap.user == c.user).all()
441 440
442 441 c.inherit_default_ips = c.user.inherit_default_permissions
443 442 c.default_user_ip_map = UserIpMap.query()\
444 443 .filter(UserIpMap.user == User.get_default_user()).all()
445 444
446 445 defaults = c.user.get_dict()
447 446 return htmlfill.render(
448 447 render('admin/users/user_edit.html'),
449 448 defaults=defaults,
450 449 encoding="UTF-8",
451 450 force_defaults=False)
452 451
453 452 def add_ip(self, id):
454 453 """POST /user_ips:Add an existing item"""
455 454 # url('user_ips', id=ID, method='put')
456 455
457 456 ip = request.POST.get('new_ip')
458 457 user_model = UserModel()
459 458
460 459 try:
461 460 user_model.add_extra_ip(id, ip)
462 461 Session().commit()
463 462 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
464 463 except formencode.Invalid as error:
465 464 msg = error.error_dict['ip']
466 465 h.flash(msg, category='error')
467 466 except Exception:
468 467 log.error(traceback.format_exc())
469 468 h.flash(_('An error occurred while adding IP address'),
470 469 category='error')
471 470
472 471 if 'default_user' in request.POST:
473 return redirect(url('admin_permissions_ips'))
474 return redirect(url('edit_user_ips', id=id))
472 raise HTTPFound(location=url('admin_permissions_ips'))
473 raise HTTPFound(location=url('edit_user_ips', id=id))
475 474
476 475 def delete_ip(self, id):
477 476 """DELETE /user_ips_delete/id: Delete an existing item"""
478 477 # url('user_ips_delete', id=ID, method='delete')
479 478 ip_id = request.POST.get('del_ip_id')
480 479 user_model = UserModel()
481 480 user_model.delete_extra_ip(id, ip_id)
482 481 Session().commit()
483 482 h.flash(_("Removed IP address from user whitelist"), category='success')
484 483
485 484 if 'default_user' in request.POST:
486 return redirect(url('admin_permissions_ips'))
487 return redirect(url('edit_user_ips', id=id))
485 raise HTTPFound(location=url('admin_permissions_ips'))
486 raise HTTPFound(location=url('edit_user_ips', id=id))
@@ -1,197 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changelog
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 changelog controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons import request, url, session, tmpl_context as c
32 from pylons.controllers.util import redirect
33 32 from pylons.i18n.translation import _
34 from webob.exc import HTTPNotFound, HTTPBadRequest
33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
35 34
36 35 import kallithea.lib.helpers as h
37 36 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 37 from kallithea.lib.base import BaseRepoController, render
39 38 from kallithea.lib.helpers import RepoPage
40 39 from kallithea.lib.compat import json
41 40 from kallithea.lib.graphmod import graph_data
42 41 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError,\
43 42 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
44 43 from kallithea.lib.utils2 import safe_int, safe_str
45 44
46 45
47 46 log = logging.getLogger(__name__)
48 47
49 48
50 49 def _load_changelog_summary():
51 50 p = safe_int(request.GET.get('page'), 1)
52 51 size = safe_int(request.GET.get('size'), 10)
53 52
54 53 def url_generator(**kw):
55 54 return url('changelog_summary_home',
56 55 repo_name=c.db_repo.repo_name, size=size, **kw)
57 56
58 57 collection = c.db_repo_scm_instance
59 58
60 59 c.repo_changesets = RepoPage(collection, page=p,
61 60 items_per_page=size,
62 61 url=url_generator)
63 62 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
64 63 c.comments = c.db_repo.get_comments(page_revisions)
65 64 c.statuses = c.db_repo.statuses(page_revisions)
66 65
67 66
68 67 class ChangelogController(BaseRepoController):
69 68
70 69 def __before__(self):
71 70 super(ChangelogController, self).__before__()
72 71 c.affected_files_cut_off = 60
73 72
74 73 @staticmethod
75 74 def __get_cs(rev, repo):
76 75 """
77 76 Safe way to get changeset. If error occur fail with error message.
78 77
79 78 :param rev: revision to fetch
80 79 :param repo: repo instance
81 80 """
82 81
83 82 try:
84 83 return c.db_repo_scm_instance.get_changeset(rev)
85 84 except EmptyRepositoryError as e:
86 85 h.flash(h.literal(_('There are no changesets yet')),
87 86 category='error')
88 87 except RepositoryError as e:
89 88 log.error(traceback.format_exc())
90 89 h.flash(safe_str(e), category='error')
91 90 raise HTTPBadRequest()
92 91
93 92 @LoginRequired()
94 93 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
95 94 'repository.admin')
96 95 def index(self, repo_name, revision=None, f_path=None):
97 96 # Fix URL after page size form submission via GET
98 97 # TODO: Somehow just don't send this extra junk in the GET URL
99 98 if request.GET.get('set'):
100 99 request.GET.pop('set', None)
101 100 if revision is None:
102 return redirect(url('changelog_home', repo_name=repo_name, **request.GET))
103 return redirect(url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
101 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
102 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
104 103
105 104 limit = 2000
106 105 default = 100
107 106 if request.GET.get('size'):
108 107 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
109 108 session['changelog_size'] = c.size
110 109 session.save()
111 110 else:
112 111 c.size = int(session.get('changelog_size', default))
113 112 # min size must be 1
114 113 c.size = max(c.size, 1)
115 114 p = safe_int(request.GET.get('page', 1), 1)
116 115 branch_name = request.GET.get('branch', None)
117 116 if (branch_name and
118 117 branch_name not in c.db_repo_scm_instance.branches and
119 118 branch_name not in c.db_repo_scm_instance.closed_branches and
120 119 not revision):
121 return redirect(url('changelog_file_home', repo_name=c.repo_name,
120 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
122 121 revision=branch_name, f_path=f_path or ''))
123 122
124 123 if revision == 'tip':
125 124 revision = None
126 125
127 126 c.changelog_for_path = f_path
128 127 try:
129 128
130 129 if f_path:
131 130 log.debug('generating changelog for path %s', f_path)
132 131 # get the history for the file !
133 132 tip_cs = c.db_repo_scm_instance.get_changeset()
134 133 try:
135 134 collection = tip_cs.get_file_history(f_path)
136 135 except (NodeDoesNotExistError, ChangesetError):
137 136 #this node is not present at tip !
138 137 try:
139 138 cs = self.__get_cs(revision, repo_name)
140 139 collection = cs.get_file_history(f_path)
141 140 except RepositoryError as e:
142 141 h.flash(safe_str(e), category='warning')
143 redirect(h.url('changelog_home', repo_name=repo_name))
142 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
144 143 collection = list(reversed(collection))
145 144 else:
146 145 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
147 146 branch_name=branch_name)
148 147 c.total_cs = len(collection)
149 148
150 149 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
151 150 items_per_page=c.size, branch=branch_name,)
152 151
153 152 page_revisions = [x.raw_id for x in c.pagination]
154 153 c.comments = c.db_repo.get_comments(page_revisions)
155 154 c.statuses = c.db_repo.statuses(page_revisions)
156 155 except EmptyRepositoryError as e:
157 156 h.flash(safe_str(e), category='warning')
158 return redirect(url('summary_home', repo_name=c.repo_name))
157 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
159 158 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
160 159 log.error(traceback.format_exc())
161 160 h.flash(safe_str(e), category='error')
162 return redirect(url('changelog_home', repo_name=c.repo_name))
161 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
163 162
164 163 c.branch_name = branch_name
165 164 c.branch_filters = [('', _('None'))] + \
166 165 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
167 166 if c.db_repo_scm_instance.closed_branches:
168 167 prefix = _('(closed)') + ' '
169 168 c.branch_filters += [('-', '-')] + \
170 169 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
171 170 revs = []
172 171 if not f_path:
173 172 revs = [x.revision for x in c.pagination]
174 173 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
175 174
176 175 c.revision = revision # requested revision ref
177 176 c.first_revision = c.pagination[0] # pagination is never empty here!
178 177 return render('changelog/changelog.html')
179 178
180 179 @LoginRequired()
181 180 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
182 181 'repository.admin')
183 182 def changelog_details(self, cs):
184 183 if request.environ.get('HTTP_X_PARTIAL_XHR'):
185 184 c.cs = c.db_repo_scm_instance.get_changeset(cs)
186 185 return render('changelog/changelog_details.html')
187 186 raise HTTPNotFound()
188 187
189 188 @LoginRequired()
190 189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
191 190 'repository.admin')
192 191 def changelog_summary(self, repo_name):
193 192 if request.environ.get('HTTP_X_PARTIAL_XHR'):
194 193 _load_changelog_summary()
195 194
196 195 return render('changelog/changelog_summary_data.html')
197 196 raise HTTPNotFound()
@@ -1,477 +1,476 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.changeset
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 changeset controller for pylons showing changes between
19 19 revisions
20 20
21 21 This file was forked by the Kallithea project in July 2014.
22 22 Original author and date, and relevant copyright and licensing information is below:
23 23 :created_on: Apr 25, 2010
24 24 :author: marcink
25 25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 26 :license: GPLv3, see LICENSE.md for more details.
27 27 """
28 28
29 29 import logging
30 30 import traceback
31 31 from collections import defaultdict
32 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
33 32
34 33 from pylons import tmpl_context as c, request, response
35 34 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
35 from webob.exc import HTTPFound, HTTPForbidden, HTTPBadRequest, HTTPNotFound
36
37 37 from kallithea.lib.utils import jsonify
38
39 38 from kallithea.lib.vcs.exceptions import RepositoryError, \
40 39 ChangesetDoesNotExistError
41 40
42 41 from kallithea.lib.compat import json
43 42 import kallithea.lib.helpers as h
44 43 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
45 44 NotAnonymous
46 45 from kallithea.lib.base import BaseRepoController, render
47 46 from kallithea.lib.utils import action_logger
48 47 from kallithea.lib.compat import OrderedDict
49 48 from kallithea.lib import diffs
50 49 from kallithea.model.db import ChangesetComment, ChangesetStatus
51 50 from kallithea.model.comment import ChangesetCommentsModel
52 51 from kallithea.model.changeset_status import ChangesetStatusModel
53 52 from kallithea.model.meta import Session
54 53 from kallithea.model.repo import RepoModel
55 54 from kallithea.lib.diffs import LimitedDiffContainer
56 55 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
57 56 from kallithea.lib.vcs.backends.base import EmptyChangeset
58 57 from kallithea.lib.utils2 import safe_unicode
59 58 from kallithea.lib.graphmod import graph_data
60 59
61 60 log = logging.getLogger(__name__)
62 61
63 62
64 63 def _update_with_GET(params, GET):
65 64 for k in ['diff1', 'diff2', 'diff']:
66 65 params[k] += GET.getall(k)
67 66
68 67
69 68 def anchor_url(revision, path, GET):
70 69 fid = h.FID(revision, path)
71 70 return h.url.current(anchor=fid, **dict(GET))
72 71
73 72
74 73 def get_ignore_ws(fid, GET):
75 74 ig_ws_global = GET.get('ignorews')
76 75 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
77 76 if ig_ws:
78 77 try:
79 78 return int(ig_ws[0].split(':')[-1])
80 79 except ValueError:
81 80 raise HTTPBadRequest()
82 81 return ig_ws_global
83 82
84 83
85 84 def _ignorews_url(GET, fileid=None):
86 85 fileid = str(fileid) if fileid else None
87 86 params = defaultdict(list)
88 87 _update_with_GET(params, GET)
89 88 lbl = _('Show whitespace')
90 89 ig_ws = get_ignore_ws(fileid, GET)
91 90 ln_ctx = get_line_ctx(fileid, GET)
92 91 # global option
93 92 if fileid is None:
94 93 if ig_ws is None:
95 94 params['ignorews'] += [1]
96 95 lbl = _('Ignore whitespace')
97 96 ctx_key = 'context'
98 97 ctx_val = ln_ctx
99 98 # per file options
100 99 else:
101 100 if ig_ws is None:
102 101 params[fileid] += ['WS:1']
103 102 lbl = _('Ignore whitespace')
104 103
105 104 ctx_key = fileid
106 105 ctx_val = 'C:%s' % ln_ctx
107 106 # if we have passed in ln_ctx pass it along to our params
108 107 if ln_ctx:
109 108 params[ctx_key] += [ctx_val]
110 109
111 110 params['anchor'] = fileid
112 111 icon = h.literal('<i class="icon-strike"></i>')
113 112 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
114 113
115 114
116 115 def get_line_ctx(fid, GET):
117 116 ln_ctx_global = GET.get('context')
118 117 if fid:
119 118 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
120 119 else:
121 120 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
122 121 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
123 122 if ln_ctx:
124 123 ln_ctx = [ln_ctx]
125 124
126 125 if ln_ctx:
127 126 retval = ln_ctx[0].split(':')[-1]
128 127 else:
129 128 retval = ln_ctx_global
130 129
131 130 try:
132 131 return int(retval)
133 132 except Exception:
134 133 return 3
135 134
136 135
137 136 def _context_url(GET, fileid=None):
138 137 """
139 138 Generates url for context lines
140 139
141 140 :param fileid:
142 141 """
143 142
144 143 fileid = str(fileid) if fileid else None
145 144 ig_ws = get_ignore_ws(fileid, GET)
146 145 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
147 146
148 147 params = defaultdict(list)
149 148 _update_with_GET(params, GET)
150 149
151 150 # global option
152 151 if fileid is None:
153 152 if ln_ctx > 0:
154 153 params['context'] += [ln_ctx]
155 154
156 155 if ig_ws:
157 156 ig_ws_key = 'ignorews'
158 157 ig_ws_val = 1
159 158
160 159 # per file option
161 160 else:
162 161 params[fileid] += ['C:%s' % ln_ctx]
163 162 ig_ws_key = fileid
164 163 ig_ws_val = 'WS:%s' % 1
165 164
166 165 if ig_ws:
167 166 params[ig_ws_key] += [ig_ws_val]
168 167
169 168 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
170 169
171 170 params['anchor'] = fileid
172 171 icon = h.literal('<i class="icon-sort"></i>')
173 172 return h.link_to(icon, h.url.current(**params), title=lbl, class_='tooltip')
174 173
175 174
176 175 class ChangesetController(BaseRepoController):
177 176
178 177 def __before__(self):
179 178 super(ChangesetController, self).__before__()
180 179 c.affected_files_cut_off = 60
181 180
182 181 def __load_data(self):
183 182 repo_model = RepoModel()
184 183 c.users_array = repo_model.get_users_js()
185 184 c.user_groups_array = repo_model.get_user_groups_js()
186 185
187 186 def _index(self, revision, method):
188 187 c.anchor_url = anchor_url
189 188 c.ignorews_url = _ignorews_url
190 189 c.context_url = _context_url
191 190 c.fulldiff = fulldiff = request.GET.get('fulldiff')
192 191 #get ranges of revisions if preset
193 192 rev_range = revision.split('...')[:2]
194 193 enable_comments = True
195 194 c.cs_repo = c.db_repo
196 195 try:
197 196 if len(rev_range) == 2:
198 197 enable_comments = False
199 198 rev_start = rev_range[0]
200 199 rev_end = rev_range[1]
201 200 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
202 201 end=rev_end)
203 202 else:
204 203 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
205 204
206 205 c.cs_ranges = list(rev_ranges)
207 206 if not c.cs_ranges:
208 207 raise RepositoryError('Changeset range returned empty result')
209 208
210 209 except(ChangesetDoesNotExistError,), e:
211 210 log.debug(traceback.format_exc())
212 211 msg = _('Such revision does not exist for this repository')
213 212 h.flash(msg, category='error')
214 213 raise HTTPNotFound()
215 214
216 215 c.changes = OrderedDict()
217 216
218 217 c.lines_added = 0 # count of lines added
219 218 c.lines_deleted = 0 # count of lines removes
220 219
221 220 c.changeset_statuses = ChangesetStatus.STATUSES
222 221 comments = dict()
223 222 c.statuses = []
224 223 c.inline_comments = []
225 224 c.inline_cnt = 0
226 225
227 226 # Iterate over ranges (default changeset view is always one changeset)
228 227 for changeset in c.cs_ranges:
229 228 inlines = []
230 229 if method == 'show':
231 230 c.statuses.extend([ChangesetStatusModel().get_status(
232 231 c.db_repo.repo_id, changeset.raw_id)])
233 232
234 233 # Changeset comments
235 234 comments.update((com.comment_id, com)
236 235 for com in ChangesetCommentsModel()
237 236 .get_comments(c.db_repo.repo_id,
238 237 revision=changeset.raw_id))
239 238
240 239 # Status change comments - mostly from pull requests
241 240 comments.update((st.changeset_comment_id, st.comment)
242 241 for st in ChangesetStatusModel()
243 242 .get_statuses(c.db_repo.repo_id,
244 243 changeset.raw_id, with_revisions=True)
245 244 if st.changeset_comment_id is not None)
246 245
247 246 inlines = ChangesetCommentsModel()\
248 247 .get_inline_comments(c.db_repo.repo_id,
249 248 revision=changeset.raw_id)
250 249 c.inline_comments.extend(inlines)
251 250
252 251 c.changes[changeset.raw_id] = []
253 252
254 253 cs2 = changeset.raw_id
255 254 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
256 255 context_lcl = get_line_ctx('', request.GET)
257 256 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
258 257
259 258 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
260 259 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
261 260 diff_limit = self.cut_off_limit if not fulldiff else None
262 261 diff_processor = diffs.DiffProcessor(_diff,
263 262 vcs=c.db_repo_scm_instance.alias,
264 263 format='gitdiff',
265 264 diff_limit=diff_limit)
266 265 cs_changes = OrderedDict()
267 266 if method == 'show':
268 267 _parsed = diff_processor.prepare()
269 268 c.limited_diff = False
270 269 if isinstance(_parsed, LimitedDiffContainer):
271 270 c.limited_diff = True
272 271 for f in _parsed:
273 272 st = f['stats']
274 273 c.lines_added += st['added']
275 274 c.lines_deleted += st['deleted']
276 275 fid = h.FID(changeset.raw_id, f['filename'])
277 276 diff = diff_processor.as_html(enable_comments=enable_comments,
278 277 parsed_lines=[f])
279 278 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
280 279 diff, st]
281 280 else:
282 281 # downloads/raw we only need RAW diff nothing else
283 282 diff = diff_processor.as_raw()
284 283 cs_changes[''] = [None, None, None, None, diff, None]
285 284 c.changes[changeset.raw_id] = cs_changes
286 285
287 286 #sort comments in creation order
288 287 c.comments = [com for com_id, com in sorted(comments.items())]
289 288
290 289 # count inline comments
291 290 for __, lines in c.inline_comments:
292 291 for comments in lines.values():
293 292 c.inline_cnt += len(comments)
294 293
295 294 if len(c.cs_ranges) == 1:
296 295 c.changeset = c.cs_ranges[0]
297 296 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
298 297 for x in c.changeset.parents])
299 298 if method == 'download':
300 299 response.content_type = 'text/plain'
301 300 response.content_disposition = 'attachment; filename=%s.diff' \
302 301 % revision[:12]
303 302 return diff
304 303 elif method == 'patch':
305 304 response.content_type = 'text/plain'
306 305 c.diff = safe_unicode(diff)
307 306 return render('changeset/patch_changeset.html')
308 307 elif method == 'raw':
309 308 response.content_type = 'text/plain'
310 309 return diff
311 310 elif method == 'show':
312 311 self.__load_data()
313 312 if len(c.cs_ranges) == 1:
314 313 return render('changeset/changeset.html')
315 314 else:
316 315 c.cs_ranges_org = None
317 316 c.cs_comments = {}
318 317 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
319 318 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
320 319 return render('changeset/changeset_range.html')
321 320
322 321 @LoginRequired()
323 322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
324 323 'repository.admin')
325 324 def index(self, revision, method='show'):
326 325 return self._index(revision, method=method)
327 326
328 327 @LoginRequired()
329 328 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 329 'repository.admin')
331 330 def changeset_raw(self, revision):
332 331 return self._index(revision, method='raw')
333 332
334 333 @LoginRequired()
335 334 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
336 335 'repository.admin')
337 336 def changeset_patch(self, revision):
338 337 return self._index(revision, method='patch')
339 338
340 339 @LoginRequired()
341 340 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
342 341 'repository.admin')
343 342 def changeset_download(self, revision):
344 343 return self._index(revision, method='download')
345 344
346 345 @LoginRequired()
347 346 @NotAnonymous()
348 347 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
349 348 'repository.admin')
350 349 @jsonify
351 350 def comment(self, repo_name, revision):
352 351 status = request.POST.get('changeset_status')
353 352 text = request.POST.get('text', '').strip()
354 353
355 354 c.comment = comment = ChangesetCommentsModel().create(
356 355 text=text,
357 356 repo=c.db_repo.repo_id,
358 357 user=c.authuser.user_id,
359 358 revision=revision,
360 359 f_path=request.POST.get('f_path'),
361 360 line_no=request.POST.get('line'),
362 361 status_change=(ChangesetStatus.get_status_lbl(status)
363 362 if status else None)
364 363 )
365 364
366 365 # get status if set !
367 366 if status:
368 367 # if latest status was from pull request and it's closed
369 368 # disallow changing status !
370 369 # dont_allow_on_closed_pull_request = True !
371 370
372 371 try:
373 372 ChangesetStatusModel().set_status(
374 373 c.db_repo.repo_id,
375 374 status,
376 375 c.authuser.user_id,
377 376 comment,
378 377 revision=revision,
379 378 dont_allow_on_closed_pull_request=True
380 379 )
381 380 except StatusChangeOnClosedPullRequestError:
382 381 log.debug(traceback.format_exc())
383 382 msg = _('Changing status on a changeset associated with '
384 383 'a closed pull request is not allowed')
385 384 h.flash(msg, category='warning')
386 return redirect(h.url('changeset_home', repo_name=repo_name,
385 raise HTTPFound(location=h.url('changeset_home', repo_name=repo_name,
387 386 revision=revision))
388 387 action_logger(self.authuser,
389 388 'user_commented_revision:%s' % revision,
390 389 c.db_repo, self.ip_addr, self.sa)
391 390
392 391 Session().commit()
393 392
394 393 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
395 return redirect(h.url('changeset_home', repo_name=repo_name,
394 raise HTTPFound(location=h.url('changeset_home', repo_name=repo_name,
396 395 revision=revision))
397 396 #only ajax below
398 397 data = {
399 398 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
400 399 }
401 400 if comment is not None:
402 401 data.update(comment.get_dict())
403 402 data.update({'rendered_text':
404 403 render('changeset/changeset_comment_block.html')})
405 404
406 405 return data
407 406
408 407 @LoginRequired()
409 408 @NotAnonymous()
410 409 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
411 410 'repository.admin')
412 411 def preview_comment(self):
413 412 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
414 413 raise HTTPBadRequest()
415 414 text = request.POST.get('text')
416 415 if text:
417 416 return h.rst_w_mentions(text)
418 417 return ''
419 418
420 419 @LoginRequired()
421 420 @NotAnonymous()
422 421 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
423 422 'repository.admin')
424 423 @jsonify
425 424 def delete_comment(self, repo_name, comment_id):
426 425 co = ChangesetComment.get(comment_id)
427 426 if not co:
428 427 raise HTTPBadRequest()
429 428 owner = co.author.user_id == c.authuser.user_id
430 429 repo_admin = h.HasRepoPermissionAny('repository.admin')
431 430 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
432 431 ChangesetCommentsModel().delete(comment=co)
433 432 Session().commit()
434 433 return True
435 434 else:
436 435 raise HTTPForbidden()
437 436
438 437 @LoginRequired()
439 438 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
440 439 'repository.admin')
441 440 @jsonify
442 441 def changeset_info(self, repo_name, revision):
443 442 if request.is_xhr:
444 443 try:
445 444 return c.db_repo_scm_instance.get_changeset(revision)
446 445 except ChangesetDoesNotExistError as e:
447 446 return EmptyChangeset(message=str(e))
448 447 else:
449 448 raise HTTPBadRequest()
450 449
451 450 @LoginRequired()
452 451 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
453 452 'repository.admin')
454 453 @jsonify
455 454 def changeset_children(self, repo_name, revision):
456 455 if request.is_xhr:
457 456 changeset = c.db_repo_scm_instance.get_changeset(revision)
458 457 result = {"results": []}
459 458 if changeset.children:
460 459 result = {"results": changeset.children}
461 460 return result
462 461 else:
463 462 raise HTTPBadRequest()
464 463
465 464 @LoginRequired()
466 465 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 466 'repository.admin')
468 467 @jsonify
469 468 def changeset_parents(self, repo_name, revision):
470 469 if request.is_xhr:
471 470 changeset = c.db_repo_scm_instance.get_changeset(revision)
472 471 result = {"results": []}
473 472 if changeset.parents:
474 473 result = {"results": changeset.parents}
475 474 return result
476 475 else:
477 476 raise HTTPBadRequest()
@@ -1,296 +1,295 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.compare
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 compare controller for pylons showing differences between two
19 19 repos, branches, bookmarks or tips
20 20
21 21 This file was forked by the Kallithea project in July 2014.
22 22 Original author and date, and relevant copyright and licensing information is below:
23 23 :created_on: May 6, 2012
24 24 :author: marcink
25 25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 26 :license: GPLv3, see LICENSE.md for more details.
27 27 """
28 28
29 29
30 30 import logging
31 31 import re
32 32
33 from webob.exc import HTTPBadRequest
34 33 from pylons import request, tmpl_context as c, url
35 from pylons.controllers.util import redirect
36 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPBadRequest
37 36
38 37 from kallithea.lib.utils2 import safe_str
39 38 from kallithea.lib.vcs.utils.hgcompat import unionrepo
40 39 from kallithea.lib import helpers as h
41 40 from kallithea.lib.base import BaseRepoController, render
42 41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 42 from kallithea.lib import diffs
44 43 from kallithea.model.db import Repository
45 44 from kallithea.lib.diffs import LimitedDiffContainer
46 45 from kallithea.controllers.changeset import _ignorews_url, _context_url
47 46 from kallithea.lib.graphmod import graph_data
48 47 from kallithea.lib.compat import json
49 48
50 49 log = logging.getLogger(__name__)
51 50
52 51
53 52 class CompareController(BaseRepoController):
54 53
55 54 def __before__(self):
56 55 super(CompareController, self).__before__()
57 56
58 57 @staticmethod
59 58 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
60 59 """
61 60 Returns lists of changesets that can be merged from org_repo@org_rev
62 61 to other_repo@other_rev
63 62 ... and the other way
64 63 ... and the ancestor that would be used for merge
65 64
66 65 :param org_repo: repo object, that is most likely the original repo we forked from
67 66 :param org_rev: the revision we want our compare to be made
68 67 :param other_repo: repo object, most likely the fork of org_repo. It has
69 68 all changesets that we need to obtain
70 69 :param other_rev: revision we want out compare to be made on other_repo
71 70 """
72 71 ancestor = None
73 72 if org_rev == other_rev:
74 73 org_changesets = []
75 74 other_changesets = []
76 75 ancestor = org_rev
77 76
78 77 elif alias == 'hg':
79 78 #case two independent repos
80 79 if org_repo != other_repo:
81 80 hgrepo = unionrepo.unionrepository(other_repo.baseui,
82 81 other_repo.path,
83 82 org_repo.path)
84 83 # all ancestors of other_rev will be in other_repo and
85 84 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
86 85
87 86 #no remote compare do it on the same repository
88 87 else:
89 88 hgrepo = other_repo._repo
90 89
91 90 if org_repo.EMPTY_CHANGESET in (org_rev, other_rev):
92 91 # work around unexpected behaviour in Mercurial < 3.4
93 92 ancestor = org_repo.EMPTY_CHANGESET
94 93 else:
95 94 ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
96 95 if ancestors:
97 96 # FIXME: picks arbitrary ancestor - but there is usually only one
98 97 try:
99 98 ancestor = hgrepo[ancestors.first()].hex()
100 99 except AttributeError:
101 100 # removed in hg 3.2
102 101 ancestor = hgrepo[ancestors[0]].hex()
103 102
104 103 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
105 104 other_rev, org_rev, org_rev)
106 105 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
107 106 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
108 107 org_rev, other_rev, other_rev)
109 108
110 109 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
111 110
112 111 elif alias == 'git':
113 112 if org_repo != other_repo:
114 113 from dulwich.repo import Repo
115 114 from dulwich.client import SubprocessGitClient
116 115
117 116 gitrepo = Repo(org_repo.path)
118 117 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
119 118
120 119 gitrepo_remote = Repo(other_repo.path)
121 120 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
122 121
123 122 revs = []
124 123 for x in gitrepo_remote.get_walker(include=[other_rev],
125 124 exclude=[org_rev]):
126 125 revs.append(x.commit.id)
127 126
128 127 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
129 128 if other_changesets:
130 129 ancestor = other_changesets[0].parents[0].raw_id
131 130 else:
132 131 # no changesets from other repo, ancestor is the other_rev
133 132 ancestor = other_rev
134 133
135 134 else:
136 135 so, se = org_repo.run_git_command(
137 136 ['log', '--reverse', '--pretty=format:%H',
138 137 '-s', '%s..%s' % (org_rev, other_rev)]
139 138 )
140 139 other_changesets = [org_repo.get_changeset(cs)
141 140 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
142 141 so, se = org_repo.run_git_command(
143 142 ['merge-base', org_rev, other_rev]
144 143 )
145 144 ancestor = re.findall(r'[0-9a-fA-F]{40}', so)[0]
146 145 org_changesets = []
147 146
148 147 else:
149 148 raise Exception('Bad alias only git and hg is allowed')
150 149
151 150 return other_changesets, org_changesets, ancestor
152 151
153 152 @LoginRequired()
154 153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
155 154 'repository.admin')
156 155 def index(self, repo_name):
157 156 c.compare_home = True
158 157 org_repo = c.db_repo.repo_name
159 158 other_repo = request.GET.get('other_repo', org_repo)
160 159 c.a_repo = Repository.get_by_repo_name(org_repo)
161 160 c.cs_repo = Repository.get_by_repo_name(other_repo)
162 161 c.a_ref_name = c.cs_ref_name = _('Select changeset')
163 162 return render('compare/compare_diff.html')
164 163
165 164 @LoginRequired()
166 165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
167 166 'repository.admin')
168 167 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
169 168 org_ref_name = org_ref_name.strip()
170 169 other_ref_name = other_ref_name.strip()
171 170
172 171 org_repo = c.db_repo.repo_name
173 172 other_repo = request.GET.get('other_repo', org_repo)
174 173 # If merge is True:
175 174 # Show what org would get if merged with other:
176 175 # List changesets that are ancestors of other but not of org.
177 176 # New changesets in org is thus ignored.
178 177 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
179 178 # If merge is False:
180 179 # Make a raw diff from org to other, no matter if related or not.
181 180 # Changesets in one and not in the other will be ignored
182 181 merge = bool(request.GET.get('merge'))
183 182 # fulldiff disables cut_off_limit
184 183 c.fulldiff = request.GET.get('fulldiff')
185 184 # partial uses compare_cs.html template directly
186 185 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
187 186 # as_form puts hidden input field with changeset revisions
188 187 c.as_form = partial and request.GET.get('as_form')
189 188 # swap url for compare_diff page - never partial and never as_form
190 189 c.swap_url = h.url('compare_url',
191 190 repo_name=other_repo,
192 191 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
193 192 other_repo=org_repo,
194 193 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
195 194 merge=merge or '')
196 195
197 196 # set callbacks for generating markup for icons
198 197 c.ignorews_url = _ignorews_url
199 198 c.context_url = _context_url
200 199 ignore_whitespace = request.GET.get('ignorews') == '1'
201 200 line_context = request.GET.get('context', 3)
202 201
203 202 org_repo = Repository.get_by_repo_name(org_repo)
204 203 other_repo = Repository.get_by_repo_name(other_repo)
205 204
206 205 if org_repo is None:
207 206 msg = 'Could not find org repo %s' % org_repo
208 207 log.error(msg)
209 208 h.flash(msg, category='error')
210 return redirect(url('compare_home', repo_name=c.repo_name))
209 raise HTTPFound(location=url('compare_home', repo_name=c.repo_name))
211 210
212 211 if other_repo is None:
213 212 msg = 'Could not find other repo %s' % other_repo
214 213 log.error(msg)
215 214 h.flash(msg, category='error')
216 return redirect(url('compare_home', repo_name=c.repo_name))
215 raise HTTPFound(location=url('compare_home', repo_name=c.repo_name))
217 216
218 217 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
219 218 msg = 'compare of two different kind of remote repos not available'
220 219 log.error(msg)
221 220 h.flash(msg, category='error')
222 return redirect(url('compare_home', repo_name=c.repo_name))
221 raise HTTPFound(location=url('compare_home', repo_name=c.repo_name))
223 222
224 223 c.a_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name,
225 224 returnempty=True)
226 225 c.cs_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
227 226
228 227 c.compare_home = False
229 228 c.a_repo = org_repo
230 229 c.a_ref_name = org_ref_name
231 230 c.a_ref_type = org_ref_type
232 231 c.cs_repo = other_repo
233 232 c.cs_ref_name = other_ref_name
234 233 c.cs_ref_type = other_ref_type
235 234
236 235 c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
237 236 org_repo.scm_instance.alias, org_repo.scm_instance, c.a_rev,
238 237 other_repo.scm_instance, c.cs_rev)
239 238 raw_ids = [x.raw_id for x in c.cs_ranges]
240 239 c.cs_comments = other_repo.get_comments(raw_ids)
241 240 c.statuses = other_repo.statuses(raw_ids)
242 241
243 242 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
244 243 c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
245 244
246 245 if partial:
247 246 return render('compare/compare_cs.html')
248 247 if merge and c.ancestor:
249 248 # case we want a simple diff without incoming changesets,
250 249 # previewing what will be merged.
251 250 # Make the diff on the other repo (which is known to have other_rev)
252 251 log.debug('Using ancestor %s as rev1 instead of %s',
253 252 c.ancestor, c.a_rev)
254 253 rev1 = c.ancestor
255 254 org_repo = other_repo
256 255 else: # comparing tips, not necessarily linearly related
257 256 if merge:
258 257 log.error('Unable to find ancestor revision')
259 258 if org_repo != other_repo:
260 259 # TODO: we could do this by using hg unionrepo
261 260 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
262 261 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
263 262 raise HTTPBadRequest
264 263 rev1 = c.a_rev
265 264
266 265 diff_limit = self.cut_off_limit if not c.fulldiff else None
267 266
268 267 log.debug('running diff between %s and %s in %s',
269 268 rev1, c.cs_rev, org_repo.scm_instance.path)
270 269 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
271 270 ignore_whitespace=ignore_whitespace,
272 271 context=line_context)
273 272
274 273 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
275 274 diff_limit=diff_limit)
276 275 _parsed = diff_processor.prepare()
277 276
278 277 c.limited_diff = False
279 278 if isinstance(_parsed, LimitedDiffContainer):
280 279 c.limited_diff = True
281 280
282 281 c.files = []
283 282 c.changes = {}
284 283 c.lines_added = 0
285 284 c.lines_deleted = 0
286 285 for f in _parsed:
287 286 st = f['stats']
288 287 if not st['binary']:
289 288 c.lines_added += st['added']
290 289 c.lines_deleted += st['deleted']
291 290 fid = h.FID('', f['filename'])
292 291 c.files.append([fid, f['operation'], f['filename'], f['stats']])
293 292 htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
294 293 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
295 294
296 295 return render('compare/compare_diff.html')
@@ -1,800 +1,800 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.files
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Files controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 21, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import logging
30 30 import traceback
31 31 import tempfile
32 32 import shutil
33 33
34 34 from pylons import request, response, tmpl_context as c, url
35 35 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
36 from webob.exc import HTTPFound
37
37 38 from kallithea.lib.utils import jsonify, action_logger
38
39 39 from kallithea.lib import diffs
40 40 from kallithea.lib import helpers as h
41 41
42 42 from kallithea.lib.compat import OrderedDict
43 43 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
44 44 str2bool
45 45 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
46 46 from kallithea.lib.base import BaseRepoController, render
47 47 from kallithea.lib.vcs.backends.base import EmptyChangeset
48 48 from kallithea.lib.vcs.conf import settings
49 49 from kallithea.lib.vcs.exceptions import RepositoryError, \
50 50 ChangesetDoesNotExistError, EmptyRepositoryError, \
51 51 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
52 52 NodeDoesNotExistError, ChangesetError, NodeError
53 53 from kallithea.lib.vcs.nodes import FileNode
54 54
55 55 from kallithea.model.repo import RepoModel
56 56 from kallithea.model.scm import ScmModel
57 57 from kallithea.model.db import Repository
58 58
59 59 from kallithea.controllers.changeset import anchor_url, _ignorews_url,\
60 60 _context_url, get_line_ctx, get_ignore_ws
61 61 from webob.exc import HTTPNotFound
62 62 from kallithea.lib.exceptions import NonRelativePathError
63 63
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class FilesController(BaseRepoController):
69 69
70 70 def __before__(self):
71 71 super(FilesController, self).__before__()
72 72 c.cut_off_limit = self.cut_off_limit
73 73
74 74 def __get_cs(self, rev, silent_empty=False):
75 75 """
76 76 Safe way to get changeset if error occur it redirects to tip with
77 77 proper message
78 78
79 79 :param rev: revision to fetch
80 80 :silent_empty: return None if repository is empty
81 81 """
82 82
83 83 try:
84 84 return c.db_repo_scm_instance.get_changeset(rev)
85 85 except EmptyRepositoryError as e:
86 86 if silent_empty:
87 87 return None
88 88 url_ = url('files_add_home',
89 89 repo_name=c.repo_name,
90 90 revision=0, f_path='', anchor='edit')
91 91 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
92 92 h.flash(h.literal(_('There are no files yet. %s') % add_new),
93 93 category='warning')
94 94 raise HTTPNotFound()
95 95 except(ChangesetDoesNotExistError, LookupError), e:
96 96 msg = _('Such revision does not exist for this repository')
97 97 h.flash(msg, category='error')
98 98 raise HTTPNotFound()
99 99 except RepositoryError as e:
100 100 h.flash(safe_str(e), category='error')
101 101 raise HTTPNotFound()
102 102
103 103 def __get_filenode(self, cs, path):
104 104 """
105 105 Returns file_node or raise HTTP error.
106 106
107 107 :param cs: given changeset
108 108 :param path: path to lookup
109 109 """
110 110
111 111 try:
112 112 file_node = cs.get_node(path)
113 113 if file_node.is_dir():
114 114 raise RepositoryError('given path is a directory')
115 115 except(ChangesetDoesNotExistError,), e:
116 116 msg = _('Such revision does not exist for this repository')
117 117 h.flash(msg, category='error')
118 118 raise HTTPNotFound()
119 119 except RepositoryError as e:
120 120 h.flash(safe_str(e), category='error')
121 121 raise HTTPNotFound()
122 122
123 123 return file_node
124 124
125 125 @LoginRequired()
126 126 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
127 127 'repository.admin')
128 128 def index(self, repo_name, revision, f_path, annotate=False):
129 129 # redirect to given revision from form if given
130 130 post_revision = request.POST.get('at_rev', None)
131 131 if post_revision:
132 132 cs = self.__get_cs(post_revision) # FIXME - unused!
133 133
134 134 c.revision = revision
135 135 c.changeset = self.__get_cs(revision)
136 136 c.branch = request.GET.get('branch', None)
137 137 c.f_path = f_path
138 138 c.annotate = annotate
139 139 cur_rev = c.changeset.revision
140 140
141 141 # prev link
142 142 try:
143 143 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
144 144 c.url_prev = url('files_home', repo_name=c.repo_name,
145 145 revision=prev_rev.raw_id, f_path=f_path)
146 146 if c.branch:
147 147 c.url_prev += '?branch=%s' % c.branch
148 148 except (ChangesetDoesNotExistError, VCSError):
149 149 c.url_prev = '#'
150 150
151 151 # next link
152 152 try:
153 153 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
154 154 c.url_next = url('files_home', repo_name=c.repo_name,
155 155 revision=next_rev.raw_id, f_path=f_path)
156 156 if c.branch:
157 157 c.url_next += '?branch=%s' % c.branch
158 158 except (ChangesetDoesNotExistError, VCSError):
159 159 c.url_next = '#'
160 160
161 161 # files or dirs
162 162 try:
163 163 c.file = c.changeset.get_node(f_path)
164 164
165 165 if c.file.is_file():
166 166 c.load_full_history = False
167 167 file_last_cs = c.file.last_changeset
168 168 c.file_changeset = (c.changeset
169 169 if c.changeset.revision < file_last_cs.revision
170 170 else file_last_cs)
171 171 #determine if we're on branch head
172 172 _branches = c.db_repo_scm_instance.branches
173 173 c.on_branch_head = revision in _branches.keys() + _branches.values()
174 174 _hist = []
175 175 c.file_history = []
176 176 if c.load_full_history:
177 177 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
178 178
179 179 c.authors = []
180 180 for a in set([x.author for x in _hist]):
181 181 c.authors.append((h.email(a), h.person(a)))
182 182 else:
183 183 c.authors = c.file_history = []
184 184 except RepositoryError as e:
185 185 h.flash(safe_str(e), category='error')
186 186 raise HTTPNotFound()
187 187
188 188 if request.environ.get('HTTP_X_PARTIAL_XHR'):
189 189 return render('files/files_ypjax.html')
190 190
191 191 # TODO: tags and bookmarks?
192 192 c.revision_options = [(c.changeset.raw_id,
193 193 _('%s at %s') % (c.changeset.branch, h.short_id(c.changeset.raw_id)))] + \
194 194 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
195 195 if c.db_repo_scm_instance.closed_branches:
196 196 prefix = _('(closed)') + ' '
197 197 c.revision_options += [('-', '-')] + \
198 198 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
199 199
200 200 return render('files/files.html')
201 201
202 202 @LoginRequired()
203 203 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
204 204 'repository.admin')
205 205 @jsonify
206 206 def history(self, repo_name, revision, f_path):
207 207 changeset = self.__get_cs(revision)
208 208 f_path = f_path
209 209 _file = changeset.get_node(f_path)
210 210 if _file.is_file():
211 211 file_history, _hist = self._get_node_history(changeset, f_path)
212 212
213 213 res = []
214 214 for obj in file_history:
215 215 res.append({
216 216 'text': obj[1],
217 217 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
218 218 })
219 219
220 220 data = {
221 221 'more': False,
222 222 'results': res
223 223 }
224 224 return data
225 225
226 226 @LoginRequired()
227 227 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
228 228 'repository.admin')
229 229 def authors(self, repo_name, revision, f_path):
230 230 changeset = self.__get_cs(revision)
231 231 f_path = f_path
232 232 _file = changeset.get_node(f_path)
233 233 if _file.is_file():
234 234 file_history, _hist = self._get_node_history(changeset, f_path)
235 235 c.authors = []
236 236 for a in set([x.author for x in _hist]):
237 237 c.authors.append((h.email(a), h.person(a)))
238 238 return render('files/files_history_box.html')
239 239
240 240 @LoginRequired()
241 241 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
242 242 'repository.admin')
243 243 def rawfile(self, repo_name, revision, f_path):
244 244 cs = self.__get_cs(revision)
245 245 file_node = self.__get_filenode(cs, f_path)
246 246
247 247 response.content_disposition = 'attachment; filename=%s' % \
248 248 safe_str(f_path.split(Repository.url_sep())[-1])
249 249
250 250 response.content_type = file_node.mimetype
251 251 return file_node.content
252 252
253 253 @LoginRequired()
254 254 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
255 255 'repository.admin')
256 256 def raw(self, repo_name, revision, f_path):
257 257 cs = self.__get_cs(revision)
258 258 file_node = self.__get_filenode(cs, f_path)
259 259
260 260 raw_mimetype_mapping = {
261 261 # map original mimetype to a mimetype used for "show as raw"
262 262 # you can also provide a content-disposition to override the
263 263 # default "attachment" disposition.
264 264 # orig_type: (new_type, new_dispo)
265 265
266 266 # show images inline:
267 267 'image/x-icon': ('image/x-icon', 'inline'),
268 268 'image/png': ('image/png', 'inline'),
269 269 'image/gif': ('image/gif', 'inline'),
270 270 'image/jpeg': ('image/jpeg', 'inline'),
271 271 'image/svg+xml': ('image/svg+xml', 'inline'),
272 272 }
273 273
274 274 mimetype = file_node.mimetype
275 275 try:
276 276 mimetype, dispo = raw_mimetype_mapping[mimetype]
277 277 except KeyError:
278 278 # we don't know anything special about this, handle it safely
279 279 if file_node.is_binary:
280 280 # do same as download raw for binary files
281 281 mimetype, dispo = 'application/octet-stream', 'attachment'
282 282 else:
283 283 # do not just use the original mimetype, but force text/plain,
284 284 # otherwise it would serve text/html and that might be unsafe.
285 285 # Note: underlying vcs library fakes text/plain mimetype if the
286 286 # mimetype can not be determined and it thinks it is not
287 287 # binary.This might lead to erroneous text display in some
288 288 # cases, but helps in other cases, like with text files
289 289 # without extension.
290 290 mimetype, dispo = 'text/plain', 'inline'
291 291
292 292 if dispo == 'attachment':
293 293 dispo = 'attachment; filename=%s' % \
294 294 safe_str(f_path.split(os.sep)[-1])
295 295
296 296 response.content_disposition = dispo
297 297 response.content_type = mimetype
298 298 return file_node.content
299 299
300 300 @LoginRequired()
301 301 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
302 302 def delete(self, repo_name, revision, f_path):
303 303 repo = c.db_repo
304 304 if repo.enable_locking and repo.locked[0]:
305 305 h.flash(_('This repository has been locked by %s on %s')
306 306 % (h.person_by_id(repo.locked[0]),
307 307 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
308 308 'warning')
309 return redirect(h.url('files_home',
309 raise HTTPFound(location=h.url('files_home',
310 310 repo_name=repo_name, revision='tip'))
311 311
312 312 # check if revision is a branch identifier- basically we cannot
313 313 # create multiple heads via file editing
314 314 _branches = repo.scm_instance.branches
315 315 # check if revision is a branch name or branch hash
316 316 if revision not in _branches.keys() + _branches.values():
317 317 h.flash(_('You can only delete files with revision '
318 318 'being a valid branch '), category='warning')
319 return redirect(h.url('files_home',
319 raise HTTPFound(location=h.url('files_home',
320 320 repo_name=repo_name, revision='tip',
321 321 f_path=f_path))
322 322
323 323 r_post = request.POST
324 324
325 325 c.cs = self.__get_cs(revision)
326 326 c.file = self.__get_filenode(c.cs, f_path)
327 327
328 328 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
329 329 c.f_path = f_path
330 330 node_path = f_path
331 331 author = self.authuser.full_contact
332 332
333 333 if r_post:
334 334 message = r_post.get('message') or c.default_message
335 335
336 336 try:
337 337 nodes = {
338 338 node_path: {
339 339 'content': ''
340 340 }
341 341 }
342 342 self.scm_model.delete_nodes(
343 343 user=c.authuser.user_id, repo=c.db_repo,
344 344 message=message,
345 345 nodes=nodes,
346 346 parent_cs=c.cs,
347 347 author=author,
348 348 )
349 349
350 350 h.flash(_('Successfully deleted file %s') % f_path,
351 351 category='success')
352 352 except Exception:
353 353 log.error(traceback.format_exc())
354 354 h.flash(_('Error occurred during commit'), category='error')
355 return redirect(url('changeset_home',
355 raise HTTPFound(location=url('changeset_home',
356 356 repo_name=c.repo_name, revision='tip'))
357 357
358 358 return render('files/files_delete.html')
359 359
360 360 @LoginRequired()
361 361 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
362 362 def edit(self, repo_name, revision, f_path):
363 363 repo = c.db_repo
364 364 if repo.enable_locking and repo.locked[0]:
365 365 h.flash(_('This repository has been locked by %s on %s')
366 366 % (h.person_by_id(repo.locked[0]),
367 367 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
368 368 'warning')
369 return redirect(h.url('files_home',
369 raise HTTPFound(location=h.url('files_home',
370 370 repo_name=repo_name, revision='tip'))
371 371
372 372 # check if revision is a branch identifier- basically we cannot
373 373 # create multiple heads via file editing
374 374 _branches = repo.scm_instance.branches
375 375 # check if revision is a branch name or branch hash
376 376 if revision not in _branches.keys() + _branches.values():
377 377 h.flash(_('You can only edit files with revision '
378 378 'being a valid branch '), category='warning')
379 return redirect(h.url('files_home',
379 raise HTTPFound(location=h.url('files_home',
380 380 repo_name=repo_name, revision='tip',
381 381 f_path=f_path))
382 382
383 383 r_post = request.POST
384 384
385 385 c.cs = self.__get_cs(revision)
386 386 c.file = self.__get_filenode(c.cs, f_path)
387 387
388 388 if c.file.is_binary:
389 return redirect(url('files_home', repo_name=c.repo_name,
389 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
390 390 revision=c.cs.raw_id, f_path=f_path))
391 391 c.default_message = _('Edited file %s via Kallithea') % (f_path)
392 392 c.f_path = f_path
393 393
394 394 if r_post:
395 395
396 396 old_content = c.file.content
397 397 sl = old_content.splitlines(1)
398 398 first_line = sl[0] if sl else ''
399 399 # modes: 0 - Unix, 1 - Mac, 2 - DOS
400 400 mode = detect_mode(first_line, 0)
401 401 content = convert_line_endings(r_post.get('content', ''), mode)
402 402
403 403 message = r_post.get('message') or c.default_message
404 404 author = self.authuser.full_contact
405 405
406 406 if content == old_content:
407 407 h.flash(_('No changes'), category='warning')
408 return redirect(url('changeset_home', repo_name=c.repo_name,
408 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
409 409 revision='tip'))
410 410 try:
411 411 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
412 412 repo_name=repo_name, cs=c.cs,
413 413 user=self.authuser.user_id,
414 414 author=author, message=message,
415 415 content=content, f_path=f_path)
416 416 h.flash(_('Successfully committed to %s') % f_path,
417 417 category='success')
418 418 except Exception:
419 419 log.error(traceback.format_exc())
420 420 h.flash(_('Error occurred during commit'), category='error')
421 return redirect(url('changeset_home',
421 raise HTTPFound(location=url('changeset_home',
422 422 repo_name=c.repo_name, revision='tip'))
423 423
424 424 return render('files/files_edit.html')
425 425
426 426 @LoginRequired()
427 427 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
428 428 def add(self, repo_name, revision, f_path):
429 429
430 430 repo = Repository.get_by_repo_name(repo_name)
431 431 if repo.enable_locking and repo.locked[0]:
432 432 h.flash(_('This repository has been locked by %s on %s')
433 433 % (h.person_by_id(repo.locked[0]),
434 434 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
435 435 'warning')
436 return redirect(h.url('files_home',
436 raise HTTPFound(location=h.url('files_home',
437 437 repo_name=repo_name, revision='tip'))
438 438
439 439 r_post = request.POST
440 440 c.cs = self.__get_cs(revision, silent_empty=True)
441 441 if c.cs is None:
442 442 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
443 443 c.default_message = (_('Added file via Kallithea'))
444 444 c.f_path = f_path
445 445
446 446 if r_post:
447 447 unix_mode = 0
448 448 content = convert_line_endings(r_post.get('content', ''), unix_mode)
449 449
450 450 message = r_post.get('message') or c.default_message
451 451 filename = r_post.get('filename')
452 452 location = r_post.get('location', '')
453 453 file_obj = r_post.get('upload_file', None)
454 454
455 455 if file_obj is not None and hasattr(file_obj, 'filename'):
456 456 filename = file_obj.filename
457 457 content = file_obj.file
458 458
459 459 if hasattr(content, 'file'):
460 460 # non posix systems store real file under file attr
461 461 content = content.file
462 462
463 463 if not content:
464 464 h.flash(_('No content'), category='warning')
465 return redirect(url('changeset_home', repo_name=c.repo_name,
465 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
466 466 revision='tip'))
467 467 if not filename:
468 468 h.flash(_('No filename'), category='warning')
469 return redirect(url('changeset_home', repo_name=c.repo_name,
469 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
470 470 revision='tip'))
471 471 #strip all crap out of file, just leave the basename
472 472 filename = os.path.basename(filename)
473 473 node_path = os.path.join(location, filename)
474 474 author = self.authuser.full_contact
475 475
476 476 try:
477 477 nodes = {
478 478 node_path: {
479 479 'content': content
480 480 }
481 481 }
482 482 self.scm_model.create_nodes(
483 483 user=c.authuser.user_id, repo=c.db_repo,
484 484 message=message,
485 485 nodes=nodes,
486 486 parent_cs=c.cs,
487 487 author=author,
488 488 )
489 489
490 490 h.flash(_('Successfully committed to %s') % node_path,
491 491 category='success')
492 492 except NonRelativePathError as e:
493 493 h.flash(_('Location must be relative path and must not '
494 494 'contain .. in path'), category='warning')
495 return redirect(url('changeset_home', repo_name=c.repo_name,
495 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
496 496 revision='tip'))
497 497 except (NodeError, NodeAlreadyExistsError) as e:
498 498 h.flash(_(e), category='error')
499 499 except Exception:
500 500 log.error(traceback.format_exc())
501 501 h.flash(_('Error occurred during commit'), category='error')
502 return redirect(url('changeset_home',
502 raise HTTPFound(location=url('changeset_home',
503 503 repo_name=c.repo_name, revision='tip'))
504 504
505 505 return render('files/files_add.html')
506 506
507 507 @LoginRequired()
508 508 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
509 509 'repository.admin')
510 510 def archivefile(self, repo_name, fname):
511 511 fileformat = None
512 512 revision = None
513 513 ext = None
514 514 subrepos = request.GET.get('subrepos') == 'true'
515 515
516 516 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
517 517 archive_spec = fname.split(ext_data[1])
518 518 if len(archive_spec) == 2 and archive_spec[1] == '':
519 519 fileformat = a_type or ext_data[1]
520 520 revision = archive_spec[0]
521 521 ext = ext_data[1]
522 522
523 523 try:
524 524 dbrepo = RepoModel().get_by_repo_name(repo_name)
525 525 if not dbrepo.enable_downloads:
526 526 return _('Downloads disabled') # TODO: do something else?
527 527
528 528 if c.db_repo_scm_instance.alias == 'hg':
529 529 # patch and reset hooks section of UI config to not run any
530 530 # hooks on fetching archives with subrepos
531 531 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
532 532 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
533 533
534 534 cs = c.db_repo_scm_instance.get_changeset(revision)
535 535 content_type = settings.ARCHIVE_SPECS[fileformat][0]
536 536 except ChangesetDoesNotExistError:
537 537 return _('Unknown revision %s') % revision
538 538 except EmptyRepositoryError:
539 539 return _('Empty repository')
540 540 except (ImproperArchiveTypeError, KeyError):
541 541 return _('Unknown archive type')
542 542
543 543 from kallithea import CONFIG
544 544 rev_name = cs.raw_id[:12]
545 545 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
546 546 safe_str(rev_name), ext)
547 547
548 548 archive_path = None
549 549 cached_archive_path = None
550 550 archive_cache_dir = CONFIG.get('archive_cache_dir')
551 551 if archive_cache_dir and not subrepos: # TOOD: subrepo caching?
552 552 if not os.path.isdir(archive_cache_dir):
553 553 os.makedirs(archive_cache_dir)
554 554 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
555 555 if os.path.isfile(cached_archive_path):
556 556 log.debug('Found cached archive in %s', cached_archive_path)
557 557 archive_path = cached_archive_path
558 558 else:
559 559 log.debug('Archive %s is not yet cached', archive_name)
560 560
561 561 if archive_path is None:
562 562 # generate new archive
563 563 fd, archive_path = tempfile.mkstemp()
564 564 log.debug('Creating new temp archive in %s', archive_path)
565 565 with os.fdopen(fd, 'wb') as stream:
566 566 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
567 567 # stream (and thus fd) has been closed by cs.fill_archive
568 568 if cached_archive_path is not None:
569 569 # we generated the archive - move it to cache
570 570 log.debug('Storing new archive in %s', cached_archive_path)
571 571 shutil.move(archive_path, cached_archive_path)
572 572 archive_path = cached_archive_path
573 573
574 574 def get_chunked_archive(archive_path):
575 575 stream = open(archive_path, 'rb')
576 576 while True:
577 577 data = stream.read(16 * 1024)
578 578 if not data:
579 579 break
580 580 yield data
581 581 stream.close()
582 582 if archive_path != cached_archive_path:
583 583 log.debug('Destroying temp archive %s', archive_path)
584 584 os.remove(archive_path)
585 585
586 586 action_logger(user=c.authuser,
587 587 action='user_downloaded_archive:%s' % (archive_name),
588 588 repo=repo_name, ipaddr=self.ip_addr, commit=True)
589 589
590 590 response.content_disposition = str('attachment; filename=%s' % (archive_name))
591 591 response.content_type = str(content_type)
592 592 return get_chunked_archive(archive_path)
593 593
594 594 @LoginRequired()
595 595 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
596 596 'repository.admin')
597 597 def diff(self, repo_name, f_path):
598 598 ignore_whitespace = request.GET.get('ignorews') == '1'
599 599 line_context = request.GET.get('context', 3)
600 600 diff2 = request.GET.get('diff2', '')
601 601 diff1 = request.GET.get('diff1', '') or diff2
602 602 c.action = request.GET.get('diff')
603 603 c.no_changes = diff1 == diff2
604 604 c.f_path = f_path
605 605 c.big_diff = False
606 606 c.anchor_url = anchor_url
607 607 c.ignorews_url = _ignorews_url
608 608 c.context_url = _context_url
609 609 c.changes = OrderedDict()
610 610 c.changes[diff2] = []
611 611
612 612 #special case if we want a show rev only, it's impl here
613 613 #to reduce JS and callbacks
614 614
615 615 if request.GET.get('show_rev'):
616 616 if str2bool(request.GET.get('annotate', 'False')):
617 617 _url = url('files_annotate_home', repo_name=c.repo_name,
618 618 revision=diff1, f_path=c.f_path)
619 619 else:
620 620 _url = url('files_home', repo_name=c.repo_name,
621 621 revision=diff1, f_path=c.f_path)
622 622
623 return redirect(_url)
623 raise HTTPFound(location=_url)
624 624 try:
625 625 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
626 626 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
627 627 try:
628 628 node1 = c.changeset_1.get_node(f_path)
629 629 if node1.is_dir():
630 630 raise NodeError('%s path is a %s not a file'
631 631 % (node1, type(node1)))
632 632 except NodeDoesNotExistError:
633 633 c.changeset_1 = EmptyChangeset(cs=diff1,
634 634 revision=c.changeset_1.revision,
635 635 repo=c.db_repo_scm_instance)
636 636 node1 = FileNode(f_path, '', changeset=c.changeset_1)
637 637 else:
638 638 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
639 639 node1 = FileNode(f_path, '', changeset=c.changeset_1)
640 640
641 641 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
642 642 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
643 643 try:
644 644 node2 = c.changeset_2.get_node(f_path)
645 645 if node2.is_dir():
646 646 raise NodeError('%s path is a %s not a file'
647 647 % (node2, type(node2)))
648 648 except NodeDoesNotExistError:
649 649 c.changeset_2 = EmptyChangeset(cs=diff2,
650 650 revision=c.changeset_2.revision,
651 651 repo=c.db_repo_scm_instance)
652 652 node2 = FileNode(f_path, '', changeset=c.changeset_2)
653 653 else:
654 654 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
655 655 node2 = FileNode(f_path, '', changeset=c.changeset_2)
656 656 except (RepositoryError, NodeError):
657 657 log.error(traceback.format_exc())
658 return redirect(url('files_home', repo_name=c.repo_name,
658 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
659 659 f_path=f_path))
660 660
661 661 if c.action == 'download':
662 662 _diff = diffs.get_gitdiff(node1, node2,
663 663 ignore_whitespace=ignore_whitespace,
664 664 context=line_context)
665 665 diff = diffs.DiffProcessor(_diff, format='gitdiff')
666 666
667 667 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
668 668 response.content_type = 'text/plain'
669 669 response.content_disposition = (
670 670 'attachment; filename=%s' % diff_name
671 671 )
672 672 return diff.as_raw()
673 673
674 674 elif c.action == 'raw':
675 675 _diff = diffs.get_gitdiff(node1, node2,
676 676 ignore_whitespace=ignore_whitespace,
677 677 context=line_context)
678 678 diff = diffs.DiffProcessor(_diff, format='gitdiff')
679 679 response.content_type = 'text/plain'
680 680 return diff.as_raw()
681 681
682 682 else:
683 683 fid = h.FID(diff2, node2.path)
684 684 line_context_lcl = get_line_ctx(fid, request.GET)
685 685 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
686 686
687 687 lim = request.GET.get('fulldiff') or self.cut_off_limit
688 688 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
689 689 filenode_new=node2,
690 690 cut_off_limit=lim,
691 691 ignore_whitespace=ign_whitespace_lcl,
692 692 line_context=line_context_lcl,
693 693 enable_comments=False)
694 694 op = ''
695 695 filename = node1.path
696 696 cs_changes = {
697 697 'fid': [cs1, cs2, op, filename, diff, st]
698 698 }
699 699 c.changes = cs_changes
700 700
701 701 return render('files/file_diff.html')
702 702
703 703 @LoginRequired()
704 704 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
705 705 'repository.admin')
706 706 def diff_2way(self, repo_name, f_path):
707 707 diff1 = request.GET.get('diff1', '')
708 708 diff2 = request.GET.get('diff2', '')
709 709 try:
710 710 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
711 711 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
712 712 try:
713 713 node1 = c.changeset_1.get_node(f_path)
714 714 if node1.is_dir():
715 715 raise NodeError('%s path is a %s not a file'
716 716 % (node1, type(node1)))
717 717 except NodeDoesNotExistError:
718 718 c.changeset_1 = EmptyChangeset(cs=diff1,
719 719 revision=c.changeset_1.revision,
720 720 repo=c.db_repo_scm_instance)
721 721 node1 = FileNode(f_path, '', changeset=c.changeset_1)
722 722 else:
723 723 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
724 724 node1 = FileNode(f_path, '', changeset=c.changeset_1)
725 725
726 726 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
727 727 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
728 728 try:
729 729 node2 = c.changeset_2.get_node(f_path)
730 730 if node2.is_dir():
731 731 raise NodeError('%s path is a %s not a file'
732 732 % (node2, type(node2)))
733 733 except NodeDoesNotExistError:
734 734 c.changeset_2 = EmptyChangeset(cs=diff2,
735 735 revision=c.changeset_2.revision,
736 736 repo=c.db_repo_scm_instance)
737 737 node2 = FileNode(f_path, '', changeset=c.changeset_2)
738 738 else:
739 739 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
740 740 node2 = FileNode(f_path, '', changeset=c.changeset_2)
741 741 except ChangesetDoesNotExistError as e:
742 742 msg = _('Such revision does not exist for this repository')
743 743 h.flash(msg, category='error')
744 744 raise HTTPNotFound()
745 745 c.node1 = node1
746 746 c.node2 = node2
747 747 c.cs1 = c.changeset_1
748 748 c.cs2 = c.changeset_2
749 749
750 750 return render('files/diff_2way.html')
751 751
752 752 def _get_node_history(self, cs, f_path, changesets=None):
753 753 """
754 754 get changesets history for given node
755 755
756 756 :param cs: changeset to calculate history
757 757 :param f_path: path for node to calculate history for
758 758 :param changesets: if passed don't calculate history and take
759 759 changesets defined in this list
760 760 """
761 761 # calculate history based on tip
762 762 tip_cs = c.db_repo_scm_instance.get_changeset()
763 763 if changesets is None:
764 764 try:
765 765 changesets = tip_cs.get_file_history(f_path)
766 766 except (NodeDoesNotExistError, ChangesetError):
767 767 #this node is not present at tip !
768 768 changesets = cs.get_file_history(f_path)
769 769 hist_l = []
770 770
771 771 changesets_group = ([], _("Changesets"))
772 772 branches_group = ([], _("Branches"))
773 773 tags_group = ([], _("Tags"))
774 774 for chs in changesets:
775 775 #_branch = '(%s)' % chs.branch if (cs.repository.alias == 'hg') else ''
776 776 _branch = chs.branch
777 777 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
778 778 changesets_group[0].append((chs.raw_id, n_desc,))
779 779 hist_l.append(changesets_group)
780 780
781 781 for name, chs in c.db_repo_scm_instance.branches.items():
782 782 branches_group[0].append((chs, name),)
783 783 hist_l.append(branches_group)
784 784
785 785 for name, chs in c.db_repo_scm_instance.tags.items():
786 786 tags_group[0].append((chs, name),)
787 787 hist_l.append(tags_group)
788 788
789 789 return hist_l, changesets
790 790
791 791 @LoginRequired()
792 792 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
793 793 'repository.admin')
794 794 @jsonify
795 795 def nodelist(self, repo_name, revision, f_path):
796 796 if request.environ.get('HTTP_X_PARTIAL_XHR'):
797 797 cs = self.__get_cs(revision)
798 798 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
799 799 flat=False)
800 800 return {'nodes': _d + _f}
@@ -1,191 +1,191 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.forks
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 forks controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 23, 2011
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import formencode
30 30 import traceback
31 31 from formencode import htmlfill
32 32
33 33 from pylons import tmpl_context as c, request, url
34 from pylons.controllers.util import redirect
35 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
36 36
37 37 import kallithea.lib.helpers as h
38 38
39 39 from kallithea.lib.helpers import Page
40 40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator, HasPermissionAny
42 42 from kallithea.lib.base import BaseRepoController, render
43 43 from kallithea.model.db import Repository, UserFollowing, User, Ui
44 44 from kallithea.model.repo import RepoModel
45 45 from kallithea.model.forms import RepoForkForm
46 46 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
47 47 from kallithea.lib.utils2 import safe_int
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ForksController(BaseRepoController):
53 53
54 54 def __before__(self):
55 55 super(ForksController, self).__before__()
56 56
57 57 def __load_defaults(self):
58 58 repo_group_perms = ['group.admin']
59 59 if HasPermissionAny('hg.create.write_on_repogroup.true')():
60 60 repo_group_perms.append('group.write')
61 61 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
62 62
63 63 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
64 64
65 65 c.can_update = Ui.get_by_key(Ui.HOOK_UPDATE).ui_active
66 66
67 67 def __load_data(self, repo_name=None):
68 68 """
69 69 Load defaults settings for edit, and update
70 70
71 71 :param repo_name:
72 72 """
73 73 self.__load_defaults()
74 74
75 75 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
76 76 repo = db_repo.scm_instance
77 77
78 78 if c.repo_info is None:
79 79 h.not_mapped_error(repo_name)
80 return redirect(url('repos'))
80 raise HTTPFound(location=url('repos'))
81 81
82 82 c.default_user_id = User.get_default_user().user_id
83 83 c.in_public_journal = UserFollowing.query()\
84 84 .filter(UserFollowing.user_id == c.default_user_id)\
85 85 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
86 86
87 87 if c.repo_info.stats:
88 88 last_rev = c.repo_info.stats.stat_on_revision+1
89 89 else:
90 90 last_rev = 0
91 91 c.stats_revision = last_rev
92 92
93 93 c.repo_last_rev = repo.count() if repo.revisions else 0
94 94
95 95 if last_rev == 0 or c.repo_last_rev == 0:
96 96 c.stats_percentage = 0
97 97 else:
98 98 c.stats_percentage = '%.2f' % ((float((last_rev)) /
99 99 c.repo_last_rev) * 100)
100 100
101 101 defaults = RepoModel()._get_defaults(repo_name)
102 102 # alter the description to indicate a fork
103 103 defaults['description'] = ('fork of repository: %s \n%s'
104 104 % (defaults['repo_name'],
105 105 defaults['description']))
106 106 # add suffix to fork
107 107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
108 108
109 109 return defaults
110 110
111 111 @LoginRequired()
112 112 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
113 113 'repository.admin')
114 114 def forks(self, repo_name):
115 115 p = safe_int(request.GET.get('page', 1), 1)
116 116 repo_id = c.db_repo.repo_id
117 117 d = []
118 118 for r in Repository.get_repo_forks(repo_id):
119 119 if not HasRepoPermissionAny(
120 120 'repository.read', 'repository.write', 'repository.admin'
121 121 )(r.repo_name, 'get forks check'):
122 122 continue
123 123 d.append(r)
124 124 c.forks_pager = Page(d, page=p, items_per_page=20)
125 125
126 126 if request.environ.get('HTTP_X_PARTIAL_XHR'):
127 127 return render('/forks/forks_data.html')
128 128
129 129 return render('/forks/forks.html')
130 130
131 131 @LoginRequired()
132 132 @NotAnonymous()
133 133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
134 134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 135 'repository.admin')
136 136 def fork(self, repo_name):
137 137 c.repo_info = Repository.get_by_repo_name(repo_name)
138 138 if not c.repo_info:
139 139 h.not_mapped_error(repo_name)
140 return redirect(url('home'))
140 raise HTTPFound(location=url('home'))
141 141
142 142 defaults = self.__load_data(repo_name)
143 143
144 144 return htmlfill.render(
145 145 render('forks/fork.html'),
146 146 defaults=defaults,
147 147 encoding="UTF-8",
148 148 force_defaults=False)
149 149
150 150 @LoginRequired()
151 151 @NotAnonymous()
152 152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
153 153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
154 154 'repository.admin')
155 155 def fork_create(self, repo_name):
156 156 self.__load_defaults()
157 157 c.repo_info = Repository.get_by_repo_name(repo_name)
158 158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
159 159 repo_groups=c.repo_groups,
160 160 landing_revs=c.landing_revs_choices)()
161 161 form_result = {}
162 162 task_id = None
163 163 try:
164 164 form_result = _form.to_python(dict(request.POST))
165 165
166 166 # an approximation that is better than nothing
167 167 if not Ui.get_by_key(Ui.HOOK_UPDATE).ui_active:
168 168 form_result['update_after_clone'] = False
169 169
170 170 # create fork is done sometimes async on celery, db transaction
171 171 # management is handled there.
172 172 task = RepoModel().create_fork(form_result, self.authuser.user_id)
173 173 from celery.result import BaseAsyncResult
174 174 if isinstance(task, BaseAsyncResult):
175 175 task_id = task.task_id
176 176 except formencode.Invalid as errors:
177 177 return htmlfill.render(
178 178 render('forks/fork.html'),
179 179 defaults=errors.value,
180 180 errors=errors.error_dict or {},
181 181 prefix_error=False,
182 182 encoding="UTF-8",
183 183 force_defaults=False)
184 184 except Exception:
185 185 log.error(traceback.format_exc())
186 186 h.flash(_('An error occurred during repository forking %s') %
187 187 repo_name, category='error')
188 188
189 return redirect(h.url('repo_creating_home',
189 raise HTTPFound(location=h.url('repo_creating_home',
190 190 repo_name=form_result['repo_name_full'],
191 191 task_id=task_id))
@@ -1,265 +1,264 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.login
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Login controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 22, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import logging
30 30 import re
31 31 import formencode
32 32
33 33 from formencode import htmlfill
34 from pylons.i18n.translation import _
35 from pylons import request, session, tmpl_context as c, url
34 36 from webob.exc import HTTPFound, HTTPBadRequest
35 from pylons.i18n.translation import _
36 from pylons.controllers.util import redirect
37 from pylons import request, session, tmpl_context as c, url
38 37
39 38 import kallithea.lib.helpers as h
40 39 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
41 40 from kallithea.lib.base import BaseController, log_in_user, render
42 41 from kallithea.lib.exceptions import UserCreationError
43 42 from kallithea.lib.utils2 import safe_str
44 43 from kallithea.model.db import User, Setting
45 44 from kallithea.model.forms import \
46 45 LoginForm, RegisterForm, PasswordResetRequestForm, PasswordResetConfirmationForm
47 46 from kallithea.model.user import UserModel
48 47 from kallithea.model.meta import Session
49 48
50 49
51 50 log = logging.getLogger(__name__)
52 51
53 52
54 53 class LoginController(BaseController):
55 54
56 55 def __before__(self):
57 56 super(LoginController, self).__before__()
58 57
59 58 def _validate_came_from(self, came_from,
60 59 _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")):
61 60 """Return True if came_from is valid and can and should be used.
62 61
63 62 Determines if a URI reference is valid and relative to the origin;
64 63 or in RFC 3986 terms, whether it matches this production:
65 64
66 65 origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ]
67 66
68 67 with the exception that '%' escapes are not validated and '#' is
69 68 allowed inside the fragment part.
70 69 """
71 70 return _re.match(came_from) is not None
72 71
73 72 def index(self):
74 73 c.came_from = safe_str(request.GET.get('came_from', ''))
75 74 if c.came_from:
76 75 if not self._validate_came_from(c.came_from):
77 76 log.error('Invalid came_from (not server-relative): %r', c.came_from)
78 77 raise HTTPBadRequest()
79 78 else:
80 79 c.came_from = url('home')
81 80
82 81 not_default = self.authuser.username != User.DEFAULT_USER
83 82 ip_allowed = AuthUser.check_ip_allowed(self.authuser, self.ip_addr)
84 83
85 84 # redirect if already logged in
86 85 if self.authuser.is_authenticated and not_default and ip_allowed:
87 86 raise HTTPFound(location=c.came_from)
88 87
89 88 if request.POST:
90 89 # import Login Form validator class
91 90 login_form = LoginForm()
92 91 try:
93 92 c.form_result = login_form.to_python(dict(request.POST))
94 93 # form checks for username/password, now we're authenticated
95 94 username = c.form_result['username']
96 95 user = User.get_by_username(username, case_insensitive=True)
97 96 except formencode.Invalid as errors:
98 97 defaults = errors.value
99 98 # remove password from filling in form again
100 99 del defaults['password']
101 100 return htmlfill.render(
102 101 render('/login.html'),
103 102 defaults=errors.value,
104 103 errors=errors.error_dict or {},
105 104 prefix_error=False,
106 105 encoding="UTF-8",
107 106 force_defaults=False)
108 107 except UserCreationError as e:
109 108 # container auth or other auth functions that create users on
110 109 # the fly can throw this exception signaling that there's issue
111 110 # with user creation, explanation should be provided in
112 111 # Exception itself
113 112 h.flash(e, 'error')
114 113 else:
115 114 log_in_user(user, c.form_result['remember'],
116 115 is_external_auth=False)
117 116 raise HTTPFound(location=c.came_from)
118 117
119 118 return render('/login.html')
120 119
121 120 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
122 121 'hg.register.manual_activate')
123 122 def register(self):
124 123 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
125 124 .AuthUser.permissions['global']
126 125
127 126 settings = Setting.get_app_settings()
128 127 captcha_private_key = settings.get('captcha_private_key')
129 128 c.captcha_active = bool(captcha_private_key)
130 129 c.captcha_public_key = settings.get('captcha_public_key')
131 130
132 131 if request.POST:
133 132 register_form = RegisterForm()()
134 133 try:
135 134 form_result = register_form.to_python(dict(request.POST))
136 135 form_result['active'] = c.auto_active
137 136
138 137 if c.captcha_active:
139 138 from kallithea.lib.recaptcha import submit
140 139 response = submit(request.POST.get('recaptcha_challenge_field'),
141 140 request.POST.get('recaptcha_response_field'),
142 141 private_key=captcha_private_key,
143 142 remoteip=self.ip_addr)
144 143 if c.captcha_active and not response.is_valid:
145 144 _value = form_result
146 145 _msg = _('Bad captcha')
147 146 error_dict = {'recaptcha_field': _msg}
148 147 raise formencode.Invalid(_msg, _value, None,
149 148 error_dict=error_dict)
150 149
151 150 UserModel().create_registration(form_result)
152 151 h.flash(_('You have successfully registered into Kallithea'),
153 152 category='success')
154 153 Session().commit()
155 return redirect(url('login_home'))
154 raise HTTPFound(location=url('login_home'))
156 155
157 156 except formencode.Invalid as errors:
158 157 return htmlfill.render(
159 158 render('/register.html'),
160 159 defaults=errors.value,
161 160 errors=errors.error_dict or {},
162 161 prefix_error=False,
163 162 encoding="UTF-8",
164 163 force_defaults=False)
165 164 except UserCreationError as e:
166 165 # container auth or other auth functions that create users on
167 166 # the fly can throw this exception signaling that there's issue
168 167 # with user creation, explanation should be provided in
169 168 # Exception itself
170 169 h.flash(e, 'error')
171 170
172 171 return render('/register.html')
173 172
174 173 def password_reset(self):
175 174 settings = Setting.get_app_settings()
176 175 captcha_private_key = settings.get('captcha_private_key')
177 176 c.captcha_active = bool(captcha_private_key)
178 177 c.captcha_public_key = settings.get('captcha_public_key')
179 178
180 179 if request.POST:
181 180 password_reset_form = PasswordResetRequestForm()()
182 181 try:
183 182 form_result = password_reset_form.to_python(dict(request.POST))
184 183 if c.captcha_active:
185 184 from kallithea.lib.recaptcha import submit
186 185 response = submit(request.POST.get('recaptcha_challenge_field'),
187 186 request.POST.get('recaptcha_response_field'),
188 187 private_key=captcha_private_key,
189 188 remoteip=self.ip_addr)
190 189 if c.captcha_active and not response.is_valid:
191 190 _value = form_result
192 191 _msg = _('Bad captcha')
193 192 error_dict = {'recaptcha_field': _msg}
194 193 raise formencode.Invalid(_msg, _value, None,
195 194 error_dict=error_dict)
196 195 redirect_link = UserModel().send_reset_password_email(form_result)
197 196 h.flash(_('A password reset confirmation code has been sent'),
198 197 category='success')
199 return redirect(redirect_link)
198 raise HTTPFound(location=redirect_link)
200 199
201 200 except formencode.Invalid as errors:
202 201 return htmlfill.render(
203 202 render('/password_reset.html'),
204 203 defaults=errors.value,
205 204 errors=errors.error_dict or {},
206 205 prefix_error=False,
207 206 encoding="UTF-8",
208 207 force_defaults=False)
209 208
210 209 return render('/password_reset.html')
211 210
212 211 def password_reset_confirmation(self):
213 212 # This controller handles both GET and POST requests, though we
214 213 # only ever perform the actual password change on POST (since
215 214 # GET requests are not allowed to have side effects, and do not
216 215 # receive automatic CSRF protection).
217 216
218 217 # The template needs the email address outside of the form.
219 218 c.email = request.params.get('email')
220 219
221 220 if not request.POST:
222 221 return htmlfill.render(
223 222 render('/password_reset_confirmation.html'),
224 223 defaults=dict(request.params),
225 224 encoding='UTF-8')
226 225
227 226 form = PasswordResetConfirmationForm()()
228 227 try:
229 228 form_result = form.to_python(dict(request.POST))
230 229 except formencode.Invalid as errors:
231 230 return htmlfill.render(
232 231 render('/password_reset_confirmation.html'),
233 232 defaults=errors.value,
234 233 errors=errors.error_dict or {},
235 234 prefix_error=False,
236 235 encoding='UTF-8')
237 236
238 237 if not UserModel().verify_reset_password_token(
239 238 form_result['email'],
240 239 form_result['timestamp'],
241 240 form_result['token'],
242 241 ):
243 242 return htmlfill.render(
244 243 render('/password_reset_confirmation.html'),
245 244 defaults=form_result,
246 245 errors={'token': _('Invalid password reset token')},
247 246 prefix_error=False,
248 247 encoding='UTF-8')
249 248
250 249 UserModel().reset_password(form_result['email'], form_result['password'])
251 250 h.flash(_('Successfully updated password'), category='success')
252 return redirect(url('login_home'))
251 raise HTTPFound(location=url('login_home'))
253 252
254 253 def logout(self):
255 254 session.delete()
256 255 log.info('Logging out and deleting session for user')
257 redirect(url('home'))
256 raise HTTPFound(location=url('home'))
258 257
259 258 def authentication_token(self):
260 259 """Return the CSRF protection token for the session - just like it
261 260 could have been screen scraped from a page with a form.
262 261 Only intended for testing but might also be useful for other kinds
263 262 of automation.
264 263 """
265 264 return h.authentication_token()
@@ -1,796 +1,794 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.pullrequests
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 pull requests controller for Kallithea for initializing pull requests
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: May 7, 2012
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31 import re
32 32
33 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
34
35 33 from pylons import request, tmpl_context as c, url
36 from pylons.controllers.util import redirect
37 34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
38 36
39 37 from kallithea.lib.vcs.utils.hgcompat import unionrepo
40 38 from kallithea.lib.compat import json
41 39 from kallithea.lib.base import BaseRepoController, render
42 40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
43 41 NotAnonymous
44 42 from kallithea.lib.helpers import Page
45 43 from kallithea.lib import helpers as h
46 44 from kallithea.lib import diffs
47 45 from kallithea.lib.exceptions import UserInvalidException
48 46 from kallithea.lib.utils import action_logger, jsonify
49 47 from kallithea.lib.vcs.utils import safe_str
50 48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError
51 49 from kallithea.lib.diffs import LimitedDiffContainer
52 50 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment,\
53 51 PullRequestReviewers, User
54 52 from kallithea.model.pull_request import PullRequestModel
55 53 from kallithea.model.meta import Session
56 54 from kallithea.model.repo import RepoModel
57 55 from kallithea.model.comment import ChangesetCommentsModel
58 56 from kallithea.model.changeset_status import ChangesetStatusModel
59 57 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
60 58 from kallithea.lib.utils2 import safe_int
61 59 from kallithea.controllers.changeset import _ignorews_url, _context_url
62 60 from kallithea.controllers.compare import CompareController
63 61 from kallithea.lib.graphmod import graph_data
64 62
65 63 log = logging.getLogger(__name__)
66 64
67 65
68 66 class PullrequestsController(BaseRepoController):
69 67
70 68 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
71 69 """return a structure with repo's interesting changesets, suitable for
72 70 the selectors in pullrequest.html
73 71
74 72 rev: a revision that must be in the list somehow and selected by default
75 73 branch: a branch that must be in the list and selected by default - even if closed
76 74 branch_rev: a revision of which peers should be preferred and available."""
77 75 # list named branches that has been merged to this named branch - it should probably merge back
78 76 peers = []
79 77
80 78 if rev:
81 79 rev = safe_str(rev)
82 80
83 81 if branch:
84 82 branch = safe_str(branch)
85 83
86 84 if branch_rev:
87 85 branch_rev = safe_str(branch_rev)
88 86 # a revset not restricting to merge() would be better
89 87 # (especially because it would get the branch point)
90 88 # ... but is currently too expensive
91 89 # including branches of children could be nice too
92 90 peerbranches = set()
93 91 for i in repo._repo.revs(
94 92 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
95 93 branch_rev, branch_rev):
96 94 abranch = repo.get_changeset(i).branch
97 95 if abranch not in peerbranches:
98 96 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
99 97 peers.append((n, abranch))
100 98 peerbranches.add(abranch)
101 99
102 100 selected = None
103 101 tiprev = repo.tags.get('tip')
104 102 tipbranch = None
105 103
106 104 branches = []
107 105 for abranch, branchrev in repo.branches.iteritems():
108 106 n = 'branch:%s:%s' % (abranch, branchrev)
109 107 desc = abranch
110 108 if branchrev == tiprev:
111 109 tipbranch = abranch
112 110 desc = '%s (current tip)' % desc
113 111 branches.append((n, desc))
114 112 if rev == branchrev:
115 113 selected = n
116 114 if branch == abranch:
117 115 if not rev:
118 116 selected = n
119 117 branch = None
120 118 if branch: # branch not in list - it is probably closed
121 119 branchrev = repo.closed_branches.get(branch)
122 120 if branchrev:
123 121 n = 'branch:%s:%s' % (branch, branchrev)
124 122 branches.append((n, _('%s (closed)') % branch))
125 123 selected = n
126 124 branch = None
127 125 if branch:
128 126 log.debug('branch %r not found in %s', branch, repo)
129 127
130 128 bookmarks = []
131 129 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
132 130 n = 'book:%s:%s' % (bookmark, bookmarkrev)
133 131 bookmarks.append((n, bookmark))
134 132 if rev == bookmarkrev:
135 133 selected = n
136 134
137 135 tags = []
138 136 for tag, tagrev in repo.tags.iteritems():
139 137 if tag == 'tip':
140 138 continue
141 139 n = 'tag:%s:%s' % (tag, tagrev)
142 140 tags.append((n, tag))
143 141 if rev == tagrev:
144 142 selected = n
145 143
146 144 # prio 1: rev was selected as existing entry above
147 145
148 146 # prio 2: create special entry for rev; rev _must_ be used
149 147 specials = []
150 148 if rev and selected is None:
151 149 selected = 'rev:%s:%s' % (rev, rev)
152 150 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
153 151
154 152 # prio 3: most recent peer branch
155 153 if peers and not selected:
156 154 selected = peers[0][0]
157 155
158 156 # prio 4: tip revision
159 157 if not selected:
160 158 if h.is_hg(repo):
161 159 if tipbranch:
162 160 selected = 'branch:%s:%s' % (tipbranch, tiprev)
163 161 else:
164 162 selected = 'tag:null:' + repo.EMPTY_CHANGESET
165 163 tags.append((selected, 'null'))
166 164 else:
167 165 if 'master' in repo.branches:
168 166 selected = 'branch:master:%s' % repo.branches['master']
169 167 else:
170 168 k, v = repo.branches.items()[0]
171 169 selected = 'branch:%s:%s' % (k, v)
172 170
173 171 groups = [(specials, _("Special")),
174 172 (peers, _("Peer branches")),
175 173 (bookmarks, _("Bookmarks")),
176 174 (branches, _("Branches")),
177 175 (tags, _("Tags")),
178 176 ]
179 177 return [g for g in groups if g[0]], selected
180 178
181 179 def _get_is_allowed_change_status(self, pull_request):
182 180 if pull_request.is_closed():
183 181 return False
184 182
185 183 owner = self.authuser.user_id == pull_request.user_id
186 184 reviewer = self.authuser.user_id in [x.user_id for x in
187 185 pull_request.reviewers]
188 186 return self.authuser.admin or owner or reviewer
189 187
190 188 @LoginRequired()
191 189 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
192 190 'repository.admin')
193 191 def show_all(self, repo_name):
194 192 c.from_ = request.GET.get('from_') or ''
195 193 c.closed = request.GET.get('closed') or ''
196 194 c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
197 195 c.repo_name = repo_name
198 196 p = safe_int(request.GET.get('page', 1), 1)
199 197
200 198 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
201 199
202 200 return render('/pullrequests/pullrequest_show_all.html')
203 201
204 202 @LoginRequired()
205 203 @NotAnonymous()
206 204 def show_my(self):
207 205 c.closed = request.GET.get('closed') or ''
208 206
209 207 def _filter(pr):
210 208 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
211 209 if not c.closed:
212 210 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
213 211 return s
214 212
215 213 c.my_pull_requests = _filter(PullRequest.query()\
216 214 .filter(PullRequest.user_id ==
217 215 self.authuser.user_id)\
218 216 .all())
219 217
220 218 c.participate_in_pull_requests = _filter(PullRequest.query()\
221 219 .join(PullRequestReviewers)\
222 220 .filter(PullRequestReviewers.user_id ==
223 221 self.authuser.user_id)\
224 222 )
225 223
226 224 return render('/pullrequests/pullrequest_show_my.html')
227 225
228 226 @LoginRequired()
229 227 @NotAnonymous()
230 228 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
231 229 'repository.admin')
232 230 def index(self):
233 231 org_repo = c.db_repo
234 232 org_scm_instance = org_repo.scm_instance
235 233 try:
236 234 org_scm_instance.get_changeset()
237 235 except EmptyRepositoryError as e:
238 236 h.flash(h.literal(_('There are no changesets yet')),
239 237 category='warning')
240 redirect(url('summary_home', repo_name=org_repo.repo_name))
238 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
241 239
242 240 org_rev = request.GET.get('rev_end')
243 241 # rev_start is not directly useful - its parent could however be used
244 242 # as default for other and thus give a simple compare view
245 243 rev_start = request.GET.get('rev_start')
246 244 other_rev = None
247 245 if rev_start:
248 246 starters = org_repo.get_changeset(rev_start).parents
249 247 if starters:
250 248 other_rev = starters[0].raw_id
251 249 else:
252 250 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
253 251 branch = request.GET.get('branch')
254 252
255 253 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
256 254 c.default_cs_repo = org_repo.repo_name
257 255 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
258 256
259 257 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
260 258 if default_cs_ref_type != 'branch':
261 259 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
262 260
263 261 # add org repo to other so we can open pull request against peer branches on itself
264 262 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
265 263
266 264 if org_repo.parent:
267 265 # add parent of this fork also and select it.
268 266 # use the same branch on destination as on source, if available.
269 267 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
270 268 c.a_repo = org_repo.parent
271 269 c.a_refs, c.default_a_ref = self._get_repo_refs(
272 270 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
273 271
274 272 else:
275 273 c.a_repo = org_repo
276 274 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
277 275
278 276 # gather forks and add to this list ... even though it is rare to
279 277 # request forks to pull from their parent
280 278 for fork in org_repo.forks:
281 279 c.a_repos.append((fork.repo_name, fork.repo_name))
282 280
283 281 return render('/pullrequests/pullrequest.html')
284 282
285 283 @LoginRequired()
286 284 @NotAnonymous()
287 285 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
288 286 'repository.admin')
289 287 @jsonify
290 288 def repo_info(self, repo_name):
291 289 repo = RepoModel()._get_repo(repo_name)
292 290 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
293 291 return {
294 292 'description': repo.description.split('\n', 1)[0],
295 293 'selected_ref': selected_ref,
296 294 'refs': refs,
297 295 }
298 296
299 297 @LoginRequired()
300 298 @NotAnonymous()
301 299 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
302 300 'repository.admin')
303 301 def create(self, repo_name):
304 302 repo = RepoModel()._get_repo(repo_name)
305 303 try:
306 304 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
307 305 except formencode.Invalid as errors:
308 306 log.error(traceback.format_exc())
309 307 log.error(str(errors))
310 308 msg = _('Error creating pull request: %s') % errors.msg
311 309 h.flash(msg, 'error')
312 310 raise HTTPBadRequest
313 311
314 312 # heads up: org and other might seem backward here ...
315 313 org_repo_name = _form['org_repo']
316 314 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
317 315 org_repo = RepoModel()._get_repo(org_repo_name)
318 316 (org_ref_type,
319 317 org_ref_name,
320 318 org_rev) = org_ref.split(':')
321 319 if org_ref_type == 'rev':
322 320 org_ref_type = 'branch'
323 321 cs = org_repo.scm_instance.get_changeset(org_rev)
324 322 org_ref = '%s:%s:%s' % (org_ref_type, cs.branch, cs.raw_id)
325 323
326 324 other_repo_name = _form['other_repo']
327 325 other_ref = _form['other_ref'] # will have symbolic name and head revision
328 326 other_repo = RepoModel()._get_repo(other_repo_name)
329 327 (other_ref_type,
330 328 other_ref_name,
331 329 other_rev) = other_ref.split(':')
332 330
333 331 cs_ranges, _cs_ranges_not, ancestor_rev = \
334 332 CompareController._get_changesets(org_repo.scm_instance.alias,
335 333 other_repo.scm_instance, other_rev, # org and other "swapped"
336 334 org_repo.scm_instance, org_rev,
337 335 )
338 336 if ancestor_rev is None:
339 337 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
340 338 revisions = [cs_.raw_id for cs_ in cs_ranges]
341 339
342 340 # hack: ancestor_rev is not an other_rev but we want to show the
343 341 # requested destination and have the exact ancestor
344 342 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
345 343
346 344 reviewers = _form['review_members']
347 345
348 346 title = _form['pullrequest_title']
349 347 if not title:
350 348 if org_repo_name == other_repo_name:
351 349 title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
352 350 h.short_ref(other_ref_type, other_ref_name))
353 351 else:
354 352 title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
355 353 other_repo_name, h.short_ref(other_ref_type, other_ref_name))
356 354 description = _form['pullrequest_desc'].strip() or _('No description')
357 355 try:
358 356 pull_request = PullRequestModel().create(
359 357 self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
360 358 other_ref, revisions, reviewers, title, description
361 359 )
362 360 Session().commit()
363 361 h.flash(_('Successfully opened new pull request'),
364 362 category='success')
365 363 except UserInvalidException as u:
366 364 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
367 365 raise HTTPBadRequest()
368 366 except Exception:
369 367 h.flash(_('Error occurred while creating pull request'),
370 368 category='error')
371 369 log.error(traceback.format_exc())
372 return redirect(url('pullrequest_home', repo_name=repo_name))
370 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
373 371
374 return redirect(pull_request.url())
372 raise HTTPFound(location=pull_request.url())
375 373
376 374 def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
377 375 org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
378 376 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
379 377 new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
380 378
381 379 other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
382 380 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
383 381 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
384 382 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
385 383
386 384 cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
387 385 other_repo.scm_instance, new_other_rev, # org and other "swapped"
388 386 org_repo.scm_instance, new_org_rev)
389 387
390 388 old_revisions = set(old_pull_request.revisions)
391 389 revisions = [cs.raw_id for cs in cs_ranges]
392 390 new_revisions = [r for r in revisions if r not in old_revisions]
393 391 lost = old_revisions.difference(revisions)
394 392
395 393 infos = ['This is an update of %s "%s".' %
396 394 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
397 395 pull_request_id=old_pull_request.pull_request_id),
398 396 old_pull_request.title)]
399 397
400 398 if lost:
401 399 infos.append(_('Missing changesets since the previous pull request:'))
402 400 for r in old_pull_request.revisions:
403 401 if r in lost:
404 402 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
405 403 infos.append(' %s "%s"' % (h.short_id(r), rev_desc))
406 404
407 405 if new_revisions:
408 406 infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
409 407 for r in reversed(revisions):
410 408 if r in new_revisions:
411 409 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
412 410 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
413 411
414 412 if ancestor_rev == other_rev:
415 413 infos.append(_("Ancestor didn't change - show diff since previous version:"))
416 414 infos.append(h.canonical_url('compare_url',
417 415 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
418 416 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
419 417 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
420 418 )) # note: linear diff, merge or not doesn't matter
421 419 else:
422 420 infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
423 421 else:
424 422 infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
425 423 # TODO: fail?
426 424
427 425 # hack: ancestor_rev is not an other_ref but we want to show the
428 426 # requested destination and have the exact ancestor
429 427 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
430 428 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
431 429
432 430 try:
433 431 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
434 432 v = int(old_v) + 1
435 433 except (AttributeError, ValueError):
436 434 v = 2
437 435 title = '%s (v%s)' % (title.strip(), v)
438 436
439 437 # using a mail-like separator, insert new update info at the top of the list
440 438 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
441 439 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
442 440 if len(descriptions) > 1:
443 441 description += '\n\n' + descriptions[1].strip()
444 442
445 443 try:
446 444 pull_request = PullRequestModel().create(
447 445 self.authuser.user_id,
448 446 old_pull_request.org_repo.repo_name, new_org_ref,
449 447 old_pull_request.other_repo.repo_name, new_other_ref,
450 448 revisions, reviewers_ids, title, description
451 449 )
452 450 except UserInvalidException as u:
453 451 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
454 452 raise HTTPBadRequest()
455 453 except Exception:
456 454 h.flash(_('Error occurred while creating pull request'),
457 455 category='error')
458 456 log.error(traceback.format_exc())
459 return redirect(old_pull_request.url())
457 raise HTTPFound(location=old_pull_request.url())
460 458
461 459 ChangesetCommentsModel().create(
462 460 text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
463 461 repo=old_pull_request.other_repo.repo_id,
464 462 user=c.authuser.user_id,
465 463 pull_request=old_pull_request.pull_request_id,
466 464 closing_pr=True)
467 465 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
468 466
469 467 Session().commit()
470 468 h.flash(_('Pull request update created'),
471 469 category='success')
472 470
473 return redirect(pull_request.url())
471 raise HTTPFound(location=pull_request.url())
474 472
475 473 # pullrequest_post for PR editing
476 474 @LoginRequired()
477 475 @NotAnonymous()
478 476 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
479 477 'repository.admin')
480 478 def post(self, repo_name, pull_request_id):
481 479 pull_request = PullRequest.get_or_404(pull_request_id)
482 480 if pull_request.is_closed():
483 481 raise HTTPForbidden()
484 482 assert pull_request.other_repo.repo_name == repo_name
485 483 #only owner or admin can update it
486 484 owner = pull_request.owner.user_id == c.authuser.user_id
487 485 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
488 486 if not (h.HasPermissionAny('hg.admin') or repo_admin or owner):
489 487 raise HTTPForbidden()
490 488
491 489 _form = PullRequestPostForm()().to_python(request.POST)
492 490 reviewers_ids = [int(s) for s in _form['review_members']]
493 491
494 492 if _form['updaterev']:
495 493 return self.create_update(pull_request,
496 494 _form['updaterev'],
497 495 _form['pullrequest_title'],
498 496 _form['pullrequest_desc'],
499 497 reviewers_ids)
500 498
501 499 old_description = pull_request.description
502 500 pull_request.title = _form['pullrequest_title']
503 501 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
504 502 pull_request.owner = User.get_by_username(_form['owner'])
505 503 user = User.get(c.authuser.user_id)
506 504 try:
507 505 PullRequestModel().mention_from_description(user, pull_request, old_description)
508 506 PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
509 507 except UserInvalidException as u:
510 508 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
511 509 raise HTTPBadRequest()
512 510
513 511 Session().commit()
514 512 h.flash(_('Pull request updated'), category='success')
515 513
516 return redirect(pull_request.url())
514 raise HTTPFound(location=pull_request.url())
517 515
518 516 @LoginRequired()
519 517 @NotAnonymous()
520 518 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
521 519 'repository.admin')
522 520 @jsonify
523 521 def delete(self, repo_name, pull_request_id):
524 522 pull_request = PullRequest.get_or_404(pull_request_id)
525 523 #only owner can delete it !
526 524 if pull_request.owner.user_id == c.authuser.user_id:
527 525 PullRequestModel().delete(pull_request)
528 526 Session().commit()
529 527 h.flash(_('Successfully deleted pull request'),
530 528 category='success')
531 return redirect(url('my_pullrequests'))
529 raise HTTPFound(location=url('my_pullrequests'))
532 530 raise HTTPForbidden()
533 531
534 532 @LoginRequired()
535 533 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
536 534 'repository.admin')
537 535 def show(self, repo_name, pull_request_id, extra=None):
538 536 repo_model = RepoModel()
539 537 c.users_array = repo_model.get_users_js()
540 538 c.user_groups_array = repo_model.get_user_groups_js()
541 539 c.pull_request = PullRequest.get_or_404(pull_request_id)
542 540 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
543 541 cc_model = ChangesetCommentsModel()
544 542 cs_model = ChangesetStatusModel()
545 543
546 544 # pull_requests repo_name we opened it against
547 545 # ie. other_repo must match
548 546 if repo_name != c.pull_request.other_repo.repo_name:
549 547 raise HTTPNotFound
550 548
551 549 # load compare data into template context
552 550 c.cs_repo = c.pull_request.org_repo
553 551 (c.cs_ref_type,
554 552 c.cs_ref_name,
555 553 c.cs_rev) = c.pull_request.org_ref.split(':')
556 554
557 555 c.a_repo = c.pull_request.other_repo
558 556 (c.a_ref_type,
559 557 c.a_ref_name,
560 558 c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor
561 559
562 560 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
563 561 c.cs_repo = c.cs_repo
564 562 c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
565 563 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
566 564 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
567 565 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
568 566
569 567 c.is_range = False
570 568 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
571 569 cs_a = org_scm_instance.get_changeset(c.a_rev)
572 570 root_parents = c.cs_ranges[0].parents
573 571 c.is_range = cs_a in root_parents
574 572 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
575 573
576 574 avail_revs = set()
577 575 avail_show = []
578 576 c.cs_branch_name = c.cs_ref_name
579 577 other_scm_instance = c.a_repo.scm_instance
580 578 c.update_msg = ""
581 579 c.update_msg_other = ""
582 580 if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
583 581 if c.cs_ref_type != 'branch':
584 582 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
585 583 c.a_branch_name = c.a_ref_name
586 584 if c.a_ref_type != 'branch':
587 585 try:
588 586 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
589 587 except EmptyRepositoryError:
590 588 c.a_branch_name = 'null' # not a branch name ... but close enough
591 589 # candidates: descendants of old head that are on the right branch
592 590 # and not are the old head itself ...
593 591 # and nothing at all if old head is a descendant of target ref name
594 592 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
595 593 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
596 594 elif c.pull_request.is_closed():
597 595 c.update_msg = _('This pull request has been closed and can not be updated.')
598 596 else: # look for descendants of PR head on source branch in org repo
599 597 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
600 598 revs[0], c.cs_branch_name)
601 599 if len(avail_revs) > 1: # more than just revs[0]
602 600 # also show changesets that not are descendants but would be merged in
603 601 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
604 602 if org_scm_instance.path != other_scm_instance.path:
605 603 # Note: org_scm_instance.path must come first so all
606 604 # valid revision numbers are 100% org_scm compatible
607 605 # - both for avail_revs and for revset results
608 606 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
609 607 org_scm_instance.path,
610 608 other_scm_instance.path)
611 609 else:
612 610 hgrepo = org_scm_instance._repo
613 611 show = set(hgrepo.revs('::%ld & !::%s & !::%s',
614 612 avail_revs, revs[0], targethead))
615 613 c.update_msg = _('This pull request can be updated with changes on %s:') % c.cs_branch_name
616 614 else:
617 615 show = set()
618 616 avail_revs = set() # drop revs[0]
619 617 c.update_msg = _('No changesets found for updating this pull request.')
620 618
621 619 # TODO: handle branch heads that not are tip-most
622 620 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
623 621 if brevs:
624 622 # also show changesets that are on branch but neither ancestors nor descendants
625 623 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
626 624 show.add(revs[0]) # make sure graph shows this so we can see how they relate
627 625 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
628 626 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
629 627
630 628 avail_show = sorted(show, reverse=True)
631 629
632 630 elif org_scm_instance.alias == 'git':
633 631 c.update_msg = _("Git pull requests don't support updates yet.")
634 632
635 633 c.avail_revs = avail_revs
636 634 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
637 635 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
638 636
639 637 raw_ids = [x.raw_id for x in c.cs_ranges]
640 638 c.cs_comments = c.cs_repo.get_comments(raw_ids)
641 639 c.statuses = c.cs_repo.statuses(raw_ids)
642 640
643 641 ignore_whitespace = request.GET.get('ignorews') == '1'
644 642 line_context = request.GET.get('context', 3)
645 643 c.ignorews_url = _ignorews_url
646 644 c.context_url = _context_url
647 645 c.fulldiff = request.GET.get('fulldiff')
648 646 diff_limit = self.cut_off_limit if not c.fulldiff else None
649 647
650 648 # we swap org/other ref since we run a simple diff on one repo
651 649 log.debug('running diff between %s and %s in %s',
652 650 c.a_rev, c.cs_rev, org_scm_instance.path)
653 651 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
654 652 ignore_whitespace=ignore_whitespace,
655 653 context=line_context)
656 654
657 655 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
658 656 diff_limit=diff_limit)
659 657 _parsed = diff_processor.prepare()
660 658
661 659 c.limited_diff = False
662 660 if isinstance(_parsed, LimitedDiffContainer):
663 661 c.limited_diff = True
664 662
665 663 c.files = []
666 664 c.changes = {}
667 665 c.lines_added = 0
668 666 c.lines_deleted = 0
669 667
670 668 for f in _parsed:
671 669 st = f['stats']
672 670 c.lines_added += st['added']
673 671 c.lines_deleted += st['deleted']
674 672 fid = h.FID('', f['filename'])
675 673 c.files.append([fid, f['operation'], f['filename'], f['stats']])
676 674 htmldiff = diff_processor.as_html(enable_comments=True,
677 675 parsed_lines=[f])
678 676 c.changes[fid] = [f['operation'], f['filename'], htmldiff]
679 677
680 678 # inline comments
681 679 c.inline_cnt = 0
682 680 c.inline_comments = cc_model.get_inline_comments(
683 681 c.db_repo.repo_id,
684 682 pull_request=pull_request_id)
685 683 # count inline comments
686 684 for __, lines in c.inline_comments:
687 685 for comments in lines.values():
688 686 c.inline_cnt += len(comments)
689 687 # comments
690 688 c.comments = cc_model.get_comments(c.db_repo.repo_id,
691 689 pull_request=pull_request_id)
692 690
693 691 # (badly named) pull-request status calculation based on reviewer votes
694 692 (c.pull_request_reviewers,
695 693 c.pull_request_pending_reviewers,
696 694 c.current_voting_result,
697 695 ) = cs_model.calculate_pull_request_result(c.pull_request)
698 696 c.changeset_statuses = ChangesetStatus.STATUSES
699 697
700 698 c.as_form = False
701 699 c.ancestor = None # there is one - but right here we don't know which
702 700 return render('/pullrequests/pullrequest_show.html')
703 701
704 702 @LoginRequired()
705 703 @NotAnonymous()
706 704 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
707 705 'repository.admin')
708 706 @jsonify
709 707 def comment(self, repo_name, pull_request_id):
710 708 pull_request = PullRequest.get_or_404(pull_request_id)
711 709
712 710 status = request.POST.get('changeset_status')
713 711 close_pr = request.POST.get('save_close')
714 712 f_path = request.POST.get('f_path')
715 713 line_no = request.POST.get('line')
716 714
717 715 if (status or close_pr) and (f_path or line_no):
718 716 # status votes and closing is only possible in general comments
719 717 raise HTTPBadRequest()
720 718
721 719 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
722 720 if not allowed_to_change_status:
723 721 if status or close_pr:
724 722 h.flash(_('No permission to change pull request status'), 'error')
725 723 raise HTTPForbidden()
726 724
727 725 text = request.POST.get('text', '').strip()
728 726 if close_pr:
729 727 text = _('Closing.') + '\n' + text
730 728
731 729 comment = ChangesetCommentsModel().create(
732 730 text=text,
733 731 repo=c.db_repo.repo_id,
734 732 user=c.authuser.user_id,
735 733 pull_request=pull_request_id,
736 734 f_path=f_path,
737 735 line_no=line_no,
738 736 status_change=(ChangesetStatus.get_status_lbl(status)
739 737 if status and allowed_to_change_status else None),
740 738 closing_pr=close_pr
741 739 )
742 740
743 741 action_logger(self.authuser,
744 742 'user_commented_pull_request:%s' % pull_request_id,
745 743 c.db_repo, self.ip_addr, self.sa)
746 744
747 745 if status:
748 746 ChangesetStatusModel().set_status(
749 747 c.db_repo.repo_id,
750 748 status,
751 749 c.authuser.user_id,
752 750 comment,
753 751 pull_request=pull_request_id
754 752 )
755 753
756 754 if close_pr:
757 755 PullRequestModel().close_pull_request(pull_request_id)
758 756 action_logger(self.authuser,
759 757 'user_closed_pull_request:%s' % pull_request_id,
760 758 c.db_repo, self.ip_addr, self.sa)
761 759
762 760 Session().commit()
763 761
764 762 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
765 return redirect(pull_request.url())
763 raise HTTPFound(location=pull_request.url())
766 764
767 765 data = {
768 766 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
769 767 }
770 768 if comment is not None:
771 769 c.comment = comment
772 770 data.update(comment.get_dict())
773 771 data.update({'rendered_text':
774 772 render('changeset/changeset_comment_block.html')})
775 773
776 774 return data
777 775
778 776 @LoginRequired()
779 777 @NotAnonymous()
780 778 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
781 779 'repository.admin')
782 780 @jsonify
783 781 def delete_comment(self, repo_name, comment_id):
784 782 co = ChangesetComment.get(comment_id)
785 783 if co.pull_request.is_closed():
786 784 #don't allow deleting comments on closed pull request
787 785 raise HTTPForbidden()
788 786
789 787 owner = co.author.user_id == c.authuser.user_id
790 788 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
791 789 if h.HasPermissionAny('hg.admin') or repo_admin or owner:
792 790 ChangesetCommentsModel().delete(comment=co)
793 791 Session().commit()
794 792 return True
795 793 else:
796 794 raise HTTPForbidden()
@@ -1,1325 +1,1324 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.auth
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 authentication and permission libraries
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27 import time
28 28 import os
29 29 import logging
30 30 import traceback
31 31 import hashlib
32 32 import itertools
33 33 import collections
34 34
35 35 from decorator import decorator
36 36
37 37 from pylons import url, request, session
38 from pylons.controllers.util import redirect
39 38 from pylons.i18n.translation import _
40 39 from webhelpers.pylonslib import secure_form
41 40 from sqlalchemy import or_
42 41 from sqlalchemy.orm.exc import ObjectDeletedError
43 42 from sqlalchemy.orm import joinedload
44 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
45 44
46 45 from kallithea import __platform__, is_windows, is_unix
47 46 from kallithea.lib.vcs.utils.lazy import LazyProperty
48 47 from kallithea.model import meta
49 48 from kallithea.model.meta import Session
50 49 from kallithea.model.user import UserModel
51 50 from kallithea.model.db import User, Repository, Permission, \
52 51 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
53 52 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
54 53 UserGroup, UserApiKeys
55 54
56 55 from kallithea.lib.utils2 import safe_unicode, aslist
57 56 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
58 57 get_user_group_slug, conditional_cache
59 58 from kallithea.lib.caching_query import FromCache
60 59
61 60
62 61 log = logging.getLogger(__name__)
63 62
64 63
65 64 class PasswordGenerator(object):
66 65 """
67 66 This is a simple class for generating password from different sets of
68 67 characters
69 68 usage::
70 69
71 70 passwd_gen = PasswordGenerator()
72 71 #print 8-letter password containing only big and small letters
73 72 of alphabet
74 73 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 74 """
76 75 ALPHABETS_NUM = r'''1234567890'''
77 76 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 77 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 78 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 79 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 80 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 81 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 82 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 83 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 84 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86 85
87 86 def gen_password(self, length, alphabet=ALPHABETS_FULL):
88 87 assert len(alphabet) <= 256, alphabet
89 88 l = []
90 89 while len(l) < length:
91 90 i = ord(os.urandom(1))
92 91 if i < len(alphabet):
93 92 l.append(alphabet[i])
94 93 return ''.join(l)
95 94
96 95
97 96 class KallitheaCrypto(object):
98 97
99 98 @classmethod
100 99 def hash_string(cls, str_):
101 100 """
102 101 Cryptographic function used for password hashing based on pybcrypt
103 102 or Python's own OpenSSL wrapper on windows
104 103
105 104 :param password: password to hash
106 105 """
107 106 if is_windows:
108 107 return hashlib.sha256(str_).hexdigest()
109 108 elif is_unix:
110 109 import bcrypt
111 110 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
112 111 else:
113 112 raise Exception('Unknown or unsupported platform %s' \
114 113 % __platform__)
115 114
116 115 @classmethod
117 116 def hash_check(cls, password, hashed):
118 117 """
119 118 Checks matching password with it's hashed value, runs different
120 119 implementation based on platform it runs on
121 120
122 121 :param password: password
123 122 :param hashed: password in hashed form
124 123 """
125 124
126 125 if is_windows:
127 126 return hashlib.sha256(password).hexdigest() == hashed
128 127 elif is_unix:
129 128 import bcrypt
130 129 return bcrypt.hashpw(password, hashed) == hashed
131 130 else:
132 131 raise Exception('Unknown or unsupported platform %s' \
133 132 % __platform__)
134 133
135 134
136 135 def get_crypt_password(password):
137 136 return KallitheaCrypto.hash_string(password)
138 137
139 138
140 139 def check_password(password, hashed):
141 140 return KallitheaCrypto.hash_check(password, hashed)
142 141
143 142
144 143
145 144 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
146 145 explicit, algo):
147 146 RK = 'repositories'
148 147 GK = 'repositories_groups'
149 148 UK = 'user_groups'
150 149 GLOBAL = 'global'
151 150 PERM_WEIGHTS = Permission.PERM_WEIGHTS
152 151 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
153 152
154 153 def _choose_perm(new_perm, cur_perm):
155 154 new_perm_val = PERM_WEIGHTS[new_perm]
156 155 cur_perm_val = PERM_WEIGHTS[cur_perm]
157 156 if algo == 'higherwin':
158 157 if new_perm_val > cur_perm_val:
159 158 return new_perm
160 159 return cur_perm
161 160 elif algo == 'lowerwin':
162 161 if new_perm_val < cur_perm_val:
163 162 return new_perm
164 163 return cur_perm
165 164
166 165 #======================================================================
167 166 # fetch default permissions
168 167 #======================================================================
169 168 default_user = User.get_by_username('default', cache=True)
170 169 default_user_id = default_user.user_id
171 170
172 171 default_repo_perms = Permission.get_default_perms(default_user_id)
173 172 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
174 173 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
175 174
176 175 if user_is_admin:
177 176 #==================================================================
178 177 # admin users have all rights;
179 178 # based on default permissions, just set everything to admin
180 179 #==================================================================
181 180 permissions[GLOBAL].add('hg.admin')
182 181 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
183 182
184 183 # repositories
185 184 for perm in default_repo_perms:
186 185 r_k = perm.UserRepoToPerm.repository.repo_name
187 186 p = 'repository.admin'
188 187 permissions[RK][r_k] = p
189 188
190 189 # repository groups
191 190 for perm in default_repo_groups_perms:
192 191 rg_k = perm.UserRepoGroupToPerm.group.group_name
193 192 p = 'group.admin'
194 193 permissions[GK][rg_k] = p
195 194
196 195 # user groups
197 196 for perm in default_user_group_perms:
198 197 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
199 198 p = 'usergroup.admin'
200 199 permissions[UK][u_k] = p
201 200 return permissions
202 201
203 202 #==================================================================
204 203 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
205 204 #==================================================================
206 205
207 206 # default global permissions taken from the default user
208 207 default_global_perms = UserToPerm.query()\
209 208 .filter(UserToPerm.user_id == default_user_id)\
210 209 .options(joinedload(UserToPerm.permission))
211 210
212 211 for perm in default_global_perms:
213 212 permissions[GLOBAL].add(perm.permission.permission_name)
214 213
215 214 # defaults for repositories, taken from default user
216 215 for perm in default_repo_perms:
217 216 r_k = perm.UserRepoToPerm.repository.repo_name
218 217 if perm.Repository.private and not (perm.Repository.user_id == user_id):
219 218 # disable defaults for private repos,
220 219 p = 'repository.none'
221 220 elif perm.Repository.user_id == user_id:
222 221 # set admin if owner
223 222 p = 'repository.admin'
224 223 else:
225 224 p = perm.Permission.permission_name
226 225
227 226 permissions[RK][r_k] = p
228 227
229 228 # defaults for repository groups taken from default user permission
230 229 # on given group
231 230 for perm in default_repo_groups_perms:
232 231 rg_k = perm.UserRepoGroupToPerm.group.group_name
233 232 p = perm.Permission.permission_name
234 233 permissions[GK][rg_k] = p
235 234
236 235 # defaults for user groups taken from default user permission
237 236 # on given user group
238 237 for perm in default_user_group_perms:
239 238 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
240 239 p = perm.Permission.permission_name
241 240 permissions[UK][u_k] = p
242 241
243 242 #======================================================================
244 243 # !! OVERRIDE GLOBALS !! with user permissions if any found
245 244 #======================================================================
246 245 # those can be configured from groups or users explicitly
247 246 _configurable = set([
248 247 'hg.fork.none', 'hg.fork.repository',
249 248 'hg.create.none', 'hg.create.repository',
250 249 'hg.usergroup.create.false', 'hg.usergroup.create.true'
251 250 ])
252 251
253 252 # USER GROUPS comes first
254 253 # user group global permissions
255 254 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
256 255 .options(joinedload(UserGroupToPerm.permission))\
257 256 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
258 257 UserGroupMember.users_group_id))\
259 258 .filter(UserGroupMember.user_id == user_id)\
260 259 .join((UserGroup, UserGroupMember.users_group_id ==
261 260 UserGroup.users_group_id))\
262 261 .filter(UserGroup.users_group_active == True)\
263 262 .order_by(UserGroupToPerm.users_group_id)\
264 263 .all()
265 264 # need to group here by groups since user can be in more than
266 265 # one group
267 266 _grouped = [[x, list(y)] for x, y in
268 267 itertools.groupby(user_perms_from_users_groups,
269 268 lambda x:x.users_group)]
270 269 for gr, perms in _grouped:
271 270 # since user can be in multiple groups iterate over them and
272 271 # select the lowest permissions first (more explicit)
273 272 ##TODO: do this^^
274 273 if not gr.inherit_default_permissions:
275 274 # NEED TO IGNORE all configurable permissions and
276 275 # replace them with explicitly set
277 276 permissions[GLOBAL] = permissions[GLOBAL]\
278 277 .difference(_configurable)
279 278 for perm in perms:
280 279 permissions[GLOBAL].add(perm.permission.permission_name)
281 280
282 281 # user specific global permissions
283 282 user_perms = Session().query(UserToPerm)\
284 283 .options(joinedload(UserToPerm.permission))\
285 284 .filter(UserToPerm.user_id == user_id).all()
286 285
287 286 if not user_inherit_default_permissions:
288 287 # NEED TO IGNORE all configurable permissions and
289 288 # replace them with explicitly set
290 289 permissions[GLOBAL] = permissions[GLOBAL]\
291 290 .difference(_configurable)
292 291
293 292 for perm in user_perms:
294 293 permissions[GLOBAL].add(perm.permission.permission_name)
295 294 ## END GLOBAL PERMISSIONS
296 295
297 296 #======================================================================
298 297 # !! PERMISSIONS FOR REPOSITORIES !!
299 298 #======================================================================
300 299 #======================================================================
301 300 # check if user is part of user groups for this repository and
302 301 # fill in his permission from it. _choose_perm decides of which
303 302 # permission should be selected based on selected method
304 303 #======================================================================
305 304
306 305 # user group for repositories permissions
307 306 user_repo_perms_from_users_groups = \
308 307 Session().query(UserGroupRepoToPerm, Permission, Repository,)\
309 308 .join((Repository, UserGroupRepoToPerm.repository_id ==
310 309 Repository.repo_id))\
311 310 .join((Permission, UserGroupRepoToPerm.permission_id ==
312 311 Permission.permission_id))\
313 312 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
314 313 UserGroup.users_group_id))\
315 314 .filter(UserGroup.users_group_active == True)\
316 315 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
317 316 UserGroupMember.users_group_id))\
318 317 .filter(UserGroupMember.user_id == user_id)\
319 318 .all()
320 319
321 320 multiple_counter = collections.defaultdict(int)
322 321 for perm in user_repo_perms_from_users_groups:
323 322 r_k = perm.UserGroupRepoToPerm.repository.repo_name
324 323 multiple_counter[r_k] += 1
325 324 p = perm.Permission.permission_name
326 325 cur_perm = permissions[RK][r_k]
327 326
328 327 if perm.Repository.user_id == user_id:
329 328 # set admin if owner
330 329 p = 'repository.admin'
331 330 else:
332 331 if multiple_counter[r_k] > 1:
333 332 p = _choose_perm(p, cur_perm)
334 333 permissions[RK][r_k] = p
335 334
336 335 # user explicit permissions for repositories, overrides any specified
337 336 # by the group permission
338 337 user_repo_perms = Permission.get_default_perms(user_id)
339 338 for perm in user_repo_perms:
340 339 r_k = perm.UserRepoToPerm.repository.repo_name
341 340 cur_perm = permissions[RK][r_k]
342 341 # set admin if owner
343 342 if perm.Repository.user_id == user_id:
344 343 p = 'repository.admin'
345 344 else:
346 345 p = perm.Permission.permission_name
347 346 if not explicit:
348 347 p = _choose_perm(p, cur_perm)
349 348 permissions[RK][r_k] = p
350 349
351 350 #======================================================================
352 351 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
353 352 #======================================================================
354 353 #======================================================================
355 354 # check if user is part of user groups for this repository groups and
356 355 # fill in his permission from it. _choose_perm decides of which
357 356 # permission should be selected based on selected method
358 357 #======================================================================
359 358 # user group for repo groups permissions
360 359 user_repo_group_perms_from_users_groups = \
361 360 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup)\
362 361 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
363 362 .join((Permission, UserGroupRepoGroupToPerm.permission_id
364 363 == Permission.permission_id))\
365 364 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
366 365 UserGroup.users_group_id))\
367 366 .filter(UserGroup.users_group_active == True)\
368 367 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
369 368 == UserGroupMember.users_group_id))\
370 369 .filter(UserGroupMember.user_id == user_id)\
371 370 .all()
372 371
373 372 multiple_counter = collections.defaultdict(int)
374 373 for perm in user_repo_group_perms_from_users_groups:
375 374 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
376 375 multiple_counter[g_k] += 1
377 376 p = perm.Permission.permission_name
378 377 cur_perm = permissions[GK][g_k]
379 378 if multiple_counter[g_k] > 1:
380 379 p = _choose_perm(p, cur_perm)
381 380 permissions[GK][g_k] = p
382 381
383 382 # user explicit permissions for repository groups
384 383 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
385 384 for perm in user_repo_groups_perms:
386 385 rg_k = perm.UserRepoGroupToPerm.group.group_name
387 386 p = perm.Permission.permission_name
388 387 cur_perm = permissions[GK][rg_k]
389 388 if not explicit:
390 389 p = _choose_perm(p, cur_perm)
391 390 permissions[GK][rg_k] = p
392 391
393 392 #======================================================================
394 393 # !! PERMISSIONS FOR USER GROUPS !!
395 394 #======================================================================
396 395 # user group for user group permissions
397 396 user_group_user_groups_perms = \
398 397 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup)\
399 398 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
400 399 == UserGroup.users_group_id))\
401 400 .join((Permission, UserGroupUserGroupToPerm.permission_id
402 401 == Permission.permission_id))\
403 402 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
404 403 == UserGroupMember.users_group_id))\
405 404 .filter(UserGroupMember.user_id == user_id)\
406 405 .join((UserGroup, UserGroupMember.users_group_id ==
407 406 UserGroup.users_group_id), aliased=True, from_joinpoint=True)\
408 407 .filter(UserGroup.users_group_active == True)\
409 408 .all()
410 409
411 410 multiple_counter = collections.defaultdict(int)
412 411 for perm in user_group_user_groups_perms:
413 412 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
414 413 multiple_counter[g_k] += 1
415 414 p = perm.Permission.permission_name
416 415 cur_perm = permissions[UK][g_k]
417 416 if multiple_counter[g_k] > 1:
418 417 p = _choose_perm(p, cur_perm)
419 418 permissions[UK][g_k] = p
420 419
421 420 #user explicit permission for user groups
422 421 user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
423 422 for perm in user_user_groups_perms:
424 423 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
425 424 p = perm.Permission.permission_name
426 425 cur_perm = permissions[UK][u_k]
427 426 if not explicit:
428 427 p = _choose_perm(p, cur_perm)
429 428 permissions[UK][u_k] = p
430 429
431 430 return permissions
432 431
433 432
434 433 def allowed_api_access(controller_name, whitelist=None, api_key=None):
435 434 """
436 435 Check if given controller_name is in whitelist API access
437 436 """
438 437 if not whitelist:
439 438 from kallithea import CONFIG
440 439 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
441 440 sep=',')
442 441 log.debug('whitelist of API access is: %s', whitelist)
443 442 api_access_valid = controller_name in whitelist
444 443 if api_access_valid:
445 444 log.debug('controller:%s is in API whitelist', controller_name)
446 445 else:
447 446 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
448 447 if api_key:
449 448 #if we use API key and don't have access it's a warning
450 449 log.warning(msg)
451 450 else:
452 451 log.debug(msg)
453 452 return api_access_valid
454 453
455 454
456 455 class AuthUser(object):
457 456 """
458 457 Represents a Kallithea user, including various authentication and
459 458 authorization information. Typically used to store the current user,
460 459 but is also used as a generic user information data structure in
461 460 parts of the code, e.g. user management.
462 461
463 462 Constructed from a database `User` object, a user ID or cookie dict,
464 463 it looks up the user (if needed) and copies all attributes to itself,
465 464 adding various non-persistent data. If lookup fails but anonymous
466 465 access to Kallithea is enabled, the default user is loaded instead.
467 466
468 467 `AuthUser` does not by itself authenticate users and the constructor
469 468 sets the `is_authenticated` field to False, except when falling back
470 469 to the default anonymous user (if enabled). It's up to other parts
471 470 of the code to check e.g. if a supplied password is correct, and if
472 471 so, set `is_authenticated` to True.
473 472
474 473 However, `AuthUser` does refuse to load a user that is not `active`.
475 474 """
476 475
477 476 def __init__(self, user_id=None, dbuser=None,
478 477 is_external_auth=False):
479 478
480 479 self.is_authenticated = False
481 480 self.is_external_auth = is_external_auth
482 481
483 482 user_model = UserModel()
484 483 self.anonymous_user = User.get_default_user(cache=True)
485 484
486 485 # These attributes will be overriden by fill_data, below, unless the
487 486 # requested user cannot be found and the default anonymous user is
488 487 # not enabled.
489 488 self.user_id = None
490 489 self.username = None
491 490 self.api_key = None
492 491 self.name = ''
493 492 self.lastname = ''
494 493 self.email = ''
495 494 self.admin = False
496 495 self.inherit_default_permissions = False
497 496
498 497 # Look up database user, if necessary.
499 498 if user_id is not None:
500 499 log.debug('Auth User lookup by USER ID %s', user_id)
501 500 dbuser = user_model.get(user_id)
502 501 else:
503 502 # Note: dbuser is allowed to be None.
504 503 log.debug('Auth User lookup by database user %s', dbuser)
505 504
506 505 is_user_loaded = self._fill_data(dbuser)
507 506
508 507 # If user cannot be found, try falling back to anonymous.
509 508 if not is_user_loaded:
510 509 is_user_loaded = self._fill_data(self.anonymous_user)
511 510
512 511 # The anonymous user is always "logged in".
513 512 if self.user_id == self.anonymous_user.user_id:
514 513 self.is_authenticated = True
515 514
516 515 if not self.username:
517 516 self.username = 'None'
518 517
519 518 log.debug('Auth User is now %s', self)
520 519
521 520 def _fill_data(self, dbuser):
522 521 """
523 522 Copies database fields from a `db.User` to this `AuthUser`. Does
524 523 not copy `api_keys` and `permissions` attributes.
525 524
526 525 Checks that `dbuser` is `active` (and not None) before copying;
527 526 returns True on success.
528 527 """
529 528 if dbuser is not None and dbuser.active:
530 529 log.debug('filling %s data', dbuser)
531 530 for k, v in dbuser.get_dict().iteritems():
532 531 assert k not in ['api_keys', 'permissions']
533 532 setattr(self, k, v)
534 533 return True
535 534 return False
536 535
537 536 @LazyProperty
538 537 def permissions(self):
539 538 return self.__get_perms(user=self, cache=False)
540 539
541 540 @property
542 541 def api_keys(self):
543 542 return self._get_api_keys()
544 543
545 544 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
546 545 """
547 546 Fills user permission attribute with permissions taken from database
548 547 works for permissions given for repositories, and for permissions that
549 548 are granted to groups
550 549
551 550 :param user: `AuthUser` instance
552 551 :param explicit: In case there are permissions both for user and a group
553 552 that user is part of, explicit flag will define if user will
554 553 explicitly override permissions from group, if it's False it will
555 554 make decision based on the algo
556 555 :param algo: algorithm to decide what permission should be choose if
557 556 it's multiple defined, eg user in two different groups. It also
558 557 decides if explicit flag is turned off how to specify the permission
559 558 for case when user is in a group + have defined separate permission
560 559 """
561 560 user_id = user.user_id
562 561 user_is_admin = user.is_admin
563 562 user_inherit_default_permissions = user.inherit_default_permissions
564 563
565 564 log.debug('Getting PERMISSION tree')
566 565 compute = conditional_cache('short_term', 'cache_desc',
567 566 condition=cache, func=_cached_perms_data)
568 567 return compute(user_id, user_is_admin,
569 568 user_inherit_default_permissions, explicit, algo)
570 569
571 570 def _get_api_keys(self):
572 571 api_keys = [self.api_key]
573 572 for api_key in UserApiKeys.query()\
574 573 .filter(UserApiKeys.user_id == self.user_id)\
575 574 .filter(or_(UserApiKeys.expires == -1,
576 575 UserApiKeys.expires >= time.time())).all():
577 576 api_keys.append(api_key.api_key)
578 577
579 578 return api_keys
580 579
581 580 @property
582 581 def is_admin(self):
583 582 return self.admin
584 583
585 584 @property
586 585 def repositories_admin(self):
587 586 """
588 587 Returns list of repositories you're an admin of
589 588 """
590 589 return [x[0] for x in self.permissions['repositories'].iteritems()
591 590 if x[1] == 'repository.admin']
592 591
593 592 @property
594 593 def repository_groups_admin(self):
595 594 """
596 595 Returns list of repository groups you're an admin of
597 596 """
598 597 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
599 598 if x[1] == 'group.admin']
600 599
601 600 @property
602 601 def user_groups_admin(self):
603 602 """
604 603 Returns list of user groups you're an admin of
605 604 """
606 605 return [x[0] for x in self.permissions['user_groups'].iteritems()
607 606 if x[1] == 'usergroup.admin']
608 607
609 608 @staticmethod
610 609 def check_ip_allowed(user, ip_addr):
611 610 """
612 611 Check if the given IP address (a `str`) is allowed for the given
613 612 user (an `AuthUser` or `db.User`).
614 613 """
615 614 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
616 615 inherit_from_default=user.inherit_default_permissions)
617 616 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
618 617 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
619 618 return True
620 619 else:
621 620 log.info('Access for IP:%s forbidden, '
622 621 'not in %s' % (ip_addr, allowed_ips))
623 622 return False
624 623
625 624 def __repr__(self):
626 625 return "<AuthUser('id:%s[%s] auth:%s')>"\
627 626 % (self.user_id, self.username, self.is_authenticated)
628 627
629 628 def set_authenticated(self, authenticated=True):
630 629 if self.user_id != self.anonymous_user.user_id:
631 630 self.is_authenticated = authenticated
632 631
633 632 def to_cookie(self):
634 633 """ Serializes this login session to a cookie `dict`. """
635 634 return {
636 635 'user_id': self.user_id,
637 636 'is_authenticated': self.is_authenticated,
638 637 'is_external_auth': self.is_external_auth,
639 638 }
640 639
641 640 @staticmethod
642 641 def from_cookie(cookie):
643 642 """
644 643 Deserializes an `AuthUser` from a cookie `dict`.
645 644 """
646 645
647 646 au = AuthUser(
648 647 user_id=cookie.get('user_id'),
649 648 is_external_auth=cookie.get('is_external_auth', False),
650 649 )
651 650 if not au.is_authenticated and au.user_id is not None:
652 651 # user is not authenticated and not empty
653 652 au.set_authenticated(cookie.get('is_authenticated'))
654 653 return au
655 654
656 655 @classmethod
657 656 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
658 657 _set = set()
659 658
660 659 if inherit_from_default:
661 660 default_ips = UserIpMap.query().filter(UserIpMap.user ==
662 661 User.get_default_user(cache=True))
663 662 if cache:
664 663 default_ips = default_ips.options(FromCache("sql_cache_short",
665 664 "get_user_ips_default"))
666 665
667 666 # populate from default user
668 667 for ip in default_ips:
669 668 try:
670 669 _set.add(ip.ip_addr)
671 670 except ObjectDeletedError:
672 671 # since we use heavy caching sometimes it happens that we get
673 672 # deleted objects here, we just skip them
674 673 pass
675 674
676 675 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
677 676 if cache:
678 677 user_ips = user_ips.options(FromCache("sql_cache_short",
679 678 "get_user_ips_%s" % user_id))
680 679
681 680 for ip in user_ips:
682 681 try:
683 682 _set.add(ip.ip_addr)
684 683 except ObjectDeletedError:
685 684 # since we use heavy caching sometimes it happens that we get
686 685 # deleted objects here, we just skip them
687 686 pass
688 687 return _set or set(['0.0.0.0/0', '::/0'])
689 688
690 689
691 690 def set_available_permissions(config):
692 691 """
693 692 This function will propagate pylons globals with all available defined
694 693 permission given in db. We don't want to check each time from db for new
695 694 permissions since adding a new permission also requires application restart
696 695 ie. to decorate new views with the newly created permission
697 696
698 697 :param config: current pylons config instance
699 698
700 699 """
701 700 log.info('getting information about all available permissions')
702 701 try:
703 702 sa = meta.Session
704 703 all_perms = sa.query(Permission).all()
705 704 config['available_permissions'] = [x.permission_name for x in all_perms]
706 705 finally:
707 706 meta.Session.remove()
708 707
709 708
710 709 #==============================================================================
711 710 # CHECK DECORATORS
712 711 #==============================================================================
713 712
714 713 def redirect_to_login(message=None):
715 714 from kallithea.lib import helpers as h
716 715 p = request.path_qs
717 716 if message:
718 717 h.flash(h.literal(message), category='warning')
719 718 log.debug('Redirecting to login page, origin: %s', p)
720 return redirect(url('login_home', came_from=p))
719 raise HTTPFound(location=url('login_home', came_from=p))
721 720
722 721
723 722 class LoginRequired(object):
724 723 """
725 724 Must be logged in to execute this function else
726 725 redirect to login page
727 726
728 727 :param api_access: if enabled this checks only for valid auth token
729 728 and grants access based on valid token
730 729 """
731 730
732 731 def __init__(self, api_access=False):
733 732 self.api_access = api_access
734 733
735 734 def __call__(self, func):
736 735 return decorator(self.__wrapper, func)
737 736
738 737 def __wrapper(self, func, *fargs, **fkwargs):
739 738 controller = fargs[0]
740 739 user = controller.authuser
741 740 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
742 741 log.debug('Checking access for user %s @ %s', user, loc)
743 742
744 743 if not AuthUser.check_ip_allowed(user, controller.ip_addr):
745 744 return redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
746 745
747 746 # check if we used an API key and it's a valid one
748 747 api_key = request.GET.get('api_key')
749 748 if api_key is not None:
750 749 # explicit controller is enabled or API is in our whitelist
751 750 if self.api_access or allowed_api_access(loc, api_key=api_key):
752 751 if api_key in user.api_keys:
753 752 log.info('user %s authenticated with API key ****%s @ %s',
754 753 user, api_key[-4:], loc)
755 754 return func(*fargs, **fkwargs)
756 755 else:
757 756 log.warning('API key ****%s is NOT valid', api_key[-4:])
758 757 return redirect_to_login(_('Invalid API key'))
759 758 else:
760 759 # controller does not allow API access
761 760 log.warning('API access to %s is not allowed', loc)
762 761 raise HTTPForbidden()
763 762
764 763 # Only allow the following HTTP request methods. (We sometimes use POST
765 764 # requests with a '_method' set to 'PUT' or 'DELETE'; but that is only
766 765 # used for the route lookup, and does not affect request.method.)
767 766 if request.method not in ['GET', 'HEAD', 'POST', 'PUT']:
768 767 raise HTTPMethodNotAllowed()
769 768
770 769 # Make sure CSRF token never appears in the URL. If so, invalidate it.
771 770 if secure_form.token_key in request.GET:
772 771 log.error('CSRF key leak detected')
773 772 session.pop(secure_form.token_key, None)
774 773 session.save()
775 774 from kallithea.lib import helpers as h
776 775 h.flash(_("CSRF token leak has been detected - all form tokens have been expired"),
777 776 category='error')
778 777
779 778 # CSRF protection: Whenever a request has ambient authority (whether
780 779 # through a session cookie or its origin IP address), it must include
781 780 # the correct token, unless the HTTP method is GET or HEAD (and thus
782 781 # guaranteed to be side effect free. In practice, the only situation
783 782 # where we allow side effects without ambient authority is when the
784 783 # authority comes from an API key; and that is handled above.
785 784 if request.method not in ['GET', 'HEAD']:
786 785 token = request.POST.get(secure_form.token_key)
787 786 if not token or token != secure_form.authentication_token():
788 787 log.error('CSRF check failed')
789 788 raise HTTPForbidden()
790 789
791 790 # WebOb already ignores request payload parameters for anything other
792 791 # than POST/PUT, but double-check since other Kallithea code relies on
793 792 # this assumption.
794 793 if request.method not in ['POST', 'PUT'] and request.POST:
795 794 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
796 795 raise HTTPBadRequest()
797 796
798 797 # regular user authentication
799 798 if user.is_authenticated:
800 799 log.info('user %s authenticated with regular auth @ %s', user, loc)
801 800 return func(*fargs, **fkwargs)
802 801 else:
803 802 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
804 803 return redirect_to_login()
805 804
806 805 class NotAnonymous(object):
807 806 """
808 807 Must be logged in to execute this function else
809 808 redirect to login page"""
810 809
811 810 def __call__(self, func):
812 811 return decorator(self.__wrapper, func)
813 812
814 813 def __wrapper(self, func, *fargs, **fkwargs):
815 814 cls = fargs[0]
816 815 self.user = cls.authuser
817 816
818 817 log.debug('Checking if user is not anonymous @%s', cls)
819 818
820 819 anonymous = self.user.username == User.DEFAULT_USER
821 820
822 821 if anonymous:
823 822 return redirect_to_login(_('You need to be a registered user to '
824 823 'perform this action'))
825 824 else:
826 825 return func(*fargs, **fkwargs)
827 826
828 827
829 828 class PermsDecorator(object):
830 829 """Base class for controller decorators"""
831 830
832 831 def __init__(self, *required_perms):
833 832 self.required_perms = set(required_perms)
834 833 self.user_perms = None
835 834
836 835 def __call__(self, func):
837 836 return decorator(self.__wrapper, func)
838 837
839 838 def __wrapper(self, func, *fargs, **fkwargs):
840 839 cls = fargs[0]
841 840 self.user = cls.authuser
842 841 self.user_perms = self.user.permissions
843 842 log.debug('checking %s permissions %s for %s %s',
844 843 self.__class__.__name__, self.required_perms, cls, self.user)
845 844
846 845 if self.check_permissions():
847 846 log.debug('Permission granted for %s %s', cls, self.user)
848 847 return func(*fargs, **fkwargs)
849 848
850 849 else:
851 850 log.debug('Permission denied for %s %s', cls, self.user)
852 851 anonymous = self.user.username == User.DEFAULT_USER
853 852
854 853 if anonymous:
855 854 return redirect_to_login(_('You need to be signed in to view this page'))
856 855 else:
857 856 raise HTTPForbidden()
858 857
859 858 def check_permissions(self):
860 859 """Dummy function for overriding"""
861 860 raise Exception('You have to write this function in child class')
862 861
863 862
864 863 class HasPermissionAllDecorator(PermsDecorator):
865 864 """
866 865 Checks for access permission for all given predicates. All of them
867 866 have to be meet in order to fulfill the request
868 867 """
869 868
870 869 def check_permissions(self):
871 870 if self.required_perms.issubset(self.user_perms.get('global')):
872 871 return True
873 872 return False
874 873
875 874
876 875 class HasPermissionAnyDecorator(PermsDecorator):
877 876 """
878 877 Checks for access permission for any of given predicates. In order to
879 878 fulfill the request any of predicates must be meet
880 879 """
881 880
882 881 def check_permissions(self):
883 882 if self.required_perms.intersection(self.user_perms.get('global')):
884 883 return True
885 884 return False
886 885
887 886
888 887 class HasRepoPermissionAllDecorator(PermsDecorator):
889 888 """
890 889 Checks for access permission for all given predicates for specific
891 890 repository. All of them have to be meet in order to fulfill the request
892 891 """
893 892
894 893 def check_permissions(self):
895 894 repo_name = get_repo_slug(request)
896 895 try:
897 896 user_perms = set([self.user_perms['repositories'][repo_name]])
898 897 except KeyError:
899 898 return False
900 899 if self.required_perms.issubset(user_perms):
901 900 return True
902 901 return False
903 902
904 903
905 904 class HasRepoPermissionAnyDecorator(PermsDecorator):
906 905 """
907 906 Checks for access permission for any of given predicates for specific
908 907 repository. In order to fulfill the request any of predicates must be meet
909 908 """
910 909
911 910 def check_permissions(self):
912 911 repo_name = get_repo_slug(request)
913 912 try:
914 913 user_perms = set([self.user_perms['repositories'][repo_name]])
915 914 except KeyError:
916 915 return False
917 916
918 917 if self.required_perms.intersection(user_perms):
919 918 return True
920 919 return False
921 920
922 921
923 922 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
924 923 """
925 924 Checks for access permission for all given predicates for specific
926 925 repository group. All of them have to be meet in order to fulfill the request
927 926 """
928 927
929 928 def check_permissions(self):
930 929 group_name = get_repo_group_slug(request)
931 930 try:
932 931 user_perms = set([self.user_perms['repositories_groups'][group_name]])
933 932 except KeyError:
934 933 return False
935 934
936 935 if self.required_perms.issubset(user_perms):
937 936 return True
938 937 return False
939 938
940 939
941 940 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
942 941 """
943 942 Checks for access permission for any of given predicates for specific
944 943 repository group. In order to fulfill the request any of predicates must be meet
945 944 """
946 945
947 946 def check_permissions(self):
948 947 group_name = get_repo_group_slug(request)
949 948 try:
950 949 user_perms = set([self.user_perms['repositories_groups'][group_name]])
951 950 except KeyError:
952 951 return False
953 952
954 953 if self.required_perms.intersection(user_perms):
955 954 return True
956 955 return False
957 956
958 957
959 958 class HasUserGroupPermissionAllDecorator(PermsDecorator):
960 959 """
961 960 Checks for access permission for all given predicates for specific
962 961 user group. All of them have to be meet in order to fulfill the request
963 962 """
964 963
965 964 def check_permissions(self):
966 965 group_name = get_user_group_slug(request)
967 966 try:
968 967 user_perms = set([self.user_perms['user_groups'][group_name]])
969 968 except KeyError:
970 969 return False
971 970
972 971 if self.required_perms.issubset(user_perms):
973 972 return True
974 973 return False
975 974
976 975
977 976 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
978 977 """
979 978 Checks for access permission for any of given predicates for specific
980 979 user group. In order to fulfill the request any of predicates must be meet
981 980 """
982 981
983 982 def check_permissions(self):
984 983 group_name = get_user_group_slug(request)
985 984 try:
986 985 user_perms = set([self.user_perms['user_groups'][group_name]])
987 986 except KeyError:
988 987 return False
989 988
990 989 if self.required_perms.intersection(user_perms):
991 990 return True
992 991 return False
993 992
994 993
995 994 #==============================================================================
996 995 # CHECK FUNCTIONS
997 996 #==============================================================================
998 997 class PermsFunction(object):
999 998 """Base function for other check functions"""
1000 999
1001 1000 def __init__(self, *perms):
1002 1001 self.required_perms = set(perms)
1003 1002 self.user_perms = None
1004 1003 self.repo_name = None
1005 1004 self.group_name = None
1006 1005
1007 1006 def __call__(self, check_location='', user=None):
1008 1007 if not user:
1009 1008 #TODO: remove this someday,put as user as attribute here
1010 1009 user = request.user
1011 1010
1012 1011 # init auth user if not already given
1013 1012 if not isinstance(user, AuthUser):
1014 1013 user = AuthUser(user.user_id)
1015 1014
1016 1015 cls_name = self.__class__.__name__
1017 1016 check_scope = {
1018 1017 'HasPermissionAll': '',
1019 1018 'HasPermissionAny': '',
1020 1019 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1021 1020 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1022 1021 'HasRepoGroupPermissionAll': 'group:%s' % self.group_name,
1023 1022 'HasRepoGroupPermissionAny': 'group:%s' % self.group_name,
1024 1023 }.get(cls_name, '?')
1025 1024 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1026 1025 self.required_perms, user, check_scope,
1027 1026 check_location or 'unspecified location')
1028 1027 if not user:
1029 1028 log.debug('Empty request user')
1030 1029 return False
1031 1030 self.user_perms = user.permissions
1032 1031 if self.check_permissions():
1033 1032 log.debug('Permission to %s granted for user: %s @ %s',
1034 1033 check_scope, user,
1035 1034 check_location or 'unspecified location')
1036 1035 return True
1037 1036
1038 1037 else:
1039 1038 log.debug('Permission to %s denied for user: %s @ %s',
1040 1039 check_scope, user,
1041 1040 check_location or 'unspecified location')
1042 1041 return False
1043 1042
1044 1043 def check_permissions(self):
1045 1044 """Dummy function for overriding"""
1046 1045 raise Exception('You have to write this function in child class')
1047 1046
1048 1047
1049 1048 class HasPermissionAll(PermsFunction):
1050 1049 def check_permissions(self):
1051 1050 if self.required_perms.issubset(self.user_perms.get('global')):
1052 1051 return True
1053 1052 return False
1054 1053
1055 1054
1056 1055 class HasPermissionAny(PermsFunction):
1057 1056 def check_permissions(self):
1058 1057 if self.required_perms.intersection(self.user_perms.get('global')):
1059 1058 return True
1060 1059 return False
1061 1060
1062 1061
1063 1062 class HasRepoPermissionAll(PermsFunction):
1064 1063 def __call__(self, repo_name=None, check_location='', user=None):
1065 1064 self.repo_name = repo_name
1066 1065 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1067 1066
1068 1067 def check_permissions(self):
1069 1068 if not self.repo_name:
1070 1069 self.repo_name = get_repo_slug(request)
1071 1070
1072 1071 try:
1073 1072 self._user_perms = set(
1074 1073 [self.user_perms['repositories'][self.repo_name]]
1075 1074 )
1076 1075 except KeyError:
1077 1076 return False
1078 1077 if self.required_perms.issubset(self._user_perms):
1079 1078 return True
1080 1079 return False
1081 1080
1082 1081
1083 1082 class HasRepoPermissionAny(PermsFunction):
1084 1083 def __call__(self, repo_name=None, check_location='', user=None):
1085 1084 self.repo_name = repo_name
1086 1085 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1087 1086
1088 1087 def check_permissions(self):
1089 1088 if not self.repo_name:
1090 1089 self.repo_name = get_repo_slug(request)
1091 1090
1092 1091 try:
1093 1092 self._user_perms = set(
1094 1093 [self.user_perms['repositories'][self.repo_name]]
1095 1094 )
1096 1095 except KeyError:
1097 1096 return False
1098 1097 if self.required_perms.intersection(self._user_perms):
1099 1098 return True
1100 1099 return False
1101 1100
1102 1101
1103 1102 class HasRepoGroupPermissionAny(PermsFunction):
1104 1103 def __call__(self, group_name=None, check_location='', user=None):
1105 1104 self.group_name = group_name
1106 1105 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
1107 1106
1108 1107 def check_permissions(self):
1109 1108 try:
1110 1109 self._user_perms = set(
1111 1110 [self.user_perms['repositories_groups'][self.group_name]]
1112 1111 )
1113 1112 except KeyError:
1114 1113 return False
1115 1114 if self.required_perms.intersection(self._user_perms):
1116 1115 return True
1117 1116 return False
1118 1117
1119 1118
1120 1119 class HasRepoGroupPermissionAll(PermsFunction):
1121 1120 def __call__(self, group_name=None, check_location='', user=None):
1122 1121 self.group_name = group_name
1123 1122 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
1124 1123
1125 1124 def check_permissions(self):
1126 1125 try:
1127 1126 self._user_perms = set(
1128 1127 [self.user_perms['repositories_groups'][self.group_name]]
1129 1128 )
1130 1129 except KeyError:
1131 1130 return False
1132 1131 if self.required_perms.issubset(self._user_perms):
1133 1132 return True
1134 1133 return False
1135 1134
1136 1135
1137 1136 class HasUserGroupPermissionAny(PermsFunction):
1138 1137 def __call__(self, user_group_name=None, check_location='', user=None):
1139 1138 self.user_group_name = user_group_name
1140 1139 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
1141 1140
1142 1141 def check_permissions(self):
1143 1142 try:
1144 1143 self._user_perms = set(
1145 1144 [self.user_perms['user_groups'][self.user_group_name]]
1146 1145 )
1147 1146 except KeyError:
1148 1147 return False
1149 1148 if self.required_perms.intersection(self._user_perms):
1150 1149 return True
1151 1150 return False
1152 1151
1153 1152
1154 1153 class HasUserGroupPermissionAll(PermsFunction):
1155 1154 def __call__(self, user_group_name=None, check_location='', user=None):
1156 1155 self.user_group_name = user_group_name
1157 1156 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
1158 1157
1159 1158 def check_permissions(self):
1160 1159 try:
1161 1160 self._user_perms = set(
1162 1161 [self.user_perms['user_groups'][self.user_group_name]]
1163 1162 )
1164 1163 except KeyError:
1165 1164 return False
1166 1165 if self.required_perms.issubset(self._user_perms):
1167 1166 return True
1168 1167 return False
1169 1168
1170 1169
1171 1170 #==============================================================================
1172 1171 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1173 1172 #==============================================================================
1174 1173 class HasPermissionAnyMiddleware(object):
1175 1174 def __init__(self, *perms):
1176 1175 self.required_perms = set(perms)
1177 1176
1178 1177 def __call__(self, user, repo_name):
1179 1178 # repo_name MUST be unicode, since we handle keys in permission
1180 1179 # dict by unicode
1181 1180 repo_name = safe_unicode(repo_name)
1182 1181 usr = AuthUser(user.user_id)
1183 1182 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1184 1183 self.username = user.username
1185 1184 self.repo_name = repo_name
1186 1185 return self.check_permissions()
1187 1186
1188 1187 def check_permissions(self):
1189 1188 log.debug('checking VCS protocol '
1190 1189 'permissions %s for user:%s repository:%s', self.user_perms,
1191 1190 self.username, self.repo_name)
1192 1191 if self.required_perms.intersection(self.user_perms):
1193 1192 log.debug('Permission to repo: %s granted for user: %s @ %s',
1194 1193 self.repo_name, self.username, 'PermissionMiddleware')
1195 1194 return True
1196 1195 log.debug('Permission to repo: %s denied for user: %s @ %s',
1197 1196 self.repo_name, self.username, 'PermissionMiddleware')
1198 1197 return False
1199 1198
1200 1199
1201 1200 #==============================================================================
1202 1201 # SPECIAL VERSION TO HANDLE API AUTH
1203 1202 #==============================================================================
1204 1203 class _BaseApiPerm(object):
1205 1204 def __init__(self, *perms):
1206 1205 self.required_perms = set(perms)
1207 1206
1208 1207 def __call__(self, check_location=None, user=None, repo_name=None,
1209 1208 group_name=None):
1210 1209 cls_name = self.__class__.__name__
1211 1210 check_scope = 'user:%s' % (user)
1212 1211 if repo_name:
1213 1212 check_scope += ', repo:%s' % (repo_name)
1214 1213
1215 1214 if group_name:
1216 1215 check_scope += ', repo group:%s' % (group_name)
1217 1216
1218 1217 log.debug('checking cls:%s %s %s @ %s',
1219 1218 cls_name, self.required_perms, check_scope, check_location)
1220 1219 if not user:
1221 1220 log.debug('Empty User passed into arguments')
1222 1221 return False
1223 1222
1224 1223 ## process user
1225 1224 if not isinstance(user, AuthUser):
1226 1225 user = AuthUser(user.user_id)
1227 1226 if not check_location:
1228 1227 check_location = 'unspecified'
1229 1228 if self.check_permissions(user.permissions, repo_name, group_name):
1230 1229 log.debug('Permission to %s granted for user: %s @ %s',
1231 1230 check_scope, user, check_location)
1232 1231 return True
1233 1232
1234 1233 else:
1235 1234 log.debug('Permission to %s denied for user: %s @ %s',
1236 1235 check_scope, user, check_location)
1237 1236 return False
1238 1237
1239 1238 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1240 1239 """
1241 1240 implement in child class should return True if permissions are ok,
1242 1241 False otherwise
1243 1242
1244 1243 :param perm_defs: dict with permission definitions
1245 1244 :param repo_name: repo name
1246 1245 """
1247 1246 raise NotImplementedError()
1248 1247
1249 1248
1250 1249 class HasPermissionAllApi(_BaseApiPerm):
1251 1250 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1252 1251 if self.required_perms.issubset(perm_defs.get('global')):
1253 1252 return True
1254 1253 return False
1255 1254
1256 1255
1257 1256 class HasPermissionAnyApi(_BaseApiPerm):
1258 1257 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1259 1258 if self.required_perms.intersection(perm_defs.get('global')):
1260 1259 return True
1261 1260 return False
1262 1261
1263 1262
1264 1263 class HasRepoPermissionAllApi(_BaseApiPerm):
1265 1264 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1266 1265 try:
1267 1266 _user_perms = set([perm_defs['repositories'][repo_name]])
1268 1267 except KeyError:
1269 1268 log.warning(traceback.format_exc())
1270 1269 return False
1271 1270 if self.required_perms.issubset(_user_perms):
1272 1271 return True
1273 1272 return False
1274 1273
1275 1274
1276 1275 class HasRepoPermissionAnyApi(_BaseApiPerm):
1277 1276 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1278 1277 try:
1279 1278 _user_perms = set([perm_defs['repositories'][repo_name]])
1280 1279 except KeyError:
1281 1280 log.warning(traceback.format_exc())
1282 1281 return False
1283 1282 if self.required_perms.intersection(_user_perms):
1284 1283 return True
1285 1284 return False
1286 1285
1287 1286
1288 1287 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1289 1288 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1290 1289 try:
1291 1290 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1292 1291 except KeyError:
1293 1292 log.warning(traceback.format_exc())
1294 1293 return False
1295 1294 if self.required_perms.intersection(_user_perms):
1296 1295 return True
1297 1296 return False
1298 1297
1299 1298 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1300 1299 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1301 1300 try:
1302 1301 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1303 1302 except KeyError:
1304 1303 log.warning(traceback.format_exc())
1305 1304 return False
1306 1305 if self.required_perms.issubset(_user_perms):
1307 1306 return True
1308 1307 return False
1309 1308
1310 1309 def check_ip_access(source_ip, allowed_ips=None):
1311 1310 """
1312 1311 Checks if source_ip is a subnet of any of allowed_ips.
1313 1312
1314 1313 :param source_ip:
1315 1314 :param allowed_ips: list of allowed ips together with mask
1316 1315 """
1317 1316 from kallithea.lib import ipaddr
1318 1317 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
1319 1318 if isinstance(allowed_ips, (tuple, list, set)):
1320 1319 for ip in allowed_ips:
1321 1320 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1322 1321 log.debug('IP %s is network %s',
1323 1322 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
1324 1323 return True
1325 1324 return False
@@ -1,539 +1,538 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14
15 15 """
16 16 kallithea.lib.base
17 17 ~~~~~~~~~~~~~~~~~~
18 18
19 19 The base Controller API
20 20 Provides the BaseController class for subclassing. And usage in different
21 21 controllers
22 22
23 23 This file was forked by the Kallithea project in July 2014.
24 24 Original author and date, and relevant copyright and licensing information is below:
25 25 :created_on: Oct 06, 2010
26 26 :author: marcink
27 27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 28 :license: GPLv3, see LICENSE.md for more details.
29 29 """
30 30
31 31 import datetime
32 32 import logging
33 33 import time
34 34 import traceback
35 35
36 36 import webob.exc
37 37 import paste.httpexceptions
38 38 import paste.auth.basic
39 39 import paste.httpheaders
40 40
41 41 from pylons import config, tmpl_context as c, request, session, url
42 42 from pylons.controllers import WSGIController
43 from pylons.controllers.util import redirect
44 43 from pylons.templating import render_mako as render # don't remove this import
45 44 from pylons.i18n.translation import _
46 45
47 46 from kallithea import __version__, BACKENDS
48 47
49 48 from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
50 49 safe_str, safe_int
51 50 from kallithea.lib import auth_modules
52 51 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
53 52 from kallithea.lib.utils import get_repo_slug
54 53 from kallithea.lib.exceptions import UserCreationError
55 54 from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
56 55 from kallithea.model import meta
57 56
58 57 from kallithea.model.db import Repository, Ui, User, Setting
59 58 from kallithea.model.notification import NotificationModel
60 59 from kallithea.model.scm import ScmModel
61 60 from kallithea.model.pull_request import PullRequestModel
62 61
63 62 log = logging.getLogger(__name__)
64 63
65 64
66 65 def _filter_proxy(ip):
67 66 """
68 67 HEADERS can have multiple ips inside the left-most being the original
69 68 client, and each successive proxy that passed the request adding the IP
70 69 address where it received the request from.
71 70
72 71 :param ip:
73 72 """
74 73 if ',' in ip:
75 74 _ips = ip.split(',')
76 75 _first_ip = _ips[0].strip()
77 76 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
78 77 return _first_ip
79 78 return ip
80 79
81 80
82 81 def _get_ip_addr(environ):
83 82 proxy_key = 'HTTP_X_REAL_IP'
84 83 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
85 84 def_key = 'REMOTE_ADDR'
86 85
87 86 ip = environ.get(proxy_key)
88 87 if ip:
89 88 return _filter_proxy(ip)
90 89
91 90 ip = environ.get(proxy_key2)
92 91 if ip:
93 92 return _filter_proxy(ip)
94 93
95 94 ip = environ.get(def_key, '0.0.0.0')
96 95 return _filter_proxy(ip)
97 96
98 97
99 98 def _get_access_path(environ):
100 99 path = environ.get('PATH_INFO')
101 100 org_req = environ.get('pylons.original_request')
102 101 if org_req:
103 102 path = org_req.environ.get('PATH_INFO')
104 103 return path
105 104
106 105
107 106 def log_in_user(user, remember, is_external_auth):
108 107 """
109 108 Log a `User` in and update session and cookies. If `remember` is True,
110 109 the session cookie is set to expire in a year; otherwise, it expires at
111 110 the end of the browser session.
112 111
113 112 Returns populated `AuthUser` object.
114 113 """
115 114 user.update_lastlogin()
116 115 meta.Session().commit()
117 116
118 117 auth_user = AuthUser(dbuser=user,
119 118 is_external_auth=is_external_auth)
120 119 auth_user.set_authenticated()
121 120
122 121 # Start new session to prevent session fixation attacks.
123 122 session.invalidate()
124 123 session['authuser'] = cookie = auth_user.to_cookie()
125 124
126 125 # If they want to be remembered, update the cookie.
127 126 # NOTE: Assumes that beaker defaults to browser session cookie.
128 127 if remember:
129 128 t = datetime.datetime.now() + datetime.timedelta(days=365)
130 129 session._set_cookie_expires(t)
131 130
132 131 session.save()
133 132
134 133 log.info('user %s is now authenticated and stored in '
135 134 'session, session attrs %s', user.username, cookie)
136 135
137 136 # dumps session attrs back to cookie
138 137 session._update_cookie_out()
139 138
140 139 return auth_user
141 140
142 141
143 142 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
144 143
145 144 def __init__(self, realm, authfunc, auth_http_code=None):
146 145 self.realm = realm
147 146 self.authfunc = authfunc
148 147 self._rc_auth_http_code = auth_http_code
149 148
150 149 def build_authentication(self):
151 150 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
152 151 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
153 152 # return 403 if alternative http return code is specified in
154 153 # Kallithea config
155 154 return paste.httpexceptions.HTTPForbidden(headers=head)
156 155 return paste.httpexceptions.HTTPUnauthorized(headers=head)
157 156
158 157 def authenticate(self, environ):
159 158 authorization = paste.httpheaders.AUTHORIZATION(environ)
160 159 if not authorization:
161 160 return self.build_authentication()
162 161 (authmeth, auth) = authorization.split(' ', 1)
163 162 if 'basic' != authmeth.lower():
164 163 return self.build_authentication()
165 164 auth = auth.strip().decode('base64')
166 165 _parts = auth.split(':', 1)
167 166 if len(_parts) == 2:
168 167 username, password = _parts
169 168 if self.authfunc(username, password, environ) is not None:
170 169 return username
171 170 return self.build_authentication()
172 171
173 172 __call__ = authenticate
174 173
175 174
176 175 class BaseVCSController(object):
177 176
178 177 def __init__(self, application, config):
179 178 self.application = application
180 179 self.config = config
181 180 # base path of repo locations
182 181 self.basepath = self.config['base_path']
183 182 # authenticate this VCS request using the authentication modules
184 183 self.authenticate = BasicAuth('', auth_modules.authenticate,
185 184 config.get('auth_ret_code'))
186 185 self.ip_addr = '0.0.0.0'
187 186
188 187 def _handle_request(self, environ, start_response):
189 188 raise NotImplementedError()
190 189
191 190 def _get_by_id(self, repo_name):
192 191 """
193 192 Gets a special pattern _<ID> from clone url and tries to replace it
194 193 with a repository_name for support of _<ID> permanent URLs
195 194
196 195 :param repo_name:
197 196 """
198 197
199 198 data = repo_name.split('/')
200 199 if len(data) >= 2:
201 200 from kallithea.lib.utils import get_repo_by_id
202 201 by_id_match = get_repo_by_id(repo_name)
203 202 if by_id_match:
204 203 data[1] = by_id_match
205 204
206 205 return '/'.join(data)
207 206
208 207 def _invalidate_cache(self, repo_name):
209 208 """
210 209 Sets cache for this repository for invalidation on next access
211 210
212 211 :param repo_name: full repo name, also a cache key
213 212 """
214 213 ScmModel().mark_for_invalidation(repo_name)
215 214
216 215 def _check_permission(self, action, user, repo_name, ip_addr=None):
217 216 """
218 217 Checks permissions using action (push/pull) user and repository
219 218 name
220 219
221 220 :param action: push or pull action
222 221 :param user: `User` instance
223 222 :param repo_name: repository name
224 223 """
225 224 # check IP
226 225 ip_allowed = AuthUser.check_ip_allowed(user, ip_addr)
227 226 if ip_allowed:
228 227 log.info('Access for IP:%s allowed', ip_addr)
229 228 else:
230 229 return False
231 230
232 231 if action == 'push':
233 232 if not HasPermissionAnyMiddleware('repository.write',
234 233 'repository.admin')(user,
235 234 repo_name):
236 235 return False
237 236
238 237 else:
239 238 #any other action need at least read permission
240 239 if not HasPermissionAnyMiddleware('repository.read',
241 240 'repository.write',
242 241 'repository.admin')(user,
243 242 repo_name):
244 243 return False
245 244
246 245 return True
247 246
248 247 def _get_ip_addr(self, environ):
249 248 return _get_ip_addr(environ)
250 249
251 250 def _check_ssl(self, environ):
252 251 """
253 252 Checks the SSL check flag and returns False if SSL is not present
254 253 and required True otherwise
255 254 """
256 255 #check if we have SSL required ! if not it's a bad request !
257 256 if str2bool(Ui.get_by_key('push_ssl').ui_value):
258 257 org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
259 258 if org_proto != 'https':
260 259 log.debug('proto is %s and SSL is required BAD REQUEST !',
261 260 org_proto)
262 261 return False
263 262 return True
264 263
265 264 def _check_locking_state(self, environ, action, repo, user_id):
266 265 """
267 266 Checks locking on this repository, if locking is enabled and lock is
268 267 present returns a tuple of make_lock, locked, locked_by.
269 268 make_lock can have 3 states None (do nothing) True, make lock
270 269 False release lock, This value is later propagated to hooks, which
271 270 do the locking. Think about this as signals passed to hooks what to do.
272 271
273 272 """
274 273 locked = False # defines that locked error should be thrown to user
275 274 make_lock = None
276 275 repo = Repository.get_by_repo_name(repo)
277 276 user = User.get(user_id)
278 277
279 278 # this is kind of hacky, but due to how mercurial handles client-server
280 279 # server see all operation on changeset; bookmarks, phases and
281 280 # obsolescence marker in different transaction, we don't want to check
282 281 # locking on those
283 282 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
284 283 locked_by = repo.locked
285 284 if repo and repo.enable_locking and not obsolete_call:
286 285 if action == 'push':
287 286 #check if it's already locked !, if it is compare users
288 287 user_id, _date = repo.locked
289 288 if user.user_id == user_id:
290 289 log.debug('Got push from user %s, now unlocking', user)
291 290 # unlock if we have push from user who locked
292 291 make_lock = False
293 292 else:
294 293 # we're not the same user who locked, ban with 423 !
295 294 locked = True
296 295 if action == 'pull':
297 296 if repo.locked[0] and repo.locked[1]:
298 297 locked = True
299 298 else:
300 299 log.debug('Setting lock on repo %s by %s', repo, user)
301 300 make_lock = True
302 301
303 302 else:
304 303 log.debug('Repository %s do not have locking enabled', repo)
305 304 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
306 305 make_lock, locked, locked_by)
307 306 return make_lock, locked, locked_by
308 307
309 308 def __call__(self, environ, start_response):
310 309 start = time.time()
311 310 try:
312 311 return self._handle_request(environ, start_response)
313 312 finally:
314 313 log = logging.getLogger('kallithea.' + self.__class__.__name__)
315 314 log.debug('Request time: %.3fs', time.time() - start)
316 315 meta.Session.remove()
317 316
318 317
319 318 class BaseController(WSGIController):
320 319
321 320 def __before__(self):
322 321 """
323 322 __before__ is called before controller methods and after __call__
324 323 """
325 324 c.kallithea_version = __version__
326 325 rc_config = Setting.get_app_settings()
327 326
328 327 # Visual options
329 328 c.visual = AttributeDict({})
330 329
331 330 ## DB stored
332 331 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
333 332 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
334 333 c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
335 334 c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
336 335 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
337 336 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
338 337 c.visual.show_version = str2bool(rc_config.get('show_version'))
339 338 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
340 339 c.visual.gravatar_url = rc_config.get('gravatar_url')
341 340
342 341 c.ga_code = rc_config.get('ga_code')
343 342 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
344 343 if c.ga_code and '<' not in c.ga_code:
345 344 c.ga_code = '''<script type="text/javascript">
346 345 var _gaq = _gaq || [];
347 346 _gaq.push(['_setAccount', '%s']);
348 347 _gaq.push(['_trackPageview']);
349 348
350 349 (function() {
351 350 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
352 351 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
353 352 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
354 353 })();
355 354 </script>''' % c.ga_code
356 355 c.site_name = rc_config.get('title')
357 356 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
358 357
359 358 ## INI stored
360 359 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
361 360 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
362 361
363 362 c.instance_id = config.get('instance_id')
364 363 c.issues_url = config.get('bugtracker', url('issues_url'))
365 364 # END CONFIG VARS
366 365
367 366 c.repo_name = get_repo_slug(request) # can be empty
368 367 c.backends = BACKENDS.keys()
369 368 c.unread_notifications = NotificationModel()\
370 369 .get_unread_cnt_for_user(c.authuser.user_id)
371 370
372 371 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
373 372
374 373 c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
375 374
376 375 self.sa = meta.Session
377 376 self.scm_model = ScmModel(self.sa)
378 377
379 378 @staticmethod
380 379 def _determine_auth_user(api_key, session_authuser):
381 380 """
382 381 Create an `AuthUser` object given the API key (if any) and the
383 382 value of the authuser session cookie.
384 383 """
385 384
386 385 # Authenticate by API key
387 386 if api_key:
388 387 # when using API_KEY we are sure user exists.
389 388 return AuthUser(dbuser=User.get_by_api_key(api_key),
390 389 is_external_auth=True)
391 390
392 391 # Authenticate by session cookie
393 392 # In ancient login sessions, 'authuser' may not be a dict.
394 393 # In that case, the user will have to log in again.
395 394 if isinstance(session_authuser, dict):
396 395 try:
397 396 return AuthUser.from_cookie(session_authuser)
398 397 except UserCreationError as e:
399 398 # container auth or other auth functions that create users on
400 399 # the fly can throw UserCreationError to signal issues with
401 400 # user creation. Explanation should be provided in the
402 401 # exception object.
403 402 from kallithea.lib import helpers as h
404 403 h.flash(e, 'error', logf=log.error)
405 404
406 405 # Authenticate by auth_container plugin (if enabled)
407 406 if any(
408 407 auth_modules.importplugin(name).is_container_auth
409 408 for name in Setting.get_auth_plugins()
410 409 ):
411 410 try:
412 411 user_info = auth_modules.authenticate('', '', request.environ)
413 412 except UserCreationError as e:
414 413 from kallithea.lib import helpers as h
415 414 h.flash(e, 'error', logf=log.error)
416 415 else:
417 416 if user_info is not None:
418 417 username = user_info['username']
419 418 user = User.get_by_username(username, case_insensitive=True)
420 419 return log_in_user(user, remember=False,
421 420 is_external_auth=True)
422 421
423 422 # User is anonymous
424 423 return AuthUser()
425 424
426 425 def __call__(self, environ, start_response):
427 426 """Invoke the Controller"""
428 427
429 428 # WSGIController.__call__ dispatches to the Controller method
430 429 # the request is routed to. This routing information is
431 430 # available in environ['pylons.routes_dict']
432 431 try:
433 432 self.ip_addr = _get_ip_addr(environ)
434 433 # make sure that we update permissions each time we call controller
435 434
436 435 #set globals for auth user
437 436 self.authuser = c.authuser = request.user = self._determine_auth_user(
438 437 request.GET.get('api_key'),
439 438 session.get('authuser'),
440 439 )
441 440
442 441 log.info('IP: %s User: %s accessed %s',
443 442 self.ip_addr, self.authuser,
444 443 safe_unicode(_get_access_path(environ)),
445 444 )
446 445 return WSGIController.__call__(self, environ, start_response)
447 446 finally:
448 447 meta.Session.remove()
449 448
450 449
451 450 class BaseRepoController(BaseController):
452 451 """
453 452 Base class for controllers responsible for loading all needed data for
454 453 repository loaded items are
455 454
456 455 c.db_repo_scm_instance: instance of scm repository
457 456 c.db_repo: instance of db
458 457 c.repository_followers: number of followers
459 458 c.repository_forks: number of forks
460 459 c.repository_following: weather the current user is following the current repo
461 460 """
462 461
463 462 def __before__(self):
464 463 super(BaseRepoController, self).__before__()
465 464 if c.repo_name: # extracted from routes
466 465 _dbr = Repository.get_by_repo_name(c.repo_name)
467 466 if not _dbr:
468 467 return
469 468
470 469 log.debug('Found repository in database %s with state `%s`',
471 470 safe_unicode(_dbr), safe_unicode(_dbr.repo_state))
472 471 route = getattr(request.environ.get('routes.route'), 'name', '')
473 472
474 473 # allow to delete repos that are somehow damages in filesystem
475 474 if route in ['delete_repo']:
476 475 return
477 476
478 477 if _dbr.repo_state in [Repository.STATE_PENDING]:
479 478 if route in ['repo_creating_home']:
480 479 return
481 480 check_url = url('repo_creating_home', repo_name=c.repo_name)
482 return redirect(check_url)
481 raise webob.exc.HTTPFound(location=check_url)
483 482
484 483 dbr = c.db_repo = _dbr
485 484 c.db_repo_scm_instance = c.db_repo.scm_instance
486 485 if c.db_repo_scm_instance is None:
487 486 log.error('%s this repository is present in database but it '
488 487 'cannot be created as an scm instance', c.repo_name)
489 488 from kallithea.lib import helpers as h
490 489 h.flash(h.literal(_('Repository not found in the filesystem')),
491 490 category='error')
492 491 raise paste.httpexceptions.HTTPNotFound()
493 492
494 493 # some globals counter for menu
495 494 c.repository_followers = self.scm_model.get_followers(dbr)
496 495 c.repository_forks = self.scm_model.get_forks(dbr)
497 496 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
498 497 c.repository_following = self.scm_model.is_following_repo(
499 498 c.repo_name, self.authuser.user_id)
500 499
501 500 @staticmethod
502 501 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
503 502 """
504 503 Safe way to get changeset. If error occurs show error.
505 504 """
506 505 from kallithea.lib import helpers as h
507 506 try:
508 507 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
509 508 except EmptyRepositoryError as e:
510 509 if returnempty:
511 510 return repo.scm_instance.EMPTY_CHANGESET
512 511 h.flash(h.literal(_('There are no changesets yet')),
513 512 category='error')
514 513 raise webob.exc.HTTPNotFound()
515 514 except ChangesetDoesNotExistError as e:
516 515 h.flash(h.literal(_('Changeset not found')),
517 516 category='error')
518 517 raise webob.exc.HTTPNotFound()
519 518 except RepositoryError as e:
520 519 log.error(traceback.format_exc())
521 520 h.flash(safe_str(e), category='error')
522 521 raise webob.exc.HTTPBadRequest()
523 522
524 523
525 524 class WSGIResultCloseCallback(object):
526 525 """Wrap a WSGI result and let close call close after calling the
527 526 close method on the result.
528 527 """
529 528 def __init__(self, result, close):
530 529 self._result = result
531 530 self._close = close
532 531
533 532 def __iter__(self):
534 533 return iter(self._result)
535 534
536 535 def close(self):
537 536 if hasattr(self._result, 'close'):
538 537 self._result.close()
539 538 self._close()
General Comments 0
You need to be logged in to leave comments. Login now