diff --git a/.gitignore b/.gitignore index 7b01616e..d54f9d69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.swp +*.mo burpui-dev.cfg* burpui/RELEASE devel.sh diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 00000000..623278b6 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,10 @@ +[python: **.py] +encoding = utf-8 + +[jinja2: **/templates/**.html] +encoding = utf-8 +extensions=jinja2.ext.autoescape,jinja2.ext.with_ + +[jinja2: **/templates/**.js] +encoding = utf-8 +extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/burpui/__init__.py b/burpui/__init__.py index 82bf2c70..4bed7731 100644 --- a/burpui/__init__.py +++ b/burpui/__init__.py @@ -179,6 +179,7 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu :returns: A :class:`burpui.server.BUIServer` object """ + from flask import g from flask_login import LoginManager from flask_bower import Bower from .utils import basic_login_from_request, ReverseProxied, lookup_file @@ -186,6 +187,7 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu from .routes import view from .api import api, apibp from .ext.cache import cache + from .ext.i18n import babel, get_locale logger = logging.getLogger('burp-ui') @@ -261,6 +263,9 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu app.gunicorn = gunicorn app.config['CFG'] = None + + # Some config + # FIXME: strange behavior when bundling errors # app.config['BUNDLE_ERRORS'] = True @@ -391,6 +396,10 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu force_scheduling_now() except: pass + + # Initialize i18n + babel.init_app(app) + # Create SQLAlchemy if enabled create_db(app) @@ -447,6 +456,10 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu if app.auth != 'none': return basic_login_from_request(request, app) + @app.before_request + def before_request(): + g.locale = get_locale() + return app diff --git a/burpui/ext/i18n.py b/burpui/ext/i18n.py new file mode 100644 index 00000000..946743c8 --- /dev/null +++ b/burpui/ext/i18n.py @@ -0,0 +1,28 @@ +# -*- coding: utf8 -*- +""" +.. module:: burpui.ext.i18n + :platform: Unix + :synopsis: Burp-UI external Internationalization module. + +.. moduleauthor:: Ziirish + +""" +from flask import request +from flask_babel import Babel +from flask_login import current_user +from ..config import config + +babel = Babel() + +LANGUAGES = { + 'en': 'English', + 'fr': 'Français', +} +config['LANGUAGES'] = LANGUAGES + +@babel.localeselector +def get_locale(): + locale = None + if current_user and not current_user.is_anonymous: + locale = getattr(current_user, 'language', None) + return locale or request.accept_languages.best_match(config['LANGUAGES'].keys()) diff --git a/burpui/forms.py b/burpui/forms.py index f0610092..ec1d9c99 100644 --- a/burpui/forms.py +++ b/burpui/forms.py @@ -7,12 +7,15 @@ .. moduleauthor:: Ziirish """ +from .ext.i18n import LANGUAGES, get_locale from flask_wtf import Form -from wtforms import TextField, PasswordField, BooleanField, validators +from flask_babel import gettext +from wtforms import TextField, PasswordField, BooleanField, SelectField, validators class LoginForm(Form): - username = TextField('Username', [validators.Length(min=2, max=25)]) - password = PasswordField('Password', [validators.Required()]) - remember = BooleanField('Remember me', [validators.Optional()]) + username = TextField(gettext('Username'), [validators.Length(min=2, max=25)]) + password = PasswordField(gettext('Password'), [validators.Required()]) + language = SelectField(gettext('Language'), choices=LANGUAGES.items(), default=get_locale) + remember = BooleanField(gettext('Remember me'), [validators.Optional()]) diff --git a/burpui/manage.py b/burpui/manage.py index 335195bf..f0a25698 100644 --- a/burpui/manage.py +++ b/burpui/manage.py @@ -44,6 +44,25 @@ def create_manager(): manager, migrate, app = create_manager() +@manager.command +def init_translation(language): + os.system('pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot burpui') + os.system('pybabel init -i messages.pot -d burpui/translations -l {}'.format(language)) + os.unlink('messages.pot') + + +@manager.command +def update_translation(): + os.system('pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot burpui') + os.system('pybabel update -i messages.pot -d burpui/translations') + os.unlink('messages.pot') + + +@manager.command +def compile_translation(): + os.system('pybabel compile -f -d burpui/translations') + + @manager.command def create_user(name, backend='BASIC', password=None, ask=False): print('[*] Adding \'{}\' user...'.format(name)) diff --git a/burpui/misc/auth/handler.py b/burpui/misc/auth/handler.py index cffb7532..8775f736 100644 --- a/burpui/misc/auth/handler.py +++ b/burpui/misc/auth/handler.py @@ -49,6 +49,7 @@ class UserHandler(BUIuser): sess = session._get_current_object() self.active = False self.authenticated = sess.get('authenticated', False) + self.language = sess.get('language', None) self.backends = backends self.back = None self.name = name @@ -83,6 +84,7 @@ class UserHandler(BUIuser): self.authenticated = self.real.login(passwd) sess = session._get_current_object() sess['authenticated'] = self.authenticated + sess['language'] = self.language return self.authenticated def get_id(self): diff --git a/burpui/routes.py b/burpui/routes.py index c670ed66..b5d73574 100644 --- a/burpui/routes.py +++ b/burpui/routes.py @@ -12,12 +12,14 @@ import math from flask import request, render_template, redirect, url_for, abort, \ flash, Blueprint as FlaskBlueprint, session, current_app from flask_login import login_user, login_required, logout_user, current_user +from flask_babel import gettext from .server import BUIServer # noqa from ._compat import quote from .forms import LoginForm from .exceptions import BUIserverException from .utils import human_readable as _hr +from .ext.i18n import LANGUAGES, get_locale class Blueprint(FlaskBlueprint): @@ -164,14 +166,14 @@ def live_monitor(server=None, name=None): bui.cli.is_one_backup_running() if bui.standalone: if not bui.cli.running: - flash('Sorry, there are no running backups', 'warning') + flash(gettext('Sorry, there are no running backups'), 'warning') return redirect(url_for('.home')) else: run = False for a in bui.cli.servers: run = run or (a in bui.cli.running and bui.cli.running[a]) if not run: - flash('Sorry, there are no running backups', 'warning') + flash(gettext('Sorry, there are no running backups'), 'warning') return redirect(url_for('.home')) return render_template( @@ -191,7 +193,9 @@ def edit_server_initiated_restore(server=None, name=None): to = None if not data or not data['found']: flash( - 'Sorry, there are no restore file found for this client', + gettext( + 'Sorry, there are no restore file found for this client' + ), 'warning' ) return redirect(url_for('.home')) @@ -227,8 +231,11 @@ def client_browse(server=None, name=None, backup=None, encrypted=None, edit = bui.cli.is_server_restore(to, server) if not edit or not edit['found']: flash( - 'Sorry, there are no restore file found for this client', + gettext( + 'Sorry, there are no restore file found for this client' + ), 'warning' + ) edit = None else: @@ -369,14 +376,16 @@ def servers_report(): @view.route('/login', methods=['POST', 'GET']) def login(): form = LoginForm(request.form) + if form.validate_on_submit(): user = bui.uhandler.user(form.username.data) + user.language = form.language.data if user.is_active and user.login(form.password.data): login_user(user, remember=form.remember.data) - flash('Logged in successfully', 'success') + flash(gettext('Logged in successfully'), 'success') return redirect(request.args.get("next") or url_for('.home')) else: - flash('Wrong username or password', 'danger') + flash(gettext('Wrong username or password'), 'danger') return render_template('login.html', form=form, login=True) @@ -386,6 +395,8 @@ def logout(): sess = session._get_current_object() if 'authenticated' in sess: sess.pop('authenticated') + if 'language' in sess: + sess.pop('language') logout_user() return redirect(url_for('.home')) diff --git a/burpui/templates/client.html b/burpui/templates/client.html index 436d84ef..6fc1efa7 100644 --- a/burpui/templates/client.html +++ b/burpui/templates/client.html @@ -5,12 +5,12 @@ {% include "small_topbar.html" %}
@@ -18,22 +18,22 @@

Backups

- Toggle column: Number - + Toggle column: {{ _('Number') }} - Date - - Bytes received - - Estimated size - - Deletable - + {{ _('Bytes received') }} - + {{ _('Estimated size') }} - + {{ _('Deletable') }} - Status

- + - - - + + + @@ -41,24 +41,24 @@
Number{{ _('Number') }} DateBytes receivedEstimated sizeDeletable{{ _('Bytes received') }}{{ _('Estimated size') }}{{ _('Deletable') }} Status

- +
diff --git a/burpui/templates/clients.html b/burpui/templates/clients.html index 418ca240..e4a4a34f 100644 --- a/burpui/templates/clients.html +++ b/burpui/templates/clients.html @@ -5,10 +5,10 @@ {% include "small_topbar.html" %}
@@ -19,9 +19,9 @@ - - - + + + diff --git a/burpui/templates/layout.html b/burpui/templates/layout.html index 6902c87d..d7835fc1 100644 --- a/burpui/templates/layout.html +++ b/burpui/templates/layout.html @@ -56,6 +56,12 @@ + + + {% if g.locale != 'en' -%} + + {% endif -%} {% if report -%} @@ -101,7 +107,6 @@ ================================================== --> - diff --git a/burpui/templates/login.html b/burpui/templates/login.html index ea479569..ba326fe5 100644 --- a/burpui/templates/login.html +++ b/burpui/templates/login.html @@ -4,9 +4,10 @@ {% include "notifications.html" %}
- {% call macros.render_form(form, action_text='Login', class_='form-signin', btn_class='btn btn-info btn-lg btn-primary btn-block') %} - {{ macros.render_field(form.username, placeholder='Username', type='text', autofocus=true) }} - {{ macros.render_field(form.password, placeholder='Password', type='password') }} + {% call macros.render_form(form, action_text=_('Login'), class_='form-signin', btn_class='btn btn-info btn-lg btn-primary btn-block') %} + {{ macros.render_field(form.username, placeholder=_('Username'), type='text', autofocus=true) }} + {{ macros.render_field(form.password, placeholder=_('Password'), type='password') }} + {{ macros.render_field(form.language) }} {{ macros.render_checkbox_field(form.remember) }} {% endcall %}
diff --git a/burpui/templates/macros.html b/burpui/templates/macros.html index 1bc48019..3313aa3c 100644 --- a/burpui/templates/macros.html +++ b/burpui/templates/macros.html @@ -11,7 +11,6 @@ {% macro render_field(field, label_visible=true) -%}
- {# {% if (field.id != 'csrf_token' or field.type != 'HiddenField' or field.type !='CSRFTokenField') and label_visible %} #} {% if (field.id != 'csrf_token' or field.type != 'HiddenField') %} {% endif %} @@ -88,6 +87,8 @@ {{ render_checkbox_field(f) }} {% elif f.type == 'RadioField' %} {{ render_radio_field(f) }} + {% elif f.type == 'SelectField' %} + {{ render_select_field(f) }} {% else %} {{ render_field(f) }} {% endif %} diff --git a/burpui/templates/servers.html b/burpui/templates/servers.html index 94456efc..467b3e5c 100644 --- a/burpui/templates/servers.html +++ b/burpui/templates/servers.html @@ -4,16 +4,16 @@
{% include "small_topbar.html" %}
-

Servers

+

{{ _('Servers') }}

NameStateLast Backup{{ _('Name') }}{{ _('State') }}{{ _('Last Backup') }}
- + diff --git a/burpui/templates/sidebar.html b/burpui/templates/sidebar.html index 004404f3..813d9db7 100644 --- a/burpui/templates/sidebar.html +++ b/burpui/templates/sidebar.html @@ -11,32 +11,32 @@ {% endif -%} diff --git a/burpui/templates/sideconfig.html b/burpui/templates/sideconfig.html index e79474ed..f0a214b4 100644 --- a/burpui/templates/sideconfig.html +++ b/burpui/templates/sideconfig.html @@ -1,6 +1,6 @@
Name{{ _('Name') }} Clients Status