# -*- coding: utf-8 -*- # Copyright (C) 2010-2016 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 # (only), as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # This program is dual-licensed. If you wish to learn more about the # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ """ settings controller for rhodecode admin """ import collections import logging import urllib2 import datetime import formencode from formencode import htmlfill import packaging.version from pylons import request, tmpl_context as c, url, config from pylons.controllers.util import redirect from pylons.i18n.translation import _, lazy_ugettext from webob.exc import HTTPBadRequest import rhodecode from rhodecode.admin.navigation import navigation_list from rhodecode.lib import auth from rhodecode.lib import helpers as h from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.base import BaseController, render from rhodecode.lib.celerylib import tasks, run_task from rhodecode.lib.utils import repo2db_mapper from rhodecode.lib.utils2 import ( str2bool, safe_unicode, AttributeDict, safe_int) from rhodecode.lib.compat import OrderedDict from rhodecode.lib.ext_json import json from rhodecode.lib.utils import jsonify from rhodecode.model.db import RhodeCodeUi, Repository from rhodecode.model.forms import ApplicationSettingsForm, \ ApplicationUiSettingsForm, ApplicationVisualisationForm, \ LabsSettingsForm, IssueTrackerPatternsForm from rhodecode.model.scm import ScmModel from rhodecode.model.notification import EmailNotificationModel from rhodecode.model.meta import Session from rhodecode.model.settings import ( IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound, SettingsModel) from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER log = logging.getLogger(__name__) class SettingsController(BaseController): """REST Controller styled on the Atom Publishing Protocol""" # To properly map this controller, ensure your config/routing.py # file has a resource setup: # map.resource('setting', 'settings', controller='admin/settings', # path_prefix='/admin', name_prefix='admin_') @LoginRequired() def __before__(self): super(SettingsController, self).__before__() c.labs_active = str2bool( rhodecode.CONFIG.get('labs_settings_active', 'true')) c.navlist = navigation_list(request) def _get_hg_ui_settings(self): ret = RhodeCodeUi.query().all() if not ret: raise Exception('Could not get application ui settings !') settings = {} for each in ret: k = each.ui_key v = each.ui_value if k == '/': k = 'root_path' if k in ['push_ssl', 'publish']: v = str2bool(v) if k.find('.') != -1: k = k.replace('.', '_') if each.ui_section in ['hooks', 'extensions']: v = each.ui_active settings[each.ui_section + '_' + k] = v return settings @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() @jsonify def delete_svn_pattern(self): if not request.is_xhr: raise HTTPBadRequest() delete_pattern_id = request.POST.get('delete_svn_pattern') model = VcsSettingsModel() try: model.delete_global_svn_pattern(delete_pattern_id) except SettingNotFound: raise HTTPBadRequest() Session().commit() return True @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_vcs_update(self): """POST /admin/settings: All items in the collection""" # url('admin_settings_vcs') c.active = 'vcs' model = VcsSettingsModel() c.svn_branch_patterns = model.get_global_svn_branch_patterns() c.svn_tag_patterns = model.get_global_svn_tag_patterns() application_form = ApplicationUiSettingsForm()() try: form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: h.flash( _("Some form inputs contain invalid data."), category='error') return htmlfill.render( render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8", force_defaults=False ) try: if c.visual.allow_repo_location_change: model.update_global_path_setting( form_result['paths_root_path']) model.update_global_ssl_setting(form_result['web_push_ssl']) model.update_global_hook_settings(form_result) model.create_or_update_global_svn_settings(form_result) model.create_or_update_global_hg_settings(form_result) model.create_or_update_global_pr_settings(form_result) except Exception: log.exception("Exception while updating settings") h.flash(_('Error occurred during updating ' 'application settings'), category='error') else: Session().commit() h.flash(_('Updated VCS settings'), category='success') return redirect(url('admin_settings_vcs')) return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') def settings_vcs(self): """GET /admin/settings: All items in the collection""" # url('admin_settings_vcs') c.active = 'vcs' model = VcsSettingsModel() c.svn_branch_patterns = model.get_global_svn_branch_patterns() c.svn_tag_patterns = model.get_global_svn_tag_patterns() return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_mapping_update(self): """POST /admin/settings/mapping: All items in the collection""" # url('admin_settings_mapping') c.active = 'mapping' rm_obsolete = request.POST.get('destroy', False) invalidate_cache = request.POST.get('invalidate', False) log.debug( 'rescanning repo location with destroy obsolete=%s', rm_obsolete) if invalidate_cache: log.debug('invalidating all repositories cache') for repo in Repository.get_all(): ScmModel().mark_for_invalidation(repo.repo_name, delete=True) filesystem_repos = ScmModel().repo_scan() added, removed = repo2db_mapper(filesystem_repos, rm_obsolete) _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-' h.flash(_('Repositories successfully ' 'rescanned added: %s ; removed: %s') % (_repr(added), _repr(removed)), category='success') return redirect(url('admin_settings_mapping')) @HasPermissionAllDecorator('hg.admin') def settings_mapping(self): """GET /admin/settings/mapping: All items in the collection""" # url('admin_settings_mapping') c.active = 'mapping' return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_global_update(self): """POST /admin/settings/global: All items in the collection""" # url('admin_settings_global') c.active = 'global' application_form = ApplicationSettingsForm()() try: form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: return htmlfill.render( render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8", force_defaults=False) try: settings = [ ('title', 'rhodecode_title'), ('realm', 'rhodecode_realm'), ('pre_code', 'rhodecode_pre_code'), ('post_code', 'rhodecode_post_code'), ('captcha_public_key', 'rhodecode_captcha_public_key'), ('captcha_private_key', 'rhodecode_captcha_private_key'), ] for setting, form_key in settings: sett = SettingsModel().create_or_update_setting( setting, form_result[form_key]) Session().add(sett) Session().commit() SettingsModel().invalidate_settings_cache() h.flash(_('Updated application settings'), category='success') except Exception: log.exception("Exception while updating application settings") h.flash( _('Error occurred during updating application settings'), category='error') return redirect(url('admin_settings_global')) @HasPermissionAllDecorator('hg.admin') def settings_global(self): """GET /admin/settings/global: All items in the collection""" # url('admin_settings_global') c.active = 'global' return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_visual_update(self): """POST /admin/settings/visual: All items in the collection""" # url('admin_settings_visual') c.active = 'visual' application_form = ApplicationVisualisationForm()() try: form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: return htmlfill.render( render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, encoding="UTF-8", force_defaults=False ) try: settings = [ ('show_public_icon', 'rhodecode_show_public_icon', 'bool'), ('show_private_icon', 'rhodecode_show_private_icon', 'bool'), ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'), ('repository_fields', 'rhodecode_repository_fields', 'bool'), ('dashboard_items', 'rhodecode_dashboard_items', 'int'), ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'), ('show_version', 'rhodecode_show_version', 'bool'), ('use_gravatar', 'rhodecode_use_gravatar', 'bool'), ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'), ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'), ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'), ('support_url', 'rhodecode_support_url', 'unicode'), ('show_revision_number', 'rhodecode_show_revision_number', 'bool'), ('show_sha_length', 'rhodecode_show_sha_length', 'int'), ] for setting, form_key, type_ in settings: sett = SettingsModel().create_or_update_setting( setting, form_result[form_key], type_) Session().add(sett) Session().commit() SettingsModel().invalidate_settings_cache() h.flash(_('Updated visualisation settings'), category='success') except Exception: log.exception("Exception updating visualization settings") h.flash(_('Error occurred during updating ' 'visualisation settings'), category='error') return redirect(url('admin_settings_visual')) @HasPermissionAllDecorator('hg.admin') def settings_visual(self): """GET /admin/settings/visual: All items in the collection""" # url('admin_settings_visual') c.active = 'visual' return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_issuetracker_test(self): if request.is_xhr: return h.urlify_commit_message( request.POST.get('test_text', ''), 'repo_group/test_repo1') else: raise HTTPBadRequest() @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_issuetracker_delete(self): uid = request.POST.get('uid') IssueTrackerSettingsModel().delete_entries(uid) h.flash(_('Removed issue tracker entry'), category='success') return redirect(url('admin_settings_issuetracker')) @HasPermissionAllDecorator('hg.admin') def settings_issuetracker(self): """GET /admin/settings/issue-tracker: All items in the collection""" # url('admin_settings_issuetracker') c.active = 'issuetracker' defaults = SettingsModel().get_all_settings() entry_key = 'rhodecode_issuetracker_pat_' c.issuetracker_entries = {} for k, v in defaults.items(): if k.startswith(entry_key): uid = k[len(entry_key):] c.issuetracker_entries[uid] = None for uid in c.issuetracker_entries: c.issuetracker_entries[uid] = AttributeDict({ 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid), 'url': defaults.get('rhodecode_issuetracker_url_' + uid), 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid), 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid), }) return render('admin/settings/settings.html') @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_issuetracker_save(self): settings_model = IssueTrackerSettingsModel() form = IssueTrackerPatternsForm()().to_python(request.POST) for uid in form['delete_patterns']: settings_model.delete_entries(uid) for pattern in form['patterns']: for setting, value, type_ in pattern: sett = settings_model.create_or_update_setting( setting, value, type_) Session().add(sett) Session().commit() SettingsModel().invalidate_settings_cache() h.flash(_('Updated issue tracker entries'), category='success') return redirect(url('admin_settings_issuetracker')) @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_email_update(self): """POST /admin/settings/email: All items in the collection""" # url('admin_settings_email') c.active = 'email' test_email = request.POST.get('test_email') if not test_email: h.flash(_('Please enter email address'), category='error') return redirect(url('admin_settings_email')) email_kwargs = { 'date': datetime.datetime.now(), 'user': c.rhodecode_user, 'rhodecode_version': c.rhodecode_version } (subject, headers, email_body, email_body_plaintext) = EmailNotificationModel().render_email( EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs) recipients = [test_email] if test_email else None run_task(tasks.send_email, recipients, subject, email_body_plaintext, email_body) h.flash(_('Send email task created'), category='success') return redirect(url('admin_settings_email')) @HasPermissionAllDecorator('hg.admin') def settings_email(self): """GET /admin/settings/email: All items in the collection""" # url('admin_settings_email') c.active = 'email' c.rhodecode_ini = rhodecode.CONFIG return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_hooks_update(self): """POST or DELETE /admin/settings/hooks: All items in the collection""" # url('admin_settings_hooks') c.active = 'hooks' if c.visual.allow_custom_hooks_settings: ui_key = request.POST.get('new_hook_ui_key') ui_value = request.POST.get('new_hook_ui_value') hook_id = request.POST.get('hook_id') new_hook = False model = SettingsModel() try: if ui_value and ui_key: model.create_or_update_hook(ui_key, ui_value) h.flash(_('Added new hook'), category='success') new_hook = True elif hook_id: RhodeCodeUi.delete(hook_id) Session().commit() # check for edits update = False _d = request.POST.dict_of_lists() for k, v in zip(_d.get('hook_ui_key', []), _d.get('hook_ui_value_new', [])): model.create_or_update_hook(k, v) update = True if update and not new_hook: h.flash(_('Updated hooks'), category='success') Session().commit() except Exception: log.exception("Exception during hook creation") h.flash(_('Error occurred during hook creation'), category='error') return redirect(url('admin_settings_hooks')) @HasPermissionAllDecorator('hg.admin') def settings_hooks(self): """GET /admin/settings/hooks: All items in the collection""" # url('admin_settings_hooks') c.active = 'hooks' model = SettingsModel() c.hooks = model.get_builtin_hooks() c.custom_hooks = model.get_custom_hooks() return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding="UTF-8", force_defaults=False) @HasPermissionAllDecorator('hg.admin') def settings_search(self): """GET /admin/settings/search: All items in the collection""" # url('admin_settings_search') c.active = 'search' from rhodecode.lib.index import searcher_from_config searcher = searcher_from_config(config) c.statistics = searcher.statistics() return render('admin/settings/settings.html') @HasPermissionAllDecorator('hg.admin') def settings_system(self): """GET /admin/settings/system: All items in the collection""" # url('admin_settings_system') snapshot = str2bool(request.GET.get('snapshot')) c.active = 'system' defaults = self._form_defaults() c.rhodecode_ini = rhodecode.CONFIG c.rhodecode_update_url = defaults.get('rhodecode_update_url') server_info = ScmModel().get_server_info(request.environ) for key, val in server_info.iteritems(): setattr(c, key, val) if c.disk['percent'] > 90: h.flash(h.literal(_( 'Critical: your disk space is very low <b>%s%%</b> used' % c.disk['percent'])), 'error') elif c.disk['percent'] > 70: h.flash(h.literal(_( 'Warning: your disk space is running low <b>%s%%</b> used' % c.disk['percent'])), 'warning') try: c.uptime_age = h._age( h.time_to_datetime(c.boot_time), False, show_suffix=False) except TypeError: c.uptime_age = c.boot_time try: c.system_memory = '%s/%s, %s%% (%s%%) used%s' % ( h.format_byte_size_binary(c.memory['used']), h.format_byte_size_binary(c.memory['total']), c.memory['percent2'], c.memory['percent'], ' %s' % c.memory['error'] if 'error' in c.memory else '') except TypeError: c.system_memory = 'NOT AVAILABLE' rhodecode_ini_safe = rhodecode.CONFIG.copy() blacklist = [ 'rhodecode_license_key', 'routes.map', 'pylons.h', 'pylons.app_globals', 'pylons.environ_config', 'sqlalchemy.db1.url', ('app_conf', 'sqlalchemy.db1.url') ] for k in blacklist: if isinstance(k, tuple): section, key = k if section in rhodecode_ini_safe: rhodecode_ini_safe[section].pop(key, None) else: rhodecode_ini_safe.pop(k, None) c.rhodecode_ini_safe = rhodecode_ini_safe # TODO: marcink, figure out how to allow only selected users to do this c.allowed_to_snapshot = False if snapshot: if c.allowed_to_snapshot: return render('admin/settings/settings_system_snapshot.html') else: h.flash('You are not allowed to do this', category='warning') return htmlfill.render( render('admin/settings/settings.html'), defaults=defaults, encoding="UTF-8", force_defaults=False) @staticmethod def get_update_data(update_url): """Return the JSON update data.""" ver = rhodecode.__version__ log.debug('Checking for upgrade on `%s` server', update_url) opener = urllib2.build_opener() opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)] response = opener.open(update_url) response_data = response.read() data = json.loads(response_data) return data @HasPermissionAllDecorator('hg.admin') def settings_system_update(self): """GET /admin/settings/system/updates: All items in the collection""" # url('admin_settings_system_update') defaults = self._form_defaults() update_url = defaults.get('rhodecode_update_url', '') _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">%s</div>' % (s) try: data = self.get_update_data(update_url) except urllib2.URLError as e: log.exception("Exception contacting upgrade server") return _err('Failed to contact upgrade server: %r' % e) except ValueError as e: log.exception("Bad data sent from update server") return _err('Bad data sent from update server') latest = data['versions'][0] c.update_url = update_url c.latest_data = latest c.latest_ver = latest['version'] c.cur_ver = rhodecode.__version__ c.should_upgrade = False if (packaging.version.Version(c.latest_ver) > packaging.version.Version(c.cur_ver)): c.should_upgrade = True c.important_notices = latest['general'] return render('admin/settings/settings_system_update.html') @HasPermissionAllDecorator('hg.admin') def settings_supervisor(self): c.rhodecode_ini = rhodecode.CONFIG c.active = 'supervisor' c.supervisor_procs = OrderedDict([ (SUPERVISOR_MASTER, {}), ]) c.log_size = 10240 supervisor = SupervisorModel() _connection = supervisor.get_connection( c.rhodecode_ini.get('supervisor.uri')) c.connection_error = None try: _connection.supervisor.getAllProcessInfo() except Exception as e: c.connection_error = str(e) log.exception("Exception reading supervisor data") return render('admin/settings/settings.html') groupid = c.rhodecode_ini.get('supervisor.group_id') # feed our group processes to the main for proc in supervisor.get_group_processes(_connection, groupid): c.supervisor_procs[proc['name']] = {} for k in c.supervisor_procs.keys(): try: # master process info if k == SUPERVISOR_MASTER: _data = supervisor.get_master_state(_connection) _data['name'] = 'supervisor master' _data['description'] = 'pid %s, id: %s, ver: %s' % ( _data['pid'], _data['id'], _data['ver']) c.supervisor_procs[k] = _data else: procid = groupid + ":" + k c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid) except Exception as e: log.exception("Exception reading supervisor data") c.supervisor_procs[k] = {'_rhodecode_error': str(e)} return render('admin/settings/settings.html') @HasPermissionAllDecorator('hg.admin') def settings_supervisor_log(self, procid): import rhodecode c.rhodecode_ini = rhodecode.CONFIG c.active = 'supervisor_tail' supervisor = SupervisorModel() _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri')) groupid = c.rhodecode_ini.get('supervisor.group_id') procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid c.log_size = 10240 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1 c.log = supervisor.read_process_log(_connection, procid, offset, 0) return render('admin/settings/settings.html') @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def settings_labs_update(self): """POST /admin/settings/labs: All items in the collection""" # url('admin_settings/labs', method={'POST'}) c.active = 'labs' application_form = LabsSettingsForm()() try: form_result = application_form.to_python(dict(request.POST)) except formencode.Invalid as errors: h.flash( _('Some form inputs contain invalid data.'), category='error') return htmlfill.render( render('admin/settings/settings.html'), defaults=errors.value, errors=errors.error_dict or {}, prefix_error=False, encoding='UTF-8', force_defaults=False ) try: session = Session() for setting in _LAB_SETTINGS: setting_name = setting.key[len('rhodecode_'):] sett = SettingsModel().create_or_update_setting( setting_name, form_result[setting.key], setting.type) session.add(sett) except Exception: log.exception('Exception while updating lab settings') h.flash(_('Error occurred during updating labs settings'), category='error') else: Session().commit() SettingsModel().invalidate_settings_cache() h.flash(_('Updated Labs settings'), category='success') return redirect(url('admin_settings_labs')) return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding='UTF-8', force_defaults=False) @HasPermissionAllDecorator('hg.admin') def settings_labs(self): """GET /admin/settings/labs: All items in the collection""" # url('admin_settings_labs') if not c.labs_active: redirect(url('admin_settings')) c.active = 'labs' c.lab_settings = _LAB_SETTINGS return htmlfill.render( render('admin/settings/settings.html'), defaults=self._form_defaults(), encoding='UTF-8', force_defaults=False) def _form_defaults(self): defaults = SettingsModel().get_all_settings() defaults.update(self._get_hg_ui_settings()) defaults.update({ 'new_svn_branch': '', 'new_svn_tag': '', }) return defaults # :param key: name of the setting including the 'rhodecode_' prefix # :param type: the RhodeCodeSetting type to use. # :param group: the i18ned group in which we should dispaly this setting # :param label: the i18ned label we should display for this setting # :param help: the i18ned help we should dispaly for this setting LabSetting = collections.namedtuple( 'LabSetting', ('key', 'type', 'group', 'label', 'help')) # This list has to be kept in sync with the form # rhodecode.model.forms.LabsSettingsForm. _LAB_SETTINGS = [ ]