├── foris ├── tests │ ├── __init__.py │ ├── configs.tar.gz │ ├── init_env.sh │ ├── utils.py │ └── test_data.py ├── middleware │ ├── __init__.py │ ├── backend_data.py │ └── bottle_csrf.py ├── config │ └── pages │ │ ├── __init__.py │ │ ├── about.py │ │ ├── dns.py │ │ ├── time.py │ │ ├── guest.py │ │ ├── lan.py │ │ ├── wan.py │ │ ├── wifi.py │ │ ├── password.py │ │ ├── networks.py │ │ ├── notifications.py │ │ └── guide.py ├── static │ ├── sass │ │ ├── _mixins.sass │ │ ├── ie8.sass │ │ ├── screen.sass │ │ ├── ui │ │ │ ├── _animations.sass │ │ │ ├── _eula.sass │ │ │ ├── _tables.sass │ │ │ ├── _notifications.sass │ │ │ ├── _messages.sass │ │ │ ├── _spinner.sass │ │ │ ├── _treeview.sass │ │ │ ├── _buttons.sass │ │ │ ├── _forms.sass │ │ │ └── _main_nav.sass │ │ ├── fonts.sass │ │ ├── _branding.sass │ │ ├── _typography.sass │ │ ├── _main.sass │ │ ├── _variables.sass │ │ └── pages │ │ │ ├── _login.sass │ │ │ └── _config.sass │ ├── img │ │ ├── fail.png │ │ ├── loader.gif │ │ ├── favicon.ico │ │ ├── icon-help.png │ │ ├── icon-menu.png │ │ ├── success.png │ │ ├── logo-turris.png │ │ ├── icon-opened-lock.png │ │ ├── QR_icon.svg │ │ └── workflow-min.svg │ ├── fonts │ │ ├── Roboto-Bold.woff │ │ ├── fa-brands-400.woff │ │ ├── fa-solid-900.woff │ │ ├── Roboto-Regular.woff │ │ └── fa-regular-400.woff │ ├── .inline-assets │ │ ├── icon-fail.png │ │ ├── icon-refresh.png │ │ ├── icon-success.png │ │ ├── message-error.png │ │ ├── message-info.png │ │ ├── message-success.png │ │ └── message-warning.png │ ├── css │ │ ├── fa-brands.min.css │ │ ├── fa-solid.min.css │ │ ├── fa-regular.min.css │ │ └── vex.css │ ├── config.rb │ └── js │ │ └── contrib │ │ └── html5.js ├── langs │ ├── cs.py │ ├── da.py │ ├── de.py │ ├── fr.py │ ├── hu.py │ ├── nb.py │ ├── pl.py │ ├── ru.py │ ├── it.py │ ├── lt.py │ ├── sk.py │ └── __init__.py ├── utils │ ├── tzdata.pickle2 │ ├── countries.pickle2 │ ├── tzinfo.py │ ├── caches.py │ ├── addresses.py │ ├── dynamic_assets.py │ ├── translators.py │ └── messages.py ├── __init__.py ├── templates │ ├── _foris_version.html.j2 │ ├── config │ │ ├── _message.html.j2 │ │ ├── _no_interface_up_warning.html.j2 │ │ ├── _no_interface_warning.html.j2 │ │ ├── _dhcp_clients_table.html.j2 │ │ ├── main.html.j2 │ │ ├── _wifi_edit.html.j2 │ │ ├── password.html.j2 │ │ ├── about.html.j2 │ │ ├── finished.html.j2 │ │ ├── guest.html.j2 │ │ ├── _networks_button.html.j2 │ │ ├── lan.html.j2 │ │ ├── _wifi_form.html.j2 │ │ ├── _remote_tokens.html.j2 │ │ ├── wan.html.j2 │ │ ├── wifi.html.j2 │ │ ├── notifications.html.j2 │ │ ├── profile.html.j2 │ │ ├── maintenance.html.j2 │ │ └── _connection_test.html.j2 │ ├── _lang_flat.html.j2 │ ├── _turris_device.html.j2 │ ├── _messages.html.j2 │ ├── _field.html.j2 │ ├── _notifications.html.j2 │ ├── index.html.j2 │ ├── includes │ │ └── updater_eula.html.j2 │ ├── javascript │ │ ├── parsley.messages.js.j2 │ │ └── foris.js.j2 │ └── _layout.html.j2 ├── config_handlers │ ├── __init__.py │ ├── backups.py │ ├── base.py │ ├── profile.py │ ├── networks.py │ └── dns.py ├── caches.py ├── ubus │ └── __init__.py ├── config_app.py ├── state.py ├── backend.py └── plugins │ └── __init__.py ├── babel.cfg ├── foris_plugins └── __init__.py ├── examples └── sample_plugin │ ├── foris_plugins │ ├── __init__.py │ └── sample │ │ ├── templates │ │ ├── sample │ │ │ ├── _records.html.j2 │ │ │ └── sample.html.j2 │ │ └── javascript │ │ │ └── sample │ │ │ └── sample.js.j2 │ │ ├── static │ │ ├── sass │ │ │ └── sample.sass │ │ ├── img │ │ │ └── logo-dark.svg │ │ └── js │ │ │ └── sample.js │ │ └── __init__.py │ ├── README.rst │ └── setup.py ├── .gitignore ├── runtests ├── README.md ├── Makefile └── setup.py /foris/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foris/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foris/config/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /foris/static/sass/_mixins.sass: -------------------------------------------------------------------------------- 1 | =rounded-borders 2 | +border-radius(0.4em) 3 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [jinja2: **/templates/**.j2] 2 | encoding = utf-8 3 | 4 | [python: **.py] 5 | -------------------------------------------------------------------------------- /foris_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /foris/static/img/fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/fail.png -------------------------------------------------------------------------------- /foris/langs/cs.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "cs" 4 | iso3 = "cze" 5 | 6 | name = u"Čeština" 7 | -------------------------------------------------------------------------------- /foris/langs/da.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "da" 4 | iso3 = "dan" 5 | 6 | name = u"Dansk" 7 | -------------------------------------------------------------------------------- /foris/langs/de.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "de" 4 | iso3 = "deu" 5 | 6 | name = u"Deutsch" 7 | -------------------------------------------------------------------------------- /foris/langs/fr.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "fr" 4 | iso3 = "fra" 5 | 6 | name = u"Français" 7 | -------------------------------------------------------------------------------- /foris/langs/hu.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "hu" 4 | iso3 = "hun" 5 | 6 | name = u"Magyar" 7 | -------------------------------------------------------------------------------- /foris/langs/nb.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "nb" 4 | iso3 = "nob" 5 | 6 | name = u"Bokmål" 7 | -------------------------------------------------------------------------------- /foris/langs/pl.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "pl" 4 | iso3 = "pol" 5 | 6 | name = u"Polski" 7 | -------------------------------------------------------------------------------- /foris/langs/ru.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "ru" 4 | iso3 = "rus" 5 | 6 | name = u"Русский" 7 | -------------------------------------------------------------------------------- /foris/static/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/loader.gif -------------------------------------------------------------------------------- /foris/tests/configs.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/tests/configs.tar.gz -------------------------------------------------------------------------------- /foris/utils/tzdata.pickle2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/utils/tzdata.pickle2 -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /foris/langs/it.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "it" 4 | iso3 = "ita" 5 | 6 | 7 | name = "Italiano" 8 | -------------------------------------------------------------------------------- /foris/langs/lt.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "lt" 4 | iso3 = "lit" 5 | 6 | name = u"Lietuvių kalba" 7 | -------------------------------------------------------------------------------- /foris/langs/sk.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | iso2 = "sk" 4 | iso3 = "svk" 5 | 6 | name = u"Slovenčina" 7 | -------------------------------------------------------------------------------- /foris/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/favicon.ico -------------------------------------------------------------------------------- /foris/static/img/icon-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/icon-help.png -------------------------------------------------------------------------------- /foris/static/img/icon-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/icon-menu.png -------------------------------------------------------------------------------- /foris/static/img/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/success.png -------------------------------------------------------------------------------- /foris/utils/countries.pickle2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/utils/countries.pickle2 -------------------------------------------------------------------------------- /foris/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | __version__ = "101.1.2" 4 | 5 | BASE_DIR = os.path.dirname(__file__) 6 | -------------------------------------------------------------------------------- /foris/static/img/logo-turris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/logo-turris.png -------------------------------------------------------------------------------- /foris/templates/_foris_version.html.j2: -------------------------------------------------------------------------------- 1 | Foris {{ foris_info.foris_version }} 2 | -------------------------------------------------------------------------------- /foris/static/fonts/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/fonts/Roboto-Bold.woff -------------------------------------------------------------------------------- /foris/static/sass/ie8.sass: -------------------------------------------------------------------------------- 1 | $breakpoint-no-queries: true 2 | $breakpoint-no-query-fallbacks: false 3 | 4 | @import main -------------------------------------------------------------------------------- /foris/static/fonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/fonts/fa-brands-400.woff -------------------------------------------------------------------------------- /foris/static/fonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/fonts/fa-solid-900.woff -------------------------------------------------------------------------------- /foris/static/img/icon-opened-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/img/icon-opened-lock.png -------------------------------------------------------------------------------- /foris/static/sass/screen.sass: -------------------------------------------------------------------------------- 1 | $breakpoint-no-queries: false 2 | $breakpoint-no-query-fallbacks: false 3 | 4 | @import main -------------------------------------------------------------------------------- /foris/static/fonts/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/fonts/Roboto-Regular.woff -------------------------------------------------------------------------------- /foris/static/fonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/fonts/fa-regular-400.woff -------------------------------------------------------------------------------- /foris/static/.inline-assets/icon-fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/icon-fail.png -------------------------------------------------------------------------------- /foris/static/.inline-assets/icon-refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/icon-refresh.png -------------------------------------------------------------------------------- /foris/static/.inline-assets/icon-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/icon-success.png -------------------------------------------------------------------------------- /foris/static/.inline-assets/message-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/message-error.png -------------------------------------------------------------------------------- /foris/static/.inline-assets/message-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/message-info.png -------------------------------------------------------------------------------- /foris/static/.inline-assets/message-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/message-success.png -------------------------------------------------------------------------------- /foris/static/.inline-assets/message-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CZ-NIC/foris/HEAD/foris/static/.inline-assets/message-warning.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.sassc 3 | *.scssc 4 | *.mo 5 | messages.po 6 | foris/static/css/* 7 | foris/static/js/*.min.js 8 | foris/templates/_layout.tpl 9 | /plugins/ 10 | -------------------------------------------------------------------------------- /foris/templates/config/_message.html.j2: -------------------------------------------------------------------------------- 1 | {% if message %} 2 |
3 | {{ message.text|safe }} 4 |
5 | {% endif %} 6 | -------------------------------------------------------------------------------- /foris/templates/config/_no_interface_up_warning.html.j2: -------------------------------------------------------------------------------- 1 | {% if foris_info.device != "turris" and not foris_info.turris_os_version.startswith("3.") %} 2 |
3 | {% trans %}All network interfaces of this network are currently down.{% endtrans %} 4 |
5 | {% endif %} 6 | -------------------------------------------------------------------------------- /foris/templates/_lang_flat.html.j2: -------------------------------------------------------------------------------- 1 | {% trans %}Language{% endtrans %}: 2 | {{ iso2to3.get(lang(), lang()) }} 3 | {% for code in translations %} 4 | {% if code != lang() %} 5 | | {{ iso2to3.get(code, code) }} 6 | {% endif %} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /foris/templates/_turris_device.html.j2: -------------------------------------------------------------------------------- 1 | {% if foris_info.device == "mox" %} 2 | Turris MOX 3 | {% elif foris_info.device == "omnia" %} 4 | Turris OMNIA 5 | {% else %} 6 | Turris 1.X 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /examples/sample_plugin/README.rst: -------------------------------------------------------------------------------- 1 | Foris sample plugin 2 | =================== 3 | This is a sample plugin for foris 4 | 5 | Requirements 6 | ============ 7 | 8 | * foris 9 | * foris-controller-sample-module 10 | 11 | Installation 12 | ============ 13 | 14 | ``python setup.py install`` 15 | 16 | or 17 | 18 | ``pip install .`` 19 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/templates/sample/_records.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% for idx, value in records %} 5 | 6 | {% endfor %} 7 | 8 |
#{% trans %}Value{% endtrans %}
{{ idx }}{{ value }}
9 | -------------------------------------------------------------------------------- /foris/templates/_messages.html.j2: -------------------------------------------------------------------------------- 1 | {% for message in get_messages() %} 2 |
3 | {{ message.text|safe }} 4 |
5 | {% endfor %} 6 | 7 | 12 | -------------------------------------------------------------------------------- /foris/templates/config/_no_interface_warning.html.j2: -------------------------------------------------------------------------------- 1 | {% if foris_info.device != "turris" and not foris_info.turris_os_version.startswith("3.") %} 2 |
3 | {% trans %}This network currently doesn't contain any devices. The changes you make here will become fully functional after you assign a network interface to this network.{% endtrans %} 4 |
5 | {% endif %} 6 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_animations.sass: -------------------------------------------------------------------------------- 1 | @keyframes keyframes_rotate 2 | from 3 | transform: rotate(0deg) 4 | to 5 | transform: rotate(360deg) 6 | 7 | .rotate 8 | animation: keyframes_rotate 2s linear infinite 9 | 10 | @keyframes keyframes_bounce 11 | from 12 | transform: scale(1.0) 13 | to 14 | transform: scale(1.1) 15 | 16 | .bounce 17 | animation: keyframes_bounce 0.5s ease infinite 18 | -------------------------------------------------------------------------------- /foris/static/sass/fonts.sass: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family: 'Roboto' 3 | font-style: normal 4 | font-weight: 400 5 | src: local('Roboto'), local('Roboto-Regular'), url('../fonts/Roboto-Regular.woff') format('woff') 6 | 7 | @font-face 8 | font-family: 'Roboto' 9 | font-style: normal 10 | font-weight: 700 11 | src: local('Roboto Bold'), local('Roboto-Bold'), url('../fonts/Roboto-Bold.woff') format('woff') 12 | -------------------------------------------------------------------------------- /runtests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # to run the tests you'd need to have following packages installed: 4 | # * pytest 5 | # * nose 6 | # * mock 7 | # 8 | # you can install it via pip 9 | 10 | cd foris 11 | if [ "$1" == "coverage" ]; then 12 | shift 13 | nosetests --with-coverage --cover-xml --cover-xml-file=/tmp/foris_coverage.xml --cover-erase --cover-package=. --verbose $@ 14 | else 15 | pytest --verbose $@ 16 | fi 17 | -------------------------------------------------------------------------------- /foris/static/sass/_branding.sass: -------------------------------------------------------------------------------- 1 | @import url('./fonts.css') 2 | 3 | $highlight-color: #00a2e2 4 | $highlight-color-active: lighten(desaturate($highlight-color, 20), 15) 5 | 6 | 7 | // overwrite variables defined in _variables.sass 8 | $sidebar-active-tab-color: $highlight-color 9 | $button-color: $highlight-color 10 | $button-color-active: $highlight-color-active 11 | 12 | $base-font: 'Roboto', Helvetica, Arial, sans-serif 13 | $base-font-size: 16px 14 | $heading-font: $base-font 15 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/static/sass/sample.sass: -------------------------------------------------------------------------------- 1 | /* a simple sass example */ 2 | #records 3 | td, th 4 | padding: 0.3em 0.8em 5 | border-bottom: 1px solid #fff 6 | tr:first-child td 7 | padding-top: 0.5em 8 | tbody tr 9 | &:nth-child(2n) td 10 | background: #f2f2f2 11 | &:hover td 12 | border-bottom: 1px solid #00a2e2 13 | th 14 | font-weight: bold 15 | border-bottom: 1px solid #ddd 16 | margin-bottom: 2px 17 | tbody td 18 | &:nth-child(2), &:nth-child(3), &:nth-child(4) 19 | text-align: right 20 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_eula.sass: -------------------------------------------------------------------------------- 1 | #updater-eula 2 | text-align: left 3 | 4 | #eula-text 5 | text-align: left 6 | display: none // toggled by JS 7 | border: 1px solid #999 8 | padding: 1em 9 | margin-bottom: 1em 10 | 11 | ul 12 | list-style: disc 13 | margin-left: 1.5em 14 | margin-bottom: 1em 15 | 16 | .eula-summary 17 | font-weight: bold 18 | 19 | #updater-eula-form 20 | label 21 | display: block 22 | margin-left: 3em 23 | max-width: 100% 24 | 25 | label[for='field-agreed_1'] 26 | font-weight: bold 27 | 28 | .radio-inputs 29 | width: 100% 30 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_tables.sass: -------------------------------------------------------------------------------- 1 | table 2 | td, th 3 | padding: 0.3em 0.8em 4 | border-bottom: 1px solid #fff 5 | tr:first-child td 6 | padding-top: 0.5em 7 | tbody tr 8 | &:nth-child(2n) td 9 | background: #f2f2f2 10 | &:hover td 11 | border-bottom: 1px solid #00a2e2 12 | th 13 | font-weight: bold 14 | border-bottom: 1px solid #ddd 15 | margin-bottom: 2px 16 | tbody td 17 | text-align: center 18 | tbody tr td:first-child 19 | text-align: left 20 | 21 | .rotate-90 22 | transform: rotate(90deg) 23 | 24 | .indented-item 25 | padding-left: 1.5em 26 | font-size: 80% 27 | -------------------------------------------------------------------------------- /foris/static/css/fa-brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.3.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;src:url(../fonts/fa-brands-400.eot);src:url(../fonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-brands-400.woff2) format("woff2"),url(../fonts/fa-brands-400.woff) format("woff"),url(../fonts/fa-brands-400.ttf) format("truetype"),url(../fonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"} 6 | -------------------------------------------------------------------------------- /foris/static/css/fa-solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../fonts/fa-solid-900.eot);src:url(../fonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-solid-900.woff2) format("woff2"),url(../fonts/fa-solid-900.woff) format("woff"),url(../fonts/fa-solid-900.ttf) format("truetype"),url(../fonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900} 6 | -------------------------------------------------------------------------------- /foris/static/css/fa-regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../fonts/fa-regular-400.eot);src:url(../fonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../fonts/fa-regular-400.woff2) format("woff2"),url(../fonts/fa-regular-400.woff) format("woff"),url(../fonts/fa-regular-400.ttf) format("truetype"),url(../fonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400} 6 | -------------------------------------------------------------------------------- /foris/static/config.rb: -------------------------------------------------------------------------------- 1 | # Require any additional compass plugins here. 2 | require 'breakpoint' 3 | 4 | # use development environment by default 5 | environment = :development 6 | 7 | # Set this to the root of your project when deployed: 8 | http_path = "/" 9 | css_dir = "css" 10 | sass_dir = "sass" 11 | images_dir = "img" 12 | javascripts_dir = "js" 13 | 14 | # You can select your preferred output style here (can be overridden via the command line): 15 | # output_style = (environment == :production) ? :compressed : :nested 16 | 17 | line_comments = (environment == :production) ? false : true 18 | 19 | preferred_syntax = :sass 20 | 21 | sourcemap = (environment == :production) ? false : true -------------------------------------------------------------------------------- /foris/templates/_field.html.j2: -------------------------------------------------------------------------------- 1 | {% if field.hidden %} 2 | {{ field.render()|safe }} 3 | {% else %} 4 |
5 | {{ field.label_tag|safe }} 6 | {{ field.render()|safe }} 7 | {% if field.hint %} 8 | {% trans %}Hint{% endtrans %}: {{ helpers.remove_html_tags(field.hint) }} 9 | 10 | {% endif %} 11 | {% if field.errors %} 12 |
13 | 16 |
17 | {% endif %} 18 |
19 | {% endif %} 20 | -------------------------------------------------------------------------------- /foris/tests/init_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | green () { echo -e "\033[32m$1\033[0m"; } 4 | yellow () { echo -e "\033[33m$1\033[0m"; } 5 | require_python_module () { 6 | # syntax: required_python_module verbose_name module_name 7 | if `python -c "import $2" > /dev/null 2>&1`; then 8 | green "$1 module found." 9 | else 10 | yellow "Installing $1 module." 11 | easy_install $1 12 | fi 13 | } 14 | 15 | if [ ! -x `which easy_install` ]; then 16 | yellow "Installing easy_install." 17 | curl -k https://bootstrap.pypa.io/ez_setup.py | python - --insecure 18 | else 19 | green "Found easy_install." 20 | fi 21 | 22 | require_python_module Webtest webtest 23 | require_python_module mock mock 24 | require_python_module nose nose 25 | require_python_module coverage coverage 26 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_notifications.sass: -------------------------------------------------------------------------------- 1 | .notification 2 | +rounded-borders 3 | color: #000 4 | padding: 1.5em 5 | margin-bottom: 1em 6 | position: relative 7 | 8 | h2 9 | +adjust-font-size-to(18px) 10 | 11 | .buttons 12 | margin-top: 1em 13 | 14 | .dismiss 15 | position: absolute 16 | right: 0.2em 17 | top: 0.2em 18 | text-decoration: none 19 | font-weight: bold 20 | font-size: 200% 21 | color: inherit 22 | 23 | &.restart 24 | background-color: $message-color-error 25 | color: #fff 26 | 27 | .button 28 | background-color: #fff 29 | color: $message-color-error 30 | 31 | &.error 32 | background-color: $message-color-warning 33 | 34 | &.update 35 | background-color: $message-color-success 36 | 37 | &.news 38 | background-color: $message-color-info 39 | 40 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_messages.sass: -------------------------------------------------------------------------------- 1 | .message 2 | +rounded-borders 3 | background: 4 | position: 0.8em 50% 5 | repeat: no-repeat 6 | 7 | color: #000 8 | padding: 1em 1em 1em 3.2em 9 | margin-bottom: 0.5em 10 | 11 | a 12 | color: #000 13 | 14 | &.success 15 | background-image: inline-image('../.inline-assets/message-success.png') 16 | background-color: $message-color-success 17 | 18 | &.info 19 | background-image: inline-image('../.inline-assets/message-info.png') 20 | background-color: $message-color-info 21 | 22 | &.warning 23 | background-image: inline-image('../.inline-assets/message-warning.png') 24 | background-color: $message-color-warning 25 | 26 | &.error 27 | background-image: inline-image('../.inline-assets/message-error.png') 28 | background-color: $message-color-error 29 | 30 | &, & a 31 | color: #fff 32 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_spinner.sass: -------------------------------------------------------------------------------- 1 | @keyframes foris-spinner 2 | from 3 | transform: rotate(0deg) 4 | to 5 | transform: rotate(360deg) 6 | 7 | #foris-spinner-frame 8 | position: fixed 9 | top: 0 10 | left: 0 11 | display: flex 12 | background-color: rgba(0, 0, 0, 0.5) 13 | height: 100vh 14 | width: 100vw 15 | justify-content: center 16 | align-items: center 17 | z-index: 9990 18 | 19 | #foris-spinner 20 | box-sizing: border-box 21 | position: fixed 22 | display: flex 23 | width: 250px 24 | height: 250px 25 | border-radius: 50% 26 | border: 5px solid #fff 27 | border-top-color: #00a2e2 28 | background-color: rgba(0, 0, 0, 0.5) 29 | animation: foris-spinner .6s linear infinite 30 | 31 | #foris-spinner-text 32 | display: flex 33 | justify-content: center 34 | text-align: center 35 | color: white 36 | z-index: 9999 37 | width: 175px 38 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/templates/javascript/sample/sample.js.j2: -------------------------------------------------------------------------------- 1 | // Translation has to be handeled dynamically in jinja2 template here 2 | Foris.sampleMessages = { 3 | chartLabel: "{% trans %}Chart Data{% endtrans %}", 4 | chartTitle: "{% trans %}Example chart{% endtrans %}", 5 | chartTimeAxis: "{% trans %}Time axis{% endtrans %}", 6 | chartValueAxis: "{% trans %}Value axis{% endtrans %}" 7 | } 8 | 9 | // Register on websockets events (just reload chart is used) 10 | Foris.addWsHanlder("sample", (msg) => { 11 | switch(msg.action) { 12 | case "reload_chart": 13 | $.get('{{ url("config_ajax", page_name="sample") }}', {action: "get_records"}) 14 | .done((response) => { 15 | $("#records-table").replaceWith(response); 16 | Foris.update_sample_chart(); 17 | }) 18 | break; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /foris/config_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from .base import BaseConfigHandler 18 | 19 | 20 | __all__ = ["BaseConfigHandler"] 21 | -------------------------------------------------------------------------------- /foris/caches.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Foris - web administration interface for OpenWrt based on NETCONF 3 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | from foris.utils import LazyCache 20 | 21 | 22 | # init caches 23 | lazy_cache = LazyCache() 24 | -------------------------------------------------------------------------------- /foris/templates/config/_dhcp_clients_table.html.j2: -------------------------------------------------------------------------------- 1 |

{% trans %}DHCP clients{% endtrans %}

2 | {% if dhcp_clients %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for dhcp_client in dhcp_clients %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 | 24 |
{% trans %}Expires{% endtrans %}{% trans %}IP Address{% endtrans %}{% trans %}MAC Address{% endtrans %}{% trans %}Hostname{% endtrans %}{% trans %}Active{% endtrans %}
{{ dhcp_client.expires }}{{ dhcp_client.ip }}{{ dhcp_client.mac }}{{ dhcp_client.hostname }}
25 | {% else %} 26 | {% trans %}No DHCP clients found.{% endtrans %} 27 | {% endif %} 28 | -------------------------------------------------------------------------------- /foris/templates/config/main.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% endif %} 7 |
8 |

{{ description|safe }}

9 | {% include '_messages.html.j2' %} 10 | 11 | {% for field in form.active_fields %} 12 | {% include '_field.html.j2' %} 13 | {% endfor %} 14 |
15 | {% trans %}Discard changes{% endtrans %} 16 | 17 |
18 |
19 | {% if is_xhr is not defined %} 20 |
21 | {% endif %} 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/static/img/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon / turris / dark 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /foris/templates/config/_wifi_edit.html.j2: -------------------------------------------------------------------------------- 1 |
2 |

{{ ajax_form.title }}

3 | {% include 'config/_message.html.j2' %} 4 |
5 |

{{ form.sections[0].description|safe }}

6 | 7 | {% include 'config/_wifi_form.html.j2' %} 8 | 9 | 14 |
15 | {% trans %}Close{% endtrans %} 16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /foris/static/img/QR_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | QR coder 8 | Manually edited diagram of qr code 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /foris/templates/config/password.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% endif %} 7 |
8 | {% include '_messages.html.j2' %} 9 | 10 | {% for section in form.sections %} 11 | {% if section.description %} 12 |

{{ section.title|safe }}

13 |

{{ section.description|safe }}

14 | {% endif %} 15 | {% for field in section.active_fields %} 16 | {% include '_field.html.j2' %} 17 | {% endfor %} 18 |
19 | {% endfor %} 20 |
21 | {% trans %}Discard changes{% endtrans %} 22 | 23 |
24 |
25 | {% if is_xhr is not defined %} 26 |
27 | {% endif %} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /foris/ubus/__init__.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from __future__ import absolute_import 18 | 19 | import ubus 20 | import logging 21 | import json 22 | 23 | logger = logging.getLogger("ubus") 24 | 25 | 26 | if not ubus.get_connected(): 27 | logger.debug("Connecting to ubus.") 28 | ubus.connect() 29 | 30 | 31 | def call(obj, func, params): 32 | logger.debug("Calling function '%s'.'%s' with params '%s'" % (obj, func, json.dumps(params))) 33 | return ubus.call(obj, func, params) 34 | -------------------------------------------------------------------------------- /foris/static/sass/_typography.sass: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------- 2 | /* Typography - styling of common textual elements 3 | 4 | +establish-baseline 5 | 6 | body, html 7 | color: $foreground-color 8 | font: 9 | family: $base-font 10 | 11 | h1, h2, h3, h4, h5, h6 12 | font-family: $heading-font 13 | font-weight: bold 14 | margin: 0.4em 0 15 | &:first-child 16 | margin-top: 0 17 | 18 | em 19 | font-style: italic 20 | 21 | strong 22 | font-weight: bold 23 | 24 | h1 25 | +adjust-font-size-to(38px) 26 | 27 | h2 28 | +adjust-font-size-to(32px) 29 | 30 | h3 31 | +adjust-font-size-to(26px) 32 | 33 | h4 34 | +adjust-font-size-to(20px) 35 | 36 | p 37 | margin-bottom: 1.5em 38 | 39 | a 40 | color: $highlight-color 41 | 42 | &:hover 43 | color: $highlight-color-active 44 | 45 | &.disabled 46 | opacity: 0.5 47 | cursor: not-allowed 48 | pointer-events: none 49 | 50 | pre 51 | font-family: "Courier New", Courier, monospace 52 | font-size: 80% 53 | line-height: 110% 54 | margin: 0.5em 0 55 | 56 | ::selection 57 | background-color: $highlight-color 58 | color: #fff 59 | 60 | ::-moz-selection 61 | background-color: $highlight-color 62 | color: #fff 63 | 64 | .minor-text 65 | +adjust-font-size-to(12px) 66 | -------------------------------------------------------------------------------- /foris/templates/_notifications.html.j2: -------------------------------------------------------------------------------- 1 |
2 | {% if notifications|length > 0 %} 3 |
4 | {% set ns = namespace(show_dismiss_all=false) %} 5 | {% for notification in notifications %} 6 |
7 |

{{ helpers.make_notification_title(notification)|safe }}

8 | {{ helpers.transform_notification_message(notification["msg"])|safe }} 9 | {% if notification["severity"] == "restart" %} 10 | 13 | {% else %} 14 | {% set ns.show_dismiss_all = True %} 15 | × 16 | {% endif %} 17 |
18 | {% endfor %} 19 |
20 | 21 |
22 | 23 |
24 | {% else %} 25 | {% trans %}No new messages.{% endtrans %} 26 | {% endif %} 27 |
28 | -------------------------------------------------------------------------------- /foris/tests/utils.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output, STDOUT 2 | 3 | 4 | def uci_get(path, config_directory=None): 5 | args = ["uci"] 6 | if config_directory: 7 | args.extend(["-c", config_directory]) 8 | args.extend(["get", path]) 9 | # crop newline at the end 10 | return check_output(args)[:-1] 11 | 12 | 13 | def uci_is_empty(path, config_directory=None): 14 | args = ["uci"] 15 | if config_directory: 16 | args.extend(["-c", config_directory]) 17 | args.extend(["get", path]) 18 | args.append("; exit 0") 19 | return (check_output(" ".join(args), stderr=STDOUT, shell=True)) == "uci: Entry not found\n" 20 | 21 | 22 | def uci_set(path, value, config_directory=None): 23 | args = ["uci"] 24 | if config_directory: 25 | args.extend(["-c", config_directory]) 26 | args.extend(["set", "%s=%s" % (path, value)]) 27 | output = check_output(args) # CalledProcessError is raised on error 28 | if output != "": 29 | raise RuntimeWarning("uci set returned unexpected output: '%s'" % output) 30 | return True 31 | 32 | 33 | def uci_commit(config_directory=None): 34 | args = ["uci"] 35 | if config_directory: 36 | args.extend(["-c", config_directory]) 37 | args.append("commit") 38 | check_output(args) # CalledProcessError is raised on error 39 | return True 40 | -------------------------------------------------------------------------------- /foris/config/pages/about.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | from .base import ConfigPageMixin, JoinedPages 20 | 21 | from foris.state import current_state 22 | from foris.utils.translators import gettext_dummy as gettext 23 | 24 | 25 | class AboutConfigPage(ConfigPageMixin): 26 | slug = "about" 27 | menu_order = 99 28 | 29 | template = "config/about" 30 | template_type = "jinja2" 31 | userfriendly_title = gettext("About") 32 | 33 | def render(self, **kwargs): 34 | data = current_state.backend.perform("about", "get") 35 | # process dates etc 36 | return self.default_template(data=data, **kwargs) 37 | -------------------------------------------------------------------------------- /foris/langs/__init__.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import pkgutil 19 | 20 | 21 | # english is default 22 | DEFAULT_LANGUAGE = "en" 23 | iso2to3 = {"en": "eng"} 24 | translation_names = {"en": "English"} 25 | translations = [] 26 | 27 | for loader, name, _ in pkgutil.iter_modules([os.path.dirname(__file__)]): 28 | module = loader.find_module(name).load_module(name) 29 | translations.append(module.iso2) 30 | iso2to3[module.iso2] = module.iso3 31 | translation_names[module.iso2] = module.name 32 | 33 | translations.sort() 34 | translations.insert(0, DEFAULT_LANGUAGE) # english first 35 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_treeview.sass: -------------------------------------------------------------------------------- 1 | .treeview ul, 2 | .treeview li 3 | padding: 0 4 | margin: 0 5 | list-style: none 6 | 7 | 8 | .treeview input 9 | position: absolute 10 | opacity: 0 11 | 12 | 13 | .treeview input + label ~ ul 14 | margin-left: 32px 15 | 16 | 17 | .treeview input ~ ul 18 | display: none 19 | 20 | 21 | .treeview label, 22 | .treeview label::before 23 | cursor: pointer 24 | 25 | 26 | .treeview input:disabled + label 27 | cursor: default 28 | opacity: .6 29 | 30 | 31 | .treeview input:checked:not(:disabled) ~ ul 32 | display: block 33 | 34 | 35 | .treeview label:before 36 | font-family: FontAwesome 37 | font-size: 1.1em 38 | margin-right: 0.5em 39 | 40 | 41 | .treeview label, 42 | .treeview a, 43 | .treeview label:before 44 | height: 16px 45 | line-height: 16px 46 | 47 | 48 | .treeview label 49 | background-position: 18px 0 50 | 51 | 52 | .treeview label:before 53 | content: "+" 54 | vertical-align: middle 55 | 56 | 57 | .treeview input:checked + label:before 58 | content: "\2013" 59 | 60 | 61 | 62 | /* webkit adjacent element selector bugfix */ 63 | @media screen and (-webkit-min-device-pixel-ratio: 0) 64 | .treeview 65 | -webkit-animation: webkit-adjacent-element-selector-bugfix infinite 1s 66 | 67 | @-webkit-keyframes webkit-adjacent-element-selector-bugfix 68 | from 69 | padding: 0 70 | to 71 | padding: 0 -------------------------------------------------------------------------------- /foris/templates/config/about.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 |
5 | {% include '_messages.html.j2' %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% if data['os_branch']['mode'] == 'version' %} 19 | 20 | {% else %} 21 | 22 | {% endif %} 23 | 24 | {% if data['os_branch']['mode'] == 'branch' %} 25 | 26 | 27 | 28 | 29 | {% endif %} 30 | 31 | 32 | 33 | 34 | 35 |
{% trans %}Device{% endtrans %}{{ data['model'] }}
{% trans %}Serial number{% endtrans %}{{ data['serial']|int(base=16) }}
{% trans %}Turris OS version{% endtrans %}{{ data['os_version'] }} ({{ data['os_branch']['value'] }}){{ data['os_version'] }}
{% trans %}Turris OS branch{% endtrans %}{{ data['os_branch']['value'] }}
{% trans %}Kernel version{% endtrans %}{{ data['kernel'] }}
36 | 37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /foris/static/sass/_main.sass: -------------------------------------------------------------------------------- 1 | /*! 2 | * Foris - web administration interface for OpenWrt based on NETCONF 3 | * Copyright (C) 2013 CZ.NIC, z.s.p.o. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // third party imports 20 | @import compass/css3 21 | @import compass/reset 22 | @import compass/typography/vertical_rhythm 23 | @import compass/utilities/general/clearfix 24 | @import breakpoint 25 | 26 | @import variables 27 | @import mixins 28 | @import branding 29 | @import typography 30 | 31 | @import layout 32 | 33 | @import ui/buttons 34 | @import ui/main_nav 35 | @import ui/forms 36 | @import ui/messages 37 | @import ui/notifications 38 | @import ui/treeview 39 | @import ui/tables 40 | 41 | @import pages/login 42 | @import pages/config 43 | 44 | @import ui/eula 45 | @import ui/spinner 46 | @import ui/animations 47 | -------------------------------------------------------------------------------- /foris/config/pages/dns.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | 19 | 20 | from foris.config_handlers import dns 21 | from foris.state import current_state 22 | 23 | 24 | from .base import ConfigPageMixin 25 | 26 | 27 | class DNSConfigPage(ConfigPageMixin, dns.DNSHandler): 28 | slug = "dns" 29 | menu_order = 19 30 | 31 | template = "config/dns" 32 | template_type = "jinja2" 33 | 34 | def _action_check_connection(self): 35 | return current_state.backend.perform( 36 | "wan", "connection_test_trigger", {"test_kinds": ["dns"]} 37 | ) 38 | 39 | def call_ajax_action(self, action): 40 | if action == "check-connection": 41 | return self._action_check_connection() 42 | raise ValueError("Unknown AJAX action.") 43 | -------------------------------------------------------------------------------- /foris/utils/tzinfo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | 4 | 5 | # Load directory of countries country_code: country_name 6 | countries = pickle.load(open(os.path.join(os.path.dirname(__file__), "countries.pickle2"), "rb")) 7 | 8 | # Load TZ data tuple of tuples: (luci_tz, country, city, zoneinfo) 9 | tz_data = pickle.load(open(os.path.join(os.path.dirname(__file__), "tzdata.pickle2"), "rb")) 10 | 11 | 12 | # Set of existing regions 13 | regions = set(x[0].split("/")[0] for x in tz_data) 14 | 15 | 16 | def timezones_in_region(region): 17 | """List timezones in a region. Returns filtered tz_data items.""" 18 | return [e for e in tz_data if e[0].startswith(region)] 19 | 20 | 21 | def timezones_in_region_and_country(region, country): 22 | """List timezones in a region and country. Returns filtered tz_data items.""" 23 | return [e for e in tz_data if e[0].startswith(region) and e[1] == country] 24 | 25 | 26 | def countries_in_region(region): 27 | """List countries in a region. Returns set of country codes.""" 28 | return {e[1] for e in tz_data if e[0].startswith(region)} 29 | 30 | 31 | def get_country_for_tz(tz): 32 | """Get country code for a timezone identifier.""" 33 | filtered = [e for e in tz_data if e[0] == tz] 34 | return filtered[0][1] if filtered else None 35 | 36 | 37 | def get_zoneinfo_for_tz(tz): 38 | """Get zoneinfo record for a timezone identifier.""" 39 | filtered = [e for e in tz_data if e[0] == tz] 40 | return filtered[0][3] if filtered else None 41 | -------------------------------------------------------------------------------- /foris/utils/caches.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import logging 19 | 20 | 21 | logger = logging.getLogger("foris.caches") 22 | 23 | 24 | class SimpleCache(dict): 25 | def __init__(self, name): 26 | self.name = name 27 | 28 | def clear(self): 29 | super(SimpleCache, self).clear() 30 | logger.debug("Cache %s cleared.", self.name) 31 | 32 | def __setitem__(self, key, value): 33 | super(SimpleCache, self).__setitem__(key, value) 34 | logger.debug("Cache %s: '%s' -> '%s'.", self.name, key, value) 35 | 36 | 37 | class PerRequest(object): 38 | """ 39 | Ceched per request 40 | """ 41 | 42 | backend_data = SimpleCache("backend_data") 43 | 44 | 45 | per_request = PerRequest 46 | -------------------------------------------------------------------------------- /foris/static/sass/_variables.sass: -------------------------------------------------------------------------------- 1 | $base-font: Helvetica, Arial, sans-serif 2 | $base-font-size: 16px 3 | $heading-font: $base-font 4 | 5 | /* colors 6 | $background-color: #fff 7 | $foreground-color: #000 8 | 9 | // Turris colors as default 10 | $highlight-color: #ce1126 11 | $highlight-color-active: lighten(desaturate($highlight-color, 20), 15) 12 | 13 | $sidebar-background-color: #f2f2f2 14 | $sidebar-active-tab-color: $highlight-color 15 | $sidebar-foreground-color: #000 16 | $sidebar-foreground-disabled-color: #808080 17 | 18 | $link-color: #004477 19 | $stroke-color: #888 20 | 21 | $info-color: #3ab54a 22 | $error-color: #cc0000 23 | $loading-color: #4899de 24 | $hint-color: #cdf 25 | 26 | /* messages background colors 27 | $message-color-success: #30c215 28 | $message-color-info: #00a2e2 29 | $message-color-warning: #fc0 30 | $message-color-error: #ce1126 31 | 32 | /* buttons 33 | $button-height: 2em 34 | $button-height-touch-multiplier: 1.5 // multiplier of height in mobile layout 35 | $button-color: $highlight-color 36 | $button-color-active: $highlight-color-active 37 | $button-color-grayed: #808080 38 | 39 | /* layout variables 40 | $sidebar-width-ratio: 0.25 41 | $min-page-width: 60em 42 | $content-max-width: 52em 43 | 44 | // values autocalculated from sidebar width ratio 45 | $sidebar-width: percentage($sidebar-width-ratio) 46 | $content-width: percentage(1 - $sidebar-width-ratio) 47 | $sidebar-content-width: $sidebar-width-ratio * $min-page-width 48 | 49 | /* breakpoints 50 | $breakpoints: add-breakpoint('not-mobile', (screen 1000px)) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Foris 2 | ====== 3 | 4 | Foris is Remote Uci - it is a web GUI for configuration of system via Nuci, 5 | i.e. Using Netconf. 6 | 7 | Nowadays, Foris is in early stage of development and almost everything in it 8 | is a possible subject to change, thus there's no guaranteed API stability 9 | and all the parts should be used externally only with extreme caution, if ever... 10 | 11 | 12 | Internal redirects to reForis 13 | ----------------------------- 14 | 15 | Redirects from Foris page to reForis page. 16 | 17 | Redirects are defined in `*.csv` files inside `/usr/share/foris/reforis-links/`. 18 | File have two columns - `from url` and `to url`. 19 | 20 | Requested url's path ending is checked and if it matches a first column, 21 | message is displayed with a link to the second column's url. 22 | 23 | For example: 24 | ``` 25 | "/config/my-plugin/","/reforis/my-plugin" 26 | ``` 27 | 28 | Note that: 29 | * trailing slash in first column is important for successful matching 30 | * `*.csv` files should be a part of the reforis or reforis plugin package (so the message is displayed only if target is present). 31 | 32 | External links 33 | -------------- 34 | 35 | Redirects from Foris menu to external link. 36 | 37 | Redirects are defined in `*.csv` files inside `usr/share/foris/external-links/`. 38 | The file have up to four columns - page slug, menu title, target url and menu item order (optional). 39 | 40 | For example: 41 | ``` 42 | "openvpn-client","OpenVPN client","/reforis/openvpn-client/", 43 | "sentinel","Sentinel","/reforis/sentinel/",30 44 | ``` 45 | -------------------------------------------------------------------------------- /foris/config_app.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Foris - web administration interface for OpenWrt based on NETCONF 3 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | # builtins 19 | import logging 20 | 21 | # 3rd party 22 | import bottle 23 | 24 | # local 25 | from foris import __version__ 26 | from foris.config import init_app as init_app_config, top_index 27 | from foris.common_app import prepare_common_app 28 | 29 | 30 | logger = logging.getLogger("foris.config") 31 | 32 | 33 | def prepare_config_app(args): 34 | """ 35 | Prepare Foris main application - i.e. apply CLI arguments, mount applications, 36 | install hooks and middleware etc... 37 | 38 | :param args: arguments received from ArgumentParser.parse_args(). 39 | :return: bottle.app() for Foris 40 | """ 41 | return prepare_common_app(args, "config", init_app_config, top_index, logger) 42 | -------------------------------------------------------------------------------- /foris/config/pages/time.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | 19 | 20 | from foris.config_handlers import misc 21 | from foris.state import current_state 22 | 23 | from .base import ConfigPageMixin 24 | 25 | 26 | class TimeConfigPage(ConfigPageMixin, misc.UnifiedTimeHandler): 27 | """ Timezone / Time configuration """ 28 | 29 | slug = "time" 30 | menu_order = 18 31 | 32 | template = "config/time" 33 | template_type = "jinja2" 34 | 35 | def render(self, **kwargs): 36 | kwargs["ntp_servers"] = self.backend_data["time_settings"]["ntp_servers"] 37 | return super().render(**kwargs) 38 | 39 | def call_ajax_action(self, action): 40 | if action == "ntpdate-trigger": 41 | return current_state.backend.perform("time", "ntpdate_trigger") 42 | raise ValueError("Unknown AJAX action.") 43 | -------------------------------------------------------------------------------- /foris/templates/config/finished.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% endif %} 7 |
8 |

9 | {% trans -%} 10 | Congratulations you've successfully reached the end of this guide. 11 | Once you leave this guide you'll be granted access to the 12 | full configuration interface of this device. 13 | {%- endtrans %} 14 |

15 |

16 | {% trans -%} 17 | To further improve your security consider enabling data 18 | collect (start by selecting it in updater tab). This will allow 19 | you to be part of our security research to discover new 20 | attackers and it will also give you access to dynamic updates 21 | to your firewall to block all already known attackers. 22 | {%- endtrans %} 23 |

24 | {% include '_messages.html.j2' %} 25 | 26 |
27 | 28 |
29 |
30 | {% if is_xhr is not defined %} 31 |
32 | {% endif %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /foris/templates/config/guest.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% if interface_count < 1 %} 7 | {% include "config/_no_interface_warning.html.j2" %} 8 | {% elif interface_up_count < 1 %} 9 | {% include "config/_no_interface_up_warning.html.j2" %} 10 | {% endif %} 11 | {% endif %} 12 | {% include '_messages.html.j2' %} 13 |
14 |

{{ description|safe }}

15 | {% if form.errors %} 16 |

{{ form.render_errors()|safe }}

17 | {% endif %} 18 | 19 | {% for field in form.active_fields %} 20 | {% include '_field.html.j2' %} 21 | {% endfor %} 22 |
23 | {% trans %}Discard changes{% endtrans %} 24 | 25 |
26 |
27 | {% if is_xhr is not defined %} 28 |
29 | {% if form.current_data["guest_dhcp_enabled"] %} 30 | {% include "config/_dhcp_clients_table.html.j2" %} 31 | {% endif %} 32 | 35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | COMPILED_CSS = $(wildcard foris/static/css/*) 18 | 19 | COMPILED_L10N = $(wildcard foris/locale/*/LC_MESSAGES/*.mo) 20 | 21 | SASS_COMPILER = compass compile -s compressed -e production 22 | 23 | 24 | all: sass 25 | 26 | # target: sass - Compile SASS files to CSS files using SASS/Compass compiler. 27 | sass: 28 | @cd foris/static/; \ 29 | echo '-- Running compass $<';\ 30 | $(SASS_COMPILER) 31 | @echo 32 | 33 | # target: clean - Remove all compiled CSS and localization files. 34 | clean: 35 | rm -rf $(COMPILED_CSS) $(COMPILED_L10N) $(TPL_FILES) 36 | 37 | # target: help - Show this help. 38 | help: 39 | @egrep "^# target:" Makefile 40 | 41 | # target: messages - extract translations from sources 42 | messages: 43 | ./setup.py extract_messages --no-location -o foris/locale/foris.pot -F babel.cfg 44 | ./setup.py update_catalog -D foris -i foris/locale/foris.pot -d foris/locale/ 45 | 46 | .PHONY: all sass messages 47 | -------------------------------------------------------------------------------- /foris/config/pages/guest.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from datetime import datetime 19 | 20 | from foris.config_handlers import guest 21 | 22 | from .base import ConfigPageMixin 23 | 24 | 25 | class GuestConfigPage(ConfigPageMixin, guest.GuestHandler): 26 | slug = "guest" 27 | menu_order = 17 28 | 29 | template = "config/guest" 30 | template_type = "jinja2" 31 | 32 | def render(self, **kwargs): 33 | kwargs["dhcp_clients"] = self.backend_data["dhcp"]["clients"] 34 | kwargs["interface_count"] = self.backend_data["interface_count"] 35 | kwargs["interface_up_count"] = self.backend_data["interface_up_count"] 36 | for client in kwargs["dhcp_clients"]: 37 | if client["expires"] > 0: 38 | client["expires"] = datetime.utcfromtimestamp(client["expires"]).strftime( 39 | "%Y-%m-%d %H:%M" 40 | ) 41 | else: 42 | client["expires"] = "N/A" 43 | 44 | return super(GuestConfigPage, self).render(**kwargs) 45 | -------------------------------------------------------------------------------- /foris/config/pages/lan.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from datetime import datetime 19 | 20 | from foris.config_handlers import lan 21 | 22 | from .base import ConfigPageMixin 23 | 24 | 25 | class LanConfigPage(ConfigPageMixin, lan.LanHandler): 26 | slug = "lan" 27 | menu_order = 16 28 | 29 | template = "config/lan" 30 | template_type = "jinja2" 31 | 32 | def render(self, **kwargs): 33 | kwargs["dhcp_clients"] = self.backend_data["mode_managed"]["dhcp"]["clients"] 34 | kwargs["interface_count"] = self.backend_data["interface_count"] 35 | kwargs["interface_up_count"] = self.backend_data["interface_up_count"] 36 | for client in kwargs["dhcp_clients"]: 37 | if client["expires"] > 0: 38 | client["expires"] = datetime.utcfromtimestamp(client["expires"]).strftime( 39 | "%Y-%m-%d %H:%M" 40 | ) 41 | else: 42 | client["expires"] = "N/A" 43 | 44 | return super(LanConfigPage, self).render(**kwargs) 45 | -------------------------------------------------------------------------------- /examples/sample_plugin/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import copy 4 | 5 | from setuptools import setup 6 | from setuptools.command.build_py import build_py 7 | 8 | 9 | class BuildCmd(build_py): 10 | def run(self): 11 | # build foris plugin files 12 | from foris_plugins_distutils import build 13 | 14 | cmd = build(copy.copy(self.distribution)) 15 | cmd.ensure_finalized() 16 | cmd.run() 17 | 18 | # build package 19 | build_py.run(self) 20 | 21 | 22 | setup( 23 | name="Foris Sample Plugin", 24 | version="0", 25 | description="Sample plugin for foris web interface", 26 | author="CZ.NIC, z. s. p. o.", 27 | author_email="stepan.henek@nic.cz", 28 | url="https://gitlab.labs.nic.cz/turris/foris/foris-sample-plugin/", 29 | license="GPL-3.0", 30 | install_requires=["foris", "jinja2"], 31 | setup_requires=["babel", "libsass", "foris_plugins_distutils"], 32 | provides=["foris_plugins.sample"], 33 | packages=["foris_plugins.sample"], 34 | package_data={ 35 | "": [ 36 | "templates/**", 37 | "templates/**/*", 38 | "templates/javascript/**", 39 | "templates/javascript/**/*", 40 | "locale/**/LC_MESSAGES/*.mo", 41 | "static/css/*.css", 42 | "static/fonts/*", 43 | "static/img/*", 44 | "static/js/*.js", 45 | "static/js/contrib/*", 46 | ] 47 | }, 48 | namespace_packages=["foris_plugins"], 49 | cmdclass={"build_py": BuildCmd}, # modify build_py to build the foris files as well 50 | dependency_links=[ 51 | "git+https://gitlab.labs.nic.cz/turris/foris/foris-plugins-distutils.git" 52 | "#egg=foris_plugins_distutils" 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /foris/templates/config/_networks_button.html.j2: -------------------------------------------------------------------------------- 1 |

2 | {% trans %}module{% endtrans %}: {% if port.module_id == 0 %}{% trans %}main{% endtrans %}{% else %}{{ port.module_id }}{% endif %}
3 | {% trans %}type{% endtrans %}: {{ port.type }}
4 | {% trans %}bus{% endtrans %}: {{ port.bus }}
5 | {% if port.type == "wifi" %}{% trans %}SSID{% endtrans %}: {{ port.ssid }}
{% endif %} 6 | {% trans %}state{% endtrans %}: {% if port.state == "up" %}{% trans %}alive{% endtrans%}{% else %}{% trans %}off{% endtrans %}{% endif %}
7 | {% if port.state == "up" and port.link_speed > 0 %} 8 | {% trans %}link speed{% endtrans %}: {{ port.link_speed }} Mbit 9 | {% endif %} 10 |

11 | 29 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/templates/sample/sample.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {# this block will be inserted into the main layout #} 4 | {% block config_base %} 5 | 6 | {# The the main form can be rendered using ajax. And it might be necessary to skip some parts for that #} 7 | {% if is_xhr is not defined %} 8 |
9 | {% endif %} 10 | {# Just for fun render some images #} 11 | {% for _ in range(20) %} 12 | 13 | {% endfor %} 14 | {# Render messages that config store operation passed/failed #} 15 | {% include '_messages.html.j2' %} 16 |

{% trans %}Some generic description what this plugin does.{% endtrans %}

17 | 18 |
19 | 20 | {# For the most cases this is how the forms are rendered #} 21 | {% for field in form.active_fields %} 22 | {% include '_field.html.j2' %} 23 | {% endfor %} 24 | 25 |
26 |

{% trans %}Records{% endtrans %}

27 |
28 | {# Example to render some data #} 29 | {% include 'sample/_records.html.j2' %} 30 |
31 |

{% trans %}Chart{% endtrans %}

32 |

{% trans %}To redraw the chart using websockets just run the following command:{% endtrans %}

33 |
foris-notify-wrapper -n -m sample -a reload_chart '{}'
34 |
35 | {# Example chart in Chart.js #} 36 |
37 | 38 | {% if is_xhr is not defined %} 39 |
40 | {% endif %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /foris/templates/config/lan.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% if interface_count < 1 %} 7 | {% include "config/_no_interface_warning.html.j2" %} 8 | {% elif interface_up_count < 1 %} 9 | {% include "config/_no_interface_up_warning.html.j2" %} 10 | {% endif %} 11 | {% endif %} 12 | {% include '_messages.html.j2' %} 13 |
14 |

{{ description|safe }}

15 | {% if form.errors %} 16 |

{{ form.render_errors()|safe }}

17 | {% endif %} 18 | 19 | {% for field in form.active_fields %} 20 | {% include '_field.html.j2' %} 21 | {% endfor %} 22 |
23 | {% trans %}Discard changes{% endtrans %} 24 | 25 |
26 |
27 | {% if is_xhr is not defined %} 28 | {% if form.current_data["mode"] == "unmanaged" and form.current_data["client_proto_4"] != "none" %} 29 | {% set ipv6_test = False %} 30 | {% include "config/_connection_test.html.j2" %} 31 | {% endif %} 32 | {% if form.current_data["mode"] == "managed" and form.current_data["router_dhcp_enabled"] %} 33 | {% include "config/_dhcp_clients_table.html.j2" %} 34 | {% endif %} 35 |
36 | 39 | {% endif %} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /foris/templates/config/_wifi_form.html.j2: -------------------------------------------------------------------------------- 1 | {% for section in form.sections %} 2 | {% if section.active_fields %} 3 |
4 |

{{ section.title }}

5 | {% if section.description %} 6 |

{{ section.description }}

7 | {% endif %} 8 | {% for field in section.active_fields %} 9 | {% set radio_number = field.name.strip("abcdefghijklmnopqrstuvwxyz-_") %} 10 | {% include '_field.html.j2' %} 11 | {% if field.name.endswith("-hwmode") and form.band_conflict %} 12 |
13 |
14 |
    15 |
  • {% trans %}You set both WiFi cards to the same band. This usually does not make sense, could make cards interfere with each other (thus hinder performance) and might violate local regulation as well.{% endtrans %}
  • 16 |
17 |
18 |
19 | {% endif %} 20 | {% if field.name.endswith("-password") %} 21 |
22 | {% trans %}QR code{% endtrans %} 23 |
24 |
25 | {% endif %} 26 | {% if field.name.endswith("guest_password") %} 27 |
28 | {% trans %}QR code{% endtrans %} 29 |
30 |
31 | {% endif %} 32 | {% endfor %} 33 | {% endif %} 34 | {% endfor %} 35 | -------------------------------------------------------------------------------- /foris/config/pages/wan.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import bottle 19 | 20 | from foris.config_handlers import wan 21 | from foris.state import current_state 22 | 23 | from .base import ConfigPageMixin, JoinedPages 24 | 25 | 26 | class WanConfigPage(ConfigPageMixin, wan.WanHandler): 27 | slug = "wan" 28 | menu_order = 15 29 | 30 | template = "config/wan" 31 | template_type = "jinja2" 32 | 33 | def render(self, **kwargs): 34 | kwargs["interface_count"] = self.backend_data["interface_count"] 35 | kwargs["interface_up_count"] = self.backend_data["interface_up_count"] 36 | kwargs["wan_status"] = self.status_data 37 | return super(WanConfigPage, self).render(**kwargs) 38 | 39 | def _action_check_connection(self, ipv6=True): 40 | return current_state.backend.perform( 41 | "wan", "connection_test_trigger", {"test_kinds": ["ipv4", "ipv6"] if ipv6 else ["ipv4"]} 42 | ) 43 | 44 | def call_ajax_action(self, action): 45 | if action == "check-connection": 46 | ipv6_type = bottle.request.GET.get("ipv6_type") 47 | return self._action_check_connection(ipv6_type != "none") 48 | raise ValueError("Unknown AJAX action.") 49 | -------------------------------------------------------------------------------- /foris/templates/config/_remote_tokens.html.j2: -------------------------------------------------------------------------------- 1 | {% if tokens %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for token in tokens %} 12 | 13 | 14 | {% if token['status'] == 'revoked' %} 15 | 16 | {% elif token['status'] == 'generating' %} 17 | 18 | {% elif token['status'] == 'valid' %} 19 | 20 | {% else %} 21 | 22 | {% endif %} 23 | 36 | 37 | {% endfor %} 38 | 39 |
{% trans %}Token{% endtrans %}{% trans %}Status{% endtrans %}
{{ token["name"] }}{{ token['status'] }} 24 |
25 | 26 | 27 | 28 | {% if token['status'] == 'valid' %} 29 | 30 | {% endif %} 31 | {% if token['status'] != 'revoked' and token['status'] != 'generating' %} 32 | 33 | {% endif %} 34 |
35 |
40 | {% else %} 41 |
42 | {% endif %} 43 | -------------------------------------------------------------------------------- /foris/config_handlers/backups.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import base64 18 | 19 | from foris import fapi 20 | from foris.form import File 21 | from foris.state import current_state 22 | from foris.utils.translators import gettext_dummy as gettext, _ 23 | 24 | from .base import BaseConfigHandler 25 | 26 | 27 | class MaintenanceHandler(BaseConfigHandler): 28 | userfriendly_title = gettext("Maintenance") 29 | 30 | def get_form(self): 31 | maintenance_form = fapi.ForisForm("maintenance", self.data) 32 | maintenance_main = maintenance_form.add_section( 33 | name="restore_backup", title=_(self.userfriendly_title) 34 | ) 35 | maintenance_main.add_field(File, name="backup_file", label=_("Backup file"), required=True) 36 | 37 | def maintenance_form_cb(data): 38 | data = current_state.backend.perform( 39 | "maintain", 40 | "restore_backup", 41 | {"backup": base64.b64encode(data["backup_file"].file.read()).decode("utf-8")}, 42 | ) 43 | return "save_result", {"result": data["result"]} 44 | 45 | maintenance_form.add_callback(maintenance_form_cb) 46 | return maintenance_form 47 | -------------------------------------------------------------------------------- /foris/templates/config/wan.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% if interface_count < 1 %} 7 | {% include "config/_no_interface_warning.html.j2" %} 8 | {% elif not wan_status.up %} 9 | {% if wan_status.proto == "ppoe" %} 10 |
11 | {% trans %}You WAN configuration is probably not correct or your WAN interface hasn't been properly initialized yet.{% endtrans %} 12 |
13 | {% else %} 14 |
15 | {% trans %}WAN port has no link or it hasn't been configured yet. Your internet connection probably won't work.{% endtrans %} 16 |
17 | {% endif %} 18 | {% elif interface_up_count < 1 %} 19 | {% include "config/_no_interface_up_warning.html.j2" %} 20 | {% endif %} 21 | {% endif %} 22 |
23 |

{{ description|safe }}

24 | {% include '_messages.html.j2' %} 25 | 26 | {% for field in form.active_fields %} 27 | {% include '_field.html.j2' %} 28 | {% endfor %} 29 |
30 | {% trans %}Discard changes{% endtrans %} 31 | 32 |
33 |
34 | {% if is_xhr is not defined %} 35 | {% set ipv6_test = form.current_data["wan6_proto"] != "none" %} 36 | {% include "config/_connection_test.html.j2" %} 37 |
38 | 41 | {% endif %} 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /foris/config/pages/wifi.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | import bottle 20 | 21 | from foris.config_handlers import wifi 22 | from foris.state import current_state 23 | from foris.utils import messages 24 | from foris.utils.routing import reverse 25 | from foris.utils.translators import _ 26 | 27 | from .base import ConfigPageMixin 28 | 29 | 30 | class WifiConfigPage(ConfigPageMixin, wifi.WifiHandler): 31 | slug = "wifi" 32 | menu_order = 20 33 | 34 | template = "config/wifi" 35 | template_type = "jinja2" 36 | 37 | def _action_reset(self): 38 | 39 | if bottle.request.method != "POST": 40 | messages.error(_("Wrong HTTP method.")) 41 | bottle.redirect(reverse("config_page", page_name="wifi")) 42 | 43 | data = current_state.backend.perform("wifi", "reset") 44 | if "result" in data and data["result"] is True: 45 | messages.success(_("Wi-Fi reset was successful.")) 46 | else: 47 | messages.error(_("Failed to perform Wi-Fi reset.")) 48 | 49 | bottle.redirect(reverse("config_page", page_name="wifi")) 50 | 51 | def call_action(self, action): 52 | if action == "reset": 53 | self._action_reset() 54 | raise ValueError("Unknown action.") 55 | 56 | def save(self, *args, **kwargs): 57 | super(WifiConfigPage, self).save(no_messages=True, *args, **kwargs) 58 | return self.form.callback_results.get("result", None) 59 | -------------------------------------------------------------------------------- /foris/templates/config/wifi.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | 5 | {% if is_xhr is not defined %} 6 |
7 | {% include '_messages.html.j2' %} 8 | {% endif %} 9 | {% if not form or form.active_fields|length == 0 %} 10 |
{% trans %}We were unable to detect any wireless cards in your router.{% endtrans %}
11 | {% else %} 12 |

{{ description|safe }}

13 |
14 | 15 | {% include 'config/_wifi_form.html.j2' %} 16 | 17 | 22 |
23 | {% trans %}Discard changes{% endtrans %} 24 | 25 |
26 |
27 | {% endif %} 28 |
29 |
30 |
31 | 32 |
33 |

{% trans %}If a number of wireless cards doesn't match, you may try to reset the Wi-Fi settings. Note that this will remove the current Wi-Fi configuration and restore the default values.{% endtrans %}

34 | 35 |
36 |
37 |
38 | {% if is_xhr is not defined %} 39 |
40 | {% endif %} 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /foris/config_handlers/base.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | import logging 17 | 18 | from foris.utils.addresses import mask_to_prefix_4 19 | 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | DEFAULT_GUEST_IP = "10.111.222.1" 25 | DEFAULT_GUEST_NETWORK = "10.111.222.0" 26 | DEFAULT_GUEST_MASK = "255.255.255.0" 27 | DEFAULT_GUEST_PREFIX = mask_to_prefix_4(DEFAULT_GUEST_MASK) 28 | 29 | 30 | class BaseConfigHandler(object): 31 | def __init__(self, data=None): 32 | self.data = data 33 | self.__form_cache = None 34 | 35 | @property 36 | def form(self): 37 | if self.__form_cache is None: 38 | self.__form_cache = self.get_form() 39 | return self.__form_cache 40 | 41 | def get_form(self): 42 | """Get form. MUST be a single-section form. 43 | 44 | :return: 45 | :rtype: fapi.ForisForm 46 | """ 47 | raise NotImplementedError() 48 | 49 | def save(self, extra_callbacks=None): 50 | """ 51 | 52 | :param extra_callbacks: list of extra callbacks to call when saved 53 | :return: 54 | """ 55 | form = self.form 56 | form.validate() 57 | if extra_callbacks: 58 | for cb in extra_callbacks: 59 | form.add_callback(cb) 60 | if form.valid: 61 | form.save() 62 | return True 63 | else: 64 | return False 65 | -------------------------------------------------------------------------------- /foris/templates/config/notifications.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 |
5 | {% include '_messages.html.j2' %} 6 |

7 | {% trans %}Following notifications occurred and haven't been dismissed since the last reboot.{% endtrans %} 8 |

9 | 10 | {% include '_notifications.html.j2' %} 11 |
12 | 56 | 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /foris/utils/addresses.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | def ip_str_to_num_4(ip_str): 19 | """ Converts IPv4 to number 20 | :param ip_str: str 21 | :return: int 22 | """ 23 | res = 0 24 | try: 25 | for e in ip_str.split("."): 26 | res = res << 8 27 | res += int(e) 28 | except: 29 | raise ValueError("Incorrect IPv4 format %s" % repr(ip_str)) 30 | return res 31 | 32 | 33 | def ip_num_to_str_4(ip_number): 34 | """ Converts number to IPv4 35 | :param ip_number: int 36 | :return: str 37 | """ 38 | res = [] 39 | for i in range(4): 40 | res.append(str(ip_number & 0xFF)) 41 | ip_number = ip_number >> 8 42 | return ".".join(reversed(res)) 43 | 44 | 45 | def normalize_subnet_4(ip_address, mask): 46 | """ 1.2.3.4 255.255.0.0 -> 1.2.0.0 47 | :param ip_address: str 48 | :param mask: str 49 | :return ip address: str 50 | """ 51 | return ip_num_to_str_4(ip_str_to_num_4(ip_address) & ip_str_to_num_4(mask)) 52 | 53 | 54 | def mask_to_prefix_4(mask): 55 | """ 255.255.255.0 -> 24 56 | :param mask: str 57 | :return prefix: int 58 | """ 59 | return "{0:b}".format(ip_str_to_num_4(mask)).count("1") 60 | 61 | 62 | def prefix_to_mask_4(subnet): 63 | """ 255.255.255.0 -> 24 64 | :param prefix: int 65 | :return mask: str 66 | """ 67 | return ip_num_to_str_4(int("1" * subnet + "0" * (32 - subnet), 2)) 68 | -------------------------------------------------------------------------------- /foris/config_handlers/profile.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Foris - web administration interface 4 | # Copyright (C) 2018 CZ.NIC, z.s.p.o. 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from .base import BaseConfigHandler 20 | 21 | from foris import fapi 22 | 23 | from foris.form import Hidden 24 | from foris.state import current_state 25 | from foris.utils.translators import gettext_dummy as gettext, _ 26 | 27 | 28 | class ProfileHandler(BaseConfigHandler): 29 | """ Profile settings handler 30 | """ 31 | 32 | userfriendly_title = gettext("Guide workflow") 33 | 34 | def __init__(self, *args, **kwargs): 35 | self.load_backend_data() 36 | super(ProfileHandler, self).__init__(*args, **kwargs) 37 | 38 | def load_backend_data(self): 39 | self.backend_data = current_state.backend.perform("web", "get_guide") 40 | 41 | def get_form(self): 42 | 43 | data = {"workflow": self.backend_data["current_workflow"]} 44 | if self.data: 45 | data.update(self.data) 46 | 47 | profile_form = fapi.ForisForm("profile", data) 48 | main = profile_form.add_section(name="set_profile", title=_(self.userfriendly_title)) 49 | main.add_field(Hidden, name="workflow", value=self.backend_data["current_workflow"]) 50 | 51 | def profile_form_cb(data): 52 | result = current_state.backend.perform( 53 | "web", "update_guide", {"enabled": True, "workflow": data["workflow"]} 54 | ) 55 | return "save_result", result 56 | 57 | profile_form.add_callback(profile_form_cb) 58 | 59 | return profile_form 60 | -------------------------------------------------------------------------------- /foris/static/js/contrib/html5.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | (function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); 5 | a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; 6 | c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| 7 | "undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f); 8 | if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d 7 |
8 | {% include '_foris_version.html.j2' %} 9 |
10 |
11 | {% include '_lang_flat.html.j2' %} 12 |
13 | 14 |

{% trans %}Project:Turris{% endtrans %}

15 | 16 | {% include '_messages.html.j2' %} 17 | 18 | {% if user_authenticated() %} 19 | {% trans %}Log out{% endtrans %} 20 | {% else %} 21 |
22 | 23 | {% if next %} 24 | 25 | {% endif %} 26 | 27 | 28 | 29 |
30 | {% endif %} 31 | 37 | 38 | {% if request.urlparts.scheme == 'http' %} 39 |
40 | 41 | 52 |
53 | {% endif %} 54 | 55 | 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /foris/templates/includes/updater_eula.html.j2: -------------------------------------------------------------------------------- 1 |

2 | {% trans %}One of the most important features of router Turris are automatic system updates. Thanks to this function your router's software stays up to date and offers better protection against attacks from the Internet.{% endtrans %} 3 |

4 | 5 |

6 | {% trans %}It is highly recommended to have this feature turned on. If you decide to disable it, be warned that this might weaken the security of your router and network in case flaws in the software are found.{% endtrans %} 7 |

8 | 9 |

{% trans %}By turning the automatic updates on, you agree to this feature's license agreement. More information is available here.{% endtrans %}

10 | 11 |
12 | 13 |

{% trans %}Most important points from the license agreement:{% endtrans %}

14 | 15 |
    16 |
  • {% trans %}Automatic updates are offered to the Turris router owners free of charge.{% endtrans %}
  • 17 |
  • {% trans %}Updates are prepared exclusively by CZ.NIC, z. s. p. o.{% endtrans %}
  • 18 |
  • {% trans %}Enabling of the automatic updates is a prerequisite for additional security features of Turris router.{% endtrans %}
  • 19 |
  • {% trans %}Automatic updates take place at the time of their release, the time of installation cannot be influenced by the user.{% endtrans %}
  • 20 |
  • {% trans %}Having the automatic updates turned on can result in increased Internet traffic on your router. Expenses related to this increase are covered by you.{% endtrans %}
  • 21 |
  • {% trans %}Automatic updates cannot protect you against every attack coming from the Internet. Please do not forget to protect your workstations and other devices by installing antivirus software and explaining to your family members how to stay safe on the Internet.{% endtrans %}
  • 22 |
  • {% trans %}CZ.NIC, z. s. p. o. does not guarantee the availability of this service and is not responsible for any damages caused by the automatic updates.{% endtrans %}
  • 23 |
24 | 25 |

26 | {% trans %}By enabling of the automatic updates, you confirm that you are the owner of this Turris router and you agree with the full text of the license agreement.{% endtrans %} 27 |

28 |
29 | 30 | 36 | -------------------------------------------------------------------------------- /foris/utils/dynamic_assets.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris - web administration interface for OpenWrt based on NETCONF 3 | # Copyright (C) 2018 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | 19 | import logging 20 | import shutil 21 | import os 22 | import bottle 23 | 24 | 25 | logger = logging.getLogger("foris.utils.dynamic_assets") 26 | 27 | 28 | dynamic_assets_map = {} 29 | 30 | current_assets_path = None 31 | 32 | 33 | def reset(app_name, assets_path): 34 | """ Deletes generated assets and cleans md5 35 | """ 36 | global dynamic_assets_map 37 | dynamic_assets_map = {} 38 | target = os.path.join(assets_path, app_name) 39 | logger.debug("cleaning dynamic assets in '%s'", target) 40 | shutil.rmtree(target, ignore_errors=True) 41 | os.makedirs(target) 42 | global current_assets_path 43 | current_assets_path = target 44 | 45 | 46 | def store_template(template_name, lang): 47 | """ Creates static file from template as stores it 48 | :param template_name: should looks like this /.tpl 49 | :type template_name: str 50 | """ 51 | template_name = template_name.lstrip("/") 52 | logger.debug("Trying to store generated template '%s' (%s)", template_name, lang) 53 | 54 | # already present 55 | if (template_name, lang) in dynamic_assets_map: 56 | logger.debug("Template already generated '%s' (%s)", template_name, lang) 57 | return 58 | 59 | # store file 60 | rendered = bottle.template(template_name + ".j2", template_adapter=bottle.Jinja2Template) 61 | target_path = os.path.join(current_assets_path, lang, template_name) 62 | try: 63 | os.makedirs(os.path.dirname(target_path)) 64 | except os.error: 65 | # already exists 66 | pass 67 | with open(target_path, "wb") as f: 68 | f.write(bytearray(rendered, "utf8")) 69 | f.flush() 70 | 71 | logger.debug( 72 | "Generated template '%s' (%s) was stored to '%s'.", template_name, lang, target_path 73 | ) 74 | 75 | # mark present 76 | dynamic_assets_map[(template_name, lang)] = True 77 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_buttons.sass: -------------------------------------------------------------------------------- 1 | button 2 | border: none 3 | 4 | @mixin standard-button 5 | display: inline-block 6 | height: $button-height 7 | line-height: $button-height 8 | margin-bottom: 0 9 | width: auto 10 | 11 | .button, button 12 | +box-sizing('border-box') 13 | +rounded-borders 14 | background-color: $button-color 15 | color: #fff 16 | cursor: default 17 | display: block 18 | font: 19 | size: 90% 20 | weight: bold 21 | width: 100% 22 | text-align: center 23 | height: $button-height * $button-height-touch-multiplier 24 | line-height: $button-height * $button-height-touch-multiplier 25 | margin: 0 0.2em 0.5em 0 26 | padding: 0 1.2em 0 1.2em 27 | position: relative 28 | text-decoration: none 29 | vertical-align: middle 30 | z-index: 999 31 | 32 | +respond-to('not-mobile', $no-query: true) 33 | +standard-button 34 | 35 | &:hover 36 | background-color: $button-color-active 37 | color: #fff 38 | 39 | &:before 40 | background-color: $button-color-active 41 | 42 | 43 | &.grayed, &[disabled] 44 | background-color: $button-color-grayed 45 | 46 | &:hover, &:hover:before 47 | background-color: lighten(desaturate($button-color-grayed, 20), 15) 48 | 49 | &:before 50 | background-color: $button-color-grayed 51 | 52 | .button-arrow-right 53 | @extend .button 54 | +border-right-radius(0) 55 | 56 | &:before 57 | background: $highlight-color 58 | border: none 59 | content: " " 60 | display: block 61 | height: $button-height/sqrt(2) 62 | width: $button-height/sqrt(2) 63 | right: - $button-height / 2.8 64 | position: absolute 65 | top: 14% 66 | +transform(rotate(45deg)) 67 | z-index: -1 68 | 69 | .button-arrow-left 70 | @extend .button 71 | +border-left-radius(0) 72 | 73 | &:before 74 | background: $highlight-color 75 | border: none 76 | content: " " 77 | display: block 78 | height: $button-height/sqrt(2) 79 | width: $button-height/sqrt(2) 80 | left: - $button-height / 2.8 81 | position: absolute 82 | top: 14% 83 | +transform(rotate(45deg)) 84 | z-index: -1 85 | 86 | .button-next 87 | @extend .button-arrow-right 88 | float: right 89 | margin-top: 1em 90 | 91 | .button-prev 92 | @extend .button-arrow-left 93 | float: left 94 | margin-top: 1em 95 | 96 | // For vex dialogs button formatting 97 | .vex .vex-content .vex-dialog-form .vex-dialog-buttons button 98 | @extend .button 99 | 100 | .vex .vex-content .vex-dialog-form .vex-dialog-buttons button.vex-dialog-button-secondary 101 | background-color: $button-color-grayed 102 | 103 | &:hover, &:hover:before 104 | background-color: lighten(desaturate($button-color-grayed, 20), 15) 105 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/static/js/sample.js: -------------------------------------------------------------------------------- 1 | // Render a chart based on table data 2 | var make_chart = function() { 3 | var graph_config = { 4 | data: { 5 | datasets: [ 6 | { 7 | lineTension: 0, 8 | label: Foris.sampleMessages.chartLabel, // transaltions are defined within dynamic js 9 | data: graph_data, 10 | }, 11 | ], 12 | fill: true, 13 | }, 14 | options: { 15 | responsive: true, 16 | title: { 17 | display: true, 18 | text: Foris.sampleMessages.chartTitle // transaltions are defined within dynamic js 19 | }, 20 | tooltips: { 21 | mode: 'index', 22 | intersect: false, 23 | }, 24 | hover: { 25 | mode: 'nearest', 26 | intersect: true 27 | }, 28 | scales: { 29 | xAxes: [{ 30 | display: false, 31 | scaleLabel: { 32 | display: true, 33 | labelString: Foris.sampleMessages.chartTimeAxis // transaltions are defined within dynamic js 34 | } 35 | }], 36 | yAxes: [{ 37 | display: true, 38 | ticks: { 39 | suggestedMin: 0, 40 | suggestedMax: 100, 41 | }, 42 | scaleLabel: { 43 | display: true, 44 | labelString: Foris.sampleMessages.chartValueAxis // transaltions are defined within dynamic js 45 | } 46 | }], 47 | }, 48 | }, 49 | }; 50 | var graph_ctx = document.getElementById("canvas").getContext("2d"); 51 | graph_config.options.scales.xAxes[0].ticks = { 52 | min: graph_config.data.datasets[0].data[0].x, 53 | max: graph_config.data.datasets[0].data[graph_config.data.datasets[0].data.length - 1].x, 54 | }; 55 | Foris.lineChart = new Chart.Scatter(graph_ctx, graph_config); 56 | Foris.lineChartData = graph_config.data; 57 | Foris.lineChartOptions = graph_config.options; 58 | } 59 | 60 | // Global chart data 61 | var graph_data; 62 | 63 | // Functions which updates the chart 64 | Foris.update_sample_chart = function() { 65 | // Clear current chart 66 | $("#canvas-container").empty(); 67 | $("#canvas-container").append(''); 68 | 69 | // Set data 70 | graph_data = []; 71 | var idx = 0; 72 | $("#records-table td.table-index").each(function(idx, item) { 73 | graph_data[idx] = {x: parseInt($(item).text())}; 74 | idx++; 75 | }); 76 | idx = 0; 77 | $("#records-table td.table-value").each(function(idx, item) { 78 | graph_data[idx]["y"] = parseInt($(item).text()); 79 | idx++; 80 | }); 81 | 82 | // render chart 83 | make_chart(); 84 | } 85 | 86 | // Update chart after page is rendred 87 | $(document).ready(function() { 88 | Foris.update_sample_chart(); 89 | }); 90 | -------------------------------------------------------------------------------- /foris/utils/translators.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Foris - web administration interface for OpenWrt based on NETCONF 3 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import collections 19 | import gettext 20 | import os 21 | 22 | from jinja2.ext import InternationalizationExtension 23 | 24 | from foris import BASE_DIR 25 | from foris.langs import translations 26 | from foris.state import current_state 27 | 28 | # read locale directory 29 | locale_directory = os.path.join(BASE_DIR, "locale") 30 | 31 | 32 | class _LangDict(collections.OrderedDict): 33 | def __missing__(self, key): 34 | # return english translation if missing key 35 | return super(_LangDict, self).__getitem__("en") 36 | 37 | 38 | translations = _LangDict( 39 | (e, gettext.translation("messages", locale_directory, languages=[e], fallback=True)) 40 | for e in translations 41 | ) 42 | 43 | 44 | class SimpleDelayedTranslator(object): 45 | def __init__(self, text): 46 | self.text = text 47 | 48 | def __str__(self): 49 | return gettext(self.text) 50 | 51 | def __add__(self, other): 52 | return str(self) + other 53 | 54 | 55 | gettext = lambda x: translations[current_state.language].gettext(x) 56 | ngettext = lambda singular, plural, n: translations[current_state.language].ngettext( 57 | singular, plural, n 58 | ) 59 | gettext_dummy = lambda x: SimpleDelayedTranslator(x) 60 | 61 | _ = gettext 62 | 63 | 64 | def set_current_language(language): 65 | """Save interface language to foris.settings.lang. 66 | 67 | :param lang: language code to save 68 | :return: True on success, False otherwise 69 | """ 70 | if current_state.backend.perform("web", "set_language", {"language": language})["result"]: 71 | # Update info variable 72 | current_state.update_lang(language) 73 | return True 74 | 75 | return False 76 | 77 | 78 | # for jinja template 79 | class ForisInternationalizationExtension(InternationalizationExtension): 80 | def __init__(self, environment): 81 | super(ForisInternationalizationExtension, self).__init__(environment) 82 | self._install_callables(gettext, ngettext) 83 | 84 | 85 | i18n = ForisInternationalizationExtension 86 | -------------------------------------------------------------------------------- /foris/static/css/vex.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes vex-fadein { 2 | 0% { 3 | opacity: 0; } 4 | 100% { 5 | opacity: 1; } } 6 | 7 | @keyframes vex-fadein { 8 | 0% { 9 | opacity: 0; } 10 | 100% { 11 | opacity: 1; } } 12 | 13 | @-webkit-keyframes vex-fadeout { 14 | 0% { 15 | opacity: 1; } 16 | 100% { 17 | opacity: 0; } } 18 | 19 | @keyframes vex-fadeout { 20 | 0% { 21 | opacity: 1; } 22 | 100% { 23 | opacity: 0; } } 24 | 25 | @-webkit-keyframes vex-rotation { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); } 29 | 100% { 30 | -webkit-transform: rotate(359deg); 31 | transform: rotate(359deg); } } 32 | 33 | @keyframes vex-rotation { 34 | 0% { 35 | -webkit-transform: rotate(0deg); 36 | transform: rotate(0deg); } 37 | 100% { 38 | -webkit-transform: rotate(359deg); 39 | transform: rotate(359deg); } } 40 | 41 | .vex, .vex *, .vex *:before, .vex *:after { 42 | -moz-box-sizing: border-box; 43 | box-sizing: border-box; } 44 | 45 | .vex { 46 | position: fixed; 47 | overflow: auto; 48 | -webkit-overflow-scrolling: touch; 49 | z-index: 1111; 50 | top: 0; 51 | right: 0; 52 | bottom: 0; 53 | left: 0; } 54 | 55 | .vex-scrollbar-measure { 56 | position: absolute; 57 | top: -9999px; 58 | width: 50px; 59 | height: 50px; 60 | overflow: scroll; } 61 | 62 | .vex-overlay { 63 | -webkit-animation: vex-fadein .5s; 64 | animation: vex-fadein .5s; 65 | position: fixed; 66 | z-index: 1111; 67 | background: rgba(0, 0, 0, 0.4); 68 | top: 0; 69 | right: 0; 70 | bottom: 0; 71 | left: 0; } 72 | 73 | .vex-overlay.vex-closing { 74 | -webkit-animation: vex-fadeout .5s forwards; 75 | animation: vex-fadeout .5s forwards; } 76 | 77 | .vex-content { 78 | -webkit-animation: vex-fadein .5s; 79 | animation: vex-fadein .5s; 80 | background: #fff; } 81 | 82 | .vex.vex-closing .vex-content { 83 | -webkit-animation: vex-fadeout .5s forwards; 84 | animation: vex-fadeout .5s forwards; } 85 | 86 | .vex-close:before { 87 | font-family: Arial, sans-serif; 88 | content: "\00D7"; } 89 | 90 | .vex-dialog-form { 91 | margin: 0; } 92 | 93 | .vex-dialog-button { 94 | text-rendering: optimizeLegibility; 95 | -webkit-appearance: none; 96 | -moz-appearance: none; 97 | appearance: none; 98 | cursor: pointer; 99 | -webkit-tap-highlight-color: transparent; } 100 | 101 | .vex-loading-spinner { 102 | -webkit-animation: vex-rotation .7s linear infinite; 103 | animation: vex-rotation .7s linear infinite; 104 | box-shadow: 0 0 1em rgba(0, 0, 0, 0.1); 105 | position: fixed; 106 | z-index: 1112; 107 | margin: auto; 108 | top: 0; 109 | right: 0; 110 | bottom: 0; 111 | left: 0; 112 | height: 2em; 113 | width: 2em; 114 | background: #fff; } 115 | 116 | body.vex-open { 117 | overflow: hidden; } 118 | -------------------------------------------------------------------------------- /foris/config/pages/password.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | from foris.config_handlers import misc 20 | from foris.state import current_state 21 | from foris.utils import messages 22 | from foris.utils.translators import _ 23 | 24 | from .base import ConfigPageMixin 25 | 26 | 27 | class PasswordConfigPage(ConfigPageMixin, misc.PasswordHandler): 28 | slug = "password" 29 | menu_order = 10 30 | template = "config/password" 31 | template_type = "jinja2" 32 | 33 | def __init__(self, *args, **kwargs): 34 | super(PasswordConfigPage, self).__init__(change=current_state.password_set, *args, **kwargs) 35 | 36 | def save(self, *args, **kwargs): 37 | result = super(PasswordConfigPage, self).save(no_messages=True, *args, **kwargs) 38 | wrong_old_password = self.form.callback_results.get("wrong_old_password", False) 39 | system_password_no_error = self.form.callback_results.get("system_password_no_error", None) 40 | foris_password_no_error = self.form.callback_results.get("foris_password_no_error", None) 41 | 42 | compromised = self.form.callback_results.get("compromised") 43 | if compromised: 44 | messages.error( 45 | _( 46 | "The password you've entered has been compromised. " 47 | "It appears %(count)d times in '%(list)s' list." 48 | ) 49 | % dict(count=compromised["count"], list=compromised["list"]) 50 | ) 51 | return result 52 | 53 | if wrong_old_password: 54 | messages.error(_("Old password you entered was not valid.")) 55 | return result 56 | 57 | if system_password_no_error is not None: 58 | if system_password_no_error: 59 | messages.success(_("System password was successfully saved.")) 60 | else: 61 | messages.error(_("Failed to save system password.")) 62 | if foris_password_no_error is not None: 63 | if foris_password_no_error: 64 | messages.success(_("Foris password was successfully saved.")) 65 | else: 66 | messages.error(_("Failed to save Foris password.")) 67 | 68 | return result 69 | -------------------------------------------------------------------------------- /foris/config/pages/networks.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | from foris.config_handlers import networks 20 | from foris.state import current_state 21 | from foris.utils import messages 22 | from foris.utils.translators import _ 23 | 24 | from .base import ConfigPageMixin 25 | 26 | 27 | class NetworksConfigPage(ConfigPageMixin, networks.NetworksHandler): 28 | slug = "networks" 29 | menu_order = 14 30 | template = "config/networks" 31 | template_type = "jinja2" 32 | 33 | def render(self, **kwargs): 34 | # place non-configurable intefaces in front of configurable 35 | kwargs["networks"] = {} 36 | for network_name in self.backend_data["networks"].keys(): 37 | kwargs["networks"][network_name] = sorted( 38 | self.backend_data["networks"][network_name], 39 | key=lambda x: (1 if x["configurable"] else 0, x["slot"]), 40 | reverse=False, 41 | ) 42 | for network in kwargs["networks"][network_name]: 43 | if network["type"] == "wifi": 44 | network["slot"] = network["bus"] + network["slot"] 45 | 46 | # don't display inconfigurable devices in none network (can't be configured anyway) 47 | kwargs["networks"]["none"] = [e for e in kwargs["networks"]["none"] if e["configurable"]] 48 | 49 | return super(NetworksConfigPage, self).render(**kwargs) 50 | 51 | def save(self, *args, **kwargs): 52 | result = super(NetworksConfigPage, self).save(no_messages=True, *args, **kwargs) 53 | if self.form.callback_results["result"]: 54 | messages.success(_("Network configuration was sucessfully updated.")) 55 | else: 56 | messages.error(_("Unable to update your network configuration.")) 57 | return result 58 | 59 | @classmethod 60 | def is_enabled(cls): 61 | if current_state.device in ["turris"]: 62 | return False 63 | # Don't show in turrisOS version < "4.0" 64 | if int(current_state.turris_os_version.split(".", 1)[0]) < 4: 65 | return False 66 | return ConfigPageMixin.is_enabled_static(cls) 67 | 68 | @classmethod 69 | def is_visible(cls): 70 | return cls.is_enabled() 71 | -------------------------------------------------------------------------------- /foris/static/img/workflow-min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /foris/templates/config/profile.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 | {% if is_xhr is not defined %} 5 |
6 | {% include '_messages.html.j2' %} 7 | {% endif %} 8 | {% if is_xhr is not defined %} 9 |

{% trans %}Here you can set the guide walkthrough which will guide you through the basic configuration of your device.{% endtrans %}

10 |
11 | 12 | {% for workflow in workflows %} 13 | 24 | {% endfor %} 25 |
26 |
27 | 78 | 89 | {% endif %} 90 | 91 | {% endblock %} 92 | -------------------------------------------------------------------------------- /foris/config/pages/notifications.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | 19 | import bottle 20 | 21 | from foris.state import current_state 22 | from foris.utils.translators import gettext_dummy as gettext, _ 23 | 24 | from .base import ConfigPageMixin 25 | 26 | 27 | class NotificationsConfigPage(ConfigPageMixin): 28 | slug = "notifications" 29 | 30 | menu_order = 9 31 | 32 | template = "config/notifications" 33 | userfriendly_title = gettext("Notifications") 34 | template_type = "jinja2" 35 | 36 | def render(self, **kwargs): 37 | notifications = current_state.backend.perform( 38 | "router_notifications", "list", {"lang": current_state.language} 39 | )["notifications"] 40 | 41 | # show only non displayed notifications 42 | kwargs["notifications"] = [e for e in notifications if not e["displayed"]] 43 | 44 | return super(NotificationsConfigPage, self).render(**kwargs) 45 | 46 | def _action_dismiss_notifications(self): 47 | notification_ids = bottle.request.POST.getall("notification_ids[]") 48 | response = current_state.backend.perform( 49 | "router_notifications", "mark_as_displayed", {"ids": notification_ids} 50 | ) 51 | return response["result"], notification_ids 52 | 53 | def call_ajax_action(self, action): 54 | if action == "dismiss-notifications": 55 | bottle.response.set_header("Content-Type", "application/json") 56 | res = self._action_dismiss_notifications() 57 | if res[0]: 58 | return {"success": True, "displayedIDs": res[1]} 59 | else: 60 | return {"success": False} 61 | 62 | elif action == "list": 63 | notifications = current_state.backend.perform( 64 | "router_notifications", "list", {"lang": current_state.language} 65 | )["notifications"] 66 | return bottle.template( 67 | "_notifications.html.j2", 68 | notifications=[e for e in notifications if not e["displayed"]], 69 | template_adapter=bottle.Jinja2Template, 70 | ) 71 | 72 | raise ValueError("Unknown AJAX action.") 73 | 74 | @classmethod 75 | def get_menu_tag(cls): 76 | return { 77 | "show": True if current_state.notification_count else False, 78 | "hint": _("Number of notifications"), 79 | "text": "%d" % current_state.notification_count, 80 | } 81 | -------------------------------------------------------------------------------- /foris/templates/javascript/parsley.messages.js.j2: -------------------------------------------------------------------------------- 1 | window.ParsleyConfig = window.ParsleyConfig || {}; 2 | window.ParsleyConfig.i18n = window.ParsleyConfig.i18n || {}; 3 | 4 | // Define then the messages 5 | window.ParsleyConfig.i18n.{{ lang() }} = $.extend(window.ParsleyConfig.i18n.{{ lang() }} || {}, { 6 | defaultMessage: '{% trans %}This value seems to be invalid.{% endtrans %}', 7 | type: { 8 | email: '{% trans %}This value should be a valid email.{% endtrans %}', 9 | url: '{% trans %}This value should be a valid url.{% endtrans %}', 10 | number: '{% trans %}This value should be a valid number.{% endtrans %}', 11 | integer: '{% trans %}This value should be a valid integer.{% endtrans %}', 12 | digits: '{% trans %}This value should be digits.{% endtrans %}', 13 | alphanum: '{% trans %}This value should be alphanumeric.{% endtrans %}' 14 | }, 15 | notblank: '{% trans %}This value should not be blank.{% endtrans %}', 16 | required: '{% trans %}This value is required.{% endtrans %}', 17 | pattern: '{% trans %}This value seems to be invalid.{% endtrans %}', 18 | min: '{% trans %}This value should be greater than or equal to %s.{% endtrans %}', 19 | max: '{% trans %}This value should be lower than or equal to %s.{% endtrans %}', 20 | range: '{% trans %}This value should be between %s and %s.{% endtrans %}', 21 | floatrange: '{% trans %}This value should be between %s and %s.{% endtrans %}', 22 | minlength: '{% trans %}This value is too short. It should have %s characters or more.{% endtrans %}', 23 | maxlength: '{% trans %}This value is too long. It should have %s characters or fewer.{% endtrans %}', 24 | length: '{% trans %}This field should have between %s and %s characters.{% endtrans %}', 25 | mincheck: '{% trans %}You must select at least %s choices.{% endtrans %}', 26 | maxcheck: '{% trans %}You must select %s choices or fewer.{% endtrans %}', 27 | check: '{% trans %}You must select between %s and %s choices.{% endtrans %}', 28 | equalto: '{% trans %}This value should be the same.{% endtrans %}', 29 | bytelength: '{% trans %}This field should have between %s and %s characters.{% endtrans %}', 30 | extratype: { 31 | ipv4: '{% trans %}This is not a valid IPv4 address.{% endtrans %}', 32 | ipv4netmask: '{% trans %}This is not a valid IPv4 netmask.{% endtrans %}', 33 | ipv4prefix: '{% trans %}This is not a valid IPv4 prefix.{% endtrans %}', 34 | ipv6: '{% trans %}This is not an IPv6 address with prefix length.{% endtrans %}', 35 | anyip: '{% trans %}This is not a valid IPv4 or IPv6 address.{% endtrans %}', 36 | ipv6prefix: '{% trans %}This is not a valid IPv6 prefix.{% endtrans %}', 37 | macaddress: '{% trans %}This is not a valid MAC address.{% endtrans %}', 38 | domain: '{% trans %}This is not a valid domain name.{% endtrans %}', 39 | datetime: '{% trans %}This is not a valid time (YYYY-MM-DD HH:MM:SS).{% endtrans %}' 40 | } 41 | }); 42 | 43 | // If file is loaded after Parsley main file, auto-load locale 44 | if ('undefined' !== typeof window.ParsleyValidator) 45 | window.ParsleyValidator.addCatalog('{{ lang() }}', window.ParsleyConfig.i18n.{{ lang() }}, true); 46 | -------------------------------------------------------------------------------- /foris/middleware/backend_data.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2017 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import bottle 18 | 19 | from foris.state import current_state 20 | from foris.utils.caches import per_request 21 | 22 | 23 | class BackendData(object): 24 | """ Reads data from the backend and stores it properly. 25 | This is performed everytime when a request arrives. 26 | 27 | There can be a few running instances of foris apps (e.g config, ...). 28 | When one changes the other should reflect the change immediatelly. 29 | Therefor it is necessary to update it so frequent. 30 | """ 31 | 32 | def set_language(self, language): 33 | """ Sets the language internallly inside the running instance of foris 34 | 35 | :param language: language to be set 36 | :type language: str 37 | """ 38 | 39 | # Update info variable 40 | current_state.update_lang(language) 41 | 42 | # update bottle app as well 43 | bottle.app().lang = language 44 | 45 | def __init__(self, app): 46 | self.app = app 47 | 48 | def __call__(self, environ, start_response): 49 | 50 | # clear per request data cache 51 | per_request.backend_data.clear() 52 | 53 | try: 54 | data = current_state.backend.perform("web", "get_data") 55 | per_request.backend_data["web", "get_data", None] = data 56 | except Exception: 57 | # Exceptions raised here are not correctly processed in flup 58 | # so we don't propagate the excetion (it will fail later) 59 | # use best effort here and if e.g. backend is not running it will fail later 60 | return self.app(environ, start_response) 61 | 62 | # update language 63 | self.set_language(data["language"]) 64 | 65 | # update reboot required 66 | current_state.update_reboot_required(data["reboot_required"]) 67 | 68 | # update notification count 69 | current_state.update_notification_count(data["notification_count"]) 70 | 71 | # update updater running indicator 72 | current_state.set_updater_is_running(data["updater_running"]) 73 | 74 | # update whether password is set 75 | current_state.update_password_set(data["password_ready"]) 76 | 77 | # update turris_os_version 78 | current_state.set_turris_os_version(data["turris_os_version"]) 79 | 80 | # update device 81 | current_state.set_device(data["device"]) 82 | 83 | # initialize guide 84 | current_state.update_guide(data["guide"]) 85 | 86 | return self.app(environ, start_response) 87 | -------------------------------------------------------------------------------- /foris/templates/javascript/foris.js.j2: -------------------------------------------------------------------------------- 1 | Foris.messages.qrErrorPassword = "{% trans %}Your password contains non-standard characters. These are not forbidden, but could cause problems on some devices.{% endtrans %}"; 2 | Foris.messages.qrErrorSSID = "{% trans %}Your SSID contains non-standard characters. These are not forbidden, but could cause problems on some devices.{% endtrans %}"; 3 | Foris.messages.ok = "{% trans %}OK{% endtrans %}"; 4 | Foris.messages.error = "{% trans %}Error{% endtrans %}"; 5 | Foris.messages.loading = "{% trans %}Loading...{% endtrans %}"; 6 | Foris.messages.checkNoForward = "{% trans %}Connectivity test failed, testing connection with disabled forwarding.{% endtrans %}"; 7 | Foris.messages.lanIpChanged = "{% trans %}The IP address of your router has been changed. It should be accessible from %NEW_IP_LINK%. See the note above for more information about IP address change.{% endtrans %}"; 8 | Foris.messages.confirmDisabledUpdates = "{% trans %}You have chosen to not receive security updates. We strongly advice you to keep the automatic security updates enabled to receive all recommended updates for your device. Updating your router on regular basis is the only way how to ensure you will be protected against known threats that might target your home router device.

Do you still want to continue and stay unprotected?{% endtrans %}"; 9 | Foris.messages.confirmDisabledDNSSEC = "{% trans %}DNSSEC is a security technology that protects the DNS communication against attacks on the DNS infrastructure. We strongly recommend keeping DNSSEC validation enabled unless you know that you will be connecting your device in the network where DNSSEC is broken.

Do you still want to continue and stay unprotected?{% endtrans %}"; 10 | Foris.messages.confirmRestart = "{% trans %}Are you sure you want to restart the router?{% endtrans %}"; 11 | Foris.messages.confirmRestartExtra = (unread) => { 12 | return `{% trans unread_left="${Math.round(unread)}"%}Remaining notifications ({{ unread_left }}) will be discarded.{% endtrans %}`; 13 | }; 14 | Foris.messages.unsavedNotificationsAlert = "{% trans %}There are some unsaved changes in the notifications settings.
Do you want to discard them and test the notifications with the old settings?{% endtrans %}"; 15 | Foris.messages.rebootIn = (left) => { 16 | return `{% trans time_left="${left.toFixed(1)}" %}Your device will be rebooted in {{ time_left }} seconds.{% endtrans %}`; 17 | }; 18 | Foris.messages.networkRestartIn = (left) => { 19 | return `{% trans time_left="${left.toFixed(1)}" %}Your network will be restarted in {{ time_left }} seconds.{% endtrans %}`; 20 | }; 21 | Foris.messages.forisRestartIn = (left) => { 22 | return `{% trans time_left="${left.toFixed(1)}" %}Foris will be restarted in {{ time_left }} seconds.{% endtrans %}`; 23 | }; 24 | Foris.messages.rebootTriggered = "{% trans %}A reboot of your device was triggered.{% endtrans %}"; 25 | Foris.messages.networkRestartTriggered = "{% trans %}A network restart was triggered.{% endtrans %}"; 26 | Foris.messages.forisRestartTriggered = "{% trans %}Foris restart was triggered.{% endtrans %}"; 27 | Foris.messages.tryingToReconnect = "{% trans %}Trying to reconnect to your device.{% endtrans %}"; 28 | Foris.messages.vexYes = "{% trans %}Confirm{% endtrans %}"; 29 | Foris.messages.vexNo = "{% trans %}Cancel{% endtrans %}"; 30 | 31 | Foris.pingPath = "{{ url('ping') }}"; 32 | Foris.backendPath = "{{ url('backend-api') }}"; 33 | -------------------------------------------------------------------------------- /foris/config/pages/guide.py: -------------------------------------------------------------------------------- 1 | # 2 | # Foris 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | from foris.guide import Workflow 19 | 20 | from foris.config_handlers import profile, misc 21 | from foris.state import current_state 22 | from foris.utils import messages 23 | from foris.utils.translators import _ 24 | 25 | from .base import ConfigPageMixin 26 | 27 | 28 | class ProfileConfigPage(ConfigPageMixin, profile.ProfileHandler): 29 | slug = "profile" 30 | menu_order = 13 31 | template = "config/profile" 32 | template_type = "jinja2" 33 | 34 | def render(self, **kwargs): 35 | kwargs["workflows"] = [ 36 | Workflow( 37 | e, 38 | self.backend_data["current_workflow"] == e, 39 | self.backend_data["recommended_workflow"] == e, 40 | ) 41 | for e in self.backend_data["available_workflows"] 42 | ] 43 | 44 | # perform some workflow sorting 45 | SCORE = {"router": 1, "bridge": 2} # router first 46 | kwargs["workflows"].sort(key=lambda e: (SCORE.get(e.name, 99), e.name)) 47 | return super(ProfileConfigPage, self).render(**kwargs) 48 | 49 | def save(self, *args, **kwargs): 50 | result = super(ProfileConfigPage, self).save(no_messages=True, *args, **kwargs) 51 | if self.form.callback_results["result"]: 52 | messages.success(_("Guide workflow was sucessfully set.")) 53 | else: 54 | messages.error(_("Failed to set guide workflow.")) 55 | return result 56 | 57 | @classmethod 58 | def is_visible(cls): 59 | if not current_state.guide.enabled: 60 | return False 61 | return ConfigPageMixin.is_visible_static(cls) 62 | 63 | @classmethod 64 | def is_enabled(cls): 65 | if not current_state.guide.enabled: 66 | return False 67 | return ConfigPageMixin.is_enabled_static(cls) 68 | 69 | 70 | class GuideFinishedPage(ConfigPageMixin, misc.GuideFinishedHandler): 71 | slug = "finished" 72 | menu_order = 90 73 | 74 | template_type = "jinja2" 75 | template = "config/finished" 76 | 77 | def save(self, *args, **kwargs): 78 | result = super().save(no_messages=True, *args, **kwargs) 79 | if not self.form.callback_results["result"]: 80 | messages.error(_("Failed to finish the guide.")) 81 | return result 82 | 83 | @classmethod 84 | def is_visible(cls): 85 | if not current_state.guide.enabled: 86 | return False 87 | return ConfigPageMixin.is_visible_static(cls) 88 | 89 | @classmethod 90 | def is_enabled(cls): 91 | if not current_state.guide.enabled: 92 | return False 93 | return ConfigPageMixin.is_enabled_static(cls) 94 | -------------------------------------------------------------------------------- /foris/templates/_layout.html.j2: -------------------------------------------------------------------------------- 1 | {% if is_xhr is not defined %} 2 | 3 | 4 | 5 | 6 | {{ title + " | " if title|default(False) else "" }}{% trans %}Turris router administration interface{% endtrans %} 7 | 8 | 12 | 13 | {% if PLUGIN_STYLES is defined and PLUGIN_STYLES %} 14 | {% for static_filename in PLUGIN_STYLES %} 15 | 16 | {% endfor %} 17 | {% endif %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% if foris_info.websockets["ws_port"] %} 26 | 27 | {% endif %} 28 | {% if foris_info.websockets["ws_path"] %} 29 | 30 | {% endif %} 31 | {% if foris_info.websockets["wss_port"] %} 32 | 33 | {% endif %} 34 | {% if foris_info.websockets["wss_path"] %} 35 | 36 | {% endif %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% if PLUGIN_STATIC_SCRIPTS is defined and PLUGIN_STATIC_SCRIPTS %} 47 | {% for static_filename in PLUGIN_STATIC_SCRIPTS %} 48 | 49 | {% endfor %} 50 | {% endif %} 51 | {% if PLUGIN_DYNAMIC_SCRIPTS is defined and PLUGIN_DYNAMIC_SCRIPTS %} 52 | {% for dynamic_filename in PLUGIN_DYNAMIC_SCRIPTS %} 53 | 54 | {% endfor %} 55 | {% endif %} 56 | 57 | 58 | {% endif %} 59 | {% block base %} 60 | {% endblock %} 61 | {% if is_xhr is not defined %} 62 | 63 | 64 | {% endif %} 65 | -------------------------------------------------------------------------------- /foris/middleware/bottle_csrf.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2013 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import bottle 18 | import string 19 | 20 | from random import SystemRandom 21 | 22 | random = SystemRandom() 23 | 24 | 25 | def get_csrf_token(): 26 | session = bottle.request.environ["foris.session"] 27 | csrf_token = session.get("csrf_token") 28 | if not csrf_token: 29 | # create new token if it's not present in this session 30 | update_csrf_token() 31 | return session.get("csrf_token") 32 | 33 | 34 | def update_csrf_token(save_session=True): 35 | """Generate new CSRF token, assign it to a template variable and save it to session. 36 | 37 | This should be called on every login. 38 | """ 39 | 40 | def generate_token(): 41 | return "".join(random.choice(string.ascii_letters + string.digits) for i in range(32)) 42 | 43 | session = bottle.request.environ["foris.session"] 44 | session["csrf_token"] = generate_token() 45 | if save_session: 46 | session.save() 47 | 48 | 49 | class CSRFValidationError(bottle.HTTPError): 50 | def __init__(self, text="CSRF token validation failed."): 51 | super(CSRFValidationError, self).__init__(403, text) 52 | 53 | 54 | class CSRFPlugin(object): 55 | """Bottle plugin for protection against CSRF attacks. 56 | 57 | CSRF protection is included in every request that is not safe (safe HTTP methods are 58 | defined in RFC 2616). To disable protection, set ``disable_csrf_protect`` attribute 59 | of route to True. 60 | 61 | This plugin uses sessions, Beaker session middleware is required. 62 | """ 63 | 64 | name = "csrf" 65 | api = 2 66 | 67 | def setup(self, app): 68 | bottle.SimpleTemplate.defaults["get_csrf_token"] = get_csrf_token 69 | bottle.Jinja2Template.defaults["get_csrf_token"] = get_csrf_token 70 | 71 | def apply(self, callback, route): 72 | # make CSRF protection implicitly enabled (since it's more fool-proof) 73 | disable_csrf_protect = route.config.get("disable_csrf_protect", False) 74 | 75 | if not get_csrf_token(): 76 | update_csrf_token() 77 | 78 | if disable_csrf_protect or bottle.request.method in ("GET", "HEAD", "OPTIONS", "TRACE"): 79 | return callback 80 | 81 | def wrapper(*args, **kwargs): 82 | token = None 83 | if bottle.request.method == "POST": 84 | token = bottle.request.POST.get( 85 | "csrf_token", bottle.request.headers.get("X-CSRFToken") 86 | ) 87 | # do not refer session from outer scope! we need to get new value 88 | # in each call of the function 89 | if not token or token != get_csrf_token(): 90 | raise CSRFValidationError() 91 | 92 | return callback(*args, **kwargs) 93 | 94 | return wrapper 95 | -------------------------------------------------------------------------------- /foris/config_handlers/networks.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Foris - web administration interface 4 | # Copyright (C) 2018 CZ.NIC, z.s.p.o. 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import copy 20 | 21 | from .base import BaseConfigHandler 22 | 23 | from foris import fapi 24 | 25 | from foris.form import MultiCheckbox, Checkbox 26 | from foris.state import current_state 27 | from foris.utils.translators import gettext_dummy as gettext, _ 28 | 29 | 30 | class NetworksHandler(BaseConfigHandler): 31 | """ Networks settings handler 32 | """ 33 | 34 | userfriendly_title = gettext("Network interfaces") 35 | 36 | def load_backend_data(self): 37 | data = current_state.backend.perform("networks", "get_settings") 38 | 39 | self.backend_data = data 40 | 41 | def __init__(self, *args, **kwargs): 42 | self.load_backend_data() 43 | super(NetworksHandler, self).__init__(*args, **kwargs) 44 | 45 | def get_form(self): 46 | data = copy.deepcopy(self.backend_data) 47 | 48 | if self.data: 49 | # Update from post 50 | data.update(self.data) 51 | 52 | networks_form = fapi.ForisForm("networks", self.data) 53 | ports_section = networks_form.add_section( 54 | name="set_ports", title=_(self.userfriendly_title) 55 | ) 56 | checkboxes = [] 57 | for kind in ["wan", "lan", "guest", "none"]: 58 | checkboxes += [(e["id"], e["id"]) for e in self.backend_data["networks"][kind]] 59 | ports_section.add_field(MultiCheckbox, name="wan", args=checkboxes, multifield=True) 60 | ports_section.add_field(MultiCheckbox, name="lan", args=checkboxes, multifield=True) 61 | ports_section.add_field(MultiCheckbox, name="guest", args=checkboxes, multifield=True) 62 | ports_section.add_field(MultiCheckbox, name="none", args=checkboxes, multifield=True) 63 | 64 | ports_section.add_field(Checkbox, name="ssh_on_wan", default=False, required=False) 65 | ports_section.add_field(Checkbox, name="http_on_wan", default=False, required=False) 66 | ports_section.add_field(Checkbox, name="https_on_wan", default=False, required=False) 67 | 68 | def networks_form_cb(data): 69 | wan = data.get("wan", []) 70 | lan = data.get("lan", []) 71 | guest = data.get("guest", []) 72 | none = data.get("none", []) 73 | 74 | ssh_on_wan = bool(data.get("ssh_on_wan", "0")) 75 | http_on_wan = bool(data.get("http_on_wan", "0")) 76 | https_on_wan = bool(data.get("https_on_wan", "0")) 77 | 78 | result = current_state.backend.perform( 79 | "networks", 80 | "update_settings", 81 | { 82 | "firewall": { 83 | "ssh_on_wan": ssh_on_wan, 84 | "http_on_wan": http_on_wan, 85 | "https_on_wan": https_on_wan, 86 | }, 87 | "networks": {"lan": lan, "wan": wan, "guest": guest, "none": none}, 88 | }, 89 | ) 90 | return "save_result", result 91 | 92 | networks_form.add_callback(networks_form_cb) 93 | 94 | return networks_form 95 | -------------------------------------------------------------------------------- /foris/static/sass/pages/_login.sass: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------- 2 | /* Login page 3 | #login-page 4 | padding-top: 3em 5 | text-align: center 6 | 7 | p 8 | margin: 0 auto 2em auto 9 | width: 30em 10 | 11 | label 12 | display: none 13 | 14 | input 15 | height: 3em 16 | line-height: 3em 17 | max-width: 25em 18 | 19 | button 20 | display: inline-block 21 | font-size: 100% 22 | height: 3em 23 | line-height: 3em 24 | margin: 0.5em 1em 25 | vertical-align: middle 26 | width: 90% 27 | 28 | +respond-to('not-mobile', $no-query: true) 29 | margin: 0 30 | width: auto 31 | 32 | 33 | .message 34 | margin-left: auto 35 | margin-right: auto 36 | text-align: left 37 | width: 25em 38 | 39 | .footer 40 | font-size: 90% 41 | color: #333 42 | line-height: 160% 43 | margin-top: 2em 44 | 45 | .language-switch 46 | position: absolute 47 | right: 0.3em 48 | top: 0.3em 49 | 50 | span 51 | text-transform: uppercase 52 | 53 | a 54 | text-transform: uppercase 55 | 56 | .foris-version 57 | position: absolute 58 | display: none 59 | +respond-to('not-mobile', $no-query: true) 60 | left: 0.3em 61 | top: 0.3em 62 | display: block 63 | 64 | // special for first version of https warning in foris web administration 65 | html 66 | box-sizing: border-box 67 | 68 | * 69 | box-sizing: inherit 70 | &::before, &::after 71 | box-sizing: inherit 72 | 73 | @-webkit-keyframes flashice 74 | 0% 75 | opacity: 0 76 | left: 900px 77 | 78 | 100% 79 | opacity: 1 80 | left: 0 81 | 82 | 83 | @-moz-keyframes flashice 84 | 0% 85 | opacity: 0 86 | left: 900px 87 | 88 | 100% 89 | opacity: 1 90 | left: 0 91 | 92 | 93 | @keyframes flashice 94 | 0% 95 | opacity: 0 96 | left: 900px 97 | 98 | 100% 99 | opacity: 1 100 | left: 0 101 | 102 | #flashes 103 | font-size: 13px 104 | background: none 105 | padding: 0 106 | position: fixed 107 | right: 375px 108 | bottom: 1% 109 | width: 0 110 | z-index: 9 111 | color: #555 112 | input 113 | display: none 114 | &:checked + label 115 | display: block 116 | left: 2000px 117 | opacity: 0 118 | cursor: default 119 | transition-duration: 0.2s 120 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) 121 | label 122 | position: relative 123 | line-height: 1.7 124 | width: 360px 125 | min-height: 80px 126 | opacity: 1 127 | left: 0 128 | display: block 129 | background-color: #fff 130 | padding: 8px 4px 8px 84px 131 | margin-bottom: 15px 132 | cursor: pointer 133 | text-align: left 134 | -webkit-animation: flashice 1s ease 135 | -moz-animation: flashice 1s ease 136 | -o-animation: flashice 1s ease 137 | animation: flashice 1s ease 138 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.18), 0 8px 16px rgba(0, 0, 0, 0.36) 139 | &::after 140 | content: '\2715' 141 | display: block 142 | position: absolute 143 | top: 8px 144 | right: 12px 145 | width: 16px 146 | height: 16px 147 | font-weight: bold 148 | font-size: 16px 149 | line-height: 17px 150 | padding-left: 1px 151 | &:hover::after 152 | background: #555 153 | color: #fff 154 | border-radius: 50% 155 | span 156 | display: block 157 | margin: 0 158 | float: left 159 | margin-left: -80px 160 | width: 80px 161 | height: 80px 162 | text-align: center 163 | img 164 | display: block 165 | margin: auto 166 | strong 167 | display: inline-block 168 | font-weight: normal 169 | font-size: 14px 170 | margin-bottom: 3px 171 | small 172 | display: block 173 | margin-top: 4px 174 | color: #aaa 175 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import glob 5 | import re 6 | import copy 7 | 8 | from setuptools import setup 9 | from setuptools.command.build_py import build_py 10 | 11 | from foris import __version__ 12 | 13 | 14 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | 17 | def merge_po_files(): 18 | from babel.messages.pofile import read_po, write_po 19 | 20 | trans_dirs = glob.glob(os.path.join(BASE_DIR, "foris/locale/*/LC_MESSAGES/")) 21 | 22 | # iterate through translations 23 | for trans_dir in trans_dirs: 24 | locale = re.match(r".*\/([a-zA-Z_]+)\/LC_MESSAGES/", trans_dir).group(1) 25 | foris_path = os.path.join(trans_dir, "foris.po") 26 | if not os.path.exists(foris_path): 27 | continue 28 | 29 | # read foris translations 30 | with open(foris_path, "rb") as f: 31 | foris_catalog = read_po(f, locale=locale, domain="messages") 32 | 33 | # read tzinfo translations 34 | tzinfo_path = os.path.join(trans_dir, "tzinfo.po") 35 | if os.path.exists(tzinfo_path): 36 | with open(tzinfo_path, "rb") as f: 37 | tzinfo_catalog = read_po(f, locale=locale, domain="messages") 38 | for msg in tzinfo_catalog: 39 | # foris messages will be preffered 40 | if msg.id not in foris_catalog: 41 | # append to foris messages 42 | foris_catalog[msg.id] = msg 43 | 44 | # write merged catalogs 45 | target_path = os.path.join(trans_dir, "messages.po") 46 | with open(target_path, "wb") as f: 47 | write_po(f, foris_catalog, no_location=True) 48 | 49 | 50 | class BuildCmd(build_py): 51 | def run(self): 52 | # prepare messages.po 53 | merge_po_files() 54 | 55 | # compile translation 56 | from babel.messages import frontend as babel 57 | 58 | distribution = copy.copy(self.distribution) 59 | cmd = babel.compile_catalog(distribution) 60 | cmd.directory = os.path.join(os.path.dirname(__file__), "foris", "locale") 61 | cmd.domain = "messages" 62 | cmd.ensure_finalized() 63 | cmd.run() 64 | 65 | # run original build cmd 66 | build_py.run(self) 67 | 68 | 69 | setup( 70 | name="Foris", 71 | version=__version__, 72 | description="Web administration interface for OpenWrt based on NETCONF", 73 | author="CZ.NIC, z. s. p. o.", 74 | author_email="packaging@turris.cz", 75 | url="https://gitlab.nic.cz/turris/foris/foris/", 76 | license="GPL-3.0", 77 | install_requires=[ 78 | "jinja2", 79 | "bottle", 80 | "bottle_i18n", 81 | "pbkdf2", 82 | "flup", 83 | "ubus @ git+https://gitlab.nic.cz/turris/python-ubus.git", 84 | "paho-mqtt", 85 | ], 86 | setup_requires=["babel", "jinja2"], 87 | provides=["foris"], 88 | extras_require={"sentry": ["sentry-sdk>=0.7.9"]}, 89 | packages=[ 90 | "foris", 91 | "foris.config_handlers", 92 | "foris.config", 93 | "foris.config.pages", 94 | "foris.common", 95 | "foris.langs", 96 | "foris.plugins", 97 | "foris.utils", 98 | "foris.ubus", 99 | "foris.middleware", 100 | "foris_plugins", 101 | ], 102 | package_data={ 103 | "": [ 104 | "LICENSE", 105 | "locale/**/LC_MESSAGES/*.mo", 106 | "templates/**", 107 | "templates/**/*", 108 | "static/css/*.css", 109 | "static/fonts/*", 110 | "static/img/*", 111 | "static/js/*.js", 112 | "static/js/contrib/*", 113 | "utils/*.pickle2", 114 | ] 115 | }, 116 | namespace_packages=["foris_plugins"], 117 | cmdclass={"build_py": BuildCmd}, 118 | entry_points={"console_scripts": ["foris = foris.__main__:main"]}, 119 | ) 120 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_forms.sass: -------------------------------------------------------------------------------- 1 | input, textarea, select 2 | background-color: $background-color 3 | color: $foreground-color 4 | border: 1px solid $stroke-color 5 | font-size: 130% 6 | padding: 0.2em 7 | width: 88% 8 | display: inline-block 9 | +box-sizing('content-box') 10 | +rounded-borders 11 | 12 | +respond-to('not-mobile', $no-query: true) 13 | font-size: 80% 14 | padding: 0.5em 15 | width: 24em 16 | 17 | input[type="checkbox"], input[type="radio"] 18 | border: none 19 | width: auto 20 | 21 | input.grayed, input[disabled] 22 | background-color: $button-color-grayed 23 | color: white 24 | font-weight: bold 25 | 26 | input[type="file"] 27 | border: none 28 | 29 | .config-form, .maintenance-form 30 | +pie-clearfix 31 | margin-bottom: 1em 32 | 33 | & > .row 34 | +pie-clearfix 35 | padding: 0.25em 0 36 | margin-bottom: 0.5em 37 | 38 | .radio-inputs 39 | display: inline-block 40 | 41 | label 42 | margin-right: 1em 43 | width: auto 44 | min-width: 5em 45 | 46 | +respond-to('not-mobile', $no-query: true) 47 | margin-bottom: 0.1em 48 | 49 | .radio-inputs 50 | width: 20.125em 51 | 52 | & > label 53 | float: left 54 | display: inline-block 55 | font-weight: bold 56 | vertical-align: middle 57 | min-width: 12em 58 | max-width: 80% 59 | 60 | +respond-to('not-mobile', $no-query: true) 61 | max-width: 12em 62 | 63 | .multicheckbox 64 | float: left 65 | label 66 | display: block 67 | 68 | .field-hint 69 | cursor: pointer 70 | 71 | .field-hint, .field-loading, .field-validation-fail, .field-validation-pass 72 | vertical-align: middle 73 | 74 | .field-validation-pass 75 | background: inline-image('../.inline-assets/icon-success.png') no-repeat right 50% 76 | border-color: $info-color 77 | color: $info-color 78 | 79 | .field-validation-fail 80 | background: inline-image('../.inline-assets/icon-fail.png') no-repeat right 50% 81 | border-color: $error-color 82 | color: $error-color 83 | 84 | .validation-container, .server-validation-container, .server-validation-container-persistent 85 | color: $error-color 86 | width: 100% 87 | 88 | ul 89 | +adjust-font-size-to(14px) 90 | 91 | .hint-text 92 | +rounded-borders 93 | display: none 94 | background-color: $hint-color 95 | margin: 0.5em 0 96 | padding: 0.5em 97 | 98 | .validation-container, .server-validation-container, .hint-text, .server-validation-container-persistent 99 | +respond-to('not-mobile', $no-query: true) 100 | margin-left: 12.5em 101 | max-width: 18.5em 102 | 103 | .form-buttons 104 | +respond-to('not-mobile', $no-query: true) 105 | float: right 106 | margin-top: 1.5em 107 | 108 | 109 | #wifi-qr 110 | text-align: center 111 | display: block 112 | 113 | .qr-error 114 | color: $error-color 115 | font-weight: bold 116 | text-align: center 117 | width: 200px 118 | position: relative 119 | padding: 0 1em 1em 0 120 | 121 | .wifi-qr 122 | 123 | img 124 | display: block 125 | width: 30px 126 | height: 30px 127 | .wifi-qr-box 128 | display: none 129 | 130 | .form-note 131 | width: 100% 132 | font-size: 85% 133 | font-style: italic 134 | +respond-to('not-mobile', $no-query: true) 135 | width: 70% 136 | 137 | span.password-toggle 138 | position: relative 139 | left: -2.5em 140 | margin-right: -1.1em 141 | vertical-align: middle 142 | font-size: 130% 143 | +respond-to('not-mobile', $no-query: true) 144 | font-size: 110% 145 | 146 | .config-form .row label.hide-on-mobiles 147 | display: none 148 | +respond-to('not-mobile', $no-query: true) 149 | display: inline-block 150 | 151 | .config-form .row label.expand-on-mobiles 152 | width: 100% 153 | max-width: 100% 154 | +respond-to('not-mobile', $no-query: true) 155 | width: inherit 156 | max-width: inherit 157 | -------------------------------------------------------------------------------- /foris/static/sass/pages/_config.sass: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------- 2 | /* Config pages (i.e. most of the Foris) 3 | 4 | .config-description 5 | margin-bottom: 2.5em 6 | 7 | .config-section-description 8 | margin-bottom: 0.5em 9 | 10 | .config-form 11 | position: relative 12 | 13 | .button 14 | font-size: 100% 15 | height: 3em 16 | line-height: 3em 17 | margin-right: 0.5em 18 | 19 | /* ------------------------------------------------------------------------- 20 | /* "About" page 21 | #page-about 22 | p 23 | margin-bottom: 1em 24 | 25 | table 26 | margin-bottom: 1em 27 | width: 100% 28 | 29 | th, td 30 | padding: 0.2em 0 31 | 32 | th 33 | font-weight: bold 34 | 35 | abbr 36 | border-bottom: 1px dashed lighten($foreground-color, 50) 37 | cursor: help 38 | 39 | #registration-code-update 40 | vertical-align: middle 41 | 42 | #registration-code-loader 43 | vertical-align: middle 44 | 45 | #registration-code-fail 46 | @extend .message 47 | @extend .message.error 48 | display: none 49 | 50 | p:last-child 51 | margin-bottom: 0 52 | 53 | #registration-code 54 | font-size: 130% 55 | font-weight: bold 56 | 57 | /* ------------------------------------------------------------------------- 58 | /* "DNS" page 59 | #page-dns 60 | form 61 | margin-bottom: 1em 62 | 63 | #connection-test-fail 64 | display: none 65 | 66 | #connection-test-loader 67 | vertical-align: middle 68 | 69 | #test-results 70 | margin-bottom: 1em 71 | 72 | tr 73 | td:first-child 74 | width: 16em 75 | 76 | .test-success, .test-fail, .test-loading 77 | font-weight: bold 78 | 79 | .test-success 80 | color: $info-color 81 | 82 | .test-fail 83 | color: $error-color 84 | 85 | .test-loading 86 | color: $loading-color 87 | 88 | #connectivity-retest 89 | @extend .button 90 | margin: 0 0 2em 91 | /* ------------------------------------------------------------------------- 92 | /* "Updater" page 93 | .eula-summary 94 | font-weight: bold 95 | 96 | #updater-auto-updates-form 97 | label 98 | display: block 99 | margin-left: 3em 100 | max-width: 100% 101 | 102 | .radio-inputs 103 | width: 100% 104 | margin-bottom: 0.5em 105 | 106 | .hint-text 107 | margin-left: 3em 108 | 109 | #updater-approvals 110 | margin-bottom: 0.5em 111 | 112 | p 113 | margin-bottom: 0.5em 114 | 115 | label 116 | display: inline-block 117 | 118 | input[name="approval_timeout"] 119 | width: 5em 120 | 121 | #approval-timeout-line 122 | margin-left: 2em 123 | 124 | #updater-toggle 125 | margin-bottom: 1em 126 | label 127 | display: block 128 | margin-left: 3em 129 | 130 | .radio-inputs 131 | width: 100% 132 | 133 | #updater-approvals 134 | margin-bottom: 0.5em 135 | 136 | p 137 | margin-bottom: 0.5em 138 | 139 | label 140 | display: inline-block 141 | margin-left: 3em 142 | 143 | input[name="approval_timeout"] 144 | width: 5em 145 | 146 | #approval-timeout-line 147 | margin-left: 2em 148 | 149 | 150 | #updater-approve-changes 151 | overflow: hidden 152 | margin-bottom: .5em 153 | border: 1px solid #999 154 | padding: 1em 155 | 156 | li 157 | +respond-to('not-mobile', $no-query: true) 158 | float: left 159 | display: inline 160 | width: 50% 161 | 162 | #updater-reboot-text 163 | margin-bottom: .5em 164 | 165 | #current-approval 166 | .button-row 167 | margin-bottom: .5em 168 | button 169 | height: 2em 170 | line-height: 2em 171 | 172 | #updater-disabled-warning 173 | margin-top: .5em 174 | 175 | #language-install 176 | margin-top: 1ex 177 | margin-bottom: 1ex 178 | 179 | div.language-install-box 180 | display: inline-block 181 | margin-left: 1ex 182 | text-transform: uppercase 183 | 184 | .wifi-separator hr 185 | color: #ddd 186 | 187 | .config-form .label-button 188 | line-height: 2em 189 | height: 2em 190 | 191 | #ntp-error 192 | display: none 193 | 194 | .dynamic-link-wrapper td 195 | text-align: center 196 | 197 | .dynamic-link-wrapper div 198 | display: inline-block 199 | border: 1px solid black 200 | border-radius: 3px 201 | margin: 3px 202 | padding: 10px 203 | -------------------------------------------------------------------------------- /foris/templates/config/maintenance.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'config/base.html.j2' %} 2 | 3 | {% block config_base %} 4 |
5 | {% include '_messages.html.j2' %} 6 | 7 |

{% trans %}Notifications and automatic restarts{% endtrans %}

8 |

{% trans %}You can set the router to notify you when a specific event occurs, for example when a reboot is required, no space is left on device or an application update is installed. You can use Turris servers to send these emails. Alternatively, if you choose to use a custom server, you must enter some additional settings. These settings are the same as you enter in your email client and you can get them from the provider of your email inbox. In that case, because of security reasons, it is recommended to create a dedicated account for your router.{% endtrans %}

9 |

{% trans %}Also, when an automatic restart is required, you can specify the time you want it to occur. If you have email notifications enabled, you can also specify the interval between notification and automatic restart.{% endtrans %}

10 |
11 | 12 | {% for section in notifications_form.sections %} 13 | {% if section.active_fields %} 14 |

{{ section.title }}

15 | {% for field in section.active_fields %} 16 | {% include '_field.html.j2' %} 17 | {% endfor %} 18 | {% endif %} 19 | {% endfor %} 20 | 21 | {% if notifications_form.data['enable_smtp'] %} 22 | 23 | {% endif %} 24 |
25 | 26 |

{% trans %}Configuration backup{% endtrans %}

27 |

{% trans %}If you need to save the current configuration of this device, you can download a backup file. The configuration is saved as an unencrypted compressed archive (.tar.bz2). Passwords for this configuration interface and for the advanced configuration are not included in the backup.{% endtrans %}

28 | 31 | 32 |

{% trans %}Configuration restore{% endtrans %}

33 |

{% trans %}To restore the configuration from a backup file, upload it using following form. Keep in mind that IP address of this device might change during the process, causing unavailability of this interface.{% endtrans %}

34 |
35 | 36 | {% for field in form.active_fields %} 37 | {% include '_field.html.j2' %} 38 | {% endfor %} 39 | 40 |
41 | 42 |

{% trans %}Device reboot{% endtrans %}

43 |

{% trans %}If you need to reboot the device, click on the following button. The reboot process takes approximately 30 seconds, you will be required to log in again after the reboot.{% endtrans %}

44 | 47 | 48 | 61 |
62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /foris/utils/messages.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import bottle 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | _SESSION_KEY = "_messages" 8 | 9 | # tuple of (priority, level_name) 10 | INFO = (0, "info") 11 | SUCCESS = (10, "success") 12 | WARNING = (20, "warning") 13 | ERROR = (30, "error") 14 | 15 | 16 | class Message(object): 17 | def __init__(self, text, level, extra_classes=[]): 18 | """ 19 | Create new message instance. 20 | 21 | :param text: text of the message 22 | :param level: severity level 23 | :param extra_classes: extra classes of message 24 | """ 25 | self.text = text 26 | self.level = level 27 | self.extra_classes = extra_classes 28 | 29 | def to_json(self): 30 | return {"text": self.text, "level": self.level, "extra_classes": self.extra_classes} 31 | 32 | @staticmethod 33 | def from_json(json): 34 | return Message(json["text"], json["level"], json["extra_classes"]) 35 | 36 | @property 37 | def classes(self): 38 | """ 39 | Classes of the message. 40 | 41 | :return: space-separated list of classes 42 | """ 43 | if self.extra_classes: 44 | return " ".join([self.level[1], self.extra_classes]) 45 | return self.level[1] 46 | 47 | 48 | def get_messages(level=None, min_level=None): 49 | """ 50 | Generator function yielding messages, optionally filtered by severity level. 51 | 52 | :param level: get messages with exact level 53 | :param min_level: get messages with level specified or higher 54 | """ 55 | 56 | def should_show(): 57 | if level and msg.level[0] == level[0]: 58 | return True 59 | elif min_level and msg.level[0] >= min_level[0]: 60 | return True 61 | elif not level and not min_level: 62 | return True 63 | return False 64 | 65 | session = bottle.request.environ["foris.session"] 66 | messages = session.get(_SESSION_KEY, []) 67 | all_messages = messages[:] 68 | for msg in all_messages: 69 | if should_show(): 70 | messages.remove(msg) 71 | session[_SESSION_KEY] = messages 72 | yield Message.from_json(msg) 73 | 74 | 75 | def info(text, extra_classes=[]): 76 | """ 77 | Add new info message. 78 | 79 | :param text: text of the message 80 | :param extra_classes: extra classes of the message 81 | """ 82 | add_message(text, INFO, extra_classes) 83 | 84 | 85 | def success(text, extra_classes=[]): 86 | """ 87 | Add new success message. 88 | 89 | :param text: text of the message 90 | :param extra_classes: extra classes of the message 91 | """ 92 | add_message(text, SUCCESS, extra_classes) 93 | 94 | 95 | def warning(text, extra_classes=[]): 96 | """ 97 | Add new warning message. 98 | 99 | :param text: text of the message 100 | :param extra_classes: extra classes of the message 101 | """ 102 | add_message(text, WARNING, extra_classes) 103 | 104 | 105 | def error(text, extra_classes=[]): 106 | """ 107 | Add new error message. 108 | 109 | :param text: text of the message 110 | :param extra_classes: extra classes of the message 111 | """ 112 | add_message(text, ERROR, extra_classes) 113 | 114 | 115 | def add_message(text, level=INFO, extra_classes=[]): 116 | """ 117 | Add new message. 118 | 119 | :param text: text of the message 120 | :param level: severity level 121 | :param extra_classes: extra classes of the message 122 | """ 123 | session = bottle.request.environ["foris.session"] 124 | messages = session.get(_SESSION_KEY, []) 125 | messages.append(Message(text, level, extra_classes).to_json()) 126 | session[_SESSION_KEY] = messages 127 | 128 | 129 | def set_template_defaults(): 130 | """ 131 | Add template functions as template defaults to supplied Bottle template 132 | adapter. 133 | 134 | """ 135 | bottle.SimpleTemplate.defaults["get_messages"] = get_messages 136 | bottle.SimpleTemplate.defaults["get_alert_messages"] = functools.partial( 137 | get_messages, min_level=WARNING 138 | ) 139 | 140 | bottle.Jinja2Template.defaults["get_messages"] = get_messages 141 | bottle.Jinja2Template.defaults["get_alert_messages"] = functools.partial( 142 | get_messages, min_level=WARNING 143 | ) 144 | -------------------------------------------------------------------------------- /examples/sample_plugin/foris_plugins/sample/__init__.py: -------------------------------------------------------------------------------- 1 | import bottle 2 | import os 3 | 4 | 5 | from foris import fapi, validators 6 | from foris.config import ConfigPageMixin, add_config_page 7 | from foris.config_handlers import BaseConfigHandler 8 | from foris.form import Number 9 | from foris.plugins import ForisPlugin 10 | from foris.state import current_state 11 | from foris.utils.translators import gettext_dummy as gettext, gettext as _ 12 | 13 | 14 | # This represents a main form handler 15 | class SamplePluginConfigHandler(BaseConfigHandler): 16 | # gettext() triggers lazy_translated text 17 | # it is also used for detecting translations during foris_make_messages cmd 18 | 19 | userfriendly_title = gettext("Sample") 20 | 21 | def get_form(self): 22 | data = current_state.backend.perform("sample", "get_slices") 23 | 24 | if self.data: 25 | # Update from post (used when the form is updated via ajax) 26 | data.update(self.data) 27 | 28 | form = fapi.ForisForm("sample", data) 29 | section = form.add_section( 30 | name="main_section", 31 | title=self.userfriendly_title, 32 | ) 33 | # _() translates the string immediatelly 34 | # it is also used for detecting translations during foris_make_messages cmd 35 | section.add_field( 36 | Number, name="slices", label=_("Number of slices"), required=True, 37 | validators=validators.InRange(2, 15) 38 | ) 39 | 40 | def form_cb(data): 41 | res = current_state.backend.perform( 42 | "sample", "set_slices", {"slices": int(data["slices"])}) 43 | 44 | return "save_result", res # store {"result": ...} to be used in SamplePluginPage save() method 45 | 46 | form.add_callback(form_cb) 47 | return form 48 | 49 | 50 | # This represents a plugin page 51 | class SamplePluginPage(ConfigPageMixin, SamplePluginConfigHandler): 52 | slug = "sample" # part of the url of the plugin (.../config/) 53 | menu_order = 90 # Where it should be placed in the main menu (higher the number the lower) 54 | template = "sample/sample" # template which will be used (.html.js will be auto added) 55 | template_type = "jinja2" 56 | 57 | def get_backend_data(self): 58 | res = current_state.backend.perform("sample", "list") 59 | return res["records"] 60 | 61 | def save(self, *args, **kwargs): 62 | # Handle form result here 63 | return super(SamplePluginPage, self).save(*args, **kwargs) 64 | 65 | def _prepare_render_args(self, args): 66 | args['PLUGIN_NAME'] = SamplePlugin.PLUGIN_NAME 67 | args['PLUGIN_STYLES'] = SamplePlugin.PLUGIN_STYLES 68 | args['PLUGIN_STATIC_SCRIPTS'] = SamplePlugin.PLUGIN_STATIC_SCRIPTS 69 | args['PLUGIN_DYNAMIC_SCRIPTS'] = SamplePlugin.PLUGIN_DYNAMIC_SCRIPTS 70 | args['records'] = self.get_backend_data() 71 | 72 | def render(self, **kwargs): 73 | self._prepare_render_args(kwargs) 74 | return super(SamplePluginPage, self).render(**kwargs) 75 | 76 | def _action_get_records(self): 77 | # obtain and render the data and render a partial template (for ajax) 78 | records = self.get_backend_data() 79 | 80 | return bottle.template( 81 | "sample/_records.html.j2", 82 | records=records, 83 | template_adapter=bottle.Jinja2Template, 84 | ) 85 | 86 | def call_ajax_action(self, action): 87 | if action == "get_records": 88 | return self._action_get_records() 89 | 90 | raise ValueError("Unknown AJAX action.") 91 | 92 | 93 | # plugin definition 94 | class SamplePlugin(ForisPlugin): 95 | PLUGIN_NAME = "sample" # also shown in the url 96 | DIRNAME = os.path.dirname(os.path.abspath(__file__)) 97 | 98 | PLUGIN_STYLES = [ 99 | "css/sample.css", # path to css script generated using sass/sample.sass 100 | ] 101 | PLUGIN_STATIC_SCRIPTS = [ 102 | "js/contrib/Chart.bundle.min.js", # 3rd party static js 103 | "js/sample.js", # static js file 104 | ] 105 | PLUGIN_DYNAMIC_SCRIPTS = [ 106 | "sample.js", # dynamic js file (a template which will be rendered to javascript) 107 | ] 108 | 109 | def __init__(self, app): 110 | super(SamplePlugin, self).__init__(app) 111 | add_config_page(SamplePluginPage) 112 | -------------------------------------------------------------------------------- /foris/config_handlers/dns.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # Foris - web administration interface for OpenWrt based on NETCONF 4 | # Copyright (C) 2013 CZ.NIC, z.s.p.o. 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | from .base import BaseConfigHandler 20 | 21 | from foris import fapi 22 | from foris import validators 23 | 24 | from foris.form import Checkbox, Textbox, Dropdown 25 | from foris.state import current_state 26 | from foris.utils.translators import gettext_dummy as gettext, _ 27 | 28 | 29 | class DNSHandler(BaseConfigHandler): 30 | """ 31 | DNS-related settings 32 | """ 33 | 34 | userfriendly_title = gettext("DNS") 35 | 36 | def get_form(self): 37 | data = current_state.backend.perform("dns", "get_settings") 38 | available_forwarders = [[e["name"], e["description"]] for e in data["available_forwarders"]] 39 | data["dnssec_disabled"] = not data["dnssec_enabled"] 40 | if self.data: 41 | # Update from post 42 | data.update(self.data) 43 | data["dnssec_enabled"] = not self.data.get("dnssec_disabled", False) 44 | 45 | dns_form = fapi.ForisForm("dns", data) 46 | dns_main = dns_form.add_section(name="set_dns", title=_(self.userfriendly_title)) 47 | dns_main.add_field( 48 | Checkbox, 49 | name="forwarding_enabled", 50 | label=_("Use forwarding"), 51 | preproc=lambda val: bool(int(val)), 52 | ) 53 | available_forwarders = sorted(available_forwarders, key=lambda x: x[0]) 54 | # fill in text for forwarder (first with "" name) 55 | available_forwarders[0][1] = _("Use provider's DNS resolver") 56 | dns_main.add_field( 57 | Dropdown, name="forwarder", label=_("DNS Forwarder"), args=available_forwarders 58 | ).requires("forwarding_enabled", True) 59 | 60 | dns_main.add_field( 61 | Checkbox, 62 | name="dnssec_disabled", 63 | label=_("Disable DNSSEC"), 64 | preproc=lambda val: bool(int(val)), 65 | default=False, 66 | ) 67 | 68 | dns_main.add_field( 69 | Checkbox, 70 | name="dns_from_dhcp_enabled", 71 | label=_("Enable DHCP clients in DNS"), 72 | hint=_( 73 | "This will enable your DNS resolver to place DHCP client's " 74 | "names among the local DNS records." 75 | ), 76 | preproc=lambda val: bool(int(val)), 77 | default=False, 78 | ) 79 | dns_main.add_field( 80 | Textbox, 81 | name="dns_from_dhcp_domain", 82 | label=_("Domain of DHCP clients in DNS"), 83 | hint=_( 84 | 'This domain will be used as suffix. E.g. The result for client "android-123" ' 85 | 'and domain "my.lan" will be "android-123.my.lan".' 86 | ), 87 | validators=[validators.Domain()], 88 | ).requires("dns_from_dhcp_enabled", True) 89 | 90 | def dns_form_cb(data): 91 | msg = { 92 | "dnssec_enabled": not data.get("dnssec_disabled", False), 93 | "forwarding_enabled": data["forwarding_enabled"], 94 | "dns_from_dhcp_enabled": data["dns_from_dhcp_enabled"], 95 | } 96 | if "dns_from_dhcp_domain" in data: 97 | msg["dns_from_dhcp_domain"] = data["dns_from_dhcp_domain"] 98 | if data["forwarding_enabled"]: 99 | msg["forwarder"] = data.get("forwarder", "") 100 | res = current_state.backend.perform("dns", "update_settings", msg) 101 | return "save_result", res # store {"result": ...} to be used later... 102 | 103 | dns_form.add_callback(dns_form_cb) 104 | return dns_form 105 | -------------------------------------------------------------------------------- /foris/static/sass/ui/_main_nav.sass: -------------------------------------------------------------------------------- 1 | $nav-item-height: 3em 2 | $nav-item-border: 2px solid #ddd 3 | 4 | %nav-item-link 5 | color: $sidebar-foreground-color 6 | display: block 7 | padding: 0.8em 1.5em 8 | 9 | %bottom-nav-border 10 | &::after 11 | display: block 12 | content: " " 13 | border-bottom: $nav-item-border 14 | margin: 0 10px 15 | 16 | %nav-menu-link 17 | font-size: 80% 18 | font-weight: bold 19 | text-decoration: none 20 | 21 | #menu 22 | text-transform: uppercase 23 | 24 | a 25 | @extend %nav-menu-link 26 | 27 | &:hover 28 | text-decoration: underline 29 | 30 | nav 31 | ul 32 | li 33 | background: $sidebar-background-color 34 | 35 | &::before 36 | display: block 37 | content: " " 38 | border-top: $nav-item-border 39 | margin: 0 0.7rem 40 | 41 | a 42 | @extend %nav-item-link 43 | 44 | span.link-disabled 45 | @extend %nav-item-link 46 | @extend %nav-menu-link 47 | color: $sidebar-foreground-disabled-color 48 | 49 | 50 | &:last-child 51 | @extend %bottom-nav-border 52 | 53 | &.active 54 | background: $sidebar-active-tab-color 55 | font-weight: bold 56 | 57 | &::before, &::after 58 | border-color: $sidebar-active-tab-color 59 | 60 | & + li::before 61 | border-color: $sidebar-background-color 62 | 63 | a 64 | color: #fff 65 | 66 | span.menu-tag 67 | color: $sidebar-active-tab-color 68 | background-color: #fff 69 | 70 | span.menu-tag 71 | +border-radius(1em) 72 | float: right 73 | padding: 1px 7px 0px 7px 74 | position: relative 75 | top: -1px 76 | color: $sidebar-background-color 77 | background-color: #000 78 | font-weight: bold 79 | 80 | span.expand-tag 81 | float: right 82 | 83 | #subnav 84 | +pie-clearfix 85 | font-weight: bold 86 | color: #000 87 | text-transform: uppercase 88 | padding-bottom: 4em 89 | 90 | a 91 | @extend %nav-item-link 92 | &:hover 93 | text-decoration: underline 94 | 95 | +respond-to('not-mobile', $no-query: true) 96 | padding: 3em 0 7em 97 | 98 | a 99 | display: inline 100 | height: inherit 101 | line-height: inherit 102 | padding: 0 103 | 104 | &::after 105 | display: none 106 | 107 | // mimic same style as the main menu for mobile (underlining) 108 | #logout, li 109 | @extend %bottom-nav-border 110 | 111 | // but hide on desktop 112 | +respond-to('not-mobile', $no-query: true) 113 | &::after 114 | display: none 115 | 116 | #language-switch 117 | +box-sizing('border-box') 118 | text-align: left 119 | display: inline 120 | position: relative 121 | color: #000 122 | outline: none 123 | cursor: pointer 124 | 125 | span 126 | display: none 127 | 128 | > a 129 | display: block 130 | font-size: 80% 131 | margin-left: 10px 132 | 133 | 134 | +respond-to('not-mobile', $no-query: true) 135 | float: left 136 | width: 50% 137 | 138 | span 139 | display: block 140 | font-size: 80% 141 | margin-left: 10px 142 | 143 | &::after 144 | content: "" 145 | width: 0 146 | height: 0 147 | position: absolute 148 | right: 8px 149 | top: 60% 150 | margin-top: -5px 151 | border-style: solid 152 | border-width: 5px 5px 0 5px 153 | border-color: #000 transparent 154 | 155 | &:hover 156 | text-decoration: underline 157 | 158 | ul 159 | position: absolute 160 | top: 100% 161 | left: 0 162 | right: 0 163 | background: #e4e4e4 164 | font-weight: normal 165 | pointer-events: none 166 | opacity: 0 167 | 168 | li a 169 | display: block 170 | padding: 8px 10px 171 | 172 | &:hover 173 | background: #fff 174 | text-decoration: none 175 | 176 | &.active 177 | ul 178 | opacity: 1 179 | pointer-events: auto 180 | 181 | #logout a 182 | +box-sizing('border-box') 183 | +respond-to('not-mobile', $no-query: true) 184 | padding-left: 1em 185 | float: right 186 | width: 50% 187 | border-left: $nav-item-border 188 | 189 | #menu .submenu-item 190 | padding-left: 1.5em 191 | background-color: lighten($sidebar-background-color, 4) 192 | -------------------------------------------------------------------------------- /foris/state.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Foris - web administration interface for OpenWrt based on NETCONF 3 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | import logging 19 | 20 | from foris import __version__ as version 21 | from foris.langs import DEFAULT_LANGUAGE 22 | 23 | logger = logging.getLogger("foris.state") 24 | 25 | 26 | class ForisState(object): 27 | def __init__(self): 28 | self.foris_version = version 29 | self.language = DEFAULT_LANGUAGE 30 | self.app = None 31 | self.reboot_required = False 32 | self.assets_path = None 33 | self.sentry_running = False 34 | 35 | def update_lang(self, lang): 36 | logger.debug(f"current lang updated to '{lang}'") 37 | self.language = lang 38 | 39 | def set_app(self, app): 40 | logger.debug(f"current app updated to '{app}'") 41 | self.app = app 42 | 43 | def set_backend(self, backend): 44 | if backend.name in ["ubus", "unix-socket"]: 45 | logger.debug(f"setting backend to '{backend.name}' (path {backend.path}).") 46 | elif backend.name == "mqtt": 47 | logger.debug( 48 | f"setting backend to '{backend.name}' (host {backend.host}:{backend.port})." 49 | ) 50 | self.backend = backend 51 | 52 | def set_websocket(self, ws_port, ws_path, wss_port, wss_path): 53 | self.websockets = { 54 | "ws_port": ws_port, 55 | "ws_path": ws_path, 56 | "wss_port": wss_port, 57 | "wss_path": wss_path, 58 | } 59 | 60 | def update_reboot_required(self, required): 61 | """ Sets reboot required indicator 62 | :param required: True if reboot is required False otherwise 63 | :type required: boolean 64 | """ 65 | logger.debug(f"setting reboot_required={required}") 66 | self.reboot_required = required 67 | 68 | def update_notification_count(self, new_count): 69 | """ Updates notificaton count 70 | 71 | :param new_count: new notificaton count 72 | :type new_count: int 73 | """ 74 | logger.debug(f"setting notification_count={new_count}") 75 | self.notification_count = new_count 76 | 77 | def repr(self): 78 | return "%s (%s)" % (self.__class__, str(vars(self))) 79 | 80 | def set_updater_is_running(self, running): 81 | """ Sets whether updater is running 82 | :param running: True if updater is running False otherwise 83 | :type running: boolean 84 | """ 85 | logger.debug(f"setting updater_is_running={running}") 86 | self.updater_is_running = running 87 | 88 | def set_turris_os_version(self, version): 89 | """ Sets turris_os_version 90 | :param version: turrisOS version 91 | :type version: str 92 | """ 93 | self.turris_os_version = version 94 | 95 | def set_device(self, device): 96 | """ Sets device 97 | :param device: device where this web gui is running (omnia/mox/...) 98 | :type device: str 99 | """ 100 | self.device = device 101 | 102 | def update_password_set(self, password_set): 103 | logger.debug(f"setting password_set={password_set}") 104 | self.password_set = password_set 105 | 106 | def update_guide(self, guide_data): 107 | from foris.guide import Guide 108 | 109 | logger.debug(f"setting guide_data ({guide_data})") 110 | self.guide = Guide(guide_data) 111 | 112 | def set_assets_path(self, assets_path): 113 | logger.debug(f"setting assets_path to '{assets_path}'") 114 | self.assets_path = assets_path 115 | 116 | def set_sentry(self, running): 117 | logger.debug(f"setting sentry_running to '{running}'") 118 | self.sentry_running = running 119 | 120 | 121 | current_state = ForisState() 122 | -------------------------------------------------------------------------------- /foris/backend.py: -------------------------------------------------------------------------------- 1 | # Foris 2 | # Copyright (C) 2019 CZ.NIC, z.s.p.o. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import logging 18 | import time 19 | 20 | from foris_client.buses.base import ControllerError 21 | 22 | logger = logging.getLogger("foris.backend") 23 | 24 | 25 | class ExceptionInBackend(Exception): 26 | def __init__(self, query, remote_stacktrace, remote_description): 27 | self.query = query 28 | self.remote_stacktrace = remote_stacktrace 29 | self.remote_description = remote_description 30 | 31 | 32 | class Backend(object): 33 | DEFAULT_TIMEOUT = 30000 # in ms 34 | 35 | def __init__(self, name, **kwargs): 36 | self.name = name 37 | self.controller_id = None 38 | 39 | if name == "ubus": 40 | from foris_client.buses.ubus import UbusSender 41 | 42 | self.path = kwargs["path"] 43 | self._instance = UbusSender(kwargs["path"], default_timeout=self.DEFAULT_TIMEOUT) 44 | 45 | elif name == "unix-socket": 46 | from foris_client.buses.unix_socket import UnixSocketSender 47 | 48 | self.path = kwargs["path"] 49 | self._instance = UnixSocketSender(kwargs["path"], default_timeout=self.DEFAULT_TIMEOUT) 50 | 51 | elif name == "mqtt": 52 | from foris_client.buses.mqtt import MqttSender 53 | 54 | self.host = kwargs["host"] 55 | self.port = kwargs["port"] 56 | self.credentials = kwargs["credentials"] 57 | self.controller_id = kwargs["controller_id"] 58 | self._instance = MqttSender( 59 | kwargs["host"], 60 | kwargs["port"], 61 | default_timeout=self.DEFAULT_TIMEOUT, 62 | credentials=kwargs["credentials"], 63 | ) 64 | 65 | def __repr__(self): 66 | if self.name in ["unix-socket", "ubus"]: 67 | return "%s('%s')" % (type(self._instance).__name__, self.path) 68 | elif self.name == "mqtt": 69 | return "%s('%s:%d')" % (type(self._instance).__name__, self.host, self.port) 70 | return "%s" % type(self._instance).__name__ 71 | 72 | def perform( 73 | self, module, action, data=None, raise_exception_on_failure=True, controller_id=None 74 | ): 75 | """ Perform backend action 76 | 77 | :returns: None on error, response data otherwise 78 | :rtype: NoneType or dict 79 | :raises ExceptionInBackend: When command failed and raise_exception_on_failure is True 80 | """ 81 | response = None 82 | start_time = time.time() 83 | try: 84 | response = self._instance.send( 85 | module, action, data, controller_id=controller_id or self.controller_id 86 | ) 87 | except ControllerError as e: 88 | logger.error("Exception in backend occured.") 89 | if raise_exception_on_failure: 90 | error = e.errors[0] # right now we are dealing only with the first error 91 | msg = {"module": module, "action": action, "kind": "request"} 92 | if data is not None: 93 | msg["data"] = data 94 | raise ExceptionInBackend(msg, error["stacktrace"], error["description"]) 95 | except RuntimeError as e: 96 | # This may occure when e.g. calling function is not present in backend 97 | logger.error("RuntimeError occured during the communication with backend.") 98 | if raise_exception_on_failure: 99 | raise e 100 | except Exception as e: 101 | logger.error("Exception occured during the communication with backend. (%s)", e) 102 | raise e 103 | finally: 104 | logger.debug( 105 | "Query took %f: %s.%s - %s", time.time() - start_time, module, action, data 106 | ) 107 | 108 | return response 109 | -------------------------------------------------------------------------------- /foris/tests/test_data.py: -------------------------------------------------------------------------------- 1 | # List of wireless cards returned by the stats module 2 | stats_wireless_cards = [ 3 | { 4 | "name": "phy0", 5 | "vht-capabilities": True, 6 | "channels": [ 7 | {"disabled": False, "radar": False, "frequency": 2412, "number": 1}, 8 | {"disabled": False, "radar": False, "frequency": 2417, "number": 2}, 9 | {"disabled": False, "radar": False, "frequency": 2422, "number": 3}, 10 | {"disabled": False, "radar": False, "frequency": 2427, "number": 4}, 11 | {"disabled": False, "radar": False, "frequency": 2432, "number": 5}, 12 | {"disabled": False, "radar": False, "frequency": 2437, "number": 6}, 13 | {"disabled": False, "radar": False, "frequency": 2442, "number": 7}, 14 | {"disabled": False, "radar": False, "frequency": 2447, "number": 8}, 15 | {"disabled": False, "radar": False, "frequency": 2452, "number": 9}, 16 | {"disabled": False, "radar": False, "frequency": 2457, "number": 10}, 17 | {"disabled": False, "radar": False, "frequency": 2462, "number": 11}, 18 | {"disabled": False, "radar": False, "frequency": 2467, "number": 12}, 19 | {"disabled": False, "radar": False, "frequency": 2472, "number": 13}, 20 | {"disabled": True, "radar": False, "frequency": 2484, "number": 14}, 21 | {"disabled": False, "radar": False, "frequency": 5180, "number": 36}, 22 | {"disabled": False, "radar": False, "frequency": 5200, "number": 40}, 23 | {"disabled": False, "radar": False, "frequency": 5220, "number": 44}, 24 | {"disabled": False, "radar": False, "frequency": 5240, "number": 48}, 25 | {"disabled": False, "radar": True, "frequency": 5260, "number": 52}, 26 | {"disabled": False, "radar": True, "frequency": 5280, "number": 56}, 27 | {"disabled": False, "radar": True, "frequency": 5300, "number": 60}, 28 | {"disabled": False, "radar": True, "frequency": 5320, "number": 64}, 29 | {"disabled": False, "radar": True, "frequency": 5500, "number": 100}, 30 | {"disabled": False, "radar": True, "frequency": 5520, "number": 104}, 31 | {"disabled": False, "radar": True, "frequency": 5540, "number": 108}, 32 | {"disabled": False, "radar": True, "frequency": 5560, "number": 112}, 33 | {"disabled": False, "radar": True, "frequency": 5580, "number": 116}, 34 | {"disabled": False, "radar": True, "frequency": 5600, "number": 120}, 35 | {"disabled": False, "radar": True, "frequency": 5620, "number": 124}, 36 | {"disabled": False, "radar": True, "frequency": 5640, "number": 128}, 37 | {"disabled": False, "radar": True, "frequency": 5660, "number": 132}, 38 | {"disabled": False, "radar": True, "frequency": 5680, "number": 136}, 39 | {"disabled": False, "radar": True, "frequency": 5700, "number": 140}, 40 | {"disabled": True, "radar": False, "frequency": 5745, "number": 149}, 41 | {"disabled": True, "radar": False, "frequency": 5765, "number": 153}, 42 | {"disabled": True, "radar": False, "frequency": 5785, "number": 157}, 43 | {"disabled": True, "radar": False, "frequency": 5805, "number": 161}, 44 | {"disabled": True, "radar": False, "frequency": 5825, "number": 165}, 45 | ], 46 | }, 47 | { 48 | "name": "phy1", 49 | "vht-capabilities": False, 50 | "channels": [ 51 | {"disabled": False, "radar": False, "frequency": 2412, "number": 1}, 52 | {"disabled": False, "radar": False, "frequency": 2417, "number": 2}, 53 | {"disabled": False, "radar": False, "frequency": 2422, "number": 3}, 54 | {"disabled": False, "radar": False, "frequency": 2427, "number": 4}, 55 | {"disabled": False, "radar": False, "frequency": 2432, "number": 5}, 56 | {"disabled": False, "radar": False, "frequency": 2437, "number": 6}, 57 | {"disabled": False, "radar": False, "frequency": 2442, "number": 7}, 58 | {"disabled": False, "radar": False, "frequency": 2447, "number": 8}, 59 | {"disabled": False, "radar": False, "frequency": 2452, "number": 9}, 60 | {"disabled": False, "radar": False, "frequency": 2457, "number": 10}, 61 | {"disabled": False, "radar": False, "frequency": 2462, "number": 11}, 62 | {"disabled": False, "radar": False, "frequency": 2467, "number": 12}, 63 | {"disabled": False, "radar": False, "frequency": 2472, "number": 13}, 64 | {"disabled": True, "radar": False, "frequency": 2484, "number": 14}, 65 | ], 66 | }, 67 | ] 68 | -------------------------------------------------------------------------------- /foris/templates/config/_connection_test.html.j2: -------------------------------------------------------------------------------- 1 |

{% trans %}Connection test{% endtrans %}

2 |

{% trans %}Here you can test your connection settings. Remember to click on the Save button before running the test. Note that sometimes it takes a while before the connection is fully initialized. So it might be useful to wait for a while before running this test.{% endtrans %}

3 |
4 | {% trans %}"Unable to verify network connection.{% endtrans %} 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% if ipv6_test %} 17 | 18 | 19 | {% endif %} 20 | 21 |
{% trans %}Test type{% endtrans %}{% trans %}Status{% endtrans %}
{% trans %}IPv4 connectivity{% endtrans %}
{% trans %}IPv4 gateway connectivity{% endtrans %}
{% trans %}IPv6 connectivity{% endtrans %}
{% trans %}IPv6 gateway connectivity{% endtrans %}
22 | {% trans %}Test connection{% endtrans %} 23 | 87 | -------------------------------------------------------------------------------- /foris/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # Foris - web administration interface for OpenWrt based on NETCONF 2 | # Copyright (C) 2015 CZ.NIC, z. s. p. o. 3 | # 4 | # Foris is distributed under the terms of GNU General Public License v3. 5 | # You should have received a copy of the GNU General Public License 6 | # along with this program. If not, see . 7 | 8 | import gettext 9 | import inspect 10 | import importlib 11 | import logging 12 | import os 13 | import pkgutil 14 | import pkg_resources 15 | 16 | import bottle 17 | 18 | from foris.utils.translators import translations 19 | 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | class ForisPlugin(object): 25 | """Simple class that all Foris plugins should inherit from.""" 26 | 27 | DIRNAME = None 28 | LOAD_ORDER = 100 # smaller number means that the plugin will be loaded sooner 29 | plugin_translations = None 30 | 31 | def __init__(self, app): 32 | self.app = app 33 | if not self.DIRNAME: 34 | raise NameError("DIRNAME attribute must be set by ForisPlugin subclass.") 35 | # initialize templates 36 | template_dir = os.path.join(self.DIRNAME, "templates") 37 | bottle.TEMPLATE_PATH.append(template_dir) 38 | # initialize translations 39 | self.add_translations() 40 | 41 | def add_translations(self): 42 | """Add translations in current plugin. 43 | 44 | This approach has one design flaw - messages in the plugin apply to 45 | the whole app. This is not an issue now, but it should be examined 46 | later and replaced by a better solution. 47 | """ 48 | for lang, default_translation in translations.items(): 49 | local_translation = gettext.translation( 50 | "messages", os.path.join(self.DIRNAME, "locale"), languages=[lang], fallback=True 51 | ) 52 | default_translation.add_fallback(local_translation) 53 | 54 | 55 | class ForisPluginLoader(object): 56 | """Class for loading plugins and holding references to them in runtime.""" 57 | 58 | def __init__(self, app): 59 | self.app = app 60 | self.app.foris_plugin_loader = self 61 | self.plugins = [] 62 | 63 | def autoload_plugins(self): 64 | """Find and load plugins in foris_plugins.*""" 65 | 66 | plugin_classes = [] 67 | 68 | modules = importlib.import_module("foris_plugins") 69 | for _, mod_name, _ in pkgutil.iter_modules(modules.__path__): 70 | plugin_module_name = "foris_plugins.%s" % mod_name 71 | # try to determine version 72 | try: 73 | version = pkg_resources.get_distribution("foris_%s_plugin" % mod_name).version 74 | except pkg_resources.DistributionNotFound: 75 | version = "?" 76 | logger.debug("Found foris plugin '%s (%s)'.", mod_name, version) 77 | plugin_classes += self._get_plugin_classes(plugin_module_name) 78 | 79 | # sort plugin classes 80 | plugin_classes.sort(key=lambda x: (x.LOAD_ORDER, x.PLUGIN_NAME)) 81 | 82 | # load the plugin 83 | for plugin_class in plugin_classes: 84 | self.load_plugin(plugin_class) 85 | 86 | @staticmethod 87 | def is_foris_plugin(klass): 88 | """Check that argument klass is a valid Foris plugin. 89 | 90 | Check is True for all classes that have class named ForisPlugin 91 | in their inheritance chain. 92 | """ 93 | if not inspect.isclass(klass): 94 | return False 95 | # first element is basically klass.__name__ - throw it away 96 | mro_names = [c.__name__ for c in inspect.getmro(klass)][1:] 97 | return ForisPlugin.__name__ in mro_names 98 | 99 | def _get_plugin_classes(self, package_name): 100 | """Reads all plugin classes in package """ 101 | try: 102 | logger.info("Looking for plugins in package '%s'", package_name) 103 | package = importlib.import_module(package_name) 104 | classes = inspect.getmembers(package, self.is_foris_plugin) 105 | classes = [klass for _, klass in classes] 106 | for klass in classes: 107 | logger.debug("Plugin found %s", klass) 108 | return classes 109 | except ImportError: 110 | logger.exception("Unable to import package '%s'.", package_name) 111 | except Exception: 112 | # catching all errors - plugins should not kill Foris 113 | logger.exception("Error when loading plugin '%s': " % package_name) 114 | 115 | def load_plugin(self, plugin_class): 116 | """Load a single plugin class.""" 117 | logger.info("Loading plugin: %s", plugin_class) 118 | instance = plugin_class(self.app) 119 | self.plugins.append(instance) 120 | --------------------------------------------------------------------------------