Merge ~rmescandon/snappy-hwe-snaps/+git/wifi-connect:config-ui into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master
- Git
- lp:~rmescandon/snappy-hwe-snaps/+git/wifi-connect
- config-ui
- Merge into master
Status: | Merged |
---|---|
Approved by: | James Jesudason |
Approved revision: | 8923330dafb4f905456e11fcfc499a398dcf0a72 |
Merged at revision: | bdaa24398ed18ad69af8e7bb967f91d159092df4 |
Proposed branch: | ~rmescandon/snappy-hwe-snaps/+git/wifi-connect:config-ui |
Merge into: | ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master |
Diff against target: |
2408 lines (+1779/-192) 24 files modified
.gitignore (+3/-1) README.md (+40/-0) daemon/daemon_test.go (+4/-0) gulpfile.js (+5/-1) netman/dbus.go (+18/-2) package.json (+4/-4) server/handlers.go (+96/-16) server/handlers_test.go (+24/-0) server/middleware.go (+2/-28) server/router.go (+2/-1) snap/scriptlets/country-codes (+243/-0) snap/scriptlets/fetch_country_codes.sh (+10/-0) snap/scriptlets/fill_country_codes.sh (+39/-0) snap/snapcraft.yaml (+1/-0) static/css/application.css (+1/-1) static/sass/application.scss (+9/-5) static/templates/connecting.html (+6/-3) static/templates/management.html (+302/-73) static/templates/operational.html (+65/-47) utils/config.go (+235/-0) utils/config_test.go (+543/-0) utils/utils.go (+10/-1) wifiap/wifiap.go (+21/-1) wifiap/wifiap_test.go (+96/-8) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
System Enablement Bot | continuous-integration | Approve | |
Kyle Nitzsche (community) | Approve | ||
Konrad Zapałowicz (community) | code | Needs Fixing | |
Sheila Miguez (community) | Approve | ||
Review via email: mp+326286@code.launchpad.net |
Commit message
config-ui initial development
Description of the change
First time management portal is accessed, configuration is asked for.
If there is an existing config file having NoResetCredentials as true, that config is not shown and used existing one instead
- Backend config support including
* read/write local config, stored as $SNAP_COMMON/
* read/write remote config, set using wifi-ap.config exposed rest api
* considered all config read/write atomic operations with rollback support
* wide testing suite
- New section for config in management page
* allow user setting Portal and WiFi settings in different sections
* password - confirm password validation
* got existing remote configuration populating input fields
* show errors in page
* new 'saving' page when success
- Improved login validation
* using vanilla-fw
* skipping not needed additional info in error messages
* Enter is enabled to send password
- Applied vanilla-framework look&feel in all portals
- Updated handlers in server to fit needs
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:86fb783e427
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Roberto Mier Escandon (rmescandon) wrote : | # |
Just in case you want to test this, if "wifi.country-code" param is still not available in wifi-ap snap in store, you can apply this temporary patch to skip it:
Sheila Miguez (codersquid) wrote : | # |
I only have a few minor comments, see below.
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:7482f7df5ca
https:/
Executed test runs:
FAILURE: https:/
None: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Roberto Mier Escandon (rmescandon) wrote : | # |
Replied inline
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:71219489a59
https:/
Executed test runs:
FAILURE: https:/
None: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Sheila Miguez (codersquid) wrote : | # |
On Tue, Jun 27, 2017 at 6:17 AM, Roberto Mier Escandón <
<email address hidden>> wrote:
> Values are taken from http://
> compilation time by a scriptlet in snap/scriptlets
> Then they are transformed to be the select options and inserted in
> management.html replacing
>
I wonder if we should get it live all the time? Country codes don't change
very often do they? Maybe we could run it periodically to generate a static
list file in our repo. The test failure had me thinking of it.
Tests failed due to
../../.
Command '['/bin/sh', '/tmp/tmpwyb7k31n', '/tmp/tmp9f5mhf
non-zero exit status 127
--
<email address hidden>
Roberto Mier Escandon (rmescandon) wrote : | # |
The issue here is not adding curl as build-package for assets part in snapcraft.yaml.
But yes, you are right that the codes don't change unless a new country is created/destroyed (every 3 months or so that a new war begins :P).
That first stage of downloading codes can be executed manually and store results in a file used in compilation time.
Let me change it and populate README.md with steps to execute it
Roberto Mier Escandon (rmescandon) wrote : | # |
@Sheila code updated
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:ac6865a6120
https:/
Executed test runs:
FAILURE: https:/
None: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Sheila Miguez (codersquid) wrote : | # |
Once you fix the failing test (maybe you forgot to check in the file), I approve.
Roberto Mier Escandon (rmescandon) wrote : | # |
Oh, I didn't want to include country_code file there (the got from remote), but I'm afraid ci needs it :)
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:e23a9f8c13c
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Kyle Nitzsche (knitzsche) wrote : | # |
Having only run the code so far, some issues pop up:
- the user is only required to reset passphrase and password, so can we mark those two fields with an asterisk to indicate required onload?
- I think you can remove: "You need to set these config values before continuing:"
- The passphrase "Error: Value is too short. Must have 13 characters at least" is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we don't support hex)
- Please also change the error message to read "Error: the passphrase is too..." because "value" is unclear and it is not clear whether it applies to the passphrase or the confirm field
- can we provide a pre-populated selection list for Operation mode (like we do for channel)?
- county code: if you click into the empty country code box, then mouse over the list, then press the 'u' key, a country that does not start with 'u' is selected
- remove the "show operational portal" checkbox. By design, this can only be disabled by preconfiguration by system integrators, not at runtime
- after configuration everything and hitting apply, I got "could not save configuration, server error"
Konrad Zapałowicz (kzapalowicz) wrote : | # |
Minor wording to adjust.
I'm still thinking about reading the local & remote code part. Especially about introducing it as a Reader and Writer interfaces. Not sure yet however if it would play nice with the rest of the code and it is pretty local.
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:e05f1cd78fb
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:fbbbda4d75f
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:4b01c8f5873
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:89c9afc70f6
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Roberto Mier Escandon (rmescandon) wrote : | # |
> Having only run the code so far, some issues pop up:
> - the user is only required to reset passphrase and password, so can we mark
> those two fields with an asterisk to indicate required onload?
What happens with blank fields (imagine leaving SSID field empty). Should I not set that setting or should I delete it?. Think that when config page is loaded, wifi-ap current values populate the fields, so there shouldn't be any empty except password ones.
My opinion is that all them are required and the value provided here is the value to set when apply configuration button is pressed.
> - I think you can remove: "You need to set these config values before
> continuing:"
Removed
> - The passphrase "Error: Value is too short. Must have 13 characters at least"
> is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we
> don't support hex)
Changed
> - Please also change the error message to read "Error: the passphrase is
> too..." because "value" is unclear and it is not clear whether it applies to
> the passphrase or the confirm field
That was a generic method, that's the reason for having "value" instead of password or passphrase. I've added an additional parameter to customize error message depending on the input fields.
> - can we provide a pre-populated selection list for Operation mode (like we do
> for channel)?
Good idea. Done
> - county code: if you click into the empty country code box, then mouse over
> the list, then press the 'u' key, a country that does not start with 'u' is
> selected
Weird. Repeating steps i get Uganda. If I continue pressing u I see switch amongst other countries starting with u
> - remove the "show operational portal" checkbox. By design, this can only be
> disabled by preconfiguration by system integrators, not at runtime
Done
> - after configuration everything and hitting apply, I got "could not save
> configuration, server error"
I've updated error message to show more specific messages
Roberto Mier Escandon (rmescandon) wrote : | # |
@Konrad, Replied/fixed your comments.
Related with using readers and writers for the config. Looks like another valid aproach, but that would require some changes that I'm not sure if they worth...
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:11e4e3c697c
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Kyle Nitzsche (knitzsche) wrote : | # |
On 06/29/2017 12:17 PM, Roberto Mier Escandón wrote:
>> Having only run the code so far, some issues pop up:
>> - the user is only required to reset passphrase and password, so can we mark
>> those two fields with an asterisk to indicate required onload?
> What happens with blank fields (imagine leaving SSID field empty). Should I not set that setting or should I delete it?. Think that when config page is loaded, wifi-ap current values populate the fields, so there shouldn't be any empty except password ones.
> My opinion is that all them are required and the value provided here is the value to set when apply configuration button is pressed.
Right, all fields should be pre-populated with current values (except
password and passphrase and country code if country code is not yet set)
On first use, the user must reset passphrase and password, so these I
think should have an asterisk in this First Use case to indicate they
are required.
Form validation should not accept any blanks fields *except* for
passphrase and password IF they have already reset the creds OR
no-reset-creds is true 9screenly case). So SSID should never be empty.
Run time params should ONLY be reset if they are different (to prevent
unnecessary wifi-ap Down > UP.
Therefore the message at the bottom should say:
This may take down the device Wi-Fi AP and disconnect you. Please
reconnect in a minute or two
Also:
Please change the title "Access Point" to "Wi-Fi Access Point"
And change the title "Portal" to "Web Portal"
>
>> - I think you can remove: "You need to set these config values before
>> continuing:"
> Removed
>
thx
>> - The passphrase "Error: Value is too short. Must have 13 characters at least"
>> is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we
>> don't support hex)
> Changed
>
thx
>> - Please also change the error message to read "Error: the passphrase is
>> too..." because "value" is unclear and it is not clear whether it applies to
>> the passphrase or the confirm field
> That was a generic method, that's the reason for having "value" instead of password or passphrase. I've added an additional parameter to customize error message depending on the input fields.
thx
>> - can we provide a pre-populated selection list for Operation mode (like we do
>> for channel)?
> Good idea. Done
awesome
>
>> - county code: if you click into the empty country code box, then mouse over
>> the list, then press the 'u' key, a country that does not start with 'u' is
>> selected
> Weird. Repeating steps i get Uganda. If I continue pressing u I see switch amongst other countries starting with u
this issue is reproducible for me, are you skipping the mouse-over step?
>
>> - remove the "show operational portal" checkbox. By design, this can only be
>> disabled by preconfiguration by system integrators, not at runtime
> Done
great
>
>> - after configuration everything and hitting apply, I got "could not save
>> configuration, server error"
> I've updated error message to show more specific messages
>
Yes, now I see "Error:Error saving config: Error writing remote
configuration: wifi-ap set operation failed: "Failed: Invalid ke...
Roberto Mier Escandon (rmescandon) wrote : | # |
Ready to re-review.
The issue with combo box, tomorrow share your screen for me to see it. I think I'm not doing same steps.
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:d68f74461a5
https:/
Executed test runs:
FAILURE: https:/
None: https:/
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:8923330dafb
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Kyle Nitzsche (knitzsche) wrote : | # |
Adding some notes from external conversation:
config content should be always available (not just on first use)
config content should show on first use of management portal and be available from management portal at any later time
config content should physically be part of management.html (not in a separate html page). Visibility of parts is controlled by css appropriately.
Just to be clear, pre-config.json should NEVER be written to since it represents the system-integrator's pre config via gadget, if any
The only value you need to read from pre-config.json is portal.
I don't think we need to write any config.json. Any changed values should be set after form validates in wifi-ap (and the portal password if that is set)
All params on the config page except for portal.password should get their values for display from wifi-ap (no local storage of these values)
Emtpy fields (except for portal password and wifi passprhase) should cause form validation error.
After form validates, only changed params should be set. This prevents wifi-ap DOWN>UP if not needed.
Roberto Mier Escandon (rmescandon) wrote : | # |
>
>
> On 06/29/2017 12:17 PM, Roberto Mier Escandón wrote:
> >> Having only run the code so far, some issues pop up:
> >> - the user is only required to reset passphrase and password, so can we
> mark
> >> those two fields with an asterisk to indicate required onload?
> > What happens with blank fields (imagine leaving SSID field empty). Should I
> not set that setting or should I delete it?. Think that when config page is
> loaded, wifi-ap current values populate the fields, so there shouldn't be any
> empty except password ones.
> > My opinion is that all them are required and the value provided here is the
> value to set when apply configuration button is pressed.
>
> Right, all fields should be pre-populated with current values (except
> password and passphrase and country code if country code is not yet set)
>
> On first use, the user must reset passphrase and password, so these I
> think should have an asterisk in this First Use case to indicate they
> are required.
>
> Form validation should not accept any blanks fields *except* for
> passphrase and password IF they have already reset the creds OR
> no-reset-creds is true 9screenly case). So SSID should never be empty.
SSID and interface validate if they are blank when saving configuration.
>
> Run time params should ONLY be reset if they are different (to prevent
> unnecessary wifi-ap Down > UP.
They are only set if they are different. Before setting, old config is read
>
> Therefore the message at the bottom should say:
> This may take down the device Wi-Fi AP and disconnect you. Please
> reconnect in a minute or two
>
Changed
>
> Also:
>
> Please change the title "Access Point" to "Wi-Fi Access Point"
>
> And change the title "Portal" to "Web Portal"
>
Changed
> >
> >> - I think you can remove: "You need to set these config values before
> >> continuing:"
> > Removed
> >
>
> thx
>
> >> - The passphrase "Error: Value is too short. Must have 13 characters at
> least"
> >> is not correct. 8>= len(passphrase) <= 63 (or <=64 if hex, but so far we
> >> don't support hex)
> > Changed
> >
>
> thx
>
> >> - Please also change the error message to read "Error: the passphrase is
> >> too..." because "value" is unclear and it is not clear whether it applies
> to
> >> the passphrase or the confirm field
> > That was a generic method, that's the reason for having "value" instead of
> password or passphrase. I've added an additional parameter to customize error
> message depending on the input fields.
>
> thx
>
> >> - can we provide a pre-populated selection list for Operation mode (like we
> do
> >> for channel)?
> > Good idea. Done
>
> awesome
>
> >
> >> - county code: if you click into the empty country code box, then mouse
> over
> >> the list, then press the 'u' key, a country that does not start with 'u' is
> >> selected
> > Weird. Repeating steps i get Uganda. If I continue pressing u I see switch
> amongst other countries starting with u
>
> this issue is reproducible for me, are you skipping the mouse-over step?
>
> >
> >> - remove the "show operational portal" checkbox. By design, this can only
> be
> >> disabled by preconfiguration by system integra...
Roberto Mier Escandon (rmescandon) wrote : | # |
> Adding some notes from external conversation:
>
> config content should be always available (not just on first use)
This PR focus on first config
>
> config content should show on first use of management portal and be available
> from management portal at any later time
That has to be in another PR. This one only covers first config. Otherwise this PR can last forever adding new features not required at beginning.
>
> config content should physically be part of management.html (not in a separate
> html page). Visibility of parts is controlled by css appropriately.
I don't see the reason for this. Actually I'm completely against having all the portal in only one html template. However, so it is made.
>
> Just to be clear, pre-config.json should NEVER be written to since it
> represents the system-integrator's pre config via gadget, if any
It is not written in any case
>
> The only value you need to read from pre-config.json is portal.
> If this is true, then the user is not required to reset the portal password
> and the wifi passphrase on first use.
It is also reading portal.
>
> I don't think we need to write any config.json. Any changed values should be
> set after form validates in wifi-ap (and the portal password if that is set)
There is no config.json
>
> All params on the config page except for portal.password should get their
> values for display from wifi-ap (no local storage of these values)
All remote values are taken from wifi-ap, local ones from pre-config.json, though in config page any local one is populated
>
> Emtpy fields (except for portal password and wifi passprhase) should cause
> form validation error.
They do
>
> After form validates, only changed params should be set. This prevents wifi-ap
> DOWN>UP if not needed.
So it works
Kyle Nitzsche (knitzsche) wrote : | # |
> > Adding some notes from external conversation:
> >
> > config content should be always available (not just on first use)
> This PR focus on first config
>
>
> >
> > config content should show on first use of management portal and be
> available
> > from management portal at any later time
> That has to be in another PR. This one only covers first config. Otherwise
> this PR can last forever adding new features not required at beginning.
>
Are you saying this PR only displays config UI on first use? That's not the overall spec'd behavior. In any case, if you are planning to leave out normal configuration after first use, please create a trello card for this.
>
> >
> > config content should physically be part of management.html (not in a
> separate
> > html page). Visibility of parts is controlled by css appropriately.
> I don't see the reason for this. Actually I'm completely against having all
> the portal in only one html template. However, so it is made.
Can you please provide your reasoning?
>
>
>
> >
> > Just to be clear, pre-config.json should NEVER be written to since it
> > represents the system-integrator's pre config via gadget, if any
> It is not written in any case
>
> >
> > The only value you need to read from pre-config.json is portal.no-reset-
> creds.
> > If this is true, then the user is not required to reset the portal password
> > and the wifi passphrase on first use.
> It is also reading portal.
> anywhere
Can you drop the no-operational part? (You may have added that when you thought we wanted a Checkbox to not display the oper portal, but really that's never going to be configured by this UI.
>
> >
> > I don't think we need to write any config.json. Any changed values should be
> > set after form validates in wifi-ap (and the portal password if that is set)
> There is no config.json
>
>
> >
> > All params on the config page except for portal.password should get their
> > values for display from wifi-ap (no local storage of these values)
> All remote values are taken from wifi-ap, local ones from pre-config.json,
> though in config page any local one is populated
Which local ones are you populating?
thx
>
>
> >
> > Emtpy fields (except for portal password and wifi passprhase) should cause
> > form validation error.
> They do
>
> >
> > After form validates, only changed params should be set. This prevents wifi-
> ap
> > DOWN>UP if not needed.
> So it works
Roberto Mier Escandon (rmescandon) wrote : | # |
> > > Adding some notes from external conversation:
> > >
> > > config content should be always available (not just on first use)
> > This PR focus on first config
> >
> >
> > >
> > > config content should show on first use of management portal and be
> > available
> > > from management portal at any later time
> > That has to be in another PR. This one only covers first config. Otherwise
> > this PR can last forever adding new features not required at beginning.
> >
>
> Are you saying this PR only displays config UI on first use? That's not the
> overall spec'd behavior. In any case, if you are planning to leave out normal
> configuration after first use, please create a trello card for this.
This is the card related with this PR "As KyleN I want to have access to a simple web UI so that a user can configure the SSID and password for the AP" (That's the title. Description is empty)
From a simple web UI, we have moved to a complete configuration portal, including SSID, wifi passphrase, interface, country code (dynamically populated from remote content), channel and portal password; input verification of not empty fields, length for passwords, password and confirmation be equals, switch to official look&feel framework, replaced js alerts to notification divs, complete config backend support including a large tests suite, red asterisks on password fields, additional message div when saving config, removal of crackable login messages...
and you pretend that also a button to show config in management portal and rendering a different config page (with no red asterisks), behaving different (not requesting changing password or passphrase) should be included in this MP?
If you want to complete project in this MP just be clear and let me know. Is the overall specification applying only to this MP? I don't think so.
If you want to add another card in backlog, do it yourself. I'm not in this project on monday, I should have left it one week ago and, for helping and trying to finish my pending tasks, I've been warned about it today. So, whatever it takes to you.
>
> >
> > >
> > > config content should physically be part of management.html (not in a
> > separate
> > > html page). Visibility of parts is controlled by css appropriately.
> > I don't see the reason for this. Actually I'm completely against having all
> > the portal in only one html template. However, so it is made.
>
> Can you please provide your reasoning?
Is it really needed explaining why having a complete web with several pages in only one html file is crazy?
Is this like explaining what's the meaning of a method name?
are you kidding?
>
> >
> >
> >
> > >
> > > Just to be clear, pre-config.json should NEVER be written to since it
> > > represents the system-integrator's pre config via gadget, if any
> > It is not written in any case
> >
> > >
> > > The only value you need to read from pre-config.json is portal.no-reset-
> > creds.
> > > If this is true, then the user is not required to reset the portal
> password
> > > and the wifi passphrase on first use.
> > It is also reading portal.
> > anywhere
>
> Can you drop the no-opera...
Kyle Nitzsche (knitzsche) wrote : | # |
Hi Roberto,
Thanks for the PR.
One reason to keep the config ui in the management.html page is that after form validation, we could display your "saving config... you need to reconnect" message immediately (show the div, hide the others), whereas with this PR, that message displayed for me *after* the wifi-ap went down and I reconnected, at which time it makes no sense. I'll log this as bug.
Given the time constraints, please make one change: in wifiap.go, please delete the wifi.country-code param at the top of the Set() func, and add a //TODO comment saying this needs to be removed when wifiap.go supports it. (I think you said it is not yet supported). Otherwise, the PR is broken and you can't get passed the config page.)
Kyle Nitzsche (knitzsche) wrote : | # |
Also, the management page design has changed, and regressed I think. I filed this bug: https:/
Here's the bug I mentioned above about saving config message being displayed too late:
https:/
Kyle Nitzsche (knitzsche) wrote : | # |
Alfonso said the country will land and be published soon, so please merge this as is if possible.
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:143afe200e9
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
System Enablement Bot (system-enablement-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:630ded9b1e0
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/.gitignore b/.gitignore |
2 | index b923a21..41bf8bd 100644 |
3 | --- a/.gitignore |
4 | +++ b/.gitignore |
5 | @@ -1,3 +1,6 @@ |
6 | +!/.gitignore |
7 | +*.snap |
8 | +*.tar.bz2 |
9 | # testing files |
10 | .coverage |
11 | .tests-extras |
12 | @@ -11,4 +14,3 @@ parts/ |
13 | prime/ |
14 | snap/.snapcraft/ |
15 | stage/ |
16 | -*.snap |
17 | diff --git a/README.md b/README.md |
18 | index 1759797..e30f47d 100644 |
19 | --- a/README.md |
20 | +++ b/README.md |
21 | @@ -265,6 +265,46 @@ Restart the daemon normal loop cleanly with: |
22 | sudo wifi-connect start |
23 | ``` |
24 | |
25 | +# Configuration |
26 | + |
27 | +First time one access management portal a configuration page is shown. Once there, settings for Wi-Fi access point and portals password can be saved. |
28 | +That configuration is composed of one first section for Wi-Fi ones and another second for portal |
29 | + |
30 | +*Wi-Fi config*: |
31 | +| Config Key | Description | |
32 | +|----------------|-------------| |
33 | +| SSID | of the access point | |
34 | +| Passphrase | password to connect to access point | |
35 | +| Interface | network interface where access point must be brought up | |
36 | +| Country Code | ISO-3166 two digits code to use in Wi-Fi | |
37 | +| Channel | Wi-Fi channel | |
38 | +| Operation Mode | Wi-Fi operation mode | |
39 | + |
40 | +*Portal config*: |
41 | +| Config Key | Description | |
42 | +|----------------|-------------| |
43 | +| Password | to access any of management or operational portals | |
44 | + |
45 | +There is a button to apply configuration just below the input fields. While configuration |
46 | +is being saved a different page will inform about changes are in process to be saved. After small delay one |
47 | +should be able to reconnect to AP in order to see regular management page showing available ssids. |
48 | + |
49 | +NOTE: If there is any previous config file at $SNAP_COMMON/config.json containining 'portal.no-reset-creds' key with value true, first time accessing |
50 | +management portal won't show config page to modify it. |
51 | + |
52 | +## Country codes |
53 | + |
54 | +There is a combo box in config page to select the country code to use in the access point. |
55 | +Its values are injected in compilation time from a file stored at snap/scriptlets/country-codes. |
56 | +This file is generated dynamically when executed by hand this script: |
57 | + |
58 | +``` |
59 | +snap/scriptlets/fetch_country_codes.sh |
60 | +``` |
61 | + |
62 | +So, every time you want to update country codes to most recent ones, you have to execute |
63 | +that script by hand and, after success, rebuild and install snap |
64 | + |
65 | # Tests |
66 | ## Unit Tests |
67 | |
68 | diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go |
69 | index 177c26b..b7d0199 100644 |
70 | --- a/daemon/daemon_test.go |
71 | +++ b/daemon/daemon_test.go |
72 | @@ -163,6 +163,10 @@ func (mock *mockWifiap) SetPassphrase(p string) error { |
73 | return nil |
74 | } |
75 | |
76 | +func (mock *mockWifiap) Set(map[string]interface{}) error { |
77 | + return nil |
78 | +} |
79 | + |
80 | func TestLoadPreConfig(t *testing.T) { |
81 | PreConfigFile = "../static/tests/pre-config0.json" |
82 | config, err := LoadPreConfig() |
83 | diff --git a/gulpfile.js b/gulpfile.js |
84 | index e33a752..904f3d1 100644 |
85 | --- a/gulpfile.js |
86 | +++ b/gulpfile.js |
87 | @@ -9,4 +9,8 @@ gulp.task('sass', function() { |
88 | }).on('error', sass.logError)) |
89 | .pipe(uglifycss()) |
90 | .pipe(gulp.dest('static/css')); |
91 | -}); |
92 | \ No newline at end of file |
93 | +}); |
94 | + |
95 | +// Default: remember that these tasks get run asynchronously |
96 | +gulp.task('default', ['sass']); |
97 | + |
98 | diff --git a/netman/dbus.go b/netman/dbus.go |
99 | index 2db7107..8f36ac9 100644 |
100 | --- a/netman/dbus.go |
101 | +++ b/netman/dbus.go |
102 | @@ -20,16 +20,32 @@ package netman |
103 | import ( |
104 | "errors" |
105 | "fmt" |
106 | + "io" |
107 | "log" |
108 | "os" |
109 | "strings" |
110 | "time" |
111 | |
112 | - "io" |
113 | - |
114 | "github.com/godbus/dbus" |
115 | ) |
116 | |
117 | +// Operations defines operations for this package client |
118 | +type Operations interface { |
119 | + GetDevices() []string |
120 | + GetWifiDevices(devices []string) []string |
121 | + GetAccessPoints(devices []string, ap2device map[string]string) []string |
122 | + ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error |
123 | + Ssids() ([]SSID, map[string]string, map[string]string) |
124 | + Connected(devices []string) bool |
125 | + ConnectedWifi(wifiDevices []string) bool |
126 | + DisconnectWifi(wifiDevices []string) int |
127 | + SetIfaceManaged(iface string, state bool, devices []string) string |
128 | + WifisManaged(wifiDevices []string) (map[string]string, error) |
129 | + Unmanage() error |
130 | + Manage() error |
131 | + ScanAndWriteSsidsToFile(filepath string) bool |
132 | +} |
133 | + |
134 | // Client type to support unit test mock and runtime execution |
135 | type Client struct { |
136 | dbusClient DbusClient |
137 | diff --git a/package.json b/package.json |
138 | index d3e2671..baf164b 100644 |
139 | --- a/package.json |
140 | +++ b/package.json |
141 | @@ -2,15 +2,15 @@ |
142 | "name": "wifi-connect", |
143 | "version": "1.0.0", |
144 | "description": "Management UI to be able to switch a wireless card of a UC device into AP mode and use it to configure wireless", |
145 | - "main": "index.js", |
146 | "scripts": { |
147 | - "test": "echo \"Error: no test specified\" && exit 1" |
148 | + "test": "echo \"Error: no test specified\" && exit 1", |
149 | + "build": "gulp" |
150 | }, |
151 | "repository": { |
152 | "type": "git", |
153 | "url": "git+ssh://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect" |
154 | }, |
155 | - "author": "", |
156 | + "author": "Canonical Ltd.", |
157 | "license": "GPL-3.0", |
158 | "bugs": { |
159 | "url": "https://bugs.launchpad.net/snappy-hwe-snaps" |
160 | @@ -20,6 +20,6 @@ |
161 | "gulp": "^3.9.1", |
162 | "gulp-sass": "^3.1.0", |
163 | "gulp-uglifycss": "^1.0.8", |
164 | - "ubuntu-vanilla-theme": "0.0.35" |
165 | + "vanilla-framework": "^1.2.2" |
166 | } |
167 | } |
168 | diff --git a/server/handlers.go b/server/handlers.go |
169 | index f28c2d8..c14189f 100644 |
170 | --- a/server/handlers.go |
171 | +++ b/server/handlers.go |
172 | @@ -27,6 +27,8 @@ import ( |
173 | "text/template" |
174 | "time" |
175 | |
176 | + "strconv" |
177 | + |
178 | "launchpad.net/wifi-connect/utils" |
179 | ) |
180 | |
181 | @@ -34,19 +36,29 @@ const ( |
182 | managementTemplatePath = "/templates/management.html" |
183 | connectingTemplatePath = "/templates/connecting.html" |
184 | operationalTemplatePath = "/templates/operational.html" |
185 | + firstConfigTemplatePath = "/templates/config.html" |
186 | ) |
187 | |
188 | // ResourcesPath absolute path to web static resources |
189 | var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static") |
190 | |
191 | +// first time management portal is accessed this file is created. |
192 | +var firstConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".first_config") |
193 | + |
194 | var cw interface{} |
195 | |
196 | // Data interface representing any data included in a template |
197 | type Data interface{} |
198 | |
199 | -// SsidsData dynamic data to fulfill the SSIDs page template |
200 | -type SsidsData struct { |
201 | - Ssids []string |
202 | +// ManagementData dynamic data to fulfill the management page. |
203 | +// It can contain SSIDs list or snap configuration |
204 | +// NOTE: page is a workaround to render proper grid depending on its value (ssids or config). |
205 | +// In case authentication mechanism is modified for not to be so intrusive, we could have |
206 | +// different pages for this |
207 | +type ManagementData struct { |
208 | + Ssids []string |
209 | + Config *utils.Config |
210 | + Page string |
211 | } |
212 | |
213 | // ConnectingData dynamic data to fulfill the connect result page template |
214 | @@ -61,22 +73,41 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) { |
215 | templateAbsPath := filepath.Join(ResourcesPath, templatePath) |
216 | t, err := template.ParseFiles(templateAbsPath) |
217 | if err != nil { |
218 | - log.Printf("Error loading the template at %v: %v", templatePath, err) |
219 | - http.Error(w, err.Error(), http.StatusInternalServerError) |
220 | + msg := fmt.Sprintf("Error loading the template at %v: %v", templatePath, err) |
221 | + log.Println(msg) |
222 | + http.Error(w, msg, http.StatusInternalServerError) |
223 | return |
224 | } |
225 | |
226 | err = t.Execute(w, data) |
227 | if err != nil { |
228 | - log.Printf("Error executing the template at %v: %v", templatePath, err) |
229 | - http.Error(w, err.Error(), http.StatusInternalServerError) |
230 | + msg := fmt.Sprintf("Error executing the template at %v : %v", templatePath, err) |
231 | + log.Print(msg) |
232 | + http.Error(w, msg, http.StatusInternalServerError) |
233 | return |
234 | } |
235 | } |
236 | |
237 | -// ManagementHandler lists the current available SSIDs |
238 | +// ManagementHandler handles management portal |
239 | func ManagementHandler(w http.ResponseWriter, r *http.Request) { |
240 | - // daemon stores current available ssids in a file |
241 | + |
242 | + if utils.MustSetConfig() { |
243 | + |
244 | + config, err := utils.ReadConfig() |
245 | + if err != nil { |
246 | + msg := fmt.Sprintf("Error reading configuration: %v", err) |
247 | + log.Println(msg) |
248 | + http.Error(w, msg, http.StatusInternalServerError) |
249 | + return |
250 | + } |
251 | + |
252 | + if !config.Portal.NoResetCredentials { |
253 | + execTemplate(w, managementTemplatePath, ManagementData{Config: config, Page: "config"}) |
254 | + return |
255 | + } |
256 | + |
257 | + } |
258 | + |
259 | ssids, err := utils.ReadSsidsFile() |
260 | if err != nil { |
261 | log.Printf("Error reading SSIDs file: %v", err) |
262 | @@ -84,15 +115,58 @@ func ManagementHandler(w http.ResponseWriter, r *http.Request) { |
263 | return |
264 | } |
265 | |
266 | - data := SsidsData{Ssids: ssids} |
267 | + execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"}) |
268 | +} |
269 | + |
270 | +// SaveConfigHandler saves config received as form post parameters |
271 | +func SaveConfigHandler(w http.ResponseWriter, r *http.Request) { |
272 | + // read previous config |
273 | + config, err := utils.ReadConfig() |
274 | + if err != nil { |
275 | + msg := fmt.Sprintf("Error reading previous stored config: %v", err) |
276 | + log.Println(msg) |
277 | + http.Error(w, msg, http.StatusInternalServerError) |
278 | + return |
279 | + } |
280 | + |
281 | + r.ParseForm() |
282 | + |
283 | + config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid") |
284 | + config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase") |
285 | + config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface") |
286 | + config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode") |
287 | + config.Wifi.Channel, err = strconv.Atoi(utils.ParseFormParamSingleValue(r.Form, "Channel")) |
288 | + if err != nil { |
289 | + msg := fmt.Sprintf("Error parsing channel form value: %v", err) |
290 | + log.Println(msg) |
291 | + http.Error(w, msg, http.StatusInternalServerError) |
292 | + return |
293 | + } |
294 | + config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode") |
295 | + config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword") |
296 | + |
297 | + err = utils.WriteConfig(config) |
298 | + if err != nil { |
299 | + msg := fmt.Sprintf("Error saving config: %v", err) |
300 | + log.Println(msg) |
301 | + http.Error(w, msg, http.StatusInternalServerError) |
302 | + return |
303 | + } |
304 | |
305 | - // parse template |
306 | - execTemplate(w, managementTemplatePath, data) |
307 | + //after saving config, redirect to management portal, showing available ssids |
308 | + ssids, err := utils.ReadSsidsFile() |
309 | + if err != nil { |
310 | + msg := fmt.Sprintf("== wifi-connect/handler: Error reading SSIDs file: %v\n", err) |
311 | + log.Println(msg) |
312 | + http.Error(w, msg, http.StatusInternalServerError) |
313 | + return |
314 | + } |
315 | + |
316 | + execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"}) |
317 | } |
318 | |
319 | // ConnectHandler reads form got ssid and password and tries to connect to that network |
320 | func ConnectHandler(w http.ResponseWriter, r *http.Request) { |
321 | - |
322 | r.ParseForm() |
323 | |
324 | pwd := "" |
325 | @@ -103,7 +177,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) { |
326 | |
327 | ssids := r.Form["ssid"] |
328 | if len(ssids) == 0 { |
329 | - log.Print("SSID not available") |
330 | + msg := "SSID not available" |
331 | + log.Print(msg) |
332 | + http.Error(w, msg, http.StatusBadRequest) |
333 | return |
334 | } |
335 | ssid := ssids[0] |
336 | @@ -116,7 +192,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) { |
337 | |
338 | err := wifiapClient.Disable() |
339 | if err != nil { |
340 | - log.Printf("Error disabling AP: %v", err) |
341 | + msg := fmt.Sprintf("Error disabling AP: %v", err) |
342 | + log.Print(msg) |
343 | + http.Error(w, msg, http.StatusInternalServerError) |
344 | return |
345 | } |
346 | |
347 | @@ -127,7 +205,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) { |
348 | err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap) |
349 | //TODO signal user in portal on failure to connect |
350 | if err != nil { |
351 | - log.Printf("Failed connecting to %v.", ssid) |
352 | + msg := fmt.Sprintf("Failed connecting to %v", ssid) |
353 | + log.Println(msg) |
354 | + http.Error(w, msg, http.StatusInternalServerError) |
355 | return |
356 | } |
357 | |
358 | diff --git a/server/handlers_test.go b/server/handlers_test.go |
359 | index ad8eda7..336d242 100644 |
360 | --- a/server/handlers_test.go |
361 | +++ b/server/handlers_test.go |
362 | @@ -56,6 +56,10 @@ func (c *wifiapClientMock) SetPassphrase(string) error { |
363 | return nil |
364 | } |
365 | |
366 | +func (c *wifiapClientMock) Set(map[string]interface{}) error { |
367 | + return nil |
368 | +} |
369 | + |
370 | type netmanClientMock struct{} |
371 | |
372 | func (c *netmanClientMock) GetDevices() []string { |
373 | @@ -111,6 +115,26 @@ func (c *netmanClientMock) ScanAndWriteSsidsToFile(filepath string) bool { |
374 | |
375 | func TestManagementHandler(t *testing.T) { |
376 | |
377 | + // mock settings to not to ask for saving a first configuration |
378 | + utils.ReadConfig = func() (*utils.Config, error) { |
379 | + return &utils.Config{ |
380 | + Wifi: &utils.WifiConfig{ |
381 | + Ssid: "Ubuntu", |
382 | + Passphrase: "17Soj8/Sxh14lcpD", |
383 | + Interface: "wlp2s0", |
384 | + CountryCode: "0x31", |
385 | + Channel: 6, |
386 | + OperationMode: "g", |
387 | + }, |
388 | + Portal: &utils.PortalConfig{ |
389 | + Password: "the_password", |
390 | + NoResetCredentials: true, |
391 | + NoOperational: false, |
392 | + }, |
393 | + }, nil |
394 | + } |
395 | + utils.MustSetConfig = func() bool { return false } |
396 | + |
397 | ResourcesPath = "../static" |
398 | SsidsFile := "../static/tests/ssids" |
399 | utils.SetSsidsFile(SsidsFile) |
400 | diff --git a/server/middleware.go b/server/middleware.go |
401 | index a63c717..99bd6f9 100644 |
402 | --- a/server/middleware.go |
403 | +++ b/server/middleware.go |
404 | @@ -26,34 +26,8 @@ import ( |
405 | "launchpad.net/wifi-connect/wifiap" |
406 | ) |
407 | |
408 | -// Operations interface defining operations implemented by wifiap client |
409 | -type wifiapOperations interface { |
410 | - Show() (map[string]interface{}, error) |
411 | - Enabled() (bool, error) |
412 | - Enable() error |
413 | - Disable() error |
414 | - SetSsid(string) error |
415 | - SetPassphrase(string) error |
416 | -} |
417 | - |
418 | -type netmanOperations interface { |
419 | - GetDevices() []string |
420 | - GetWifiDevices(devices []string) []string |
421 | - GetAccessPoints(devices []string, ap2device map[string]string) []string |
422 | - ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error |
423 | - Ssids() ([]netman.SSID, map[string]string, map[string]string) |
424 | - Connected(devices []string) bool |
425 | - ConnectedWifi(wifiDevices []string) bool |
426 | - DisconnectWifi(wifiDevices []string) int |
427 | - SetIfaceManaged(iface string, state bool, devices []string) string |
428 | - WifisManaged(wifiDevices []string) (map[string]string, error) |
429 | - Unmanage() error |
430 | - Manage() error |
431 | - ScanAndWriteSsidsToFile(filepath string) bool |
432 | -} |
433 | - |
434 | -var wifiapClient wifiapOperations |
435 | -var netmanClient netmanOperations |
436 | +var wifiapClient wifiap.Operations |
437 | +var netmanClient netman.Operations |
438 | |
439 | // Middleware to pre-process web service requests |
440 | func Middleware(inner http.Handler) http.Handler { |
441 | diff --git a/server/router.go b/server/router.go |
442 | index 47e63f3..20a3716 100644 |
443 | --- a/server/router.go |
444 | +++ b/server/router.go |
445 | @@ -28,7 +28,8 @@ func managementHandler() *mux.Router { |
446 | router := mux.NewRouter() |
447 | |
448 | // Pages routes |
449 | - router.HandleFunc("/", ManagementHandler).Methods("GET") |
450 | + router.Handle("/", Middleware(http.HandlerFunc(ManagementHandler))).Methods("GET") |
451 | + router.Handle("/config", Middleware(http.HandlerFunc(SaveConfigHandler))).Methods("POST") |
452 | router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST") |
453 | router.HandleFunc("/hashit", HashItHandler).Methods("POST") |
454 | router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET") |
455 | diff --git a/snap/scriptlets/country-codes b/snap/scriptlets/country-codes |
456 | new file mode 100644 |
457 | index 0000000..d4e8a97 |
458 | --- /dev/null |
459 | +++ b/snap/scriptlets/country-codes |
460 | @@ -0,0 +1,243 @@ |
461 | +<html><head><title>ISO-3166-1 Country Names</title></head> |
462 | +<body bgcolor=#ffffff> |
463 | +<h2>ISO-3166-1 Country Names</h2> |
464 | +<a href="/iso3166/">Reproduced with permission from ISO</a> |
465 | +<p> |
466 | +Follow links for ISO-3166-2 subdivision codes |
467 | +<p> |
468 | +<a href=iso.AD.html>AD</a> : ANDORRA<br> |
469 | +<a href=iso.AE.html>AE</a> : UNITED ARAB EMIRATES<br> |
470 | +<a href=iso.AF.html>AF</a> : AFGHANISTAN<br> |
471 | +<a href=iso.AG.html>AG</a> : ANTIGUA AND BARBUDA<br> |
472 | +<a href=iso.AI.html>AI</a> : ANGUILLA<br> |
473 | +<a href=iso.AL.html>AL</a> : ALBANIA<br> |
474 | +<a href=iso.AM.html>AM</a> : ARMENIA<br> |
475 | +<a href=iso.AN.html>AN</a> : NETHERLANDS ANTILLES<br> |
476 | +<a href=iso.AO.html>AO</a> : ANGOLA<br> |
477 | +<a href=iso.AQ.html>AQ</a> : ANTARCTICA<br> |
478 | +<a href=iso.AR.html>AR</a> : ARGENTINA<br> |
479 | +<a href=iso.AS.html>AS</a> : AMERICAN SAMOA<br> |
480 | +<a href=iso.AT.html>AT</a> : AUSTRIA<br> |
481 | +<a href=iso.AU.html>AU</a> : AUSTRALIA<br> |
482 | +<a href=iso.AW.html>AW</a> : ARUBA<br> |
483 | +<a href=iso.AZ.html>AZ</a> : AZERBAIJAN<br> |
484 | +<a href=iso.BA.html>BA</a> : BOSNIA AND HERZEGOVINA<br> |
485 | +<a href=iso.BB.html>BB</a> : BARBADOS<br> |
486 | +<a href=iso.BD.html>BD</a> : BANGLADESH<br> |
487 | +<a href=iso.BE.html>BE</a> : BELGIUM<br> |
488 | +<a href=iso.BF.html>BF</a> : BURKINA FASO<br> |
489 | +<a href=iso.BG.html>BG</a> : BULGARIA<br> |
490 | +<a href=iso.BH.html>BH</a> : BAHRAIN<br> |
491 | +<a href=iso.BI.html>BI</a> : BURUNDI<br> |
492 | +<a href=iso.BJ.html>BJ</a> : BENIN<br> |
493 | +<a href=iso.BM.html>BM</a> : BERMUDA<br> |
494 | +<a href=iso.BN.html>BN</a> : BRUNEI DARUSSALAM<br> |
495 | +<a href=iso.BO.html>BO</a> : BOLIVIA<br> |
496 | +<a href=iso.BR.html>BR</a> : BRAZIL<br> |
497 | +<a href=iso.BS.html>BS</a> : BAHAMAS<br> |
498 | +<a href=iso.BT.html>BT</a> : BHUTAN<br> |
499 | +<a href=iso.BV.html>BV</a> : BOUVET ISLAND<br> |
500 | +<a href=iso.BW.html>BW</a> : BOTSWANA<br> |
501 | +<a href=iso.BY.html>BY</a> : BELARUS<br> |
502 | +<a href=iso.BZ.html>BZ</a> : BELIZE<br> |
503 | +<a href=iso.CA.html>CA</a> : CANADA<br> |
504 | +<a href=iso.CC.html>CC</a> : COCOS (KEELING) ISLANDS<br> |
505 | +<a href=iso.CD.html>CD</a> : CONGO, THE DEMOCRATIC REPUBLIC OF THE<br> |
506 | +<a href=iso.CF.html>CF</a> : CENTRAL AFRICAN REPUBLIC<br> |
507 | +<a href=iso.CG.html>CG</a> : CONGO<br> |
508 | +<a href=iso.CH.html>CH</a> : SWITZERLAND<br> |
509 | +<a href=iso.CI.html>CI</a> : C�TE D'IVOIRE<br> |
510 | +<a href=iso.CK.html>CK</a> : COOK ISLANDS<br> |
511 | +<a href=iso.CL.html>CL</a> : CHILE<br> |
512 | +<a href=iso.CM.html>CM</a> : CAMEROON<br> |
513 | +<a href=iso.CN.html>CN</a> : CHINA<br> |
514 | +<a href=iso.CO.html>CO</a> : COLOMBIA<br> |
515 | +<a href=iso.CR.html>CR</a> : COSTA RICA<br> |
516 | +<a href=iso.CU.html>CU</a> : CUBA<br> |
517 | +<a href=iso.CV.html>CV</a> : CAPE VERDE<br> |
518 | +<a href=iso.CX.html>CX</a> : CHRISTMAS ISLAND<br> |
519 | +<a href=iso.CY.html>CY</a> : CYPRUS<br> |
520 | +<a href=iso.CZ.html>CZ</a> : CZECH REPUBLIC<br> |
521 | +<a href=iso.DE.html>DE</a> : GERMANY<br> |
522 | +<a href=iso.DJ.html>DJ</a> : DJIBOUTI<br> |
523 | +<a href=iso.DK.html>DK</a> : DENMARK<br> |
524 | +<a href=iso.DM.html>DM</a> : DOMINICA<br> |
525 | +<a href=iso.DO.html>DO</a> : DOMINICAN REPUBLIC<br> |
526 | +<a href=iso.DZ.html>DZ</a> : ALGERIA<br> |
527 | +<a href=iso.EC.html>EC</a> : ECUADOR<br> |
528 | +<a href=iso.EE.html>EE</a> : ESTONIA<br> |
529 | +<a href=iso.EG.html>EG</a> : EGYPT<br> |
530 | +<a href=iso.EH.html>EH</a> : WESTERN SARARA<br> |
531 | +<a href=iso.ER.html>ER</a> : ERITREA<br> |
532 | +<a href=iso.ES.html>ES</a> : SPAIN<br> |
533 | +<a href=iso.ET.html>ET</a> : ETHIOPIA<br> |
534 | +<a href=iso.FI.html>FI</a> : FINLAND<br> |
535 | +<a href=iso.FJ.html>FJ</a> : FIJI<br> |
536 | +<a href=iso.FK.html>FK</a> : FALKLAND ISLANDS (MALVINAS)<br> |
537 | +<a href=iso.FM.html>FM</a> : MICRONESIA, FEDERATED STATES OF<br> |
538 | +<a href=iso.FO.html>FO</a> : FAROE ISLANDS<br> |
539 | +<a href=iso.FR.html>FR</a> : FRANCE<br> |
540 | +<a href=iso.GA.html>GA</a> : GABON<br> |
541 | +<a href=iso.GB.html>GB</a> : UNITED KINGDOM<br> |
542 | +<a href=iso.GD.html>GD</a> : GRENADA<br> |
543 | +<a href=iso.GE.html>GE</a> : GEORGIA<br> |
544 | +<a href=iso.GF.html>GF</a> : FRENCH GUIANA<br> |
545 | +<a href=iso.GH.html>GH</a> : GHANA<br> |
546 | +<a href=iso.GI.html>GI</a> : GIBRALTAR<br> |
547 | +<a href=iso.GL.html>GL</a> : GREENLAND<br> |
548 | +<a href=iso.GM.html>GM</a> : GAMBIA<br> |
549 | +<a href=iso.GN.html>GN</a> : GUINEA<br> |
550 | +<a href=iso.GP.html>GP</a> : GUADELOUPE<br> |
551 | +<a href=iso.GQ.html>GQ</a> : EQUATORIAL GUINEA<br> |
552 | +<a href=iso.GR.html>GR</a> : GREECE<br> |
553 | +<a href=iso.GS.html>GS</a> : SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS<br> |
554 | +<a href=iso.GT.html>GT</a> : GUATEMALA<br> |
555 | +<a href=iso.GU.html>GU</a> : GUAM<br> |
556 | +<a href=iso.GW.html>GW</a> : GUINEA-BISSAU<br> |
557 | +<a href=iso.GY.html>GY</a> : GUYANA<br> |
558 | +<a href=iso.HK.html>HK</a> : HONG KONG<br> |
559 | +<a href=iso.HM.html>HM</a> : HEARD ISLAND AND MCDONALD ISLANDS<br> |
560 | +<a href=iso.HN.html>HN</a> : HONDURAS<br> |
561 | +<a href=iso.HR.html>HR</a> : CROATIA<br> |
562 | +<a href=iso.HT.html>HT</a> : HAITI<br> |
563 | +<a href=iso.HU.html>HU</a> : HUNGARY<br> |
564 | +<a href=iso.ID.html>ID</a> : INDONESIA<br> |
565 | +<a href=iso.IE.html>IE</a> : IRELAND<br> |
566 | +<a href=iso.IL.html>IL</a> : ISRAEL<br> |
567 | +<a href=iso.IN.html>IN</a> : INDIA<br> |
568 | +<a href=iso.IO.html>IO</a> : BRITISH INDIAN OCEAN TERRITORY<br> |
569 | +<a href=iso.IQ.html>IQ</a> : IRAQ<br> |
570 | +<a href=iso.IR.html>IR</a> : IRAN, ISLAMIC REPUBLIC OF<br> |
571 | +<a href=iso.IS.html>IS</a> : ICELAND<br> |
572 | +<a href=iso.IT.html>IT</a> : ITALY<br> |
573 | +<a href=iso.JM.html>JM</a> : JAMAICA<br> |
574 | +<a href=iso.JO.html>JO</a> : JORDAN<br> |
575 | +<a href=iso.JP.html>JP</a> : JAPAN<br> |
576 | +<a href=iso.KE.html>KE</a> : KENYA<br> |
577 | +<a href=iso.KG.html>KG</a> : KYRGYZSTAN<br> |
578 | +<a href=iso.KH.html>KH</a> : CAMBODIA<br> |
579 | +<a href=iso.KI.html>KI</a> : KIRIBATI<br> |
580 | +<a href=iso.KM.html>KM</a> : COMOROS<br> |
581 | +<a href=iso.KN.html>KN</a> : SAINT KITTS AND NEVIS<br> |
582 | +<a href=iso.KP.html>KP</a> : KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF<br> |
583 | +<a href=iso.KR.html>KR</a> : KOREA, REPUBLIC OF<br> |
584 | +<a href=iso.KW.html>KW</a> : KUWAIT<br> |
585 | +<a href=iso.KY.html>KY</a> : CAYMAN ISLANDS<br> |
586 | +<a href=iso.KZ.html>KZ</a> : KAZAKHSTAN<br> |
587 | +<a href=iso.LA.html>LA</a> : LAO PEOPLE'S DEMOCRATIC REPUBLIC<br> |
588 | +<a href=iso.LB.html>LB</a> : LEBANON<br> |
589 | +<a href=iso.LC.html>LC</a> : SAINT LUCIA<br> |
590 | +<a href=iso.LI.html>LI</a> : LIECHTENSTEIN<br> |
591 | +<a href=iso.LK.html>LK</a> : SRI LANKA<br> |
592 | +<a href=iso.LR.html>LR</a> : LIBERIA<br> |
593 | +<a href=iso.LS.html>LS</a> : LESOTHO<br> |
594 | +<a href=iso.LT.html>LT</a> : LITHUANIA<br> |
595 | +<a href=iso.LU.html>LU</a> : LUXEMBOURG<br> |
596 | +<a href=iso.LV.html>LV</a> : LATVIA<br> |
597 | +<a href=iso.LY.html>LY</a> : LIBYAN ARAB JAMABIRIYA<br> |
598 | +<a href=iso.MA.html>MA</a> : MOROCCO<br> |
599 | +<a href=iso.MC.html>MC</a> : MONACO<br> |
600 | +<a href=iso.MD.html>MD</a> : MOLDOVA, REPUBLIC OF<br> |
601 | +<a href=iso.MG.html>MG</a> : MADAGASCAR<br> |
602 | +<a href=iso.MH.html>MH</a> : MARSHALL ISLANDS<br> |
603 | +<a href=iso.MK.html>MK</a> : MACEDONIA, THE FORMER YUGOSLAV REPU8LIC OF<br> |
604 | +<a href=iso.ML.html>ML</a> : MALI<br> |
605 | +<a href=iso.MM.html>MM</a> : MYANMAR<br> |
606 | +<a href=iso.MN.html>MN</a> : MONGOLIA<br> |
607 | +<a href=iso.MO.html>MO</a> : MACAU<br> |
608 | +<a href=iso.MP.html>MP</a> : NORTHERN MARIANA ISLANDS<br> |
609 | +<a href=iso.MQ.html>MQ</a> : MARTINIQUE<br> |
610 | +<a href=iso.MR.html>MR</a> : MAURITANIA<br> |
611 | +<a href=iso.MS.html>MS</a> : MONTSERRAT<br> |
612 | +<a href=iso.MT.html>MT</a> : MALTA<br> |
613 | +<a href=iso.MU.html>MU</a> : MAURITIUS<br> |
614 | +<a href=iso.MV.html>MV</a> : MALDIVES<br> |
615 | +<a href=iso.MW.html>MW</a> : MALAWI<br> |
616 | +<a href=iso.MX.html>MX</a> : MEXICO<br> |
617 | +<a href=iso.MY.html>MY</a> : MALAYSIA<br> |
618 | +<a href=iso.MZ.html>MZ</a> : MOZAMBIQUE<br> |
619 | +<a href=iso.NA.html>NA</a> : NAMIBIA<br> |
620 | +<a href=iso.NC.html>NC</a> : NEW CALEDONIA<br> |
621 | +<a href=iso.NE.html>NE</a> : NIGER<br> |
622 | +<a href=iso.NF.html>NF</a> : NORFOLK ISLAND<br> |
623 | +<a href=iso.NG.html>NG</a> : NIGERIA<br> |
624 | +<a href=iso.NI.html>NI</a> : NICARAGUA<br> |
625 | +<a href=iso.NL.html>NL</a> : NETHERLANDS<br> |
626 | +<a href=iso.NO.html>NO</a> : NORWAY<br> |
627 | +<a href=iso.NP.html>NP</a> : NEPAL<br> |
628 | +<a href=iso.NU.html>NU</a> : NIUE<br> |
629 | +<a href=iso.NZ.html>NZ</a> : NEW ZEALAND<br> |
630 | +<a href=iso.OM.html>OM</a> : OMAN<br> |
631 | +<a href=iso.PA.html>PA</a> : PANAMA<br> |
632 | +<a href=iso.PE.html>PE</a> : PERU<br> |
633 | +<a href=iso.PF.html>PF</a> : FRENCH POLYNESIA<br> |
634 | +<a href=iso.PG.html>PG</a> : PAPUA NEW GUINEA<br> |
635 | +<a href=iso.PH.html>PH</a> : PHILIPPINES<br> |
636 | +<a href=iso.PK.html>PK</a> : PAKISTAN<br> |
637 | +<a href=iso.PL.html>PL</a> : POLAND<br> |
638 | +<a href=iso.PM.html>PM</a> : SAINT PIERRE AND MIQUELON<br> |
639 | +<a href=iso.PN.html>PN</a> : PITCAIRN<br> |
640 | +<a href=iso.PR.html>PR</a> : PUERTO RICO<br> |
641 | +<a href=iso.PT.html>PT</a> : PORTUGAL<br> |
642 | +<a href=iso.PW.html>PW</a> : PALAU<br> |
643 | +<a href=iso.PY.html>PY</a> : PARAGUAY<br> |
644 | +<a href=iso.QA.html>QA</a> : QATAR<br> |
645 | +<a href=iso.RE.html>RE</a> : R�UNION<br> |
646 | +<a href=iso.RO.html>RO</a> : ROMANIA<br> |
647 | +<a href=iso.RU.html>RU</a> : RUSSIAN FEDERATION<br> |
648 | +<a href=iso.RW.html>RW</a> : RWANDA<br> |
649 | +<a href=iso.SA.html>SA</a> : SAUDI ARABIA<br> |
650 | +<a href=iso.SB.html>SB</a> : SOLOMON ISLANDS<br> |
651 | +<a href=iso.SC.html>SC</a> : SEYCHELLES<br> |
652 | +<a href=iso.SD.html>SD</a> : SUDAN<br> |
653 | +<a href=iso.SE.html>SE</a> : SWEDEN<br> |
654 | +<a href=iso.SG.html>SG</a> : SINGAPORE<br> |
655 | +<a href=iso.SH.html>SH</a> : SAINT HELENA<br> |
656 | +<a href=iso.SI.html>SI</a> : SLOVENIA<br> |
657 | +<a href=iso.SJ.html>SJ</a> : SVALBARD AND JAN MAYEN<br> |
658 | +<a href=iso.SK.html>SK</a> : SLOVAKIA<br> |
659 | +<a href=iso.SL.html>SL</a> : SIERRA LEONE<br> |
660 | +<a href=iso.SM.html>SM</a> : SAN MARINO<br> |
661 | +<a href=iso.SN.html>SN</a> : SENEGAL<br> |
662 | +<a href=iso.SO.html>SO</a> : SOMALIA<br> |
663 | +<a href=iso.SR.html>SR</a> : SURINAME<br> |
664 | +<a href=iso.ST.html>ST</a> : SAO TOME AND PRINCIPE<br> |
665 | +<a href=iso.SV.html>SV</a> : EL SALVADOR<br> |
666 | +<a href=iso.SY.html>SY</a> : SYRIAN ARAB REPUBLIC<br> |
667 | +<a href=iso.SZ.html>SZ</a> : SWAZILAND<br> |
668 | +<a href=iso.TC.html>TC</a> : TURKS AND CAICOS ISLANDS<br> |
669 | +<a href=iso.TD.html>TD</a> : CHAD<br> |
670 | +<a href=iso.TF.html>TF</a> : FRENCH SOUTHERN TERRITORIES<br> |
671 | +<a href=iso.TG.html>TG</a> : TOGO<br> |
672 | +<a href=iso.TH.html>TH</a> : THAILAND<br> |
673 | +<a href=iso.TJ.html>TJ</a> : TAJIKISTAN<br> |
674 | +<a href=iso.TK.html>TK</a> : TOKELAU<br> |
675 | +<a href=iso.TM.html>TM</a> : TURKMENISTAN<br> |
676 | +<a href=iso.TN.html>TN</a> : TUNISIA<br> |
677 | +<a href=iso.TO.html>TO</a> : TONGA<br> |
678 | +<a href=iso.TP.html>TP</a> : EAST TIMOR<br> |
679 | +<a href=iso.TR.html>TR</a> : TURKEY<br> |
680 | +<a href=iso.TT.html>TT</a> : TRINIDAD AND TOBAGO<br> |
681 | +<a href=iso.TV.html>TV</a> : TUVALU<br> |
682 | +<a href=iso.TW.html>TW</a> : TAIWAN, PROVINCE OF CHINA<br> |
683 | +<a href=iso.TZ.html>TZ</a> : TANZANIA, UNITED REPUBLIC OF<br> |
684 | +<a href=iso.UA.html>UA</a> : UKRAINE<br> |
685 | +<a href=iso.UG.html>UG</a> : UGANDA<br> |
686 | +<a href=iso.UM.html>UM</a> : UNITED STATES MINOR OUTLYING ISLANDS<br> |
687 | +<a href=iso.US.html>US</a> : UNITED STATES<br> |
688 | +<a href=iso.UY.html>UY</a> : URUGUAY<br> |
689 | +<a href=iso.UZ.html>UZ</a> : UZBEKISTAN<br> |
690 | +<a href=iso.VE.html>VE</a> : VENEZUELA<br> |
691 | +<a href=iso.VG.html>VG</a> : VIRGIN ISLANDS, BRITISH<br> |
692 | +<a href=iso.VI.html>VI</a> : VIRGIN ISLANDS, U.S.<br> |
693 | +<a href=iso.VN.html>VN</a> : VIET NAM<br> |
694 | +<a href=iso.VU.html>VU</a> : VANUATU<br> |
695 | +<a href=iso.WF.html>WF</a> : WALLIS AND FUTUNA<br> |
696 | +<a href=iso.WS.html>WS</a> : SAMOA<br> |
697 | +<a href=iso.YE.html>YE</a> : YEMEN<br> |
698 | +<a href=iso.YT.html>YT</a> : MAYOTTE<br> |
699 | +<a href=iso.YU.html>YU</a> : YUGOSLAVIA<br> |
700 | +<a href=iso.ZA.html>ZA</a> : SOUTH AFRICA<br> |
701 | +<a href=iso.ZM.html>ZM</a> : ZAMBIA<br> |
702 | +<a href=iso.ZW.html>ZW</a> : ZIMBABWE<br> |
703 | +</body></html> |
704 | diff --git a/snap/scriptlets/fetch_country_codes.sh b/snap/scriptlets/fetch_country_codes.sh |
705 | new file mode 100755 |
706 | index 0000000..8dae525 |
707 | --- /dev/null |
708 | +++ b/snap/scriptlets/fetch_country_codes.sh |
709 | @@ -0,0 +1,10 @@ |
710 | +#!/bin/sh |
711 | +# The purpose of this script is taking ISO-3166 country codes and |
712 | +# store them locally to be used in compilation time. |
713 | +# This operation must be executed by hand when wanted to update |
714 | +# current ones shown in config page |
715 | +set -e |
716 | + |
717 | +cd "$(dirname "$0")" |
718 | + |
719 | +curl -X GET http://geotags.com/iso3166/countries.html > country-codes |
720 | \ No newline at end of file |
721 | diff --git a/snap/scriptlets/fill_country_codes.sh b/snap/scriptlets/fill_country_codes.sh |
722 | new file mode 100755 |
723 | index 0000000..451c61b |
724 | --- /dev/null |
725 | +++ b/snap/scriptlets/fill_country_codes.sh |
726 | @@ -0,0 +1,39 @@ |
727 | +#!/bin/sh |
728 | +# The purpose of this script is taking ISO-3166 country codes from country_code file |
729 | +# so that they can be updated in management portal when snap is built |
730 | +# Process is easy, get it from remote path, format using sed, and replace |
731 | +# html page were they will be used |
732 | + |
733 | +set -e |
734 | + |
735 | +COUNTRY_CODES=../../../snap/scriptlets/country-codes |
736 | + |
737 | +if [ ! -e $COUNTRY_CODES ]; then |
738 | + echo "=======================================================" |
739 | + echo "Could not find country_codes needed file for compiling." |
740 | + echo "Please, before building snap for the first time execute by hand:" |
741 | + echo "" |
742 | + echo "snap/scriptlets/fetch_country_codes.sh" |
743 | + echo "" |
744 | + echo "=======================================================" |
745 | + exit 1 |
746 | +fi |
747 | + |
748 | +# think that this is a scriptlet, executed in parts/<the_part>/build folder |
749 | +cp $COUNTRY_CODES . |
750 | + |
751 | +# remove non processable lines |
752 | +sed -i '/^<a href=iso/!d' country-codes |
753 | + |
754 | +# process lines to change its format to be html select options |
755 | +sed -i '/<a href=iso/ s/<a href=iso.*html>/<option value="/ |
756 | +s/<\/a>\ :\ /">/ |
757 | +s/<br>/<\/option>/' country-codes |
758 | + |
759 | +# add world wide default option at beginning |
760 | +sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes |
761 | + |
762 | +# replace mark in management.html template with real country-codes |
763 | +sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html |
764 | + |
765 | +echo "country codes filled ok" |
766 | diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml |
767 | index 12f6cbc..3a7cf44 100644 |
768 | --- a/snap/snapcraft.yaml |
769 | +++ b/snap/snapcraft.yaml |
770 | @@ -56,5 +56,6 @@ parts: |
771 | assets: |
772 | plugin: dump |
773 | source: . |
774 | + prepare: ../../../snap/scriptlets/fill_country_codes.sh |
775 | stage: |
776 | - static |
777 | diff --git a/static/css/application.css b/static/css/application.css |
778 | index ab1e598..b0edc63 100644 |
779 | --- a/static/css/application.css |
780 | +++ b/static/css/application.css |
781 | @@ -1 +1 @@ |
782 | -@charset "UTF-8";*{font-smoothing:subpixel-antialiased;box-sizing:border-box}button,input[type='button'],input[type='reset'],input[type='submit'],.button--primary,.button--secondary{font-smoothing:subpixel-antialiased;font-size:1em;display:inline-block;width:100%;margin:0;padding:11px 24px;text-align:center;text-decoration:none;border:0;border-radius:2px;outline:0;font-weight:300;cursor:pointer}button:hover,input[type='button']:hover,input[type='reset']:hover,input[type='submit']:hover,.button--primary:hover,.button--secondary:hover{text-decoration:none}@media only screen and (min-width:768px){button,input[type='button'],input[type='reset'],input[type='submit'],.button--primary,.button--secondary{width:auto}}.clearfix{display:block}.clearfix:after{clear:both;content:'\0020';display:block;height:0;overflow:hidden;visibility:hidden}input[type='text'],input[type='number'],input[type='search'],input[type='password'],input[type='email'],input[type='url'],input[type='tel'],textarea{-webkit-appearance:textfield;border-radius:2px;border:1px solid #cdcdcd;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-size:16px;margin:0;outline:0;padding:.6956522em .869565em;vertical-align:baseline;font-weight:300}input[type='text']:active,input[type='number']:active,input[type='search']:active,input[type='password']:active,input[type='email']:active,input[type='url']:active,input[type='tel']:active,textarea:active,input[type='text']:focus,input[type='number']:focus,input[type='search']:focus,input[type='password']:focus,input[type='email']:focus,input[type='url']:focus,input[type='tel']:focus,textarea:focus{border-color:#888;outline:0}input[type='text']::placeholder,input[type='number']::placeholder,input[type='search']::placeholder,input[type='password']::placeholder,input[type='email']::placeholder,input[type='url']::placeholder,input[type='tel']::placeholder,textarea::placeholder{color:contrast-friendly-search-color(#e95420)}input[disabled="disabled"][type='text'],input[disabled="disabled"][type='number'],input[disabled="disabled"][type='search'],input[disabled="disabled"][type='password'],input[disabled="disabled"][type='email'],input[disabled="disabled"][type='url'],input[disabled="disabled"][type='tel'],textarea[disabled="disabled"]{opacity:.5}input[type='checkbox'],input[type='radio']{border-radius:2px;border:1px solid #cdcdcd;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);height:14px;padding:0;vertical-align:middle;width:14px}.inner-wrapper{margin:0 auto;max-width:984px}[class*='-col'].last-col,[class*='-col'] [class*='-col'].last-col{margin-right:0}.one-col,.two-col,.three-col,.four-col,.five-col,.six-col,.seven-col,.eight-col,.nine-col,.ten-col,.eleven-col,.twelve-col{clear:none;margin-bottom:20px;margin-right:0;padding:0;display:inline-block;position:relative;width:100%}@media only screen and (min-width:768px){.one-col,.two-col,.three-col,.four-col,.five-col,.six-col,.seven-col,.eight-col,.nine-col,.ten-col,.eleven-col,.twelve-col{float:left}}.list-ticks--compact,.list-ticks--canonical-compact,.list-ticks,.list-ticks--canonical,.list{list-style:none;margin-left:0}.list-ticks--compact li,.list-ticks--canonical-compact li,.list-ticks li,.list-ticks--canonical li,.list li{border-bottom:1px dotted #888;margin-bottom:0;padding:10px 0}.list-ticks--compact li:last-of-type,.list-ticks--canonical-compact li:last-of-type,.list-ticks li:last-of-type,.list-ticks--canonical li:last-of-type,.list li:last-of-type,.list-ticks--compact .last-item,.list-ticks--canonical-compact .last-item,.list-ticks .last-item,.list-ticks--canonical .last-item,.list .last-item{border:0;margin-bottom:0;padding-bottom:0}.list-ticks--compact li,.list-ticks--canonical-compact li,.list-ticks li,.list-ticks--canonical li{background-repeat:no-repeat;background-position:0 1em;padding-left:25px}.box,.box-grey{background:#fff;border:1px solid #cdcdcd;padding:1.25em 20px}@media only screen and (min-width:768px){.equal-height,.equal-height--vertical-divider{display:flex;flex-wrap:wrap;flex-direction:row;width:100%}.equal-height .equal-height__item,.equal-height--vertical-divider .equal-height__item{display:flex;flex:auto;flex-direction:column}}.arrow-up,.arrow-down,.arrow-right,.arrow-left{background-position:0 0;background-repeat:no-repeat;height:11px;position:absolute;width:18px}.box,.box-grey{border-radius:4px;padding:1.333em 20px}.list-ticks--compact li,.list-ticks--canonical-compact li{border:0;padding:10px 0 0 25px}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,ol,ul,li,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;margin:0;padding:0;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,nav,section{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none}[hidden]{display:none}html{font-size:100%;overflow-y:scroll;text-size-adjust:100%}blockquote,q{quotes:none}legend{border:0}figure{margin:0}abbr,acronym{cursor:help}.link-arrow:after{content:'\0000a0›'}nav ul li h2 a:after{content:'\0000a0›'}nav ul li a:after,.carousel ul li a:after,ul li p a:after{content:''}svg:not(:root){overflow:hidden}img{border:0;height:auto;max-width:100%}img .left{margin-right:20px}img .right{margin-left:20px}.middle img{vertical-align:middle;margin-top:4em}ins{background:#fffbeb;text-decoration:none}.left{float:left}.right{float:right}.no-border{border:0}.link-top{font-size:.875em;clear:both;margin-bottom:40px;margin-top:-40px}.link-top a{background:#fff;margin-right:10px;margin-top:-35px;padding:5px;float:right}.caps{text-transform:uppercase}.touch-border,.touch-bottom{float:left}@media only screen and (min-width:768px){.touch-border,.touch-bottom{margin-bottom:-20px}}@media only screen and (min-width:984px){.touch-border,.touch-bottom{margin-bottom:-40px}}.accessibility-aid,.off-left{position:absolute;left:-999em}.external{background-size:.7em .7em;padding-right:.9em;background-image:url("https://assets.ubuntu.com/v1/f24a8aa0-external-link-orange.svg");background-position:100% top;background-repeat:no-repeat}.no-svg .external{background-image:url("https://assets.ubuntu.com/v1/f24a8aa0-external-link-orange.svg")}.text-center,.align-center{text-align:center}.no-margin-bottom{margin-bottom:0}.no-padding-bottom{padding-bottom:0}.pull-bottom-right{float:right;margin-right:-10px}@media only screen and (min-width:768px){.pull-bottom-right{margin-right:-40px;margin-bottom:-40px}}@media only screen and (min-width:984px){.pull-bottom-right{margin-right:-40px;margin-bottom:-60px}}.pull-bottom-left{float:left;margin-left:-10px}@media only screen and (min-width:768px){.pull-bottom-left{margin-left:-40px;margin-bottom:-40px}}@media only screen and (min-width:984px){.pull-bottom-left{margin-left:-40px;margin-bottom:-60px}}@media only screen and (max-width:767px){.priority-0,.not-for-small{display:none}}@media only screen and (min-width:768px){.for-mobile,.for-small{display:none}}.video-container{position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden}.video-container iframe{position:absolute;top:0;left:0;width:100%;height:100%}.video-container+.video-title{margin-top:20px}.pull-right{float:right;margin-right:-40px}@media only screen and (max-width:768px){.pull-right{margin-bottom:20px}}.pull-left{margin-left:-40px}.for-tablet,.for-medium{display:none}@media only screen and (min-width:768px){.for-tablet,.for-medium{display:block}}@media only screen and (min-width:768px) and (max-width:984px){.med-six-col .three-col{width:48.93617%}.med-six-col .three-col:nth-of-type(2n){margin-right:0}}blockquote{margin:0}blockquote>p{font-size:1em;margin:0 0 .4em}blockquote small{font-size:.8125em;line-height:1.4}.pull-quote{text-indent:0}.pull-quote p{font-size:1.5em;line-height:1.3;margin-left:.4em;padding-left:10px;padding-right:10px;text-indent:-.4em}.pull-quote p span{color:#e95420;font-weight:bold;left:-5px;line-height:0;position:relative}.pull-quote p span:last-of-type{left:5px}.pull-quote p cite{display:block;font-size:.75em;font-weight:300;margin:10px 0 0;text-indent:0}@media only screen and (min-width:768px){.pull-quote p{padding-left:0;padding-right:0;text-indent:-.7em}.pull-quote p span{top:5px;font-size:1.391304348em}.pull-quote p cite{margin-left:0;text-indent:0}}@media only screen and (min-width:984px){.pull-quote p span{top:10px}}.row-quote{border-radius:0}.row-quote blockquote{border-radius:4px;margin:0;padding:0}.row-quote blockquote p{color:#333;line-height:1.3;margin-bottom:.75em;padding-left:10px;padding-right:10px;text-indent:0}.row-quote blockquote span{color:#e95420;font-weight:bold;left:-5px;line-height:0;position:relative}.row-quote blockquote span+span{left:5px}.row-quote blockquote cite{color:#333;font-size:.75em;font-style:normal;margin-bottom:0;text-indent:0}@media only screen and (min-width:768px){.row-quote{text-indent:-.4em}.row-quote blockquote{text-indent:-7px}.row-quote blockquote p{font-size:1.5em;padding-left:0;padding-right:0;text-indent:-.4em}.row-quote blockquote span{top:5px;font-size:1.391304348em}.row-quote blockquote cite{margin-left:0;text-indent:0}}@media only screen and (min-width:984px){.row-quote blockquote{padding:0 80px 20px;text-indent:-10px}.row-quote blockquote p span{top:10px}}button,input[type='button'],input[type='reset'],input[type='submit'],.button--primary{color:#fff;background-color:#e95420}button:hover,input[type='button']:hover,input[type='reset']:hover,input[type='submit']:hover,.button--primary:hover{background-color:#d44615}button .external,input[type='button'] .external,input[type='reset'] .external,input[type='submit'] .external,.button--primary .external{background-image:url("https://assets.ubuntu.com/v1/484aba04-external-link-white.svg")}.button--secondary{color:#e95420;background-color:#fff;border:1px solid #cdcdcd}.button--secondary:hover{background-color:#efefef}@media only screen and (min-width:860px){.nav-secondary{border:1px solid #d2d2d2;border-top:0}}.nav-secondary .breadcrumb{margin-bottom:0;margin-left:0;padding-top:2px;padding-bottom:3px}@media only screen and (min-width:860px){.nav-secondary .breadcrumb{float:left;margin-left:4px}}.nav-secondary .breadcrumb li{color:#fff;display:inline-block;margin:0;width:100%}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li{display:inline-block;width:auto}}.nav-secondary .breadcrumb li:first-of-type{border-bottom:1px solid #d2d2d2}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li:first-of-type{border-bottom:0}}.nav-secondary .breadcrumb li a{color:#888;font-size:1em;display:inline-block;width:100%;margin-right:0;padding:8px 10px;text-decoration:none}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li a{font-size:.875em}}@media only screen and (min-width:860px){.nav-secondary .breadcrumb li .breadcrumb-link,.nav-secondary .breadcrumb li .breadcrumb-link--second-level,.nav-secondary .breadcrumb li .active{display:inline-block;float:none;width:auto}}.nav-secondary .breadcrumb li .breadcrumb-link:after,.nav-secondary .breadcrumb li .breadcrumb-link--second-level:after{content:'\203A';display:inline-block;height:5px;margin-left:8px;position:absolute;width:5px}.nav-secondary .second-level-nav,.nav-secondary .third-level-nav{display:none;overflow:hidden;width:100%;margin:0;padding-top:10px;padding-bottom:10px;border-bottom:1px solid #d2d2d2}@media only screen and (min-width:860px){.nav-secondary .second-level-nav,.nav-secondary .third-level-nav{display:inline;float:none;margin-bottom:0;padding:0;width:auto;border-bottom:0}}.nav-secondary .second-level-nav li,.nav-secondary .third-level-nav li{margin:0}@media only screen and (min-width:860px){.nav-secondary .second-level-nav li,.nav-secondary .third-level-nav li{float:none;width:auto}}.nav-secondary .second-level-nav li a,.nav-secondary .third-level-nav li a{font-size:.875em;display:block;width:100%;height:100%;padding:10px;color:#333}@media only screen and (max-width:860px){.nav-secondary .second-level-nav li a.active,.nav-secondary .third-level-nav li a.active{font-weight:500}}@media only screen and (min-width:860px){.nav-secondary .second-level-nav li a,.nav-secondary .third-level-nav li a{padding:8px 10px 0}.nav-secondary .second-level-nav li a.active,.nav-secondary .third-level-nav li a.active{color:#e95420}}@media only screen and (min-width:860px){.nav-secondary .second-level-nav li{width:auto}.nav-secondary .second-level-nav li .active,.nav-secondary .second-level-nav li .breadcrumb-link--second-level{color:#888}.nav-secondary .second-level-nav li .third-level-nav li a{color:#333}.nav-secondary .second-level-nav li .third-level-nav li a:hover{color:#e95420}.nav-secondary .second-level-nav li .third-level-nav li .active{color:#e95420;float:none}.nav-secondary .second-level-nav li .third-level-nav li .active:after{content:''}}.third-level-nav li a:hover{color:#e95420}.third-level-nav li .active{color:#e95420;float:none}@media only screen and (max-width:859px){#breadcrumbs:hover .second-level-nav,#breadcrumbs:hover .third-level-nav,#breadcrumbs:hover .active{border:0;display:block;overflow:visible;float:left;width:100%;position:relative;clear:both}#breadcrumbs:hover li{border:0;float:left}#breadcrumbs:hover .second-level-nav{border-top:1px solid #d2d2d2}#breadcrumbs:hover .second-level-nav li{float:left}#breadcrumbs:hover .third-level-nav{border-bottom:1px solid #d2d2d2;padding-top:0}#breadcrumbs:hover .third-level-nav li{width:50%;display:block;padding-left:10px}}label{cursor:pointer;display:block;margin-bottom:4px}label.has-error{color:#df382c}label.has-warning{color:#eca918}label.has-success{color:#38b44a}label.has-information{color:#19b6ee}input[type='reset']{display:none}input[type='search']{-webkit-appearance:none;border-radius:0}input[type='checkbox']+label,input[type='radio']+label{display:inline;margin-left:5px;vertical-align:middle;width:auto}input.has-error{border:1px solid #df382c}input.has-warning{border:1px solid #eca918}input.has-success{border:1px solid #38b44a}input.has-information{border:1px solid #19b6ee}form ul{margin-left:0;list-style:none}textarea{overflow:auto;vertical-align:top}textarea[readonly='readonly']{color:#888;cursor:default}textarea[readonly='readonly']:active,textarea[readonly='readonly']:focus{border-color:#888;outline:0}fieldset{background-color:#f7f7f7;background-position:-15px -15px;background-repeat:no-repeat;border-radius:4px;border:0;margin-bottom:8px;padding:15px 20px}@media only screen and (min-width:984px){fieldset{padding:15px 20px}}fieldset h3{border-bottom:1px dotted #cdcdcd;margin-bottom:9px;padding-bottom:10px}form input,form select,form textarea{width:100%}@media only screen and (min-width:768px){.one-col{float:left;width:6.30531%;margin-right:2.21239%}.one-col .one-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.two-col{float:left;width:14.82301%;margin-right:2.21239%}.two-col .one-col{float:left;width:43.50649%;margin-right:12.98701%}.two-col .two-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.three-col{float:left;width:23.34071%;margin-right:2.21239%}.three-col .one-col{float:left;width:27.56133%;margin-right:8.65801%}.three-col .two-col{float:left;width:63.78066%;margin-right:8.65801%}.three-col .three-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.four-col{float:left;width:31.85841%;margin-right:2.21239%}.four-col .one-col{float:left;width:20.12987%;margin-right:6.49351%}.four-col .two-col{float:left;width:46.75325%;margin-right:6.49351%}.four-col .three-col{float:left;width:73.37662%;margin-right:6.49351%}.four-col .four-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.five-col{float:left;width:40.37611%;margin-right:2.21239%}.five-col .one-col{float:left;width:15.84416%;margin-right:5.19481%}.five-col .two-col{float:left;width:36.88312%;margin-right:5.19481%}.five-col .three-col{float:left;width:57.92208%;margin-right:5.19481%}.five-col .four-col{float:left;width:78.96104%;margin-right:5.19481%}.five-col .five-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.six-col{float:left;width:48.89381%;margin-right:2.21239%}.six-col .one-col{float:left;width:13.05916%;margin-right:4.329%}.six-col .two-col{float:left;width:30.44733%;margin-right:4.329%}.six-col .three-col{float:left;width:47.8355%;margin-right:4.329%}.six-col .four-col{float:left;width:65.22367%;margin-right:4.329%}.six-col .five-col{float:left;width:82.61183%;margin-right:4.329%}.six-col .six-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.seven-col{float:left;width:57.4115%;margin-right:2.21239%}.seven-col .one-col{float:left;width:11.10522%;margin-right:3.71058%}.seven-col .two-col{float:left;width:25.92102%;margin-right:3.71058%}.seven-col .three-col{float:left;width:40.73681%;margin-right:3.71058%}.seven-col .four-col{float:left;width:55.55261%;margin-right:3.71058%}.seven-col .five-col{float:left;width:70.36841%;margin-right:3.71058%}.seven-col .six-col{float:left;width:85.1842%;margin-right:3.71058%}.seven-col .seven-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.eight-col{float:left;width:65.9292%;margin-right:2.21239%}.eight-col .one-col{float:left;width:9.65909%;margin-right:3.24675%}.eight-col .two-col{float:left;width:22.56494%;margin-right:3.24675%}.eight-col .three-col{float:left;width:35.47078%;margin-right:3.24675%}.eight-col .four-col{float:left;width:48.37662%;margin-right:3.24675%}.eight-col .five-col{float:left;width:61.28247%;margin-right:3.24675%}.eight-col .six-col{float:left;width:74.18831%;margin-right:3.24675%}.eight-col .seven-col{float:left;width:87.09416%;margin-right:3.24675%}.eight-col .eight-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.nine-col{float:left;width:74.4469%;margin-right:2.21239%}.nine-col .one-col{float:left;width:8.54578%;margin-right:2.886%}.nine-col .two-col{float:left;width:19.97755%;margin-right:2.886%}.nine-col .three-col{float:left;width:31.40933%;margin-right:2.886%}.nine-col .four-col{float:left;width:42.84111%;margin-right:2.886%}.nine-col .five-col{float:left;width:54.27289%;margin-right:2.886%}.nine-col .six-col{float:left;width:65.70467%;margin-right:2.886%}.nine-col .seven-col{float:left;width:77.13644%;margin-right:2.886%}.nine-col .eight-col{float:left;width:88.56822%;margin-right:2.886%}.nine-col .nine-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.ten-col{float:left;width:82.9646%;margin-right:2.21239%}.ten-col .one-col{float:left;width:7.66234%;margin-right:2.5974%}.ten-col .two-col{float:left;width:17.92208%;margin-right:2.5974%}.ten-col .three-col{float:left;width:28.18182%;margin-right:2.5974%}.ten-col .four-col{float:left;width:38.44156%;margin-right:2.5974%}.ten-col .five-col{float:left;width:48.7013%;margin-right:2.5974%}.ten-col .six-col{float:left;width:58.96104%;margin-right:2.5974%}.ten-col .seven-col{float:left;width:69.22078%;margin-right:2.5974%}.ten-col .eight-col{float:left;width:79.48052%;margin-right:2.5974%}.ten-col .nine-col{float:left;width:89.74026%;margin-right:2.5974%}.ten-col .ten-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.eleven-col{float:left;width:91.4823%;margin-right:2.21239%}.eleven-col .one-col{float:left;width:6.9443%;margin-right:2.36128%}.eleven-col .two-col{float:left;width:16.24987%;margin-right:2.36128%}.eleven-col .three-col{float:left;width:25.55544%;margin-right:2.36128%}.eleven-col .four-col{float:left;width:34.86101%;margin-right:2.36128%}.eleven-col .five-col{float:left;width:44.16658%;margin-right:2.36128%}.eleven-col .six-col{float:left;width:53.47215%;margin-right:2.36128%}.eleven-col .seven-col{float:left;width:62.77772%;margin-right:2.36128%}.eleven-col .eight-col{float:left;width:72.08329%;margin-right:2.36128%}.eleven-col .nine-col{float:left;width:81.38886%;margin-right:2.36128%}.eleven-col .ten-col{float:left;width:90.69443%;margin-right:2.36128%}.eleven-col .eleven-col{width:100%;margin-right:0}}@media only screen and (min-width:768px){.prepend-one{margin-left:8.5177%}.prepend-two{margin-left:17.0354%}.prepend-three{margin-left:25.5531%}.prepend-four{margin-left:34.0708%}.prepend-five{margin-left:42.5885%}.prepend-six{margin-left:51.10619%}.prepend-seven{margin-left:59.62389%}.prepend-eight{margin-left:68.14159%}.prepend-nine{margin-left:76.65929%}.prepend-ten{margin-left:85.17699%}.prepend-eleven{margin-left:93.69469%}}@media only screen and (min-width:768px){.append-one{margin-right:8.5177%}.append-two{margin-right:17.0354%}.append-three{margin-right:25.5531%}.append-four{margin-right:34.0708%}.append-five{margin-right:42.5885%}.append-six{margin-right:51.10619%}.append-seven{margin-right:59.62389%}.append-eight{margin-right:68.14159%}.append-nine{margin-right:76.65929%}.append-ten{margin-right:85.17699%}.append-eleven{margin-right:93.69469%}}@media only screen and (min-width:768px){.push-one{float:left;position:relative;margin:0 -8.5177% 0 8.5177%}.push-two{float:left;position:relative;margin:0 -19.24779% 0 19.24779%}.push-three{float:left;position:relative;margin:0 -27.76549% 0 27.76549%}.push-four{float:left;position:relative;margin:0 -36.28319% 0 36.28319%}.push-five{float:left;position:relative;margin:0 -44.80088% 0 44.80088%}.push-six{float:left;position:relative;margin:0 -53.31858% 0 53.31858%}.push-seven{float:left;position:relative;margin:0 -61.83628% 0 61.83628%}.push-eight{float:left;position:relative;margin:0 -70.35398% 0 70.35398%}.push-nine{float:left;position:relative;margin:0 -78.87168% 0 78.87168%}.push-ten{float:left;position:relative;margin:0 -87.38938% 0 87.38938%}.push-eleven{float:left;position:relative;margin:0 -95.90708% 0 95.90708%}}@media only screen and (min-width:768px){.pull-one{float:left;position:relative;margin-left:-6.30531%}.pull-two{float:left;position:relative;margin-left:17.0354%}.pull-three{float:left;position:relative;margin-left:25.5531%}.pull-four{float:left;position:relative;margin-left:34.0708%}.pull-five{float:left;position:relative;margin-left:42.5885%}.pull-six{float:left;position:relative;margin-left:51.10619%}.pull-seven{float:left;position:relative;margin-left:59.62389%}.pull-eight{float:left;position:relative;margin-left:68.14159%}.pull-nine{float:left;position:relative;margin-left:76.65929%}.pull-ten{float:left;position:relative;margin-left:85.17699%}.pull-eleven{float:left;position:relative;margin-left:93.69469%}}.banner{background:#e95420;border-bottom:0;border-top:0;box-shadow:inset 0 -1px 0 #ccc;display:block;min-width:100%;position:relative;width:auto;z-index:2}@media only screen and (min-width:860px){.banner{border-bottom:1px solid #ccc;box-shadow:none}}.banner .logo{position:relative;display:table;float:left;overflow:hidden;height:48px;margin:0 10px;border-left:0}@media only screen and (min-width:860px){.banner .logo{margin:0 15px}}.banner .logo a{display:table-cell;height:100%;vertical-align:middle;color:#fff}.banner .logo a span{display:inline-block}.banner h2{font-size:1.563em;position:relative;top:14px;left:4px;display:block;margin-bottom:0;text-transform:lowercase}.banner h2 a,.banner h2 a:link,.banner h2 a:visited{float:left;text-decoration:none;color:#fff}.banner .nav-primary{overflow:hidden;margin:0 auto;border:0}@media only screen and (min-width:860px){.banner .nav-primary{max-width:984px}}.banner .nav-primary span{display:none}.banner .nav-primary ul{display:none;float:left;width:100%;margin:0;padding:0;border-top:1px solid #ccc;box-shadow:inset 0 -1px 0 #ccc;background-color:#ebebeb}@media only screen and (min-width:860px){.banner .nav-primary ul{position:static;display:block;width:auto;border-top:0;background-color:transparent;box-shadow:none}}.banner .nav-primary ul li{float:left;width:50%;margin:0;border-right:1px solid #ccc;border-bottom:1px solid #ccc;background:transparent}@media only screen and (min-width:860px){.banner .nav-primary ul li{width:auto;border-right:0;border-bottom:0;border-left:1px solid #cc4414}.banner .nav-primary ul li:last-child{border-right:1px solid #cc4414}.banner .nav-primary ul li:last-child a{border-right:0}}@media only screen and (max-width:860px){.banner .nav-primary ul li:nth-child(2n+2){border-right:0}}.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited{font-size:1em;font-weight:300;position:relative;display:block;margin-bottom:0;padding:8px 10px;text-align:left;text-decoration:none;background-color:#ebebeb;font-smoothing:subpixel-antialiased;color:#333}@media only screen and (min-width:860px){.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited{font-size:.875em;padding:14px 14px 13px;text-align:center;border-left:1px solid #ee7f58;background-color:transparent;color:#fff}}.banner .nav-primary ul li a:hover,.banner .nav-primary ul li a:link:hover,.banner .nav-primary ul li a:visited:hover{background:#f2f2f2}@media only screen and (min-width:860px){.banner .nav-primary ul li a:hover,.banner .nav-primary ul li a:link:hover,.banner .nav-primary ul li a:visited:hover{background-color:#ed7045}}.banner .nav-primary ul li a.active,.banner .nav-primary ul li a:link.active,.banner .nav-primary ul li a:visited.active{background-color:#ddd}@media only screen and (min-width:860px){.banner .nav-primary ul li a.active,.banner .nav-primary ul li a:link.active,.banner .nav-primary ul li a:visited.active{background-color:#cc4414;border-left:0}}.banner .nav-toggle{position:absolute;top:0;right:0;display:block;width:48px;height:48px;cursor:pointer;text-indent:-99999px;background-image:url("https://assets.ubuntu.com/v1/12387180-navigation-menu-plain.svg");background-repeat:no-repeat;background-position:center center;background-size:25px auto}@media only screen and (min-width:860px){.banner .nav-toggle{display:none}}.banner .nav-toggle .nav-toggle__link{width:48px;height:48px}.banner .nav-toggle .nav-toggle__link.open{display:block}.banner .nav-toggle .nav-toggle__link.close{display:none}@media only screen and (max-width:860px){.banner #nav:hover ul{display:block}.banner #nav:hover ul ul{display:none}.banner #nav:hover .nav-toggle .nav-toggle__link.open{display:none}.banner #nav:hover .nav-toggle .nav-toggle__link.close{display:block}}ol,ul{margin-left:20px;margin-bottom:20px}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}nav ul,nav ol{list-style:none;list-style-image:none}li{font-size:1em;line-height:1.5;margin:0 0 .4em;padding:0}.list-ticks li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%2394310f' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}.no-bullets{list-style:none;margin-left:0}.combined-list ul,.combined-list div{margin-bottom:0}.combined-list .last-item{border-bottom:1px dotted #888;padding-bottom:10px}.combined-list .last-col{margin-bottom:20px}.combined-list .last-col .last-item{border-bottom:0;padding-bottom:0}@media only screen and (min-width:768px){.combined-list ul,.combined-list div{margin-bottom:20px}.combined-list .last-item{border-bottom:0;padding-bottom:0}}.inline-list{margin-left:0}.inline-list li{display:inline;list-style:none;margin-right:20px}.inline-list .last-item{margin-right:0}.list-step{list-style:none;margin-left:60px}@media only screen and (max-width:984px){.list-step__item:first-child{margin-top:10px}}@media only screen and (min-width:984px){.list-step__title{margin-bottom:0}}.list-step__bullet{box-shadow:0 1px 3px 1px rgba(51,51,51,0.2);background:#fff;border-radius:50%;padding:.3em .68em;display:inline-block;color:#e95420;margin-right:.34375em;margin-bottom:.625em;margin-left:-60px}@media only screen and (max-width:984px){.list-step__bullet{position:absolute;top:-5px}}@media only screen and (min-width:768px){.grid-list{display:flex;flex-wrap:wrap}}.grid-list__img{display:block;margin:auto;max-width:60px}.grid-list p{font-size:.875rem}.grid-list__item{border-bottom:1px dotted #888;margin-bottom:30px;display:flex}@media only screen and (max-width:768px){.grid-list__item>[class*='-col']:first-child{width:25%;padding-right:1rem}.grid-list__item>[class*='-col']:last-child{width:75%}}.grid-list__item:last-child{border-bottom:0}@media only screen and (min-width:768px){.grid-list__item{display:flex;flex-wrap:wrap;border-right:1px dotted #888;margin:0;padding:1.5em .75em 0}.grid-list__item.last-col{border-right:0}.grid-list__item.last-row{border-bottom:0}}.row{border-bottom:1px dotted #888;clear:both;padding:20px 10px 0;position:relative}.row br{display:none}.row.no-padding-bottom{padding-bottom:0}.row::after{clear:both;content:'.';display:block;height:0;visibility:hidden}.row--light{background:#fff}.row--dark{background:#333;color:#fff}.row.row-grey,.row.row--grey{background:#f7f7f7;border:0;margin-top:-1px}.no-border{border:0}.row-hero{margin-top:20px;padding-top:0}.strip{width:100%;display:block}.strip-inner-wrapper{max-width:984px;margin:auto}@media only screen and (min-width:768px){.row-hero{margin-top:40px}.row{border-radius:0;margin:0;padding:40px 20px 20px}.row-grey{margin-top:-1px}}@media only screen and (min-width:769px){.row br{display:block}}@media only screen and (min-width:984px){.row br{display:block}.row{padding:60px 20px 40px}.no-border{border:0}}.header-search [type="search"],.header-search [type="text"],.search-form [type="search"],.search-form [type="text"]{-webkit-appearance:none;background-color:#c34113;box-shadow:none;color:#333;display:block;float:left;font-size:1em;margin-bottom:0;padding:12px 10px;transition:all .5s ease-out;width:100%}.header-search .svg-search-handle,.search-form .svg-search-handle{fill:#333}.header-search .svg-search-frame,.search-form .svg-search-frame{stroke:#333}.header-search placeholder,.search-form placeholder{color:#333;opacity:1}.header-search input:placeholder,.search-form input:placeholder{color:#333;opacity:1}.header-search ::placeholder,.search-form ::placeholder{color:#333;opacity:1}.header-search [type="search"]:focus,.search-form [type="search"]:focus{background-color:#b03a11;border-color:#b3b3b3}.header-search [type=submit],.search-form [type=submit]{background:0;display:block;float:left;line-height:0;margin-left:-40px;overflow:visible;padding:3px 2px;width:auto}.header-search [type=submit]:hover,.search-form [type=submit]:hover{background:0}.header-search [type=submit] img,.search-form [type=submit] img{height:28px;width:28px;margin-top:2px}.banner .search-toggle{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 90 90'><g color='%23fff'><path fill='none' stroke-width='4' overflow='visible' enable-background='accumulate' d='M0 0h90v90H0z'/><path d='M69 36.5a33 33.5 0 1 1-66 0 33 33.5 0 1 1 66 0z' transform='matrix(.636 0 0 .627 16.114 16.12)' fill='none' stroke='%23fff' stroke-width='9.5' overflow='visible' enable-background='accumulate'/><path d='M55.77 52.92L52.94 55.75l14 14 2.83-2.83-14-14z' font-size='xx-small' fill='%23fff' stroke-width='6' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/><path d='M60.97 57.03c-1.55-3.04 1.02-3.63 2.46-0.58 1.44-0.21 3.21 4.29l9.19 9.2c2.68 2.85 3.26 2.46 5.64 2.38-2.38 2.77-2.79-0.08-5.65l-9.19-9.2c-0.73-0.75-1.78-1.19-2.83-1.19z' font-size='xx-small' fill='%23fff' stroke-width='11.8' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/></g></svg>");background-position:center center;background-repeat:no-repeat;background-size:20px 20px;display:block;height:48px;outline:0;overflow:hidden;position:absolute;right:0;text-indent:-999em;top:0;width:24px}.banner .search-toggle .search-toggle__link{width:48px;height:48px}.banner .search-toggle .search-toggle__link.open{display:block}.banner .search-toggle .search-toggle__link.close{display:none}#site-search:hover form{display:block}#site-search:hover .search-toggle .search-toggle__link.open{display:none}#site-search:hover .search-toggle .search-toggle__link.close{display:block}.header-search,.search-form{background:#f7f7f7;border:0;display:none;float:left;position:relative;margin:0;width:100%;z-index:3}.search-form.active,.header-search.active,.header-search.open{display:block}.search-form div,.header-search div{box-shadow:inset 0 -4px 4px -4px rgba(0,0,0,0.3),inset 0 5px 5px -5px rgba(0,0,0,0.3);background:#ec6d40;margin:10px;position:relative;z-index:1}.search-form form [type="search"],.header-search form [type="search"]{background:#fff;border:0;box-shadow:0 2px 2px rgba(0,0,0,0.3) inset,0 -1px 3px rgba(0,0,0,0.2) inset,0 2px 0 rgba(255,255,255,0.4);color:#fff;display:block;float:left;font-size:1em;height:auto;margin:0;padding:8px 10px;width:100%}.yes-js .header-inner .search-form,.yes-js .header-inner .header-search{display:none}.yes-js .header-inner .search-form form,.yes-js .header-inner .header-search form{margin-left:0;margin-right:0;overflow:hidden;padding:10px;position:relative;top:0;width:100%;z-index:999}@media only screen and (max-width:860px){.banner .search-toggle{right:0}.no-svg .search-toggle,.opera-mini .search-toggle{background-image:url("https://assets.ubuntu.com/v1/75d8151d-search-white.png")}}@media only screen and (min-width:860px){.banner .search-toggle{display:none}}@media only screen and (min-width:860px){.search-form,.header-search{background:0;border-right:0 none;float:right;margin-bottom:0;max-width:220px;overflow:hidden;padding:7px 0 5px 14px}.search-form form [type="text"],.search-form form [type="search"],.header-search form [type="text"],.header-search form [type="search"]{box-shadow:0 2px 4px rgba(0,0,0,0.4) inset;box-sizing:content-box;background-color:#c34113;border:0 solid #b93e12;border-width:0 0 1px;color:#fff;font-size:.813em;height:24px;margin-bottom:0;padding:.5em 2.5em .5em .5em;transition:all .5s ease 0s;width:86px}.search-form form [type="text"]:focus,.search-form form [type="search"]:focus,.header-search form [type="text"]:focus,.header-search form [type="search"]:focus{background-color:#b03a11;border-color:#94310f}.search-form .svg-search-handle,.header-search .svg-search-handle{fill:#fff}.search-form .svg-search-frame,.header-search .svg-search-frame{stroke:#fff}.search-form placeholder,.header-search placeholder{color:#fff}.search-form input:placeholder,.header-search input:placeholder{color:#fff}.search-form ::placeholder,.header-search ::placeholder{color:#fff}}@media only screen and (min-width:860px){.header-search .svg-search-handle,.search-form .svg-search-handle{fill:#fff}.header-search .svg-search-frame,.search-form .svg-search-frame{stroke:#fff}.header-search placeholder,.search-form placeholder{color:#fff}.header-search input:placeholder,.search-form input:placeholder{color:#fff}.header-search ::placeholder,.search-form ::placeholder{color:#fff}}@media only screen and (max-width:860px){.banner .nav-primary .header-search{position:relative;top:0;width:100%}.banner .nav-primary .header-search [type="search"]{background-color:#ebebeb;color:#fff}.banner .nav-primary .header-search [type="submit"]{background-color:transparent;height:38px;margin-top:0;width:32px}.banner .nav-primary .header-search [type="submit"] img,.banner .nav-primary .header-search [type="submit"] svg{max-width:none}.banner .nav-primary .header-search.open{display:block}.banner .search-toggle{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 90 90'><g color='%23fff'><path fill='none' stroke-width='4' overflow='visible' enable-background='accumulate' d='M0 0h90v90H0z'/><path d='M69 36.5a33 33.5 0 1 1-66 0 33 33.5 0 1 1 66 0z' transform='matrix(.636 0 0 .627 16.114 16.12)' fill='none' stroke='%23fff' stroke-width='9.5' overflow='visible' enable-background='accumulate'/><path d='M55.77 52.92L52.94 55.75l14 14 2.83-2.83-14-14z' font-size='xx-small' fill='%23fff' stroke-width='6' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/><path d='M60.97 57.03c-1.55-3.04 1.02-3.63 2.46-0.58 1.44-0.21 3.21 4.29l9.19 9.2c2.68 2.85 3.26 2.46 5.64 2.38-2.38 2.77-2.79-0.08-5.65l-9.19-9.2c-0.73-0.75-1.78-1.19-2.83-1.19z' font-size='xx-small' fill='%23fff' stroke-width='11.8' overflow='visible' enable-background='accumulate' font-family='Sans' class='s0'/></g></svg>");background-position:center center;background-repeat:no-repeat;background-size:25px auto;cursor:pointer;display:block;height:48px;position:absolute;right:0;text-indent:-99999px;width:48px}.no-svg .banner .search-toggle,.opera-mini .banner .search-toggle{background-image:url("https://assets.ubuntu.com/v1/2196e362-search-white.svg")}.opera-mini x:-o-prefocus,.opera-mini .banner .search-toggle{background-size:25px auto}}@media only screen and (min-width:860px){.search-form,.header-search{display:block}.search-form .svg-search-handle,.header-search .svg-search-handle{fill:#fff}.search-form .svg-search-frame,.header-search .svg-search-frame{stroke:#fff}.search-form form [type="text"]:focus,.header-search form [type="text"]:focus{width:160px}.search-form [type="search"],.search-form [type="text"],.header-search [type="search"],.header-search [type="text"]{padding:8px 10px}}@media only screen and (max-width:984px){.search-form,.header-search{margin-right:10px}}@media only screen and (max-width:860px){.banner .search-toggle{right:48px}}.ubuntu-search .nav-secondary,.search-results .nav-secondary,.search-no-results .nav-secondary{display:none}.ubuntu-search section>h1,.ubuntu-search section article h1,.search-results section>h1,.search-results section article h1,.search-no-results section>h1,.search-no-results section article h1{padding-bottom:10px;font-size:1.438em;margin-bottom:0}.ubuntu-search section>h1,.search-results section>h1,.search-no-results section>h1{border-bottom:1px dotted #cdcdcd}.ubuntu-search .main-search,.search-results .main-search,.search-no-results .main-search{background-color:transparent;margin:0 0 20px;padding:20px 0}.ubuntu-search .main-search [type="search"],.search-results .main-search [type="search"],.search-no-results .main-search [type="search"]{border:1px solid #888;float:left;font-size:2em;padding:.2em 65px .2em .2em;width:100%}.ubuntu-search .main-search [type=submit],.search-results .main-search [type=submit],.search-no-results .main-search [type=submit]{width:32px;height:38px;background-repeat:no-repeat;background-position:center 8px;background-color:transparent;background-size:28px 28px;display:block;float:left;line-height:0;margin-left:-53px;margin-top:-4px;overflow:visible;padding:4px}.ubuntu-search .main-search [type=submit] img,.search-results .main-search [type=submit] img,.search-no-results .main-search [type=submit] img{height:45px;width:45px}.ubuntu-search .main-search [type=submit]:hover,.search-results .main-search [type=submit]:hover,.search-no-results .main-search [type=submit]:hover{background:0}.ubuntu-search .search-result h1 .title-main,.search-results .search-result h1 .title-main,.search-no-results .search-result h1 .title-main{margin-right:20px}.ubuntu-search .search-result h1 .result-url,.search-results .search-result h1 .result-url,.search-no-results .search-result h1 .result-url{color:#888;display:block;overflow:hidden;padding-bottom:2px;text-overflow:ellipsis;vertical-align:bottom}.ubuntu-search .search-result h1 .result-url a,.search-results .search-result h1 .result-url a,.search-no-results .search-result h1 .result-url a{color:#888}.ubuntu-search .search-result p,.search-results .search-result p,.search-no-results .search-result p{margin-bottom:0}.ubuntu-search .num-results,.search-results .num-results,.search-no-results .num-results{display:inline-block;margin-left:20px}.ubuntu-search .bottom-results-total,.search-results .bottom-results-total,.search-no-results .bottom-results-total{margin:0;overflow:visible;padding-top:20px;text-align:center;width:100%}.ubuntu-search .bottom-nav,.search-results .bottom-nav,.search-no-results .bottom-nav{margin-top:-26px;overflow:hidden}.ubuntu-search .bottom-nav ul,.search-results .bottom-nav ul,.search-no-results .bottom-nav ul{margin-bottom:0;margin-left:0;overflow:hidden;padding:0}.ubuntu-search .bottom-nav li,.search-results .bottom-nav li,.search-no-results .bottom-nav li{float:left;margin-left:15px}.ubuntu-search .bottom-nav li:first-child,.search-results .bottom-nav li:first-child,.search-no-results .bottom-nav li:first-child{margin-left:0}.ubuntu-search .nav-back,.search-results .nav-back,.search-no-results .nav-back{float:left}.ubuntu-search .nav-back li:before,.search-results .nav-back li:before,.search-no-results .nav-back li:before{color:#e95420;content:'\2039';margin-right:5px}.ubuntu-search .nav-back .item-extreme:before,.search-results .nav-back .item-extreme:before,.search-no-results .nav-back .item-extreme:before{content:'\2039\2039'}.ubuntu-search .nav-forward,.search-results .nav-forward,.search-no-results .nav-forward{float:right}.ubuntu-search .nav-forward li:after,.search-results .nav-forward li:after,.search-no-results .nav-forward li:after{color:#e95420;content:'\203A';margin-left:5px}.ubuntu-search .nav-forward .item-extreme:after,.search-results .nav-forward .item-extreme:after,.search-no-results .nav-forward .item-extreme:after{content:'\203A\203A'}.ubuntu-search .error-notification,.search-results .error-notification,.search-no-results .error-notification{background-color:#fff;color:#333;display:block;margin-top:20px;padding:20px;width:100%}.ubuntu-search .result-line,.search-results .result-line,.search-no-results .result-line{color:#888}.ubuntu-search .results-top,.search-results .results-top,.search-no-results .results-top{border-bottom:1px dotted #cdcdcd;padding-bottom:.5em}.ubuntu-search .search-container,.search-results .search-container,.search-no-results .search-container{padding-bottom:0}@media only screen and (min-width:860px){.ubuntu-search .main-search [type=submit]{margin-left:-60px;margin-top:0}}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-l-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-l-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-r-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:700;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-b-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-b-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-li-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-li-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-ri-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-ri-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:700;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-bi-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntu-bi-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntumono-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/sites/ubuntu/latest/u/fonts/ubuntumono-r-webfont.woff") format("woff")}*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{color:#333;font-family:Ubuntu,Arial,'libra sans',sans-serif;font-size:16px;font-weight:300}a:focus{outline:thin dotted}a:hover,a:active{outline:0}a{color:#e95420;text-decoration:none}a:hover,a:active,a:focus{text-decoration:underline}strong{font-weight:400}.caps-centered,.muted-heading{font-size:.875em;margin-bottom:20px;text-align:center;text-transform:uppercase}small,.smaller{font-size:13px}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{vertical-align:text-top}sub{vertical-align:text-bottom}pre{border-radius:4px;background-color:white;padding:.6em 1em;white-space:pre-wrap;word-wrap:break-word}p+h2,ul+h2,ol+h2,pre+h2{margin-top:.5625em}header nav a:link{font-weight:normal}p+h3,ul+h3,ol+h3,pre+h3{margin-top:.783em}p+h4,ul+h4,ol+h4,pre+h4{margin-top:1.21875}ol+h2,p+h2,pre+h2,ul+h2{margin-top:.563em}ol+h3,p+h3,pre+h3,ul+h3{margin-top:.783em}ol+h4,p+h4,pre+h4,ul+h4{margin-top:1.219em}.intro{font-size:1em;line-height:1.4}.row div p:last-child,.row ul p:last-child{margin-bottom:0}.four-col p:last-child{margin-bottom:0}.note{color:#888;font-size:.813em}h1,h2,h3,h4,h5,h6{font-family:Ubuntu,Arial,'libra sans',sans-serif;font-weight:300;line-height:1.3}h1{font-size:2.8125em;margin-bottom:.5em}h2{font-size:2em;margin-bottom:.5em}h3{font-size:1.4375em;margin-bottom:.522em}h4{font-size:1.25em;font-weight:400;margin-bottom:.615em}h5{font-size:1em;font-weight:700;margin-bottom:1em}h6{font-size:.8125em;font-weight:400;margin-bottom:1em;letter-spacing:.1em;text-transform:uppercase}p,li{font-size:1em;line-height:1.5;margin:0;margin-bottom:.75em;padding:0}button,input,select,textarea{font-family:Ubuntu,Arial,'libra sans',sans-serif}@media only screen and (min-width:768px) and (max-width:984px){h1{font-size:1.5234375em;margin-bottom:.5em}h2{font-size:1.348125em;margin-bottom:.5em}h3{font-size:1.1428125em;margin-bottom:.522em}h4{font-size:1.171875em;font-weight:400;margin-bottom:.615em}h5{font-size:.9375em;font-weight:700;margin-bottom:1em}h6{font-size:.6778125em;font-weight:400;margin-bottom:1em;letter-spacing:.1em;text-transform:uppercase}.intro{font-size:1.13333em}}@media only screen and (max-width:768px){h1{font-size:1.421875em;margin-bottom:.4375em}h2{font-size:1.25825em;margin-bottom:.4375em}h3{font-size:1.066625em;margin-bottom:.45675em}h4{font-size:1.09375em;font-weight:400;margin-bottom:.538125em}h5{font-size:.875em;font-weight:700;margin-bottom:.875em}h6{font-size:.632625em;font-weight:400;margin-bottom:.875em;letter-spacing:.1em;text-transform:uppercase}p,li{font-size:.875rem}}@media only screen and (min-width:984px){p,li,code,pre{font-size:16px;line-height:1.5;margin-bottom:.75em}.intro{font-size:1.25em}}dfn{font-style:italic}code,pre{font-family:'Ubuntu Mono','Consolas','Monaco','Lucida Console','Courier New',Courier,monospace}table{border-collapse:collapse;border-spacing:0;overflow-x:scroll;margin-bottom:20px;margin:0 0 2.5em;width:100%}table th,table td{background:#f7f7f7;border:1px dotted #888;padding:15px 10px}table td{text-align:center;vertical-align:middle}table thead th{border-collapse:separate;border-spacing:0 10px;background:#fee3d2;color:#333;font-weight:normal}table tbody th{font-weight:300;text-align:left}table th[scope='col']{text-align:center}table thead th:first-of-type{text-align:left}@media only screen and (max-width:768px){table{display:block}}body footer.global #nav-global li:first-of-type a{margin-left:0}footer.global{background:0;border-top:0;box-shadow:inset 0 2px 2px -1px #cdcdcd;clear:both;display:block;padding:30px 10px 20px;position:relative;width:100%}footer.global .legal{background-image:none;clear:both;margin:0 auto;min-height:40px;position:relative;width:100%}footer.global .legal p,footer.global .legal ul{padding-left:0}footer.global h2{font-size:.75em;line-height:1.4;margin-bottom:0;padding-bottom:.5em}footer.global h2,footer.global h2 a:link,footer.global h2 a:visited{color:#333;font-weight:normal}footer.global ul{margin:0}footer.global li a:hover{color:#e95420}footer.global li ul li a:link,footer.global li ul li a:visited{color:#333;font-size:.75em}footer.global h2 a:hover,footer.global h2 a:active{color:#e95420}footer.global p{color:#333;font-size:12px;margin-bottom:0}@media only screen and (max-width:768px){footer.no-global .legal{box-shadow:0 2px 2px -1px #cdcdcd inset;box-sizing:content-box;margin-left:-10px;padding-left:10px;padding-right:10px;padding-top:10px}}@media only screen and (min-width:984px){footer.global .legal{width:984px}footer.global{padding:30px 0 20px}}svg:not(:root){overflow:hidden}figure{width:100%}figure caption{display:block;width:100%;text-align:center}object,iframe,embed,canvas,video,audio{display:block;max-width:100%;margin:0 auto 20px}audio:not([controls]){display:none;height:0}[hidden]{display:none}.video-container{position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden}.video-container iframe{position:absolute;top:0;left:0;width:100%;height:100%}.video-container h3,.video-container .video-title{margin-top:20px}.inline-logos{float:left;margin-left:0;padding:0;text-align:center;width:100%}.inline-logos .inline-logos__item{display:inline-block;margin-right:20px;padding:0 0 20px;width:36%}@media only screen and (max-width:300px){.inline-logos .inline-logos__item{margin:0 0 20px;width:100%}}.inline-logos .inline-logos__item.clear-row{clear:left}.inline-logos .inline-logos__item.last-item{border:0}.inline-logos .inline-logos__image{max-height:32px;max-width:115px;transition:all .3s ease-out;vertical-align:middle}@media only screen and (min-width:768px){.inline-logos{padding-top:20px}.inline-logos .inline-logos__item{display:inline-block;height:56px;line-height:60px;margin:0 30px 30px;width:150px}.inline-logos .inline-logos__image{float:none;height:auto;max-height:56px;max-width:150px;vertical-align:middle}}@media only screen and (min-width:984px){.inline-logos__item{margin-bottom:40px}}@media only screen and (min-width:768px){.equal-height__align-vertically{align-items:center;justify-content:center}}@media only screen and (min-width:768px){.equal-height--vertical-divider{position:relative}.equal-height--vertical-divider__item{padding-left:10px;padding-right:10px}.equal-height--vertical-divider__item:before{content:'';position:absolute;right:-10px;top:0;height:100%;width:1px;border-right:1px dotted #888}.equal-height--vertical-divider .last-col,.equal-height--vertical-divider__item:last-of-type{padding-right:0}.equal-height--vertical-divider .last-col:before,.equal-height--vertical-divider__item:last-of-type:before{border-right:0}.equal-height--vertical-divider__item:first-of-type{padding-left:0}}@media only screen and (max-width:768px){.equal-height--vertical-divider .equal-height--vertical-divider__item{border-bottom:1px dotted #888;padding-bottom:20px}.equal-height--vertical-divider .equal-height--vertical-divider__item:last-of-type{border-bottom:0;padding-bottom:inherit}}body{background-color:#f7f7f7;background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/img/backgrounds/image-background-paper.png?w=$breakpoint-medium");background-position:center top;background-repeat:repeat-y}@media only screen and (min-width:768px){body{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/img/backgrounds/image-background-paper.png")}}.wrapper{margin:0 auto;max-width:984px}.inner-wrapper{background:#fff;border-radius:4px;box-shadow:0 0 3px #cdcdcd;margin:10px 0 0;padding-bottom:0}@media only screen and (max-width:768px){.inner-wrapper{margin:10px 0 30px;padding-bottom:20px}}@media only screen and (max-width:985px){.inner-wrapper{margin:0}}@media only screen and (min-width:985px){.banner{margin-bottom:30px}}.banner .nav-primary ul{border:0}.banner .nav-primary ul li{border-left-color:#df4a16}.banner .nav-primary ul li ul{box-shadow:0 2px 2px -1px #888;border-radius:10px;background:#f7f7f7;border:1px solid #cdcdcd;display:none;float:none;margin:0;padding:5px 0 20px;position:absolute;top:51px;width:200px}.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited,.banner .nav-primary ul li a.active:link,.banner .nav-primary ul li a.active:visited{font-weight:normal}@media only screen and (min-width:768px){.banner .nav-primary ul li a,.banner .nav-primary ul li a:link,.banner .nav-primary ul li a:visited,.banner .nav-primary ul li a.active:link,.banner .nav-primary ul li a.active:visited{border-left:1px solid #eb6233}}.banner .nav-primary ul li a:hover,.banner .nav-primary ul li a:link:hover,.banner .nav-primary ul li a:visited:hover,.banner .nav-primary ul li a.active:link:hover,.banner .nav-primary ul li a.active:visited:hover{background:#ec693c;box-shadow:none;color:#fff}@media only screen and (min-width:860px){.banner .nav-primary ul ul{display:none}}.banner .nav-primary ul ul li{border:0}.banner .nav-primary ul ul li:last-child{border:0}.banner .nav-primary ul li ul li a,.banner .nav-primary ul li ul li a:link,.banner .nav-primary ul li ul li a:visited{color:#333;border:0;padding:10px 0 0 14px;font-weight:normal;text-align:left;width:186px}.banner .nav-primary ul .second-level-nav li a:link:hover,.banner .nav-primary ul .second-level-nav li a:visited:hover{background:0;color:#e95420}.banner .nav-primary ul .second-level-nav .active:link,.banner .nav-primary ul .second-level-nav .active:visited,.banner .nav-primary ul .second-level-nav .active:link:hover,.banner .nav-primary ul .second-level-nav .active:visited:hover{background:0;border:0;color:#333}.banner .nav-primary ul li:hover ul::after{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QYODgYVPPJJpQAAAT9JREFUKM+dkD9IAnEUx7+/IkVxFaxoKYxIWhqKwJw0JAJ315Yac8+1tUFo6HCp34XRdRp11z+J4BzckqShPzdEQ5G/EhPO6V5Tcg3G0QcePHjvffjygB4oXAIApFMxyPlcWM7nwulU7NfMNQqXGABsZNcCCpcKtapBtapBCpcK2fXlAAAcy3vMlUxTdgEAO9ubybPiPjWFoE6nQ5ZlUfNDULl0RAdSPuncddL30+gnCgOAlngLaocyn4nG9fmFJXj9fhARAMDr82MuEcfk7LSuFQu8DTvovO1SuTxlAKCqPFO5Ov/6FIIsy/qzROOdrst6S1V5xunomo0LrT4yOh4JDg6BMXfvIdvGs/mExutLPZpYnAIAdmNUVuwBbI1NRODxePEf2u0WHu9u4ev3rTLTfKBQaNh1qp5piWCa9/gGBheo3r6AmYcAAAAASUVORK5CYII=") no-repeat;content:'';display:block;height:8px;left:20px;position:relative;top:-13px;width:200px;z-index:999}.banner .nav-primary ul li:hover ul{display:block}@media only screen and (min-width:985px){.banner .logo{margin-left:0}}.strip-dark{background-color:#2c001e;background-image:url("https://assets.ubuntu.com/v1/62d597f6-background-grid.png");color:#fff}.strip-dark .resource{box-shadow:none;color:#333;padding-right:20px}.strip-dark .resource::before{border-bottom-width:29px;border-right-color:#2c001e;right:-2px;top:-1px}.strip-dark .resource:hover::before{border-bottom-width:34px}.context-footer{box-sizing:border-box;clear:both;display:block;padding:0 10px;position:relative;width:100%}.context-footer hr{background:#e95420;border:0;box-shadow:inset 0 2px 2px -2px #888;clear:both;height:14px;margin:0 -10px 20px}.context-footer div>div{border-bottom:1px dotted #888;padding-bottom:20px}.context-footer div>div:last-child{border:0;padding-bottom:0}.context-footer ul{margin-bottom:5px}.context-footer h3{font-size:1em;font-weight:normal;margin-bottom:.75em}@media only screen and (min-width:768px){.context-footer div>div{border:0;padding-bottom:0}.context-footer hr{margin:0 -30px 40px}.context-footer h3{font-size:1.07143125em}.context-footer p,.context-footer li,.context-footer a{font-size:.9375em}}@media only screen and (min-width:984px){.context-footer{padding:0 20px}.context-footer hr{margin:0 -20px 40px}.context-footer h3{font-size:1.14286em}.context-footer p,.context-footer li,.context-footer a{font-size:1em}}.top-link{position:absolute;left:-999em}.pull-left-20{margin-left:-20px}.pull-right-20{margin-right:-20px}.pull-left-40{margin-left:-40px}.pull-right-40{margin-right:-41px}.arrow-up{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-up.png");left:20px;top:-11px}.arrow-down{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-down.png");bottom:-11px;right:20px}.arrow-right{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-right.png");height:18px;right:-11px;top:20px;width:11px}.arrow-left{background-image:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/arrow-left.png");bottom:20px;height:18px;left:-11px;width:11px}.box{background:#fff;border:1px solid #cdcdcd}.box-grey{background:#f7f7f7;color:#333}.box-orange{background:#e95420;color:#fff}.box-highlight{border:1px solid #f7f7f7;box-shadow:0 2px 2px 0 #cdcdcd}.box-textured{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/grey-textured-background.jpg");border:0;box-shadow:0 2px 2px 0 #cdcdcd}.box-padded{background:#efefef;border:0;border-radius:4px;margin-bottom:20px;padding:6px 5px}.box-padded h3{font-size:1.21875em;margin-left:5px;margin-top:5px}.box-padded li h3{font-size:1.21875em;margin:0}.box-padded div{background:#fff;border-radius:4px;overflow:hidden;padding:8px 8px 2px}.box-padded-feature{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/patterns/soft-centre-bkg.gif") repeat scroll 0 0 #888;border-radius:4px;border:0;margin-bottom:20px;padding:11px 5px 6px}.box-padded-feature h3{color:#fff;font-size:1.21875em;margin-left:5px}.box-padded-feature h4{font-size:1em;font-weight:normal}.box-padded-feature>div{background:#fff;border-radius:4px;overflow:hidden;padding:20px 8px}.box-padded-feature div div{margin-bottom:0}.box-padded-feature .one-col{float:left;width:48px}@media only screen and (max-width:768px){.box-padded-feature .inline-icons li{display:block;float:left}.box-padded-feature .one-col{float:left;width:48px}}.resource{cursor:pointer;padding-bottom:40px;position:relative;transition:background .2s ease-out}.resource.four-col h2 a:link,.resource.four-col h2 a:visited{font-size:1.125em}.resource.twelve-col h2 a:link,.resource.twelve-col h2 a:visited{font-size:1.40625em}.resource:hover{background-color:#fafafa}.resource::after{box-shadow:0 -1px 2px 0 #ddd;content:'';height:1px;position:absolute;right:-6px;top:14px;transform:rotate(45deg);transition:all .2s ease-out;width:41px;z-index:2}.resource:hover::after{right:-9px;top:18px;width:48px}.resource::before{border-bottom:30px solid #f7f7f7;border-radius:0;border-right:30px solid #fff;box-shadow:-2px 2px 2px rgba(247,247,247,0.4);content:'';height:0;position:absolute;right:-3px;top:-2px;transition:border-width .2s ease-out;width:0;z-index:2}.resource:hover::before{border-bottom-width:35px;border-right-width:35px}.resource:last-of-type{margin-bottom:30px}.resource .content-cat{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/icons/icon-resource-hub-icon-document.png") left center no-repeat;color:#888;font-size:14px;letter-spacing:1px;margin:0;padding:0 0 0 20px;position:absolute;text-transform:uppercase}.resource .content-cat-webinar{background:url("https://assets.ubuntu.com/sites/ubuntu/latest/u/icons/icon-resource-hub-webinar.png") left center no-repeat}.resource.box-image-centered div+span img{margin-top:40px}.resource h2{padding-right:20px}.row-grey .resource::before{border-right-color:#f7f7f7}.list-ticks--compact li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%2394310f' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}.list-ticks--canonical li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%23772953' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}.list-ticks--canonical-compact li{background-image:url("data:image/svg+xml;utf8, <svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'><circle fill='%23772953' cx='7' cy='7' r='7'/><path fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /></svg>")}@media only screen and (min-width:860px){.nav-secondary{border-width:0 0 1px}.nav-secondary .breadcrumb{padding-top:8px;padding-bottom:7px}}@media only screen and (min-width:860px){.header-search [type="search"],.header-search [type="text"],.search-form [type="search"],.search-form [type="text"]{border-radius:4px}}blockquote{padding:inherit 1em}.collapse{cursor:pointer;display:block}.collapse+input{display:none}.collapse+input+div{display:none}.collapse+input:checked+div{display:block}.cheshire{display:none} |
783 | \ No newline at end of file |
784 | +[grid-demo] [class*="col-"]{background:#cdcdcd;margin-bottom:1rem}[grid-demo] [class*="col-"]{background:#cdcdcd;margin-bottom:1rem}@media screen and (max-width:400px){@-ms-viewport{width:320px}}img{max-width:100%;height:auto}@media \0screen{img{width:auto}}.row{*zoom:1;margin-right:auto;margin-left:auto;max-width:1030px;padding-left:20px;padding-right:20px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row .row{margin-right:0;margin-left:0;max-width:none;padding-right:0;padding-left:0}.mobile-col-1,.mobile-col-2,.mobile-col-3{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:4.61165%}.row .mobile-col-1:first-child,.row .mobile-col-2:first-child,.row .mobile-col-3:first-child,.first-mobile-col{margin-left:0}.mobile-col-1{width:21.54126%}.mobile-col-2{width:47.69417%}.mobile-col-3{width:73.84709%}@media screen and (min-width:620px){.tablet-col-1,.tablet-col-2,.tablet-col-3,.tablet-col-4,.tablet-col-5{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:2.91262%}.row .tablet-col-1:first-child,.row .tablet-col-2:first-child,.row .tablet-col-3:first-child,.row .tablet-col-4:first-child,.row .tablet-col-5:first-child,.first-tablet-col{margin-left:0}.tablet-col-1{width:14.23948%}.tablet-col-2{width:31.39159%}.tablet-col-3{width:48.54369%}.tablet-col-4{width:65.69579%}.tablet-col-5{width:82.8479%}}@media screen and (min-width:768px){.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:1.94175%}.row .col-1:first-child,.row .col-2:first-child,.row .col-3:first-child,.row .col-4:first-child,.row .col-5:first-child,.row .col-6:first-child,.row .col-7:first-child,.row .col-8:first-child,.row .col-9:first-child,.row .col-10:first-child,.row .col-11:first-child,.first-col{margin-left:0}.col-1{width:6.5534%}.col-2{width:15.04854%}.col-3{width:23.54369%}.col-4{width:32.03883%}.col-5{width:40.53398%}.col-6{width:49.02913%}.col-7{width:57.52427%}.col-8{width:66.01942%}.col-9{width:74.51456%}.col-10{width:83.00971%}.col-11{width:91.50485%}.prefix-1{padding-left:8.49515%}.prefix-2{padding-left:16.99029%}.prefix-3{padding-left:25.48544%}.prefix-4{padding-left:33.98058%}.prefix-5{padding-left:42.47573%}.prefix-6{padding-left:50.97087%}.prefix-7{padding-left:59.46602%}.prefix-8{padding-left:67.96117%}.prefix-9{padding-left:76.45631%}.prefix-10{padding-left:84.95146%}.prefix-11{padding-left:93.4466%}.suffix-1{padding-right:8.49515%}.suffix-2{padding-right:16.99029%}.suffix-3{padding-right:25.48544%}.suffix-4{padding-right:33.98058%}.suffix-5{padding-right:42.47573%}.suffix-6{padding-right:50.97087%}.suffix-7{padding-right:59.46602%}.suffix-8{padding-right:67.96117%}.suffix-9{padding-right:76.45631%}.suffix-10{padding-right:84.95146%}.suffix-11{padding-right:93.4466%}.push-1{left:8.49515%}.push-2{left:16.99029%}.push-3{left:25.48544%}.push-4{left:33.98058%}.push-5{left:42.47573%}.push-6{left:50.97087%}.push-7{left:59.46602%}.push-8{left:67.96117%}.push-9{left:76.45631%}.push-10{left:84.95146%}.push-11{left:93.4466%}.pull-1{right:8.49515%}.pull-2{right:16.99029%}.pull-3{right:25.48544%}.pull-4{right:33.98058%}.pull-5{right:42.47573%}.pull-6{right:50.97087%}.pull-7{right:59.46602%}.pull-8{right:67.96117%}.pull-9{right:76.45631%}.pull-10{right:84.95146%}.pull-11{right:93.4466%}.col-11 .col-1,.col-11 .col-2,.col-11 .col-3,.col-11 .col-4,.col-11 .col-5,.col-11 .col-6,.col-11 .col-7,.col-11 .col-8,.col-11 .col-9,.col-11 .col-10{margin-left:2.12202%}.col-11 .col-1{width:7.1618%}.col-11 .col-2{width:16.44562%}.col-11 .col-3{width:25.72944%}.col-11 .col-4{width:35.01326%}.col-11 .col-5{width:44.29708%}.col-11 .col-6{width:53.5809%}.col-11 .col-7{width:62.86472%}.col-11 .col-8{width:72.14854%}.col-11 .col-9{width:81.43236%}.col-11 .col-10{width:90.71618%}.col-10 .col-1,.col-10 .col-2,.col-10 .col-3,.col-10 .col-4,.col-10 .col-5,.col-10 .col-6,.col-10 .col-7,.col-10 .col-8,.col-10 .col-9{margin-left:2.33918%}.col-10 .col-1{width:7.89474%}.col-10 .col-2{width:18.12865%}.col-10 .col-3{width:28.36257%}.col-10 .col-4{width:38.59649%}.col-10 .col-5{width:48.83041%}.col-10 .col-6{width:59.06433%}.col-10 .col-7{width:69.29825%}.col-10 .col-8{width:79.53216%}.col-10 .col-9{width:89.76608%}.col-9 .col-1,.col-9 .col-2,.col-9 .col-3,.col-9 .col-4,.col-9 .col-5,.col-9 .col-6,.col-9 .col-7,.col-9 .col-8{margin-left:2.60586%}.col-9 .col-1{width:8.79479%}.col-9 .col-2{width:20.19544%}.col-9 .col-3{width:31.59609%}.col-9 .col-4{width:42.99674%}.col-9 .col-5{width:54.39739%}.col-9 .col-6{width:65.79805%}.col-9 .col-7{width:77.1987%}.col-9 .col-8{width:88.59935%}.col-8 .col-1,.col-8 .col-2,.col-8 .col-3,.col-8 .col-4,.col-8 .col-5,.col-8 .col-6,.col-8 .col-7{margin-left:2.94118%}.col-8 .col-1{width:9.92647%}.col-8 .col-2{width:22.79412%}.col-8 .col-3{width:35.66176%}.col-8 .col-4{width:48.52941%}.col-8 .col-5{width:61.39706%}.col-8 .col-6{width:74.26471%}.col-8 .col-7{width:87.13235%}.col-7 .col-1,.col-7 .col-2,.col-7 .col-3,.col-7 .col-4,.col-7 .col-5,.col-7 .col-6{margin-left:3.37553%}.col-7 .col-1{width:11.39241%}.col-7 .col-2{width:26.16034%}.col-7 .col-3{width:40.92827%}.col-7 .col-4{width:55.6962%}.col-7 .col-5{width:70.46414%}.col-7 .col-6{width:85.23207%}.col-6 .col-1,.col-6 .col-2,.col-6 .col-3,.col-6 .col-4,.col-6 .col-5{margin-left:3.9604%}.col-6 .col-1{width:13.36634%}.col-6 .col-2{width:30.69307%}.col-6 .col-3{width:48.0198%}.col-6 .col-4{width:65.34653%}.col-6 .col-5{width:82.67327%}.col-5 .col-1,.col-5 .col-2,.col-5 .col-3,.col-5 .col-4{margin-left:4.79042%}.col-5 .col-1{width:16.16766%}.col-5 .col-2{width:37.12575%}.col-5 .col-3{width:58.08383%}.col-5 .col-4{width:79.04192%}.col-4 .col-1,.col-4 .col-2,.col-4 .col-3{margin-left:6.06061%}.col-4 .col-1{width:20.45455%}.col-4 .col-2{width:46.9697%}.col-4 .col-3{width:73.48485%}.col-3 .col-1,.col-3 .col-2{margin-left:8.24742%}.col-3 .col-1{width:27.83505%}.col-3 .col-2{width:63.91753%}.col-2 .col-1{margin-left:12.90323%}.col-2 .col-1{width:43.54839%}}.row .center-col{float:none;margin-left:auto !important;margin-right:auto}@media screen and (max-width:619px){.hidden-mobile,.visible-tablet,.visible-desktop{display:none !important}}@media screen and (min-width:620px) and (max-width:767px){.visible-mobile,.hidden-tablet,.visible-desktop{display:none !important}}@media screen and (min-width:768px){.visible-mobile,.visible-tablet,.hidden-desktop{display:none !important}}[class*="col-"],.row{margin-top:0}@media screen and (max-width:767px){[class*="col-"]+[class*="col-"]{margin-top:20px}}.p-matrix__item::after,.p-navigation--light::after,.p-navigation::after,.p-navigation--dark::after,.p-inline-images::after,.u-clearfix::after{clear:both;content:'';display:block}html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:0;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}blockquote{border-left:2px solid #666}blockquote>cite{display:block}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}button{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#fff;border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%;line-height:1rem}@media only screen and (min-width:768px){button{width:auto}}button:visited{color:#111}button:active,button:focus,button:hover{background-color:#f7f7f7;border-color:#cdcdcd;text-decoration:none}button:disabled,button.is--disabled{cursor:not-allowed;opacity:.5}button:disabled:active,button:disabled:focus,button:disabled:hover,button.is--disabled:active,button.is--disabled:focus,button.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}button .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}label{cursor:pointer;display:block;font-size:1rem;line-height:1.5}label.has-error{color:#c7162b}label.has-caution{color:#f99b11}label.has-warning{color:#f99b11}label.has-success{color:#0e8420}label.has-information{color:#335280}input[type='text'],input[type='date'],input[type='datetime'],input[type='datatime-local'],input[type='month'],input[type='time'],input[type='week'],input[type='color'],input[type='number'],input[type='search'],input[type='password'],input[type='email'],input[type='url'],input[type='tel']{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-weight:300;outline:0;padding:.8rem .5333rem;vertical-align:baseline;width:100%}@media(min-width:768px){input[type='text'],input[type='date'],input[type='datetime'],input[type='datatime-local'],input[type='month'],input[type='time'],input[type='week'],input[type='color'],input[type='number'],input[type='search'],input[type='password'],input[type='email'],input[type='url'],input[type='tel']{padding:.75rem .5rem}}input[type='text']:active,input[type='text']:focus,input[type='date']:active,input[type='date']:focus,input[type='datetime']:active,input[type='datetime']:focus,input[type='datatime-local']:active,input[type='datatime-local']:focus,input[type='month']:active,input[type='month']:focus,input[type='time']:active,input[type='time']:focus,input[type='week']:active,input[type='week']:focus,input[type='color']:active,input[type='color']:focus,input[type='number']:active,input[type='number']:focus,input[type='search']:active,input[type='search']:focus,input[type='password']:active,input[type='password']:focus,input[type='email']:active,input[type='email']:focus,input[type='url']:active,input[type='url']:focus,input[type='tel']:active,input[type='tel']:focus{border-color:#666;color:#111;outline:0}input[type='text']::placeholder,input[type='date']::placeholder,input[type='datetime']::placeholder,input[type='datatime-local']::placeholder,input[type='month']::placeholder,input[type='time']::placeholder,input[type='week']::placeholder,input[type='color']::placeholder,input[type='number']::placeholder,input[type='search']::placeholder,input[type='password']::placeholder,input[type='email']::placeholder,input[type='url']::placeholder,input[type='tel']::placeholder{color:#666;opacity:1}input[type='text'][disabled],input[type='text'][disabled='disabled'],input[type='date'][disabled],input[type='date'][disabled='disabled'],input[type='datetime'][disabled],input[type='datetime'][disabled='disabled'],input[type='datatime-local'][disabled],input[type='datatime-local'][disabled='disabled'],input[type='month'][disabled],input[type='month'][disabled='disabled'],input[type='time'][disabled],input[type='time'][disabled='disabled'],input[type='week'][disabled],input[type='week'][disabled='disabled'],input[type='color'][disabled],input[type='color'][disabled='disabled'],input[type='number'][disabled],input[type='number'][disabled='disabled'],input[type='search'][disabled],input[type='search'][disabled='disabled'],input[type='password'][disabled],input[type='password'][disabled='disabled'],input[type='email'][disabled],input[type='email'][disabled='disabled'],input[type='url'][disabled],input[type='url'][disabled='disabled'],input[type='tel'][disabled],input[type='tel'][disabled='disabled']{cursor:not-allowed;opacity:.5}input[type='text'][readonly],input[type='text'][readonly='readonly'],input[type='date'][readonly],input[type='date'][readonly='readonly'],input[type='datetime'][readonly],input[type='datetime'][readonly='readonly'],input[type='datatime-local'][readonly],input[type='datatime-local'][readonly='readonly'],input[type='month'][readonly],input[type='month'][readonly='readonly'],input[type='time'][readonly],input[type='time'][readonly='readonly'],input[type='week'][readonly],input[type='week'][readonly='readonly'],input[type='color'][readonly],input[type='color'][readonly='readonly'],input[type='number'][readonly],input[type='number'][readonly='readonly'],input[type='search'][readonly],input[type='search'][readonly='readonly'],input[type='password'][readonly],input[type='password'][readonly='readonly'],input[type='email'][readonly],input[type='email'][readonly='readonly'],input[type='url'][readonly],input[type='url'][readonly='readonly'],input[type='tel'][readonly],input[type='tel'][readonly='readonly']{color:#cdcdcd;cursor:default}input[type='text'][readonly]:hover,input[type='text'][readonly]:active,input[type='text'][readonly]:focus,input[type='text'][readonly='readonly']:hover,input[type='text'][readonly='readonly']:active,input[type='text'][readonly='readonly']:focus,input[type='date'][readonly]:hover,input[type='date'][readonly]:active,input[type='date'][readonly]:focus,input[type='date'][readonly='readonly']:hover,input[type='date'][readonly='readonly']:active,input[type='date'][readonly='readonly']:focus,input[type='datetime'][readonly]:hover,input[type='datetime'][readonly]:active,input[type='datetime'][readonly]:focus,input[type='datetime'][readonly='readonly']:hover,input[type='datetime'][readonly='readonly']:active,input[type='datetime'][readonly='readonly']:focus,input[type='datatime-local'][readonly]:hover,input[type='datatime-local'][readonly]:active,input[type='datatime-local'][readonly]:focus,input[type='datatime-local'][readonly='readonly']:hover,input[type='datatime-local'][readonly='readonly']:active,input[type='datatime-local'][readonly='readonly']:focus,input[type='month'][readonly]:hover,input[type='month'][readonly]:active,input[type='month'][readonly]:focus,input[type='month'][readonly='readonly']:hover,input[type='month'][readonly='readonly']:active,input[type='month'][readonly='readonly']:focus,input[type='time'][readonly]:hover,input[type='time'][readonly]:active,input[type='time'][readonly]:focus,input[type='time'][readonly='readonly']:hover,input[type='time'][readonly='readonly']:active,input[type='time'][readonly='readonly']:focus,input[type='week'][readonly]:hover,input[type='week'][readonly]:active,input[type='week'][readonly]:focus,input[type='week'][readonly='readonly']:hover,input[type='week'][readonly='readonly']:active,input[type='week'][readonly='readonly']:focus,input[type='color'][readonly]:hover,input[type='color'][readonly]:active,input[type='color'][readonly]:focus,input[type='color'][readonly='readonly']:hover,input[type='color'][readonly='readonly']:active,input[type='color'][readonly='readonly']:focus,input[type='number'][readonly]:hover,input[type='number'][readonly]:active,input[type='number'][readonly]:focus,input[type='number'][readonly='readonly']:hover,input[type='number'][readonly='readonly']:active,input[type='number'][readonly='readonly']:focus,input[type='search'][readonly]:hover,input[type='search'][readonly]:active,input[type='search'][readonly]:focus,input[type='search'][readonly='readonly']:hover,input[type='search'][readonly='readonly']:active,input[type='search'][readonly='readonly']:focus,input[type='password'][readonly]:hover,input[type='password'][readonly]:active,input[type='password'][readonly]:focus,input[type='password'][readonly='readonly']:hover,input[type='password'][readonly='readonly']:active,input[type='password'][readonly='readonly']:focus,input[type='email'][readonly]:hover,input[type='email'][readonly]:active,input[type='email'][readonly]:focus,input[type='email'][readonly='readonly']:hover,input[type='email'][readonly='readonly']:active,input[type='email'][readonly='readonly']:focus,input[type='url'][readonly]:hover,input[type='url'][readonly]:active,input[type='url'][readonly]:focus,input[type='url'][readonly='readonly']:hover,input[type='url'][readonly='readonly']:active,input[type='url'][readonly='readonly']:focus,input[type='tel'][readonly]:hover,input[type='tel'][readonly]:active,input[type='tel'][readonly]:focus,input[type='tel'][readonly='readonly']:hover,input[type='tel'][readonly='readonly']:active,input[type='tel'][readonly='readonly']:focus{border-color:#666;outline:0}input[type='text'].has-error,input[type='date'].has-error,input[type='datetime'].has-error,input[type='datatime-local'].has-error,input[type='month'].has-error,input[type='time'].has-error,input[type='week'].has-error,input[type='color'].has-error,input[type='number'].has-error,input[type='search'].has-error,input[type='password'].has-error,input[type='email'].has-error,input[type='url'].has-error,input[type='tel'].has-error{border:1px solid #c7162b}input[type='text'].has-error:focus,input[type='date'].has-error:focus,input[type='datetime'].has-error:focus,input[type='datatime-local'].has-error:focus,input[type='month'].has-error:focus,input[type='time'].has-error:focus,input[type='week'].has-error:focus,input[type='color'].has-error:focus,input[type='number'].has-error:focus,input[type='search'].has-error:focus,input[type='password'].has-error:focus,input[type='email'].has-error:focus,input[type='url'].has-error:focus,input[type='tel'].has-error:focus{border:1px solid #c7162b}input[type='text'].has-caution,input[type='date'].has-caution,input[type='datetime'].has-caution,input[type='datatime-local'].has-caution,input[type='month'].has-caution,input[type='time'].has-caution,input[type='week'].has-caution,input[type='color'].has-caution,input[type='number'].has-caution,input[type='search'].has-caution,input[type='password'].has-caution,input[type='email'].has-caution,input[type='url'].has-caution,input[type='tel'].has-caution{border:1px solid #f99b11}input[type='text'].has-caution:focus,input[type='date'].has-caution:focus,input[type='datetime'].has-caution:focus,input[type='datatime-local'].has-caution:focus,input[type='month'].has-caution:focus,input[type='time'].has-caution:focus,input[type='week'].has-caution:focus,input[type='color'].has-caution:focus,input[type='number'].has-caution:focus,input[type='search'].has-caution:focus,input[type='password'].has-caution:focus,input[type='email'].has-caution:focus,input[type='url'].has-caution:focus,input[type='tel'].has-caution:focus{border:1px solid #f99b11}input[type='text'].has-warning,input[type='date'].has-warning,input[type='datetime'].has-warning,input[type='datatime-local'].has-warning,input[type='month'].has-warning,input[type='time'].has-warning,input[type='week'].has-warning,input[type='color'].has-warning,input[type='number'].has-warning,input[type='search'].has-warning,input[type='password'].has-warning,input[type='email'].has-warning,input[type='url'].has-warning,input[type='tel'].has-warning{border:1px solid #f99b11}input[type='text'].has-warning:focus,input[type='date'].has-warning:focus,input[type='datetime'].has-warning:focus,input[type='datatime-local'].has-warning:focus,input[type='month'].has-warning:focus,input[type='time'].has-warning:focus,input[type='week'].has-warning:focus,input[type='color'].has-warning:focus,input[type='number'].has-warning:focus,input[type='search'].has-warning:focus,input[type='password'].has-warning:focus,input[type='email'].has-warning:focus,input[type='url'].has-warning:focus,input[type='tel'].has-warning:focus{border:1px solid #f99b11}input[type='text'].has-success,input[type='date'].has-success,input[type='datetime'].has-success,input[type='datatime-local'].has-success,input[type='month'].has-success,input[type='time'].has-success,input[type='week'].has-success,input[type='color'].has-success,input[type='number'].has-success,input[type='search'].has-success,input[type='password'].has-success,input[type='email'].has-success,input[type='url'].has-success,input[type='tel'].has-success{border:1px solid #0e8420}input[type='text'].has-success:focus,input[type='date'].has-success:focus,input[type='datetime'].has-success:focus,input[type='datatime-local'].has-success:focus,input[type='month'].has-success:focus,input[type='time'].has-success:focus,input[type='week'].has-success:focus,input[type='color'].has-success:focus,input[type='number'].has-success:focus,input[type='search'].has-success:focus,input[type='password'].has-success:focus,input[type='email'].has-success:focus,input[type='url'].has-success:focus,input[type='tel'].has-success:focus{border:1px solid #0e8420}input[type='text'].has-information,input[type='date'].has-information,input[type='datetime'].has-information,input[type='datatime-local'].has-information,input[type='month'].has-information,input[type='time'].has-information,input[type='week'].has-information,input[type='color'].has-information,input[type='number'].has-information,input[type='search'].has-information,input[type='password'].has-information,input[type='email'].has-information,input[type='url'].has-information,input[type='tel'].has-information{border:1px solid #335280}input[type='text'].has-information:focus,input[type='date'].has-information:focus,input[type='datetime'].has-information:focus,input[type='datatime-local'].has-information:focus,input[type='month'].has-information:focus,input[type='time'].has-information:focus,input[type='week'].has-information:focus,input[type='color'].has-information:focus,input[type='number'].has-information:focus,input[type='search'].has-information:focus,input[type='password'].has-information:focus,input[type='email'].has-information:focus,input[type='url'].has-information:focus,input[type='tel'].has-information:focus{border:1px solid #335280}input[type='file']{outline:0;width:100%}input[type='reset']{display:none}input[type='search']{appearance:none;border-radius:0}input[type='search']::-webkit-search-results-decoration{display:none}input[type='search']::-webkit-search-cancel-button{-webkit-appearance:searchfield-cancel-button;cursor:pointer}input[type='checkbox'],input[type='radio']{float:left;height:1.5rem;margin-bottom:0;margin-right:1rem;outline:0;padding:0;vertical-align:middle;width:auto;min-height:1.5rem}input[type='checkbox'][disabled]+label,input[type='checkbox'][disabled='disabled']+label,input[type='radio'][disabled]+label,input[type='radio'][disabled='disabled']+label{cursor:not-allowed;opacity:.5}input[type='checkbox']+label,input[type='radio']+label{vertical-align:middle;width:100%}input[type='submit']{background-color:#0e8420;border:0;color:#fff;padding:.75rem 1.5rem}input[type='submit']:hover{background-color:#04280a;cursor:pointer}select{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-weight:300;outline:0;padding:.8rem .5333rem;vertical-align:baseline;width:100%;appearance:none;background:#fff url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjRweCIgd2lkdGg9IjEwcHgiIHZlcnNpb249IjEuMSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjAgMCAxMCA0Ij4gPHRpdGxlPmFjY29yZGlvbi1vcGVuPC90aXRsZT4gPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+IDxnIGlkPSJmaWx0ZXItcGFuZWwiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSIgZmlsbD0ibm9uZSI+ICA8ZyBpZD0iYWNjb3JkaW9uLW9wZW4iIGZpbGw9IiM4ODgiIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiPiAgIDxwYXRoIGlkPSJjaGV2cm9uIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIiBkPSJtNi4zNjEgMC44NjIzYzAuNTE4IDAuMzY1IDEuMDUyIDAuNzc4MSAxLjYwMSAxLjIzOCAwLjU0OSAwLjQ1ODUgMS4wODkgMC45NTE4IDEuNjIxIDEuNDc3MiAwLjE0MiAwLjE0MDQgMC4yODEgMC4yODIxIDAuNDE1IDAuNDIyNWgtMS41NDFjLTAuMzA0LTAuMjg4OC0wLjYyLTAuNTcwOS0wLjk0Ny0wLjg0NjMtMC4xMzc5LTAuMTE2MS0wLjI3NjgtMC4yMjk3LTAuNDE2OC0wLjM0MDgtMC4xNjM2LTAuMTI5Ny0wLjMyODYtMC4yNTU4LTAuNDk1NC0wLjM3ODMtMC4wODUyLTAuMDYyNS0wLjE3MDgtMC4xMjQxLTAuMjU2OC0wLjE4NDYtMC4zOTctMC4yODIxLTAuOTM1LTAuNjI1Ny0xLjMxNS0wLjg0NzZoLTAuMDU0Yy0wLjM4IDAuMjIxOS0wLjkxOCAwLjU2NTUtMS4zMTUgMC44NDc2LTAuMzk4IDAuMjgwNy0wLjc4OCAwLjU4MjktMS4xNjkgMC45MDM3LTAuMzI3IDAuMjc1NC0wLjY0MyAwLjU1NzUtMC45NDcgMC44NDYzaC0xLjU0MWMwLjEzNS0wLjE0MDQgMC4yNzMtMC4yODIxIDAuNDE1LTAuNDIyNSAwLjUzMi0wLjUyNTQgMS4wNzItMS4wMTg3IDEuNjIxLTEuNDc3MiAwLjU1LTAuNDU5OSAxLjA4My0wLjg3MyAxLjYwMS0xLjIzOCAwLjUxOS0wLjM2NDk3IDAuOTczLTAuNjUyNDEgMS4zNjItMC44NjIzIDAuMzkgMC4yMDk4OSAwLjg0NCAwLjQ5NzMzIDEuMzYyIDAuODYyM3oiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuOTk5IDIpIHJvdGF0ZSgxODApIHRyYW5zbGF0ZSgtNC45OTkgLTIpIi8+ICA8L2c+IDwvZz48L3N2Zz4=") no-repeat;background-position:top 1.3rem right 1.25rem;line-height:1rem;margin-top:.5rem;min-height:48px;text-indent:.01px;text-overflow:''}@media(min-width:768px){select{padding:.75rem .5rem}}select:active,select:focus{border-color:#666;color:#111;outline:0}select::placeholder{color:#666;opacity:1}select[disabled],select[disabled='disabled']{cursor:not-allowed;opacity:.5}select[readonly],select[readonly='readonly']{color:#cdcdcd;cursor:default}select[readonly]:hover,select[readonly]:active,select[readonly]:focus,select[readonly='readonly']:hover,select[readonly='readonly']:active,select[readonly='readonly']:focus{border-color:#666;outline:0}select.has-error{border:1px solid #c7162b}select.has-error:focus{border:1px solid #c7162b}select.has-caution{border:1px solid #f99b11}select.has-caution:focus{border:1px solid #f99b11}select.has-warning{border:1px solid #f99b11}select.has-warning:focus{border:1px solid #f99b11}select.has-success{border:1px solid #0e8420}select.has-success:focus{border:1px solid #0e8420}select.has-information{border:1px solid #335280}select.has-information:focus{border:1px solid #335280}select:hover{cursor:pointer}select[multiple],select[size]{background-image:none;height:auto;padding:.35rem .8125rem}select[multiple] option,select[size] option{font-weight:300;margin:.5rem 0}textarea{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-weight:300;outline:0;padding:.8rem .5333rem;vertical-align:baseline;width:100%;margin-top:.5rem;overflow:auto;vertical-align:top}@media(min-width:768px){textarea{padding:.75rem .5rem}}textarea:active,textarea:focus{border-color:#666;color:#111;outline:0}textarea::placeholder{color:#666;opacity:1}textarea[disabled],textarea[disabled='disabled']{cursor:not-allowed;opacity:.5}textarea[readonly],textarea[readonly='readonly']{color:#cdcdcd;cursor:default}textarea[readonly]:hover,textarea[readonly]:active,textarea[readonly]:focus,textarea[readonly='readonly']:hover,textarea[readonly='readonly']:active,textarea[readonly='readonly']:focus{border-color:#666;outline:0}textarea.has-error{border:1px solid #c7162b}textarea.has-error:focus{border:1px solid #c7162b}textarea.has-caution{border:1px solid #f99b11}textarea.has-caution:focus{border:1px solid #f99b11}textarea.has-warning{border:1px solid #f99b11}textarea.has-warning:focus{border:1px solid #f99b11}textarea.has-success{border:1px solid #0e8420}textarea.has-success:focus{border:1px solid #0e8420}textarea.has-information{border:1px solid #335280}textarea.has-information:focus{border:1px solid #335280}fieldset{background-color:#f7f7f7;background-position:-.9375rem -.9375rem;background-repeat:no-repeat;border-radius:.125rem;padding:.9375rem 1.25rem}@media only screen and (min-width:1030px){fieldset{padding:.9375rem 1.25rem}}fieldset h3{border-bottom:1px dotted #666;padding-bottom:.625rem}form+*{margin-top:.5rem}form *+input{margin-top:.75rem}form *+label{margin-top:1.25rem}form *+input[type="checkbox"],form *+input[type="radio"],form *+button,form *+input[type="submit"]{margin-top:1rem}form *+input[type="checkbox"]+label,form *+input[type="radio"]+label,form *+button+label,form *+input[type="submit"]+label{margin-top:1rem}code,samp,kbd{font-family:"Ubuntu Mono",Consolas,Monaco,Courier,monospace;font-weight:300;text-align:left}pre,code{direction:ltr;hyphens:none;line-height:1.5;margin-bottom:0;tab-size:4;white-space:pre-wrap;word-spacing:normal;word-wrap:break-word}pre{background-color:#f7f7f7;border:1px solid #666;border-radius:2px;color:#111;overflow:auto;padding:1rem;text-align:left;text-shadow:none}a{color:#007aa6;text-decoration:none}a:focus{outline:thin dotted #cdcdcd}a:hover{cursor:pointer;text-decoration:underline}a:visited{color:#005573}ol,ul{margin-bottom:0;margin-left:1rem;padding-left:1rem}ol ul,ol ol,ul ul,ul ol{margin-bottom:0}nav ol,nav ul{list-style:none;list-style-image:none}ol li+li,ul li+li{margin-top:.5rem}ol li>ul,ol li>ol,ul li>ul,ul li>ol{margin-top:.5rem}li{margin:0 0 .5rem;padding:0}dl{margin-bottom:0}dt{border-top:1px dotted #666;font-size:1rem;font-weight:400;margin-top:1rem;padding-top:1rem}dt:first-of-type{border-top:0}dd{margin-left:20px;margin-top:.5rem}.p-heading--one{font-size:2rem;font-weight:300;line-height:1.2}@media only screen and (min-width:1030px){.p-heading--one{font-size:3rem;line-height:1.25}}.p-heading--two{font-size:1.75rem;font-weight:300;line-height:1.25}@media only screen and (min-width:1030px){.p-heading--two{font-size:2rem;line-height:1.25}}@media only screen and (min-width:1030px){.p-heading--two{font-size:2.25rem;line-height:1.167}}.p-heading--three{font-size:1.5rem;font-weight:300;line-height:1.154}@media only screen and (min-width:1030px){.p-heading--three{font-size:1.75rem;line-height:1.286}}.p-heading--four{font-size:1.375rem;font-weight:300;line-height:1.364}@media only screen and (min-width:1030px){.p-heading--four{font-size:1.5rem;line-height:1.25}}.p-heading--five{font-size:1.125rem;font-weight:300;line-height:1.264}@media only screen and (min-width:1030px){.p-heading--five{font-size:1.25rem;line-height:1.143}}.p-heading--six{font-size:1rem;font-weight:400;line-height:1.412}hr{border:0;border-top:1px solid #cdcdcd;height:0}img{border:0;height:auto;max-width:100%}svg:not(:root){overflow:hidden}figure{margin-bottom:0;margin-left:0;width:100%}*+figure{margin-top:1.25rem}@media screen and (min-width:768px){*+figure{margin-top:1.75rem}}@media screen and (min-width:1030px){*+figure{margin-top:2rem}}figure caption,figure figcaption{display:block;font-style:italic;margin-top:.5rem;width:100%}object,iframe,embed,canvas,video,audio{display:block;margin:0 auto 20px;max-width:100%}audio:not([controls]){display:none;height:0}[hidden]{display:none}table{border:0;border-collapse:collapse;overflow-x:auto;width:100%}th,td{padding:1rem .75rem}td{font-weight:300;text-align:left;vertical-align:middle}thead th{border-collapse:separate;border-spacing:0 .5rem;font-weight:400;text-align:left}thead tr{border-bottom:1px solid #666}tbody tr{border-bottom:1px solid #cdcdcd}tbody th{font-weight:400;text-align:left}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/v1/50afa266-ubuntu-l-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/0194407b-ubuntu-l-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/v1/1cbafee5-ubuntu-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/81863185-ubuntu-r-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("https://assets.ubuntu.com/v1/abb07502-ubuntu-li-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/65fc9630-ubuntu-li-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("https://assets.ubuntu.com/v1/fca66073-ubuntu-ri-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/f0898c72-ubuntu-ri-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:300;src:url("https://assets.ubuntu.com/v1/871f7456-ubuntumono-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/8df3f408-ubuntumono-r-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("https://assets.ubuntu.com/v1/871f7456-ubuntumono-r-webfont.woff2") format("woff2"),url("https://assets.ubuntu.com/v1/8df3f408-ubuntumono-r-webfont.woff") format("woff")}*{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-smoothing:subpixel-antialiased}html{font-size:16px}body{color:#111;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-weight:300;line-height:1.5}h1,h2,h3,h4,h5,h6,[class^="p-heading--"]{font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;margin:0}h1+*{margin-top:1rem}@media screen and (min-width:1030px){h1+*{margin-top:1.5rem}}h5+*,h6+*{margin-top:.5rem}@media screen and (min-width:1030px){h5+*,h6+*{margin-top:.75rem}}p{margin-bottom:0}p+p{margin-top:1rem}@media screen and (min-width:1030px){p+p{margin-top:1.5rem}}button,input,select,textarea{font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif}h1{font-size:2rem;font-weight:300;line-height:1.2}@media only screen and (min-width:1030px){h1{font-size:3rem;line-height:1.25}}h2{font-size:1.75rem;font-weight:300;line-height:1.25}@media only screen and (min-width:1030px){h2{font-size:2rem;line-height:1.25}}@media only screen and (min-width:1030px){h2{font-size:2.25rem;line-height:1.167}}h3{font-size:1.5rem;font-weight:300;line-height:1.154}@media only screen and (min-width:1030px){h3{font-size:1.75rem;line-height:1.286}}h4{font-size:1.375rem;font-weight:300;line-height:1.364}@media only screen and (min-width:1030px){h4{font-size:1.5rem;line-height:1.25}}h5{font-size:1.125rem;font-weight:300;line-height:1.264}@media only screen and (min-width:1030px){h5{font-size:1.25rem;line-height:1.143}}h6{font-size:1rem;font-weight:400;line-height:1.412}li{margin-bottom:0;margin-top:0;padding-bottom:0}li>ul,li>ol{padding-top:0}li>ul>li:last-of-type,li>ol>li:last-of-type{padding-bottom:0}blockquote{margin-bottom:0;margin-left:0;padding-left:1.5rem}blockquote>p{font-size:1rem;font-style:italic;margin-top:.75rem}blockquote>cite{font-size:1rem;font-style:normal;margin-top:.75rem}strong{font-weight:400}small{font-size:.8125rem}sub,sup{font-size:.75rem;line-height:0;position:relative;vertical-align:baseline}sup{vertical-align:text-top}sub{vertical-align:text-bottom}.p-breadcrumbs{list-style:none;margin:0;padding:0;width:100%}.p-breadcrumbs__item{display:inline-block;margin:0 .75rem .25rem .25rem;position:relative}.p-breadcrumbs__item:not(:first-of-type){margin-right:-.25rem;text-indent:1rem}.p-breadcrumbs__item:first-of-type{margin-right:-.25rem}.p-breadcrumbs__item:not(:first-of-type)::before{content:'\203A';left:-.75rem;position:absolute;top:0}.p-breadcrumbs__link{color:#111;font-weight:400;text-decoration:none}.p-breadcrumbs__link:hover{color:#007aa6}.p-button{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#fff;border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button{width:auto}}.p-button:visited{color:#111}.p-button:active,.p-button:focus,.p-button:hover{background-color:#f7f7f7;border-color:#cdcdcd;text-decoration:none}.p-button:disabled,.p-button.is--disabled{cursor:not-allowed;opacity:.5}.p-button:disabled:active,.p-button:disabled:focus,.p-button:disabled:hover,.p-button.is--disabled:active,.p-button.is--disabled:focus,.p-button.is--disabled:hover{background-color:#fff;border-color:#fff}.p-button .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--neutral{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#fff;border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--neutral{width:auto}}.p-button--neutral:visited{color:#111}.p-button--neutral:active,.p-button--neutral:focus,.p-button--neutral:hover{background-color:#f7f7f7;border-color:#cdcdcd;text-decoration:none}.p-button--neutral:disabled,.p-button--neutral.is--disabled{cursor:not-allowed;opacity:.5}.p-button--neutral:disabled:active,.p-button--neutral:disabled:focus,.p-button--neutral:disabled:hover,.p-button--neutral.is--disabled:active,.p-button--neutral.is--disabled:focus,.p-button--neutral.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}.p-button--neutral .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--brand{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#333;border-color:#333;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--brand{width:auto}}.p-button--brand:visited{color:#fff}.p-button--brand:active,.p-button--brand:focus,.p-button--brand:hover{background-color:#1a1a1a;border-color:#1a1a1a;text-decoration:none}.p-button--brand:disabled,.p-button--brand.is--disabled{cursor:not-allowed;opacity:.5}.p-button--brand:disabled:active,.p-button--brand:disabled:focus,.p-button--brand:disabled:hover,.p-button--brand.is--disabled:active,.p-button--brand.is--disabled:focus,.p-button--brand.is--disabled:hover{background-color:#333;border-color:#333}.p-button--brand .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--positive{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#0e8420;border-color:#0e8420;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--positive{width:auto}}.p-button--positive:visited{color:#fff}.p-button--positive:active,.p-button--positive:focus,.p-button--positive:hover{background-color:#095615;border-color:#095615;text-decoration:none}.p-button--positive:disabled,.p-button--positive.is--disabled{cursor:not-allowed;opacity:.5}.p-button--positive:disabled:active,.p-button--positive:disabled:focus,.p-button--positive:disabled:hover,.p-button--positive.is--disabled:active,.p-button--positive.is--disabled:focus,.p-button--positive.is--disabled:hover{background-color:#0e8420;border-color:#0e8420}.p-button--positive .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--negative{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:#c7162b;border-color:#c7162b;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#fff;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--negative{width:auto}}.p-button--negative:visited{color:#fff}.p-button--negative:active,.p-button--negative:focus,.p-button--negative:hover{background-color:#991121;border-color:#991121;text-decoration:none}.p-button--negative:disabled,.p-button--negative.is--disabled{cursor:not-allowed;opacity:.5}.p-button--negative:disabled:active,.p-button--negative:disabled:focus,.p-button--negative:disabled:hover,.p-button--negative.is--disabled:active,.p-button--negative.is--disabled:focus,.p-button--negative.is--disabled:hover{background-color:#c7162b;border-color:#c7162b}.p-button--negative .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-button--base{transition-duration:.165s;transition-property:background-color;transition-timing-function:cubic-bezier(0.55,0.055,0.675,0.19);background-color:transparent;border-color:transparent;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#111;cursor:pointer;display:inline-block;font-family:"Ubuntu",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1rem;outline:0;padding:.75rem 1.5rem;text-align:center;text-decoration:none;width:100%}@media only screen and (min-width:768px){.p-button--base{width:auto}}.p-button--base:visited{color:#111}.p-button--base:active,.p-button--base:focus,.p-button--base:hover{background-color:#f7f7f7;border-color:transparent;text-decoration:none}.p-button--base:disabled,.p-button--base.is--disabled{cursor:not-allowed;opacity:.5}.p-button--base:disabled:active,.p-button--base:disabled:focus,.p-button--base:disabled:hover,.p-button--base.is--disabled:active,.p-button--base.is--disabled:focus,.p-button--base.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}.p-button--base .p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}@media(max-width:768px){[class^="p-button"].is-inline{margin-top:1.2rem}}@media(min-width:768px){[class^="p-button"].is-inline{margin-left:.75rem;width:auto}}.p-card{background:#fff;border-radius:2px;padding:1.25rem;border:1px solid #cdcdcd}.p-card__title{margin:0 0 .5rem}.p-card__content{margin-bottom:1rem;margin-top:0}.p-card__footer{border-top:1px solid #cdcdcd;margin-top:0;padding-top:1rem}.p-card__header{border-bottom:1px solid #cdcdcd;font-size:.75rem;margin-bottom:.75rem;padding-bottom:.75rem}.p-card__header>img{max-height:2rem}.p-card .p-card{margin-top:0}.p-card--highlighted{background:#fff;border-radius:2px;padding:1.25rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2)}.p-card--highlighted__title{margin:0 0 .5rem}.p-card--highlighted__content{margin-bottom:1rem;margin-top:0}.p-card--highlighted__footer{border-top:1px solid #cdcdcd;margin-top:0;padding-top:1rem}.p-card--highlighted__header{border-bottom:1px solid #cdcdcd;font-size:.75rem;margin-bottom:.75rem;padding-bottom:.75rem}.p-card--highlighted__header>img{max-height:2rem}.p-code-numbered{background:#fff;color:#111;counter-reset:line-numbering;padding:1rem 0 0;position:relative}.p-code-numbered::before{background-color:#fff;width:4.5rem}.p-code-numbered .code-line{background:#f7f7f7;display:block;margin:-1.5rem 0 0 0;padding:.5rem 1rem 0 5.5rem;position:relative}.p-code-numbered .code-line:first-child,.p-code-numbered .code-line:first-child::before{padding-top:1.25rem}.p-code-numbered .code-line:last-child,.p-code-numbered .code-line:last-child::before{padding-bottom:1rem}.p-code-numbered .code-line::before{background:#fff;border-right:1px solid #111;color:#666;content:counter(line-numbering);counter-increment:line-numbering;display:inline-block;height:9999px;left:0;margin-right:1rem;max-height:100%;padding:.5rem 1rem 1rem 1rem;pointer-events:none;position:absolute;text-align:right;top:0;user-select:none;width:4.5rem}.p-code-snippet{background-color:#fff;border:1px solid #666;border-radius:2px;display:flex;overflow:hidden;position:relative;transition:border .2s,background-color .2s;width:100%}.p-code-snippet__input{background-color:transparent;background-image:url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.999999" viewBox="0 0 16 15.999999"><g><g style="display:inline"><g style="display:inline"><path style="opacity:0.21171169;fill:none;stroke:none" d="M-.0000032.00002047h15.9999936v15.9999936H-.0000032z"/><path style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" d="M2.6660124 2.00000047c-1.77777926 0-2.6660156.0013069-2.6660156 2.0683594v8.8652346c0 2.067046.88823634 2.066406 2.6660156 2.066406h10.6679684c1.77778 0 2.666016.00064 2.666016-2.066406v-8.7988284c0-2.1333325-.888236-2.1347656-2.666016-2.1347656H2.6660124zm1.2792969 1.890625h1.1015625v1.1425781c.3388576.0282222.6418942.0778287.9101562.1484375.2682622.0635378.4794546.127873.6347657.1914063l-.2636719 1.046875c-.2047288-.0776578-.4480911-.1520607-.7304688-.2226563-.2753242-.0705955-.5930895-.1054687-.953125-.1054687-.381213 0-.6687661.0716995-.859375.2128906-.1906042.1341333-.2851562.3205247-.2851562.5605469 0 .141191.0275088.2605439.0839844.359375.0564755.0917777.1429083.1762529.2558594.2539062.1129509.0705956.2497361.1422952.4121093.2128906.1623688.0635334.3460569.1305764.5507813.2011719.2894399.1129555.560311.232304.8144531.359375.2612043.1200089.4871256.2661159.6777344.4355469.1906043.1623688.3394192.3561248.4453125.5820312.112951.2259022.1699219.4940697.1699218.8046878 0 .465928-.1441538.868173-.4335937 1.207031s-.7660922.557414-1.4296875.65625v1.324219H3.9453093v-1.292969c-.5082842-.035289-.9225545-.102332-1.2402344-.201172-.3106176-.105893-.5419546-.200441-.6972656-.285156l.359375-1.00586c.2259066.112956.4967733.214868.8144531.306641.3247377.091773.6921094.138672 1.1015625.138672.4871065 0 .8223128-.0717 1.0058594-.212891.1906088-.148248.2871094-.342004.2871094-.582031 0-.1623686-.0395298-.3038192-.1171875-.423828-.0776533-.1200133-.186934-.2265861-.328125-.3183594-.1411911-.0917733-.3101459-.1762485-.5078125-.2539062-.1906044-.0776533-.4037544-.157472-.6367188-.2421875-.2188488-.0776533-.4374056-.1667895-.65625-.265625-.2117866-.0988311-.4055469-.218184-.5820312-.359375-.1694311-.1482489-.3062161-.3245681-.4121094-.5292969-.1058933-.2047244-.1601563-.455451-.1601563-.7519531e-7-.4871065.146107-.9013768.4355469-1.2402344.2894444-.3459154.7339269-.5671801 1.3339844-.6660156v-1.1855469zm4.0546875 8.095703h3.990234v.996094h-3.990234v-.996094z"/></g></g></g></svg>');background-position:8px center;background-repeat:no-repeat;border:0;box-shadow:inset 0 1px 2px 0 rgba(0,0,0,0.12);color:#666;font-family:"Ubuntu Mono",Consolas,Monaco,Courier,monospace;font-size:1em;font-weight:300;padding:8px 8px 8px 32px;width:100%}.p-code-snippet__action{background-color:#f7f7f7;background-image:url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 16 15.999999" width="16"><g><g><g color="%23000"><path fill="none" enable-background="accumulate" d="M.174.126h16.008v16.008H.174z"/><path fill="none" enable-background="accumulate" d="M.174.126h16.008v16.008H.174z"/><path d="M5.023 11.285L11.33 4.98" stroke="%23808080" stroke-width="1.334.194" fill="none" enable-background="accumulate"/><path style="text-decoration-color:%23000000;isolation:auto;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none" d="M12.333.126c-.11.003-.22.017-.33.038-.868.174-1.42.782-1.9 1.262L8.667 2.864c-.48.48-1.087 1.03-1.26 1.9-.075.37-.044.76.096 1.166l1.77-1.77c.103-.113.216-.23.338-.352l1.438-1.438c.48-.48.887-.83 1.217-.897.042-.008.085-.014.13-.016.047-.002.096 0 .147.005.31.037.737.248 1.397.908.88.88.96 1.343.895 1.673-.067.33-.417.738-.897 1.217L12.5 6.698c-.123.123-.24.237-.354.34l-1.768 1.77c.406.14.797.17 1.168.095.868-.173 1.42-.782 1.9-1.26l1.437-1.44c.48-.478 1.087-1.03 1.26-1.898.174-.868-.223-1.843-1.26-2.88-.778-.778-1.52-1.196-2.21-1.283-.115-.014-.228-.02-.34-.016zm-7.19 7.19c-.112.004-.222.017-.33.04-.868.172-1.42.78-1.9 1.26l-1.437 1.438c-.48.48-1.087 1.03-1.26 1.9-.175.867.222 1.84 1.26 2.88 1.037 1.036 2.012 1.433 2.88 1.26.867-.174 1.42-.782 1.898-1.262l1.44-1.438c.478-.48 1.086-1.03 1.26-1.9.074-.37.043-.76-.097-1.167L7.09 12.093c-.103.114-.217.233-.34.357l-1.44 1.438c-.48.48-.886.83-1.217.896-.33.066-.793-.016-1.673-.896s-.962-1.343-.896-1.673c.066-.33.417-.738.896-1.217L3.858 9.56c.123-.123.24-.237.354-.34l1.77-1.77c-.17-.056-.336-.097-.5-.118-.114-.014-.227-.02-.34-.016z" fill="%23808080" enable-background="accumulate"/></g></g></g></svg>');background-position:center;background-repeat:no-repeat;background-size:1rem;border-color:transparent;border-left:1px solid #666;border-radius:0;display:block;flex:1;height:100%;padding:0;position:absolute;right:0;text-indent:-9999px;top:0;width:40px}.p-code-snippet__action:hover{border-color:transparent;border-left:1px solid #666}.p-footer{border-top:1px dotted #000;font-size:.75rem;margin-bottom:1rem;padding-top:.75rem}@media(min-width:768px){.p-footer{font-size:.8125rem}}.p-footer__copy{margin-bottom:0}.p-footer__links{margin:0;padding:.5rem 0}@media(min-width:768px){.p-footer__links{margin-top:0;padding:0}}.p-footer__item{display:block;margin-bottom:.25rem}@media(min-width:768px){.p-footer__item{display:inline-block}}.p-footer__item:last-child a::after{opacity:0}.p-footer__link{border-bottom:0;color:#111;font-size:.75rem}.p-footer__link:visited{color:black}.p-footer__link:hover{color:#007aa6}@media(min-width:768px){.p-footer__link{font-size:.8125rem;margin-right:1rem}.p-footer__link::after{content:'\00b7';display:inline-block;font-size:1.5rem;left:.5rem;position:relative;top:.2rem}}.p-footer__link:hover::after{color:#111}.p-matrix{list-style:none;margin:0;padding:0}@media(min-width:620px){.p-matrix{display:flex;flex-wrap:wrap}}.p-matrix__item{border-top:1px dotted #666;margin-top:0;padding:1rem}.p-matrix__item:empty{display:none}.p-matrix__item:first-child{border-top:0}@media(min-width:620px){.p-matrix__item{border-right:1px dotted #666;border-top:1px dotted #666;margin-bottom:0;padding-right:1rem;width:calc(33.333% - .666666rem)}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(-n+3){border-top:0}.p-matrix__item:nth-child(2n){border-right:1px dotted #666;padding-right:1rem}.p-matrix__item:nth-child(3n){border-right:0;padding-right:0}}.p-matrix__img,.p-matrix__content{display:inline-block;float:left;margin-top:0}.p-matrix__img{margin-right:1rem;max-width:calc(30% - 1rem)}.p-matrix__content{max-width:70%}.p-matrix__title{margin-bottom:.5rem;margin-top:0}.p-matrix__desc{margin-top:0}.p-matrix__link{border-top:0}.p-navigation--light{background-color:#f7f7f7;color:#111;position:relative;width:100%}.p-navigation--light .row{padding:0}@media(max-width:768px){.p-navigation--light .p-navigation__banner{overflow:hidden;position:relative}}.p-navigation--light .p-navigation__toggle--open,.p-navigation--light .p-navigation__toggle--close,.p-navigation--light .p-navigation__link{color:#111}.p-navigation--light .p-navigation__toggle--open:hover,.p-navigation--light .p-navigation__toggle--close:hover,.p-navigation--light .p-navigation__link:hover{border-bottom:0;text-decoration:underline}.p-navigation--light .p-navigation__toggle--open:visited,.p-navigation--light .p-navigation__toggle--close:visited,.p-navigation--light .p-navigation__link:visited{color:#111}.p-navigation--light .p-navigation__toggle--close{display:none}.p-navigation--light .p-navigation__toggle--open,.p-navigation--light .p-navigation__toggle--close{margin:0;position:absolute;right:1rem;top:calc(50% - .75rem)}@media(min-width:769px){.p-navigation--light .p-navigation__toggle--open,.p-navigation--light .p-navigation__toggle--close{display:none}}.p-navigation--light .p-navigation__logo{font-size:1.375rem;font-weight:300;line-height:1.364;align-items:center;display:flex;float:left;margin:.75rem .5rem}@media only screen and (min-width:1030px){.p-navigation--light .p-navigation__logo{font-size:1.5rem;line-height:1.25}}@media(min-width:769px){.p-navigation--light .p-navigation__logo{margin:.5rem 1.25rem}}.p-navigation--light .p-navigation__link{border-bottom:0;display:block;margin-top:0}@media(min-width:769px){.p-navigation--light .p-navigation__link{display:block;float:left;width:auto}}.p-navigation--light .p-navigation__link>a{border-bottom:0;color:#111;font-size:.93333rem}@media(min-width:769px){.p-navigation--light .p-navigation__link>a{color:#111;font-size:.875rem}}.p-navigation--light .p-navigation__link:last-child{margin-bottom:0}.p-navigation--light .p-navigation__links{background-color:#cdcdcd;clear:both;margin:0;padding:0}@media(min-width:769px){.p-navigation--light .p-navigation__links{background-color:transparent;clear:none;float:left}}.p-navigation--light .p-navigation__links .p-navigation__link{border-left:1px solid #cdcdcd;padding:.75rem .5rem}@media(max-width:768px){.p-navigation--light .p-navigation__links .p-navigation__link{background-color:#f7f7f7;border-left:0;border-top:1px solid #cdcdcd;color:#111;text-align:left}.p-navigation--light .p-navigation__links .p-navigation__link:last-child{border-bottom:1px solid #cdcdcd}}@media(min-width:769px){.p-navigation--light .p-navigation__links .p-navigation__link{padding:.75rem 1.25rem}}@media(max-width:768px){.p-navigation--light .p-navigation__links>a{color:#111;display:block}}.p-navigation--light .p-navigation__links:last-of-type{border-right:1px solid #cdcdcd}@media(max-width:768px){.p-navigation--light .p-navigation__links:last-of-type{border-bottom:0;border-right:0}}.p-navigation--light .p-navigation__nav{display:none;margin-top:0}@media(min-width:769px){.p-navigation--light .p-navigation__nav{display:block}}.p-navigation--light:target .p-navigation__toggle--open{display:none}@media(max-width:768px){.p-navigation--light:target .p-navigation__toggle--close{display:inline-block}}.p-navigation--light:target .p-navigation__nav{display:block}.p-navigation,.p-navigation--dark{background-color:#111;color:#fff;position:relative;width:100%}.p-navigation .row,.p-navigation--dark .row{padding:0}@media(max-width:768px){.p-navigation .p-navigation__banner,.p-navigation--dark .p-navigation__banner{overflow:hidden;position:relative}}.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__toggle--close,.p-navigation .p-navigation__link,.p-navigation--dark .p-navigation__toggle--open,.p-navigation--dark .p-navigation__toggle--close,.p-navigation--dark .p-navigation__link{color:#fff}.p-navigation .p-navigation__toggle--open:hover,.p-navigation .p-navigation__toggle--close:hover,.p-navigation .p-navigation__link:hover,.p-navigation--dark .p-navigation__toggle--open:hover,.p-navigation--dark .p-navigation__toggle--close:hover,.p-navigation--dark .p-navigation__link:hover{border-bottom:0;text-decoration:underline}.p-navigation .p-navigation__toggle--open:visited,.p-navigation .p-navigation__toggle--close:visited,.p-navigation .p-navigation__link:visited,.p-navigation--dark .p-navigation__toggle--open:visited,.p-navigation--dark .p-navigation__toggle--close:visited,.p-navigation--dark .p-navigation__link:visited{color:#fff}.p-navigation .p-navigation__toggle--close,.p-navigation--dark .p-navigation__toggle--close{display:none}.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__toggle--close,.p-navigation--dark .p-navigation__toggle--open,.p-navigation--dark .p-navigation__toggle--close{margin:0;position:absolute;right:1rem;top:calc(50% - .75rem)}@media(min-width:769px){.p-navigation .p-navigation__toggle--open,.p-navigation .p-navigation__toggle--close,.p-navigation--dark .p-navigation__toggle--open,.p-navigation--dark .p-navigation__toggle--close{display:none}}.p-navigation .p-navigation__logo,.p-navigation--dark .p-navigation__logo{font-size:1.375rem;font-weight:300;line-height:1.364;align-items:center;display:flex;float:left;margin:.75rem .5rem}@media only screen and (min-width:1030px){.p-navigation .p-navigation__logo,.p-navigation--dark .p-navigation__logo{font-size:1.5rem;line-height:1.25}}@media(min-width:769px){.p-navigation .p-navigation__logo,.p-navigation--dark .p-navigation__logo{margin:.5rem 1.25rem}}.p-navigation .p-navigation__link,.p-navigation--dark .p-navigation__link{border-bottom:0;display:block;margin-top:0}@media(min-width:769px){.p-navigation .p-navigation__link,.p-navigation--dark .p-navigation__link{display:block;float:left;width:auto}}.p-navigation .p-navigation__link>a,.p-navigation--dark .p-navigation__link>a{border-bottom:0;color:#111;font-size:.93333rem}@media(min-width:769px){.p-navigation .p-navigation__link>a,.p-navigation--dark .p-navigation__link>a{color:#fff;font-size:.875rem}}.p-navigation .p-navigation__link:last-child,.p-navigation--dark .p-navigation__link:last-child{margin-bottom:0}.p-navigation .p-navigation__links,.p-navigation--dark .p-navigation__links{background-color:#cdcdcd;clear:both;margin:0;padding:0}@media(min-width:769px){.p-navigation .p-navigation__links,.p-navigation--dark .p-navigation__links{background-color:transparent;clear:none;float:left}}.p-navigation .p-navigation__links .p-navigation__link,.p-navigation--dark .p-navigation__links .p-navigation__link{border-left:1px solid #cdcdcd;padding:.75rem .5rem}@media(max-width:768px){.p-navigation .p-navigation__links .p-navigation__link,.p-navigation--dark .p-navigation__links .p-navigation__link{background-color:#f7f7f7;border-left:0;border-top:1px solid #cdcdcd;color:#111;text-align:left}.p-navigation .p-navigation__links .p-navigation__link:last-child,.p-navigation--dark .p-navigation__links .p-navigation__link:last-child{border-bottom:1px solid #cdcdcd}}@media(min-width:769px){.p-navigation .p-navigation__links .p-navigation__link,.p-navigation--dark .p-navigation__links .p-navigation__link{padding:.75rem 1.25rem}}@media(max-width:768px){.p-navigation .p-navigation__links>a,.p-navigation--dark .p-navigation__links>a{color:#111;display:block}}.p-navigation .p-navigation__links:last-of-type,.p-navigation--dark .p-navigation__links:last-of-type{border-right:1px solid #cdcdcd}@media(max-width:768px){.p-navigation .p-navigation__links:last-of-type,.p-navigation--dark .p-navigation__links:last-of-type{border-bottom:0;border-right:0}}.p-navigation .p-navigation__nav,.p-navigation--dark .p-navigation__nav{display:none;margin-top:0}@media(min-width:769px){.p-navigation .p-navigation__nav,.p-navigation--dark .p-navigation__nav{display:block}}.p-navigation:target .p-navigation__toggle--open,.p-navigation--dark:target .p-navigation__toggle--open{display:none}@media(max-width:768px){.p-navigation:target .p-navigation__toggle--close,.p-navigation--dark:target .p-navigation__toggle--close{display:inline-block}}.p-navigation:target .p-navigation__nav,.p-navigation--dark:target .p-navigation__nav{display:block}.p-link--external{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");background-position:100% top;background-repeat:no-repeat;background-size:.7rem .7rem;padding-right:.9rem}.p-link--no-underline{border:0}.p-link--soft{color:#111}.p-link--soft:visited{color:#111;text-decoration:none}.p-link--soft:hover{color:#007aa6}.p-link--soft.is-selected{font-weight:400}.p-link--strong{color:#111;font-weight:400}.p-link--strong:visited{color:#111}.p-link--strong:hover{color:#007aa6;text-decoration:underline}.p-link--strong.p-link--external:hover::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--inverted{color:#f7f7f7;font-weight:400}.p-link--inverted:hover{color:#f7f7f7}.p-link--inverted:visited{color:#dedede}.p-top{border-bottom:1px dotted #cdcdcd;clear:both;margin:20px 0}.p-top__link{background:#fff;float:right;margin-right:5px;padding:0 5px;position:relative;text-decoration:none;top:-.725rem}.p-link--external.p-link--strong{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='15'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");color:#111}.p-list{list-style:none;margin-left:0;padding-left:0}.p-list__item{margin-top:.6667rem}.p-list--divided{list-style:none;margin-left:0;padding-left:0}.p-list--divided .p-list__item{margin-top:0;padding-bottom:.63rem;padding-top:.63rem;border-bottom:1px dotted #cdcdcd}.p-list--divided .p-list__item:last-of-type,.p-list--divided .p-list__item .last-item{border-bottom:0}.is-ticked{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'%3E%3Ccircle fill='%23666' cx='7' cy='7' r='7'/%3E%3Cpath fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /%3E%3C/svg%3E");background-position:0 .25rem;background-repeat:no-repeat;padding-left:25px}.p-list--divided .is-ticked{background-position:0 1rem}.p-inline-list{margin-left:0;padding-left:0}.p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem}.p-inline-list__item:last-of-type,.p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot{margin-left:0;padding-left:0}.p-inline-list--middot .p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem;position:relative}.p-inline-list--middot .p-inline-list__item:last-of-type,.p-inline-list--middot .p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot .p-inline-list__item::after{color:#666;content:'\00b7';font-size:1.4rem;line-height:0;position:absolute;right:-1rem;top:.55rem}.p-inline-list--middot .p-inline-list__item:hover::after{color:#666}.p-inline-list--middot .p-inline-list__item:last-of-type::after,.p-inline-list--middot .p-inline-list__item .last-item::after{content:''}.p-list-step{list-style:none;margin-left:60px;padding:0}.p-list-step__title{margin-top:0;position:relative}.p-list-step__title+*{margin-top:0}.p-list-step__item{clear:both;margin-left:0;margin-top:1.5rem;width:100%}.p-list-step__item:first-child{margin-top:.75rem}@media only screen and (min-width:1030px){.p-list-step__item:first-child{margin-top:0}}.p-list-step__bullet{background-color:#666;border-radius:50%;color:#fff;display:inline-block;font-size:1.5rem;height:50px;line-height:50px;margin-bottom:.625rem;margin-left:-60px;margin-right:.34375rem;text-align:center;width:50px}@media only screen and (max-width:1030px){.p-list-step__bullet{position:absolute;top:-5px}}.p-inline-images{display:block;list-style:none;text-align:center}.p-inline-images__item{display:inline-block;margin:2rem;max-width:6rem;text-align:center;vertical-align:middle;width:100%}@media(min-width:768px){.p-inline-images__item{margin:3rem;max-width:11.25rem}}.p-inline-images__item *{width:100%}.p-inline-images__img{display:inline-block;margin:2rem;max-width:6rem;text-align:center;vertical-align:middle;width:100%}@media(min-width:768px){.p-inline-images__img{margin:3rem;max-width:11.25rem}}.p-notification{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%}.p-notification__response{background-position:0 4px;background-repeat:no-repeat;background-size:16px 16px;margin:0;text-align:left}.p-notification__status{font-weight:400;margin-right:.3125rem}.p-notification__action{border-bottom:0;margin-left:.3125rem}.p-notification--positive{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#0e8420}.p-notification--positive .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='17px' height='17px' viewBox='0 0 17 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='notification-success' transform='translate(1.000000, 1.000000)'%3E%3Cg id='Page-3---colours'%3E%3Cg id='Notifications---single'%3E%3Cg id='Group'%3E%3Cg id='ICON'%3E%3Ccircle id='circle6710' stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.2500086' cy='7.2500086' r='7.2500086'%3E%3C/circle%3E%3Cpolygon id='path6712' fill='%23fff' points='11.0502986 4.1734486 10.9843986 4.2311486 6.2496486 8.3783686 3.4740786 5.9974286 2.6350186 6.9463086 6.2503386 10.7500186 11.7500086 4.9627786 11.0502986 4.1734886'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:1.5rem}.p-notification--caution{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#f99b11}.p-notification--caution .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='17px' height='17px' viewBox='0 0 17 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='notification-caution' transform='translate(1.000000, 1.000000)'%3E%3Cg id='Page-3---colours'%3E%3Cg id='Notifications---single'%3E%3Cg id='Group'%3E%3Cg id='ICON'%3E%3Ccircle id='circle5432' stroke='%23f99b11' stroke-width='1.5' fill='%23f99b11' cx='7.2500086' cy='7.2500086' r='7.2500086'%3E%3C/circle%3E%3Cpath d='M6.2500086,3.2500086 L6.2500086,8.2500086 L8.2500086,8.2500086 L8.2500086,3.2500086 L6.2500086,3.2500086 L6.2500086,3.2500086 L6.2500086,3.2500086 Z M6.2500086,9.2500086 L6.2500086,11.2500086 L8.2500086,11.2500086 L8.2500086,9.2500086 L6.2500086,9.2500086 L6.2500086,9.2500086 L6.2500086,9.2500086 Z' id='rect5434' fill='%23fff'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:1.5rem}.p-notification--negative{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#c7162b}.p-notification--negative .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='17px' viewBox='0 0 16 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-3---colours' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='Notifications---single' transform='translate(-215.000000, -271.000000)'%3E%3Cg id='Group' transform='translate(205.000000, 254.000000)'%3E%3Cg id='ICON' transform='translate(10.000000, 17.000000)'%3E%3Crect id='rect6415' x='0' y='0.36218' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle6417' stroke='%23c7162b' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8.36218' r='7.2500086'%3E%3C/circle%3E%3Cpath d='M5.00001,5.36218 L11.00001,11.36218' id='path6479-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3Cpath d='M11.00001,5.36218 L5.00001,11.36218' id='path6481-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:1.5rem}.p-notification--information{background-color:#fff;border:0;border-color:#666;border-radius:.125rem;border-style:solid;border-top-width:3px;box-shadow:0 1px 5px 1px rgba(0,0,0,0.2);font-size:1rem;overflow:hidden;padding:.625rem;text-align:center;width:100%;border-color:#335280}.p-pull-quote{border:0;margin:2rem 0 1rem;padding-left:2rem;padding-right:1.25rem;position:relative}@media(min-width:768px){.p-pull-quote{margin:1.5rem 0 1.5rem}}.p-pull-quote>p{font-size:1.5rem;font-weight:300;line-height:1.154;color:#111;font-style:normal}@media only screen and (min-width:1030px){.p-pull-quote>p{font-size:1.75rem;line-height:1.286}}.p-pull-quote>p:first-of-type::before{color:#cdcdcd;display:inline-block;font-size:2.134rem;font-weight:bold;line-height:1rem;max-width:1.25rem;content:'\201C\2002';margin-left:-1.5rem;padding-right:1.5rem;position:relative;top:.1rem}@media(min-width:768px){.p-pull-quote>p:first-of-type::before{font-size:2.5rem}}@media(min-width:1030px){.p-pull-quote>p:first-of-type::before{font-size:3rem}}@media(min-width:768px){.p-pull-quote>p:first-of-type::before{margin-left:-1.9rem;padding-right:1.9rem;top:.4rem}}.p-pull-quote>p:last-of-type{margin-bottom:0}.p-pull-quote>p:last-of-type::after{color:#cdcdcd;display:inline-block;font-size:2.134rem;font-weight:bold;line-height:1rem;max-width:1.25rem;content:'\2002\201E';margin-left:.5rem;margin-top:-.5rem;position:absolute}@media(min-width:768px){.p-pull-quote>p:last-of-type::after{font-size:2.5rem}}@media(min-width:1030px){.p-pull-quote>p:last-of-type::after{font-size:3rem}}.p-pull-quote__citation{display:inline-block;font-size:1.25rem;font-style:italic;line-height:1.5;margin-top:.75rem;width:100%}.p-strip{background-color:transparent;clear:both;margin-top:0;padding:2rem 0;width:100%}.p-strip--light{background-color:transparent;clear:both;margin-top:0;padding:2rem 0;width:100%;background-color:#f7f7f7}.p-strip--dark{background-color:transparent;clear:both;margin-top:0;padding:2rem 0;width:100%;background-color:#111;color:#fff}.p-form-validation{color:#111;line-height:1.5;margin-top:1.25rem;position:relative}.p-form-validation .p-form-validation__input{background-position:calc(100% - 1rem) .75rem;background-repeat:no-repeat;padding:.5rem 2.5rem .5rem .75rem}.p-form-validation .p-form-validation__icon{position:relative}.p-form-validation .p-form-validation__icon::after{position:absolute;right:.75rem;top:calc(50% - $sp-x-small)}.p-form-validation__message{font-size:.875rem;margin-top:.5rem}.is-error .p-form-validation__input{background-image:url("https://assets.ubuntu.com/v1/4b0cd7fc-icon-error.svg");border-color:#c7162b}.is-success .p-form-validation__input{background-image:url("https://assets.ubuntu.com/v1/94949185-icon-success.svg");border-color:#0e8420}.is-caution .p-form-validation__input{background-image:url("https://assets.ubuntu.com/v1/db30f04c-icon-caution.svg");border-color:#f99b11}*+*{margin-top:1.25rem}@media screen and (min-width:768px){*+*{margin-top:1.75rem}}@media screen and (min-width:1030px){*+*{margin-top:2rem}}*>p:first-child{margin-top:0}*+h1,*+.p-heading--one{margin-top:2rem}@media screen and (min-width:1030px){*+h1,*+.p-heading--one{margin-top:3.75rem}}*+h2,*+.p-heading--two,*+h3,*+.p-heading--three,*+h4,*+.p-heading--four,*+h5,*+.p-heading--five,*+h6,*+.p-heading--six{margin-top:1.5rem}@media screen and (min-width:1030px){*+h2,*+.p-heading--two,*+h3,*+.p-heading--three,*+h4,*+.p-heading--four,*+h5,*+.p-heading--five,*+h6,*+.p-heading--six{margin-top:2rem}}*+h2+*,*+.p-heading--two+*,*+h3+*,*+.p-heading--three+*,*+h4+*,*+.p-heading--four+*{margin-top:.5rem}@media screen and (min-width:1030px){*+h2+*,*+.p-heading--two+*,*+h3+*,*+.p-heading--three+*,*+h4+*,*+.p-heading--four+*{margin-top:1rem}}.u-float--right{float:right !important}.u-float--left{float:left !important}.u-float-right{float:right !important}@media(max-width:620px){.u-float-right--small{float:right !important}}@media(min-width:768px) and (max-width:1030px){.u-float-right--medium{float:right !important}}@media(min-width:1030px){.u-float-right--large{float:right !important}}.u-float-left{float:left !important}@media(max-width:620px){.u-float-left--small{float:left !important}}@media(min-width:768px) and (max-width:1030px){.u-float-left--medium{float:left !important}}@media(min-width:1030px){.u-float-left--large{float:left !important}}.u-embedded-media{height:0;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative}.u-embedded-media__element{height:100%;left:0;position:absolute;top:0;width:100%}@media only screen and (min-width:768px){.u-equal-height{display:flex}}.u-align--center{justify-content:center !important;text-align:center !important}.u-align--left{justify-content:flex-start !important;text-align:left !important}.u-align--right{justify-content:flex-end !important;text-align:right !important}.u-no-margin{margin:0 !important}.u-no-margin--top{margin-top:0 !important}.u-no-margin--right{margin-right:0 !important}.u-no-margin--bottom{margin-bottom:0 !important}.u-no-margin--left{margin-left:0 !important}.u-no-padding{padding:0 !important}.u-no-padding--top{padding-top:0 !important}.u-no-padding--right{padding-right:0 !important}.u-no-padding--bottom{padding-bottom:0 !important}.u-no-padding--left{padding-left:0 !important}.u-hide{display:none !important}@media screen and (max-width:768px){.u-hide--small{display:none !important}}@media(min-width:768px) and (max-width:1030px){.u-hide--medium{display:none !important}}@media screen and (min-width:1030px){.u-hide--large{display:none !important}}.u-show{display:block !important}@media screen and (max-width:768px){.u-show--small{display:block !important}}@media(min-width:768px) and (max-width:1030px){.u-show--medium{display:block !important}}@media screen and (min-width:1030px){.u-show--large{display:block !important}}.u-off-screen{height:1px !important;left:-10000px !important;overflow:hidden !important;position:absolute !important;top:auto !important;width:1px !important}@media(min-width:768px){.u-vertically-center{align-items:center !important;display:flex !important}}.u-hidden{display:none !important}@media screen and (max-width:768px){.u-hidden--small{display:none !important}}@media(min-width:768px) and (max-width:1030px){.u-hidden--medium{display:none !important}}@media screen and (min-width:1030px){.u-hidden--large{display:none !important}}.u-visible{display:block !important}@media screen and (max-width:768px){.u-visible--small{display:block !important}}@media(min-width:768px) and (max-width:1030px){.u-visible--medium{display:block !important}}@media screen and (min-width:1030px){.u-visible--large{display:block !important}}.collapse{cursor:pointer;display:block}.collapse+input{display:none}.collapse+input+div{display:none}.collapse+input:checked+div{display:block}.cheshire{display:none}.required label:after{color:#e32;content:' *';display:inline} |
785 | \ No newline at end of file |
786 | diff --git a/static/sass/application.scss b/static/sass/application.scss |
787 | index fc671d2..7a3bd95 100644 |
788 | --- a/static/sass/application.scss |
789 | +++ b/static/sass/application.scss |
790 | @@ -1,7 +1,4 @@ |
791 | -// Import the theme |
792 | -@import '../../node_modules/ubuntu-vanilla-theme/scss/theme'; |
793 | -// Run the theme |
794 | -@include ubuntu-vanilla-theme; |
795 | +@import 'node_modules/vanilla-framework/scss/build'; |
796 | |
797 | // Customise theme |
798 | .collapse{ |
799 | @@ -21,4 +18,11 @@ |
800 | |
801 | .cheshire { |
802 | display: none; |
803 | -} |
804 | \ No newline at end of file |
805 | +} |
806 | + |
807 | +.required label:after { |
808 | + color: #e32; |
809 | + content: ' *'; |
810 | + display:inline; |
811 | +} |
812 | + |
813 | diff --git a/static/templates/connecting.html b/static/templates/connecting.html |
814 | index d21a0c1..e0f8fb3 100644 |
815 | --- a/static/templates/connecting.html |
816 | +++ b/static/templates/connecting.html |
817 | @@ -12,11 +12,14 @@ |
818 | <body> |
819 | <div class="wrapper"> |
820 | <div id="main-content" class="inner-wrapper"> |
821 | + <br/> |
822 | <div class="row no-border" id="grid"> |
823 | - <h3>Connecting...</h3> |
824 | - <div class="twelve-col box" style="background-color: #eee"> |
825 | - <p>Device is now attempting to connect to <b>{{.Ssid}}</b>. <br/> |
826 | + <div class="p-notification" id="grid"> |
827 | + <p class="p-notification__response"> |
828 | + <span class="p-notification__status">Connecting...</span> |
829 | + Device is now attempting to connect to <b>{{.Ssid}}</b>. <br/> |
830 | You may try to connect to the device through the new connection.</p> |
831 | + </p> |
832 | </div> |
833 | </div> |
834 | </div> |
835 | diff --git a/static/templates/management.html b/static/templates/management.html |
836 | index 6a95224..ed554fb 100644 |
837 | --- a/static/templates/management.html |
838 | +++ b/static/templates/management.html |
839 | @@ -12,73 +12,185 @@ |
840 | <body> |
841 | <div class="wrapper"> |
842 | <div id="main-content" class="inner-wrapper"> |
843 | + <br/> |
844 | <div class="row no-border" id="login"> |
845 | - <ul class="no-bullets"> |
846 | - <li><p>Password:</p></li> |
847 | - <li><input type="password" id="passphrasePage"/></li> |
848 | - <li> |
849 | + <fieldset> |
850 | + <div class="p-form-validation" id="passphrasePage_form"> |
851 | + <label for="passphrasePage">Password:</label> |
852 | + <input class="p-form-validation__input" type="password" id="passphrasePage"/> |
853 | <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/> |
854 | <label for="showpassphrasePage">show passphrase</label> |
855 | - </li> |
856 | - <li><input type="button" value="Continue" onclick="authenticate()"/></li> |
857 | - </ul> |
858 | + </div> |
859 | + <div class="p-notification--negative cheshire" id="passphrasePage_error"> |
860 | + <p class="p-notification__response"> |
861 | + <span class="p-notification__status">Error:</span> <text id="passphrasePage_error_msg"></text> |
862 | + </p> |
863 | + </div> |
864 | + <button class="p-button--positive" onclick="authenticate()"/>Continue</button> |
865 | + </fieldset> |
866 | </div> |
867 | |
868 | - <div class="row no-border" id="grid"> |
869 | - <h2>Select Wi-Fi to connect to:</h2> |
870 | - <fieldset> |
871 | - <br/> |
872 | - <table> |
873 | + {{if eq .Page "ssids"}} |
874 | + <div class="row no-border" id="grid"> |
875 | + <h2>Select Wi-Fi to connect to:</h2> |
876 | + <ul class="p-list--divided"> |
877 | {{range $i, $ssid := .Ssids}} |
878 | - <tr> |
879 | - <th scope="row"> |
880 | - <label class="collapse" for="radio{{$i}}">{{$ssid}}</label> |
881 | - <input id="radio{{$i}}" type="radio" name="c1"> |
882 | - <div class="six-col"> |
883 | - <fieldset> |
884 | - <ul class="no-bullets"> |
885 | - <li> |
886 | - <input type="hidden" id="ssid{{$i}}" value="{{$ssid}}"/> |
887 | - <label for="passphrase">Enter passphrase:</label> |
888 | - <input type="password" id="passphrase{{$i}}"/> |
889 | - </li> |
890 | - <li> |
891 | - <input type="checkbox" id="showpassphrase{{$i}}" onchange="show_passphrase({{$i}})"/> |
892 | - <label for="showpassphrase">show passphrase</label> |
893 | - </li> |
894 | - <li> |
895 | - <div id="alert{{$i}}" class="cheshire box" style="background-color: #eee"> |
896 | - <h3>Caution!</h3> |
897 | - <p>Continuing will disconnect you from the device by taking down its Wi-Fi network. |
898 | - This page will be unavailable. After continuing, you may connect to the device |
899 | - through its new Wi-Fi connection. Proceed?.</p> |
900 | - </div> |
901 | - </li> |
902 | - <li> |
903 | - <input type="button" value="Cancel" class="button--primary" onclick="clear_row({{$i}})"/> |
904 | - <input type="button" id="connect{{$i}}" value="Connect" class="button--primary" onclick="do_connect({{$i}})"/> |
905 | - </li> |
906 | - </ul> |
907 | - </fieldset> |
908 | + <li class="p-list__item"> |
909 | + <label class="collapse" for="radio{{$i}}">{{$ssid}}</label> |
910 | + <input id="radio{{$i}}" type="radio" name="c1"> |
911 | + <div> |
912 | + <br/> |
913 | + <fieldset> |
914 | + <input type="hidden" id="ssid{{$i}}" value="{{$ssid}}"/> |
915 | + <label for="passphrase">Enter passphrase:</label> |
916 | + <input type="password" id="passphrase{{$i}}"/> |
917 | + <input type="checkbox" id="showpassphrase{{$i}}" onchange="show_passphrase({{$i}})"/> |
918 | + <label for="showpassphrase">show passphrase</label> |
919 | + <br/> |
920 | + <div id="alert{{$i}}" class="p-notification--caution cheshire"> |
921 | + <p class="p-notification__response"> |
922 | + <span class="p-notification__status">Warning:</span> |
923 | + Continuing will disconnect you from the device by taking down its Wi-Fi network. |
924 | + This page will be unavailable. After continuing, you may connect to the device |
925 | + through its new Wi-Fi connection. Proceed?. |
926 | + </p> |
927 | </div> |
928 | - </th> |
929 | - </tr> |
930 | + <button class="p-button--neutral" onclick="clear_row({{$i}})">Cancel</button> |
931 | + <input type="button" class="p-button--positive" id="connect{{$i}}" value="Connect" onclick="do_connect({{$i}})"/> |
932 | + </fieldset> |
933 | + </div> |
934 | + </li> |
935 | {{end}} |
936 | - </table> |
937 | |
938 | - <div class="box" style="background-color: #eee"> |
939 | - <h3>Warning</h3> |
940 | - <p>This takes down the device AP and disconnects you. Please reconnect in a minute or two</p> |
941 | - <input type="button" value="Refresh Wi-Fi Access Points" class="button--primary" onclick="do_refresh()"/> |
942 | + </ul> |
943 | + |
944 | + <div class="p-notification--caution"> |
945 | + <p class="p-notification__response"> |
946 | + <span class="p-notification__status">Warning:</span> |
947 | + This takes down the device AP and disconnects you. Please reconnect in a minute or two |
948 | + </p> |
949 | + <br/> |
950 | + <button class="p-button--neutral" onclick="do_refresh()">Refresh Wi-Fi Access Points</button> |
951 | </div> |
952 | + </div> |
953 | + |
954 | + {{else if eq .Page "config"}} |
955 | + <div class="row no-border" id="grid"> |
956 | + <h2>First configuration</h2> |
957 | + <br/> |
958 | + <fieldset> |
959 | + <h4>Wi-Fi Access Point</h4> |
960 | + <div class="p-form-validation" id="ssid_form"> |
961 | + <label for="ssid">SSID:</label> |
962 | + <input class="p-form-validation__input" type="text" id="ssid" required="required" value="{{.Config.Wifi.Ssid}}" placeholder="Enter SSID"/> |
963 | + </div> |
964 | + <div class="p-notification--negative cheshire" id="ssid_error"> |
965 | + <p class="p-notification__response"> |
966 | + <span class="p-notification__status">Error:</span> <text id="ssid_error_msg"></text> |
967 | + </p> |
968 | + </div> |
969 | + <div class="p-form-validation required" id="passphrase_form"> |
970 | + <label for="passphrase">Passphrase:</label> |
971 | + <input class="p-form-validation__input" type="password" id="passphrase" required="required" onblur="validate_password(this.id,'Passphrase',8,63)" value="{{.Config.Wifi.Passphrase}}" placeholder="Enter passphrase"/> |
972 | + <label for="passphrase_confirm">Confirm passphrase:</label> |
973 | + <input class="p-form-validation__input" type="password" id="passphrase_confirm" required="required" onblur="validate_password('passphrase','Passphrase',8,63)" placeholder="Confirm passphrase"/> |
974 | + </div> |
975 | + <div class="p-notification--negative cheshire" id="passphrase_error"> |
976 | + <p class="p-notification__response"> |
977 | + <span class="p-notification__status">Error:</span> <text id="passphrase_error_msg"></text> |
978 | + </p> |
979 | + </div> |
980 | + <div class="p-form-validation" id="interface_form"> |
981 | + <label for="interface">Interface:</label> |
982 | + <input type="text" id="interface" required="required" value="{{.Config.Wifi.Interface}}" placeholder="Enter network interface"/> |
983 | + </div> |
984 | + <div class="p-notification--negative cheshire" id="interface_error"> |
985 | + <p class="p-notification__response"> |
986 | + <span class="p-notification__status">Error:</span> <text id="interface_error_msg"></text> |
987 | + </p> |
988 | + </div> |
989 | + <label for="country_code">Country Code:</label> |
990 | + <select id="country_code"> |
991 | + [COUNTRY_CODE_OPTIONS] |
992 | + </select> |
993 | + <label for="channel">Channel:</label> |
994 | + <select id="channel"> |
995 | + <option value="1">1</option> |
996 | + <option value="2">2</option> |
997 | + <option value="3">3</option> |
998 | + <option value="4">4</option> |
999 | + <option value="5">5</option> |
1000 | + <option value="6">6</option> |
1001 | + <option value="7">7</option> |
1002 | + <option value="8">8</option> |
1003 | + <option value="9">9</option> |
1004 | + <option value="10">10</option> |
1005 | + <option value="11">11</option> |
1006 | + <option value="12">12</option> |
1007 | + <option value="13">13</option> |
1008 | + <option value="14">14</option> |
1009 | + </select> |
1010 | + <label for="operation_mode">Operation mode:</label> |
1011 | + <select id="operation_mode"> |
1012 | + <option value="a">IEEE 802.11a (5 GHz)</option> |
1013 | + <option value="b">IEEE 802.11b (2.4 GHz)</option> |
1014 | + <option value="g">IEEE 802.11g (2.4 GHz)</option> |
1015 | + <option value="ad">IEEE 802.11ad (60 GHz)</option> |
1016 | + </select> |
1017 | + </fieldset> |
1018 | |
1019 | + <br/> |
1020 | + |
1021 | + <fieldset> |
1022 | + <h4>Web Portal</h4> |
1023 | + <div class="p-form-validation required" id="portal_pwd_form"> |
1024 | + <label for="portal_pwd">Portal password:</label> |
1025 | + <input class="p-form-validation__input" type="password" id="portal_pwd" required="required" value="{{.Config.Portal.Password}}" onblur="validate_password(this.id,'Portal password',8)"/> |
1026 | + <label for="portal_pwd_confirm">Confirm portal password:</label> |
1027 | + <input class="p-form-validation__input" type="password" id="portal_pwd_confirm" required="required" onblur="validate_password('portal_pwd','Portal password',8)"/> |
1028 | + </div> |
1029 | + <div class="p-notification--negative cheshire" id="portal_pwd_error"> |
1030 | + <p class="p-notification__response"> |
1031 | + <span class="p-notification__status">Error:</span> <text id="portal_pwd_error_msg"></text> |
1032 | + </p> |
1033 | + </div> |
1034 | </fieldset> |
1035 | + |
1036 | + <br/> |
1037 | + |
1038 | + <div class="p-notification--caution"> |
1039 | + <p class="p-notification__response"> |
1040 | + <span class="p-notification__status">Warning:</span> |
1041 | + This may take down the device Wi-Fi AP and disconnect you. Please reconnect in a minute or two |
1042 | + </p> |
1043 | + <br/> |
1044 | + <button class="p-button--positive" onclick="saveConfig()">Apply configuration</button> |
1045 | + </div> |
1046 | + <div class="p-notification--negative cheshire" id="saveConfig_error"> |
1047 | + <p class="p-notification__response"> |
1048 | + <span class="p-notification__status">Error:</span><text id="saveConfig_error_msg">Could not save configuration. Server error.</text> |
1049 | + </p> |
1050 | + </div> |
1051 | </div> |
1052 | - |
1053 | - <div class="row no-border" id="refreshingAlert"> |
1054 | - <h3>Refreshing Wi-Fi access points...</h3> |
1055 | - <div class="twelve-col box" style="background-color: #eee"> |
1056 | - <p>You have been disconnected. The device is refreshing Wi-Fi access points. Please rejoin the device access point and reload this page in a minute or two.</p> |
1057 | + {{end}} |
1058 | + |
1059 | + <div class="row no-border"> |
1060 | + <div class="p-notification cheshire" id="refreshingAlert"> |
1061 | + <p class="p-notification__response"> |
1062 | + <span class="p-notification__status">Refreshing Wi-Fi access points...</span> |
1063 | + You have been disconnected. The device is refreshing Wi-Fi access points. |
1064 | + Please rejoin the device access point and reload this page in a minute or two. |
1065 | + </p> |
1066 | + </div> |
1067 | + </div> |
1068 | + |
1069 | + <div class="row no-border"> |
1070 | + <div class="p-notification cheshire" id="savingConfigAlert"> |
1071 | + <p class="p-notification__response"> |
1072 | + <span class="p-notification__status">Saving configuration...</span> |
1073 | + You have been disconnected. The device is saving configuration. |
1074 | + This takes down the device AP and disconnects you. Please reconnect in a minute or two |
1075 | + </p> |
1076 | </div> |
1077 | </div> |
1078 | |
1079 | @@ -95,7 +207,37 @@ |
1080 | $(document).ready(function() { |
1081 | $('#grid').css('display', 'none') |
1082 | $('#refreshingAlert').css('display', 'none') |
1083 | + $('#savingConfigAlert').css('display', 'none') |
1084 | + |
1085 | + // enable submit password when enter is pressed for login div |
1086 | + $('#passphrasePage').keydown(function(event) { |
1087 | + if (event.keyCode == 13) { |
1088 | + authenticate() |
1089 | + return false; |
1090 | + } |
1091 | + }); |
1092 | + |
1093 | + {{if eq .Page "config"}} |
1094 | + // config specific initial values for combo boxes |
1095 | + $('#channel').val('{{.Config.Wifi.Channel}}') |
1096 | + $('#country_code').val('{{.Config.Wifi.CountryCode}}') |
1097 | + $('#operation_mode').val('{{.Config.Wifi.OperationMode}}') |
1098 | + {{end}} |
1099 | + |
1100 | + // country codes select must be sorted as originally it is, but by value, not by text |
1101 | + sortCountryCodes(); |
1102 | }) |
1103 | + function sortCountryCodes() { |
1104 | + var cc_options = $("#country_code option"); |
1105 | + var selected = $("#country_code").val(); |
1106 | + |
1107 | + cc_options.sort(function(a,b) { |
1108 | + return a.text.localeCompare(b.text) |
1109 | + }) |
1110 | + |
1111 | + $("#country_code").empty().append(cc_options); |
1112 | + $("#country_code").val(selected); |
1113 | + } |
1114 | function showSsids() { |
1115 | $('#login').css('display', 'none'); |
1116 | $('#grid').css('display', 'block'); |
1117 | @@ -105,26 +247,113 @@ |
1118 | $('#refreshingAlert').css('display', 'block'); |
1119 | } |
1120 | function authenticate() { |
1121 | - var pw = $('#passphrasePage').val(); |
1122 | - if ( pw.length < 8) { |
1123 | - alert("Password must be at least 8 characters long"); |
1124 | - } else { |
1125 | - $.ajax({ |
1126 | - type: "POST", |
1127 | - url: "/hashit", |
1128 | - data: {Hash: pw} |
1129 | - }).done(function (hashRet) { |
1130 | - console.log("in ajax done.", hashRet); |
1131 | - hash = JSON.parse(hashRet); |
1132 | - console.log(hash) |
1133 | - if (hash.HashMatch) { |
1134 | - showSsids(); |
1135 | - } else { |
1136 | - alert("Your password does not match, please try again"); |
1137 | - } |
1138 | - }) |
1139 | - } |
1140 | + $.ajax({ |
1141 | + type: "POST", |
1142 | + url: "/hashit", |
1143 | + data: {Hash: $('#passphrasePage').val()} |
1144 | + }).done(function (hashRet) { |
1145 | + console.log("in ajax done.", hashRet); |
1146 | + hash = JSON.parse(hashRet); |
1147 | + console.log(hash) |
1148 | + if (hash.HashMatch) { |
1149 | + showSsids(); |
1150 | + } else { |
1151 | + $('#passphrasePage_form').addClass('is-error') |
1152 | + $('#passphrasePage_error_msg').text('Your password does not match, please try again') |
1153 | + $('#passphrasePage_error').css('display', 'block') |
1154 | + } |
1155 | + }) |
1156 | } |
1157 | + function saveConfig() { |
1158 | + if ($('#ssid').val().length == 0) { |
1159 | + $('#ssid_form').addClass('is-error') |
1160 | + $('#ssid_error_msg').text('SSID cannot be empty') |
1161 | + $('#ssid_error').css('display', 'block') |
1162 | + $('#saveConfig_error').css('display', 'block') |
1163 | + $('#saveConfig_error_msg').text('Please, correct SSID field error before applying configuration') |
1164 | + return |
1165 | + } else { |
1166 | + $('#ssid_form').removeClass('is-error') |
1167 | + $('#ssid_error').css('display', 'none') |
1168 | + } |
1169 | + |
1170 | + if (!validate_password('passphrase', 'Passphrase', 8, 63)) { |
1171 | + $('#saveConfig_error').css('display', 'block') |
1172 | + $('#saveConfig_error_msg').text('Please, correct Wi-Fi passphrase error before applying configuration') |
1173 | + return |
1174 | + } |
1175 | + |
1176 | + if ($('#interface').val().length == 0) { |
1177 | + $('#interface_form').addClass('is-error') |
1178 | + $('#interface_error_msg').text('Interface cannot be empty') |
1179 | + $('#interface_error').css('display', 'block') |
1180 | + $('#saveConfig_error').css('display', 'block') |
1181 | + $('#saveConfig_error_msg').text('Please, correct Wi-Fi interface error before applying configuration') |
1182 | + return |
1183 | + } else { |
1184 | + $('#interface_form').removeClass('is-error') |
1185 | + $('#interface_error').css('display', 'none') |
1186 | + } |
1187 | + |
1188 | + if (!validate_password('portal_pwd', 'Portal password', 8)) { |
1189 | + $('#saveConfig_error').css('display', 'block') |
1190 | + $('#saveConfig_error_msg').text('Please, correct Portal password error before applying configuration') |
1191 | + return |
1192 | + } |
1193 | + $.ajax({ |
1194 | + type: "POST", |
1195 | + url: "/config", |
1196 | + data: { |
1197 | + Ssid: $('#ssid').val(), |
1198 | + Passphrase: $('#passphrase').val(), |
1199 | + Interface:$('#interface').val(), |
1200 | + CountryCode: $('#country_code').val(), |
1201 | + Channel: $('#channel').val(), |
1202 | + OperationMode: $('#operation_mode').val(), |
1203 | + PortalPassword: $('#portal_pwd').val() |
1204 | + } |
1205 | + }).done(function (response) { |
1206 | + $('#grid').css('display', 'none'); |
1207 | + $('#saveConfig_error').css('display', 'none') |
1208 | + $('#savingConfigAlert').css('display', 'block') |
1209 | + }).fail(function(xhr, status, error) { |
1210 | + $('#saveConfig_error').css('display', 'block') |
1211 | + $('#saveConfig_error_msg').text(xhr.responseText) |
1212 | + }); |
1213 | + } |
1214 | + // function to confirm password, and confirmation are equals. |
1215 | + // tag param is the name of primary input field for password. |
1216 | + // title human readable name of the field validating here |
1217 | + // n param is the min length to consider |
1218 | + // m param is the max length allowed |
1219 | + function validate_password(tag, title, n, m) { |
1220 | + var pwd = document.getElementById(tag) |
1221 | + var pwd_confirm = document.getElementById(tag + '_confirm') |
1222 | + //validate length |
1223 | + if (n !== undefined && pwd.value.length < n) { |
1224 | + $('#' + tag + "_form").addClass('is-error') |
1225 | + $('#' + tag + '_error_msg').text( title + ' is too short. Must have ' + n + ' characters at least') |
1226 | + $('#' + tag + '_error').css('display', 'block') |
1227 | + return false |
1228 | + } |
1229 | + if (m !== undefined && pwd.value.length > m) { |
1230 | + $('#' + tag + "_form").addClass('is-error') |
1231 | + $('#' + tag + '_error_msg').text( title + ' is too long. Must have ' + m + ' characters at most') |
1232 | + $('#' + tag + '_error').css('display', 'block') |
1233 | + return false |
1234 | + } |
1235 | + //validate confirm password field matches original |
1236 | + if (pwd.value != pwd_confirm.value) { |
1237 | + $('#' + tag + "_form").addClass('is-error') |
1238 | + $('#' + tag + '_error_msg').text('Passwords don\'t match') |
1239 | + $('#' + tag + '_error').css('display', 'block') |
1240 | + return false |
1241 | + } |
1242 | + // default |
1243 | + $('#' + tag + "_form").removeClass('is-error') |
1244 | + $('#' + tag + '_error').css('display', 'none') |
1245 | + return true |
1246 | + } |
1247 | </script> |
1248 | |
1249 | |
1250 | diff --git a/static/templates/operational.html b/static/templates/operational.html |
1251 | index 1d859c5..27d4f94 100644 |
1252 | --- a/static/templates/operational.html |
1253 | +++ b/static/templates/operational.html |
1254 | @@ -11,30 +11,52 @@ |
1255 | <body> |
1256 | <div class="wrapper"> |
1257 | <div id="main-content" class="inner-wrapper"> |
1258 | - <div class="row no-border" id="login"> |
1259 | - <ul class="no-bullets"> |
1260 | - <li><p>Password:</p></li> |
1261 | - <li><input type="password" id="passphrasePage"/></li> |
1262 | - <li> |
1263 | - <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/> |
1264 | - <label for="showpassphrasePage">show passphrase</label> |
1265 | - </li> |
1266 | - <li><input type="button" value="Continue" onclick="authenticate()"/></li> |
1267 | - </ul> |
1268 | - </div> |
1269 | - <div class="row no-border" id="grid"> |
1270 | - <h2>Connected!</h2> |
1271 | - <p>The device is connected to an external WiFi AP</p> |
1272 | - <p>Click below to disconnect. Then, join the device Wifi AP, where you can select a new external AP to connect to.</p> |
1273 | - <input type="button" id="disconnect" value="Disconnect from Wifi" class="button--primary" onclick="disconnect()"/> |
1274 | - </div> |
1275 | - </div> |
1276 | + <br/> |
1277 | + <div class="row no-border" id="login"> |
1278 | + <fieldset> |
1279 | + <div class="p-form-validation" id="passphrasePage_form"> |
1280 | + <label for="passphrasePage">Password:</label> |
1281 | + <input class="p-form-validation__input" type="password" id="passphrasePage"/> |
1282 | + <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/> |
1283 | + <label for="showpassphrasePage">show passphrase</label> |
1284 | + </div> |
1285 | + <div class="p-notification--negative cheshire" id="passphrasePage_error"> |
1286 | + <p class="p-notification__response"> |
1287 | + <span class="p-notification__status">Error:</span> <text id="passphrasePage_error_msg"></text> |
1288 | + </p> |
1289 | + </div> |
1290 | + <button class="p-button--positive" onclick="authenticate()"/>Continue</button> |
1291 | + </fieldset> |
1292 | + </div> |
1293 | + |
1294 | + <div class="row no-border"> |
1295 | + <div class="p-notification--positive" id="grid"> |
1296 | + <p class="p-notification__response"> |
1297 | + <span class="p-notification__status">Connected!</span> |
1298 | + The device is connected to an external WiFi AP.<br/> |
1299 | + Click below to disconnect. Then, join the device Wifi AP, |
1300 | + where you can select a new external AP to connect to. |
1301 | + </p> |
1302 | + <br/> |
1303 | + <button class="p-button--negative" id="disconnect" onclick="disconnect()">Disconnect from Wi-Fi</button> |
1304 | + </div> |
1305 | + </div> |
1306 | + |
1307 | </div> |
1308 | |
1309 | <script> |
1310 | - $(document).ready(function(){ |
1311 | - $('#grid').css('display', 'none') |
1312 | + $(document).ready(function(){ |
1313 | + $('#grid').css('display', 'none') |
1314 | + |
1315 | + // enable submit password when enter is pressed for login div |
1316 | + $('#passphrasePage').keydown(function(event) { |
1317 | + if (event.keyCode == 13) { |
1318 | + authenticate() |
1319 | + return false; |
1320 | + } |
1321 | + }); |
1322 | }) |
1323 | + |
1324 | function showOper() { |
1325 | $('#login').css('display', 'none'); |
1326 | $('#grid').css('display', 'block'); |
1327 | @@ -45,37 +67,33 @@ |
1328 | document.getElementById('passphrase'+i).type = type |
1329 | } |
1330 | |
1331 | - function authenticate() { |
1332 | - var pw = $('#passphrasePage').val(); |
1333 | - if ( pw.length < 8) { |
1334 | - alert("Password must be at least 8 characters long"); |
1335 | - } else { |
1336 | - $.ajax({ |
1337 | - type: "POST", |
1338 | - url: "/hashit", |
1339 | - data: {Hash: pw} |
1340 | - }).done(function (hashRet) { |
1341 | - console.log("in ajax done.", hashRet); |
1342 | - hash = JSON.parse(hashRet); |
1343 | - console.log(hash) |
1344 | - if (hash.HashMatch) { |
1345 | - showOper(); |
1346 | - } else { |
1347 | - alert("Your password does not match, please try again"); |
1348 | - } |
1349 | - }) |
1350 | + function authenticate() { |
1351 | + $.ajax({ |
1352 | + type: "POST", |
1353 | + url: "/hashit", |
1354 | + data: {Hash: $('#passphrasePage').val()} |
1355 | + }).done(function (hashRet) { |
1356 | + console.log("in ajax done.", hashRet); |
1357 | + hash = JSON.parse(hashRet); |
1358 | + console.log(hash) |
1359 | + if (hash.HashMatch) { |
1360 | + showOper(); |
1361 | + } else { |
1362 | + $('#passphrasePage_form').addClass('is-error') |
1363 | + $('#passphrasePage_error_msg').text('Your password does not match, please try again') |
1364 | + $('#passphrasePage_error').css('display', 'block') |
1365 | } |
1366 | + }) |
1367 | } |
1368 | |
1369 | -function disconnect() { |
1370 | - $.ajax({ |
1371 | - url: "/disconnect" |
1372 | - }).done(function () { |
1373 | - console.log("in ajax done."); |
1374 | - |
1375 | - }) |
1376 | -} |
1377 | + function disconnect() { |
1378 | + $.ajax({ |
1379 | + url: "/disconnect" |
1380 | + }).done(function () { |
1381 | + console.log("in ajax done."); |
1382 | |
1383 | + }) |
1384 | + } |
1385 | </script> |
1386 | |
1387 | </body> |
1388 | diff --git a/utils/config.go b/utils/config.go |
1389 | new file mode 100644 |
1390 | index 0000000..6aac0f3 |
1391 | --- /dev/null |
1392 | +++ b/utils/config.go |
1393 | @@ -0,0 +1,235 @@ |
1394 | +package utils |
1395 | + |
1396 | +import ( |
1397 | + "encoding/json" |
1398 | + "fmt" |
1399 | + "io/ioutil" |
1400 | + "log" |
1401 | + "os" |
1402 | + "path/filepath" |
1403 | + "strconv" |
1404 | + "strings" |
1405 | + |
1406 | + "launchpad.net/wifi-connect/wifiap" |
1407 | +) |
1408 | + |
1409 | +var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json") |
1410 | +var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".config_done.flag") |
1411 | + |
1412 | +var wifiapClient wifiap.Operations = wifiap.DefaultClient() |
1413 | + |
1414 | +// Config this project config got from wifi-ap + custom wifi-connect params |
1415 | +type Config struct { |
1416 | + Wifi *WifiConfig |
1417 | + Portal *PortalConfig |
1418 | +} |
1419 | + |
1420 | +// WifiConfig config specific parameters for wifi configuration |
1421 | +type WifiConfig struct { |
1422 | + Ssid string `json:"wifi.ssid"` |
1423 | + Passphrase string `json:"wifi.security-passphrase"` |
1424 | + Interface string `json:"wifi.interface"` |
1425 | + CountryCode string `json:"wifi.country-code"` |
1426 | + Channel int `json:"wifi.channel"` |
1427 | + OperationMode string `json:"wifi.operation-mode"` |
1428 | +} |
1429 | + |
1430 | +// PortalConfig config specific parameters for portals configuration |
1431 | +type PortalConfig struct { |
1432 | + Password string //`json:"portal.password"` |
1433 | + NoResetCredentials bool `json:"portal.no-reset-creds"` |
1434 | + NoOperational bool `json:"portal.no-operational"` |
1435 | +} |
1436 | + |
1437 | +func (c *Config) String() string { |
1438 | + s := []string{ |
1439 | + strings.Join([]string{"--Wifi--", c.Wifi.String()}, "\n"), |
1440 | + strings.Join([]string{"--Portal--", c.Portal.String()}, "\n"), |
1441 | + } |
1442 | + return fmt.Sprintf(strings.Join(s, "\n")) |
1443 | +} |
1444 | + |
1445 | +func (c *PortalConfig) String() string { |
1446 | + s := []string{ |
1447 | + strings.Join([]string{"Password: ", c.Password}, " "), |
1448 | + strings.Join([]string{"NoResetCredentials:", strconv.FormatBool(c.NoResetCredentials)}, " "), |
1449 | + strings.Join([]string{"NoOperational:", strconv.FormatBool(c.NoOperational)}, " "), |
1450 | + } |
1451 | + return fmt.Sprintf(strings.Join(s, "\n")) |
1452 | +} |
1453 | + |
1454 | +func (c *WifiConfig) String() string { |
1455 | + s := []string{ |
1456 | + strings.Join([]string{"Ssid: ", c.Ssid}, " "), |
1457 | + strings.Join([]string{"Passphrase:", c.Passphrase}, " "), |
1458 | + strings.Join([]string{"Interface:", c.Interface}, " "), |
1459 | + strings.Join([]string{"CountryCode:", c.CountryCode}, " "), |
1460 | + strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "), |
1461 | + strings.Join([]string{"OperationMode:", c.OperationMode}, " "), |
1462 | + } |
1463 | + return fmt.Sprintf(strings.Join(s, "\n")) |
1464 | +} |
1465 | + |
1466 | +func defaultPortalConfig() *PortalConfig { |
1467 | + return &PortalConfig{ |
1468 | + Password: "", |
1469 | + NoResetCredentials: false, |
1470 | + NoOperational: false, |
1471 | + } |
1472 | +} |
1473 | + |
1474 | +// config currently stored in local json file is completely storable in PortalConfig |
1475 | +// If needed to scale, we could rewrite this method to support a more generic type |
1476 | +func readLocalConfig() (*PortalConfig, error) { |
1477 | + if _, err := os.Stat(configFile); os.IsNotExist(err) { |
1478 | + log.Printf("Warn: not found local config file at %v\n", configFile) |
1479 | + // in case there is no local config file, return a null pointer |
1480 | + return nil, nil |
1481 | + } |
1482 | + |
1483 | + fileContents, err := ioutil.ReadFile(configFile) |
1484 | + if err != nil { |
1485 | + return nil, fmt.Errorf("Error reading json config file: %v", err) |
1486 | + } |
1487 | + |
1488 | + // parameters not available in config file will be se to default value |
1489 | + portalConfig := defaultPortalConfig() |
1490 | + err = json.Unmarshal(fileContents, portalConfig) |
1491 | + if err != nil { |
1492 | + return nil, fmt.Errorf("Error unmarshalling json config file contents: %v", err) |
1493 | + } |
1494 | + |
1495 | + return portalConfig, nil |
1496 | +} |
1497 | + |
1498 | +func writeLocalConfig(p *PortalConfig) error { |
1499 | + // the only writable local config param is the password, stored as a hash |
1500 | + _, err := HashIt(p.Password) |
1501 | + if err != nil { |
1502 | + return fmt.Errorf("Could not hash portal password to file: %v", err) |
1503 | + } |
1504 | + |
1505 | + return nil |
1506 | +} |
1507 | + |
1508 | +func readRemoteParam(m map[string]interface{}, key string, defaultValue interface{}) interface{} { |
1509 | + val, ok := m[key] |
1510 | + if !ok { |
1511 | + val = defaultValue |
1512 | + log.Printf("Warning: %v key was not found in remote config", key) |
1513 | + } |
1514 | + |
1515 | + return val |
1516 | +} |
1517 | + |
1518 | +func readRemoteConfig() (*WifiConfig, error) { |
1519 | + settings, err := wifiapClient.Show() |
1520 | + if err != nil { |
1521 | + return nil, fmt.Errorf("Error reading wifi-ap remote configuration: %v", err) |
1522 | + } |
1523 | + |
1524 | + // NOTE: Preprocessing for the case of wifi.channel. |
1525 | + // In the case of the channel, it is returned as string from rest api, but we have to convert it |
1526 | + // as it is handled as int internally. |
1527 | + // In case wifi-ap provides this in future as int, this could be replaced by |
1528 | + // |
1529 | + // readRemoteParam(settings, "wifi.channel", 0).(int) |
1530 | + channel, err := strconv.Atoi(readRemoteParam(settings, "wifi.channel", "0").(string)) |
1531 | + if err != nil { |
1532 | + return nil, fmt.Errorf("Could not parse wifi.channel parameter: %v", err) |
1533 | + } |
1534 | + |
1535 | + return &WifiConfig{ |
1536 | + Ssid: readRemoteParam(settings, "wifi.ssid", "").(string), |
1537 | + Passphrase: readRemoteParam(settings, "wifi.security-passphrase", "").(string), |
1538 | + Interface: readRemoteParam(settings, "wifi.interface", "").(string), |
1539 | + CountryCode: readRemoteParam(settings, "wifi.country-code", "").(string), |
1540 | + Channel: channel, |
1541 | + OperationMode: readRemoteParam(settings, "wifi.operation-mode", "").(string), |
1542 | + }, nil |
1543 | +} |
1544 | + |
1545 | +func writeRemoteConfig(wc *WifiConfig) error { |
1546 | + params := make(map[string]interface{}) |
1547 | + params["wifi.ssid"] = wc.Ssid |
1548 | + params["wifi.security-passphrase"] = wc.Passphrase |
1549 | + params["wifi.interface"] = wc.Interface |
1550 | + params["wifi.country-code"] = wc.CountryCode |
1551 | + params["wifi.channel"] = wc.Channel |
1552 | + params["wifi.operation-mode"] = wc.OperationMode |
1553 | + |
1554 | + err := wifiapClient.Set(params) |
1555 | + if err != nil { |
1556 | + return fmt.Errorf("Error writing remote configuration: %v", err) |
1557 | + } |
1558 | + |
1559 | + return nil |
1560 | +} |
1561 | + |
1562 | +// ReadConfig reads all config, remote and local, at the same time |
1563 | +var ReadConfig = func() (*Config, error) { |
1564 | + wifiConfig, err := readRemoteConfig() |
1565 | + if err != nil { |
1566 | + return nil, err |
1567 | + } |
1568 | + |
1569 | + portalConfig, err := readLocalConfig() |
1570 | + if err != nil { |
1571 | + return nil, err |
1572 | + } |
1573 | + |
1574 | + // if local config is nil, fill returning object with default values |
1575 | + if portalConfig == nil { |
1576 | + portalConfig = defaultPortalConfig() |
1577 | + } |
1578 | + |
1579 | + return &Config{Wifi: wifiConfig, Portal: portalConfig}, nil |
1580 | +} |
1581 | + |
1582 | +// WriteConfig writes all remote and local config at the same time |
1583 | +var WriteConfig = func(c *Config) error { |
1584 | + previousRemoteConfig, err := readRemoteConfig() |
1585 | + if err != nil { |
1586 | + return fmt.Errorf("Error reading current remote config before applying new one: %v", err) |
1587 | + } |
1588 | + |
1589 | + // only write remote config if it's different from current |
1590 | + if *previousRemoteConfig != *c.Wifi { |
1591 | + err = writeRemoteConfig(c.Wifi) |
1592 | + if err != nil { |
1593 | + // if an error happens writing remote config there is no need to restore |
1594 | + // backup, as nothing shouldn't have been written |
1595 | + return err |
1596 | + } |
1597 | + } |
1598 | + |
1599 | + err = writeLocalConfig(c.Portal) |
1600 | + if err != nil { |
1601 | + // rollback |
1602 | + if previousRemoteConfig != nil { |
1603 | + backupErr := writeRemoteConfig(previousRemoteConfig) |
1604 | + if backupErr != nil { |
1605 | + return fmt.Errorf("Could not restore previous remote configuration: %v\n after error: %v", backupErr, err) |
1606 | + } |
1607 | + } |
1608 | + return err |
1609 | + } |
1610 | + |
1611 | + // write flag file for not asking more times for configuring snap before first use |
1612 | + if MustSetConfig() { |
1613 | + err = WriteFlagFile(mustConfigFlagFile) |
1614 | + if err != nil { |
1615 | + return fmt.Errorf("Error writing flag file after configuring for a first time") |
1616 | + } |
1617 | + } |
1618 | + |
1619 | + return nil |
1620 | +} |
1621 | + |
1622 | +// MustSetConfig true if one needs to configure snap before continuing |
1623 | +var MustSetConfig = func() bool { |
1624 | + if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) { |
1625 | + return true |
1626 | + } |
1627 | + return false |
1628 | +} |
1629 | diff --git a/utils/config_test.go b/utils/config_test.go |
1630 | new file mode 100644 |
1631 | index 0000000..1802a60 |
1632 | --- /dev/null |
1633 | +++ b/utils/config_test.go |
1634 | @@ -0,0 +1,543 @@ |
1635 | +package utils |
1636 | + |
1637 | +import ( |
1638 | + "fmt" |
1639 | + "io/ioutil" |
1640 | + "os" |
1641 | + "path/filepath" |
1642 | + "strconv" |
1643 | + "sync" |
1644 | + "testing" |
1645 | + "time" |
1646 | + |
1647 | + "gopkg.in/check.v1" |
1648 | +) |
1649 | + |
1650 | +const testLocalConfig = ` |
1651 | +{ |
1652 | + "portal.no-reset-creds": true, |
1653 | + "portal.no-operational": false |
1654 | +} |
1655 | +` |
1656 | + |
1657 | +const testLocalConfigBadEntry = ` |
1658 | +{ |
1659 | + "portal.password": "the_password", |
1660 | + "portal.no-reset-creds": true, |
1661 | + "bad.parameter": "bad.value", |
1662 | + "portal.no-operational": false |
1663 | +} |
1664 | +` |
1665 | + |
1666 | +const testLocalEmptyConfig = ` |
1667 | +{ |
1668 | +} |
1669 | +` |
1670 | + |
1671 | +var testPortalConfig = &PortalConfig{"the_password", true, false} |
1672 | + |
1673 | +var rand uint32 |
1674 | +var randmu sync.Mutex |
1675 | + |
1676 | +func Test(t *testing.T) { check.TestingT(t) } |
1677 | + |
1678 | +type S struct{} |
1679 | + |
1680 | +var _ = check.Suite(&S{}) |
1681 | + |
1682 | +// #################### |
1683 | +// Testing local config |
1684 | +// #################### |
1685 | +func randomName() string { |
1686 | + randmu.Lock() |
1687 | + r := rand |
1688 | + if r == 0 { |
1689 | + r = uint32(time.Now().UnixNano() + int64(os.Getpid())) |
1690 | + } |
1691 | + r = r*1664525 + 1013904223 // constants from Numerical Recipes |
1692 | + rand = r |
1693 | + randmu.Unlock() |
1694 | + return strconv.Itoa(int(1e9 + r%1e9))[1:] |
1695 | +} |
1696 | + |
1697 | +func createTempFile(content string) (*os.File, error) { |
1698 | + contentAsBytes := []byte(content) |
1699 | + |
1700 | + tmpfile, err := ioutil.TempFile("", "config") |
1701 | + if err != nil { |
1702 | + return nil, fmt.Errorf("Could not create temp file: %v", err) |
1703 | + } |
1704 | + |
1705 | + if _, err := tmpfile.Write(contentAsBytes); err != nil { |
1706 | + return nil, fmt.Errorf("Could not write contents to temp file: %v", err) |
1707 | + } |
1708 | + |
1709 | + if err := tmpfile.Close(); err != nil { |
1710 | + return nil, fmt.Errorf("Could not close tempfile properly: %v", err) |
1711 | + } |
1712 | + |
1713 | + return tmpfile, nil |
1714 | +} |
1715 | + |
1716 | +func verifyLocalConfig(c *check.C, cfg *PortalConfig, expectedPwd string, expectedNoResetCredentials bool, expectedNoOperational bool) { |
1717 | + c.Assert(cfg.Password, check.Equals, expectedPwd) |
1718 | + c.Assert(cfg.NoResetCredentials, check.Equals, expectedNoResetCredentials) |
1719 | + c.Assert(cfg.NoOperational, check.Equals, expectedNoOperational) |
1720 | +} |
1721 | + |
1722 | +func verifyDefaultLocalConfig(c *check.C, cfg *PortalConfig) { |
1723 | + verifyLocalConfig(c, cfg, "", false, false) |
1724 | +} |
1725 | + |
1726 | +func (s *S) TestReadLocalConfig(c *check.C) { |
1727 | + f, err := createTempFile(testLocalConfig) |
1728 | + c.Assert(err, check.IsNil) |
1729 | + |
1730 | + defer os.Remove(f.Name()) |
1731 | + configFile = f.Name() |
1732 | + |
1733 | + cfg, err := readLocalConfig() |
1734 | + c.Assert(err, check.IsNil) |
1735 | + |
1736 | + verifyLocalConfig(c, cfg, "", true, false) |
1737 | +} |
1738 | + |
1739 | +func (s *S) TestReadLocalConfigBadEntry(c *check.C) { |
1740 | + // No matter if there are additional not recognized params, only known should be marshalled |
1741 | + f, err := createTempFile(testLocalConfigBadEntry) |
1742 | + c.Assert(err, check.IsNil) |
1743 | + |
1744 | + defer os.Remove(f.Name()) |
1745 | + configFile = f.Name() |
1746 | + |
1747 | + cfg, err := readLocalConfig() |
1748 | + c.Assert(err, check.IsNil) |
1749 | + |
1750 | + verifyLocalConfig(c, cfg, "", true, false) |
1751 | +} |
1752 | + |
1753 | +func (s *S) TestReadLocalEmptyConfig(c *check.C) { |
1754 | + // No matter if there are additional not recognized params, only known should be marshalled |
1755 | + f, err := createTempFile(testLocalEmptyConfig) |
1756 | + c.Assert(err, check.IsNil) |
1757 | + |
1758 | + defer os.Remove(f.Name()) |
1759 | + configFile = f.Name() |
1760 | + |
1761 | + cfg, err := readLocalConfig() |
1762 | + c.Assert(err, check.IsNil) |
1763 | + |
1764 | + verifyDefaultLocalConfig(c, cfg) |
1765 | +} |
1766 | + |
1767 | +func (s *S) TestReadLocalNotExistingConfig(c *check.C) { |
1768 | + configFile = "does/not/exists/config.json" |
1769 | + |
1770 | + cfg, err := readLocalConfig() |
1771 | + c.Assert(err, check.IsNil) |
1772 | + c.Assert(cfg, check.IsNil) |
1773 | +} |
1774 | + |
1775 | +func (s *S) TestWriteLocalConfigFileDoesNotExists(c *check.C) { |
1776 | + mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) |
1777 | + defer os.Remove(mustConfigFlagFile) |
1778 | + HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName()) |
1779 | + defer os.Remove(HashFile) |
1780 | + |
1781 | + err := writeLocalConfig(testPortalConfig) |
1782 | + c.Assert(err, check.IsNil) |
1783 | + |
1784 | + ok, err := MatchingHash(testPortalConfig.Password) |
1785 | + c.Assert(err, check.IsNil) |
1786 | + c.Assert(ok, check.Equals, true) |
1787 | +} |
1788 | + |
1789 | +func (s *S) TestWriteLocalConfigFiletExists(c *check.C) { |
1790 | + mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) |
1791 | + defer os.Remove(mustConfigFlagFile) |
1792 | + |
1793 | + f, err := createTempFile("whateverbadpasswordhash") |
1794 | + c.Assert(err, check.IsNil) |
1795 | + |
1796 | + defer os.Remove(f.Name()) |
1797 | + HashFile = f.Name() |
1798 | + |
1799 | + err = writeLocalConfig(testPortalConfig) |
1800 | + c.Assert(err, check.IsNil) |
1801 | + |
1802 | + ok, err := MatchingHash(testPortalConfig.Password) |
1803 | + c.Assert(err, check.IsNil) |
1804 | + c.Assert(ok, check.Equals, true) |
1805 | +} |
1806 | + |
1807 | +// ##################### |
1808 | +// Testing remote config |
1809 | +// ##################### |
1810 | +type wifiapClientMock struct { |
1811 | + m map[string]interface{} |
1812 | +} |
1813 | + |
1814 | +func (c *wifiapClientMock) Show() (map[string]interface{}, error) { |
1815 | + return c.m, nil |
1816 | +} |
1817 | + |
1818 | +func (c *wifiapClientMock) Enable() error { |
1819 | + return nil |
1820 | +} |
1821 | + |
1822 | +func (c *wifiapClientMock) Disable() error { |
1823 | + return nil |
1824 | +} |
1825 | + |
1826 | +func (c *wifiapClientMock) Enabled() (bool, error) { |
1827 | + return true, nil |
1828 | +} |
1829 | + |
1830 | +func (c *wifiapClientMock) SetSsid(string) error { |
1831 | + return nil |
1832 | +} |
1833 | + |
1834 | +func (c *wifiapClientMock) SetPassphrase(string) error { |
1835 | + return nil |
1836 | +} |
1837 | + |
1838 | +func (c *wifiapClientMock) Set(map[string]interface{}) error { |
1839 | + return nil |
1840 | +} |
1841 | + |
1842 | +func (s *S) TestReadRemoteConfig(c *check.C) { |
1843 | + wifiapClient = &wifiapClientMock{ |
1844 | + m: map[string]interface{}{ |
1845 | + "dhcp.lease-time": "12h", |
1846 | + "dhcp.range-start": "10.0.60.2", |
1847 | + "dhcp.range-stop": "10.0.60.199", |
1848 | + "disabled": true, |
1849 | + "share.disabled": false, |
1850 | + "share.network-interface": "wlp2s0", |
1851 | + "wifi.address": "10.0.60.1", |
1852 | + "wifi.channel": "6", // in real environment, channel is returned as string |
1853 | + "wifi.hostapd-driver": "nl80211", |
1854 | + "wifi.interface": "wlp2s0", |
1855 | + "wifi.interface-mode": "direct", |
1856 | + "wifi.country-code": "0x31", |
1857 | + "wifi.netmask": "255.255.255.0", |
1858 | + "wifi.operation-mode": "g", |
1859 | + "wifi.security": "wpa2", |
1860 | + "wifi.security-passphrase": "17Soj8/Sxh14lcpD", |
1861 | + "wifi.ssid": "Ubuntu", |
1862 | + }, |
1863 | + } |
1864 | + |
1865 | + cfg, err := readRemoteConfig() |
1866 | + c.Assert(err, check.IsNil) |
1867 | + |
1868 | + expectedCfg := &WifiConfig{ |
1869 | + Ssid: "Ubuntu", |
1870 | + Passphrase: "17Soj8/Sxh14lcpD", |
1871 | + Interface: "wlp2s0", |
1872 | + CountryCode: "0x31", |
1873 | + Channel: 6, |
1874 | + OperationMode: "g", |
1875 | + } |
1876 | + |
1877 | + c.Assert(*cfg, check.Equals, *expectedCfg) |
1878 | +} |
1879 | + |
1880 | +func (s *S) TestReadRemoteConfigNotAllParams(c *check.C) { |
1881 | + wifiapClient = &wifiapClientMock{ |
1882 | + m: map[string]interface{}{ |
1883 | + "dhcp.lease-time": "12h", |
1884 | + "dhcp.range-start": "10.0.60.2", |
1885 | + "dhcp.range-stop": "10.0.60.199", |
1886 | + "share.disabled": false, |
1887 | + "share.network-interface": "wlp2s0", |
1888 | + "wifi.address": "10.0.60.1", |
1889 | + "wifi.hostapd-driver": "nl80211", |
1890 | + "wifi.interface": "wlp2s0", |
1891 | + "wifi.interface-mode": "direct", |
1892 | + "wifi.country-code": "0x31", |
1893 | + "wifi.netmask": "255.255.255.0", |
1894 | + "wifi.security": "wpa2", |
1895 | + "wifi.ssid": "Ubuntu", |
1896 | + }, |
1897 | + } |
1898 | + |
1899 | + cfg, err := readRemoteConfig() |
1900 | + c.Assert(err, check.IsNil) |
1901 | + |
1902 | + expectedCfg := &WifiConfig{ |
1903 | + Ssid: "Ubuntu", |
1904 | + Passphrase: "", |
1905 | + Interface: "wlp2s0", |
1906 | + CountryCode: "0x31", |
1907 | + Channel: 0, |
1908 | + OperationMode: "", |
1909 | + } |
1910 | + |
1911 | + c.Assert(*cfg, check.Equals, *expectedCfg) |
1912 | +} |
1913 | + |
1914 | +func (s *S) TestReadEmptyRemoteConfig(c *check.C) { |
1915 | + wifiapClient = &wifiapClientMock{} |
1916 | + |
1917 | + cfg, err := readRemoteConfig() |
1918 | + c.Assert(err, check.IsNil) |
1919 | + |
1920 | + expectedCfg := &WifiConfig{ |
1921 | + Ssid: "", |
1922 | + Passphrase: "", |
1923 | + Interface: "", |
1924 | + CountryCode: "", |
1925 | + Channel: 0, |
1926 | + OperationMode: "", |
1927 | + } |
1928 | + |
1929 | + c.Assert(*cfg, check.Equals, *expectedCfg) |
1930 | +} |
1931 | + |
1932 | +func (s *S) TestWriteRemoteConfig(c *check.C) { |
1933 | + wifiapClient = &wifiapClientMock{} |
1934 | + |
1935 | + err := writeRemoteConfig(&WifiConfig{ |
1936 | + Ssid: "Ubuntu", |
1937 | + Passphrase: "17Soj8/Sxh14lcpD", |
1938 | + Interface: "wlp2s0", |
1939 | + CountryCode: "0x31", |
1940 | + Channel: 6, |
1941 | + OperationMode: "g", |
1942 | + }) |
1943 | + |
1944 | + c.Assert(err, check.IsNil) |
1945 | +} |
1946 | + |
1947 | +// #################### |
1948 | +// Testing whole config |
1949 | +// #################### |
1950 | +func (s *S) TestWriteConfig(c *check.C) { |
1951 | + wifiapClient = &wifiapClientMock{} |
1952 | + |
1953 | + HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName()) |
1954 | + defer os.Remove(HashFile) |
1955 | + |
1956 | + mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) |
1957 | + defer os.Remove(mustConfigFlagFile) |
1958 | + |
1959 | + c.Assert(MustSetConfig(), check.Equals, true) |
1960 | + |
1961 | + err := WriteConfig(&Config{ |
1962 | + Wifi: &WifiConfig{ |
1963 | + Ssid: "Ubuntu", |
1964 | + Passphrase: "17Soj8/Sxh14lcpD", |
1965 | + Interface: "wlp2s0", |
1966 | + CountryCode: "0x31", |
1967 | + Channel: 6, |
1968 | + OperationMode: "g", |
1969 | + }, |
1970 | + Portal: &PortalConfig{ |
1971 | + Password: "thepassword", |
1972 | + NoResetCredentials: true, |
1973 | + NoOperational: false, |
1974 | + }, |
1975 | + }) |
1976 | + |
1977 | + c.Assert(err, check.IsNil) |
1978 | + |
1979 | + ok, err := MatchingHash("thepassword") |
1980 | + c.Assert(err, check.IsNil) |
1981 | + c.Assert(ok, check.Equals, true) |
1982 | + |
1983 | + c.Assert(MustSetConfig(), check.Equals, false) |
1984 | +} |
1985 | + |
1986 | +func (s *S) TestReadConfig(c *check.C) { |
1987 | + wifiapClient = &wifiapClientMock{ |
1988 | + m: map[string]interface{}{ |
1989 | + "dhcp.lease-time": "12h", |
1990 | + "dhcp.range-start": "10.0.60.2", |
1991 | + "dhcp.range-stop": "10.0.60.199", |
1992 | + "disabled": true, |
1993 | + "share.disabled": false, |
1994 | + "share.network-interface": "wlp2s0", |
1995 | + "wifi.address": "10.0.60.1", |
1996 | + "wifi.channel": "6", // in real environment, channel is returned as string |
1997 | + "wifi.hostapd-driver": "nl80211", |
1998 | + "wifi.interface": "wlp2s0", |
1999 | + "wifi.interface-mode": "direct", |
2000 | + "wifi.country-code": "0x31", |
2001 | + "wifi.netmask": "255.255.255.0", |
2002 | + "wifi.operation-mode": "g", |
2003 | + "wifi.security": "wpa2", |
2004 | + "wifi.security-passphrase": "17Soj8/Sxh14lcpD", |
2005 | + "wifi.ssid": "Ubuntu", |
2006 | + }, |
2007 | + } |
2008 | + |
2009 | + f, err := createTempFile(testLocalConfig) |
2010 | + c.Assert(err, check.IsNil) |
2011 | + |
2012 | + defer os.Remove(f.Name()) |
2013 | + configFile = f.Name() |
2014 | + |
2015 | + expectedWifiConfig := &WifiConfig{ |
2016 | + Ssid: "Ubuntu", |
2017 | + Passphrase: "17Soj8/Sxh14lcpD", |
2018 | + Interface: "wlp2s0", |
2019 | + CountryCode: "0x31", |
2020 | + Channel: 6, |
2021 | + OperationMode: "g", |
2022 | + } |
2023 | + |
2024 | + expectedPortalConfig := &PortalConfig{ |
2025 | + Password: "", |
2026 | + NoResetCredentials: true, |
2027 | + NoOperational: false, |
2028 | + } |
2029 | + |
2030 | + cfg, err := ReadConfig() |
2031 | + c.Assert(err, check.IsNil) |
2032 | + c.Assert(*cfg.Wifi, check.Equals, *expectedWifiConfig) |
2033 | + c.Assert(*cfg.Portal, check.Equals, *expectedPortalConfig) |
2034 | +} |
2035 | + |
2036 | +func (s *S) TestMustSetConfig(c *check.C) { |
2037 | + mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) |
2038 | + defer os.Remove(mustConfigFlagFile) |
2039 | + |
2040 | + HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName()) |
2041 | + defer os.Remove(HashFile) |
2042 | + |
2043 | + wifiapClient = &wifiapClientMock{} |
2044 | + |
2045 | + // config to save |
2046 | + cfg := &Config{ |
2047 | + Wifi: &WifiConfig{ |
2048 | + Ssid: "Ubuntu", |
2049 | + Passphrase: "17Soj8/Sxh14lcpD", |
2050 | + Interface: "wlp2s0", |
2051 | + CountryCode: "0x31", |
2052 | + Channel: 6, |
2053 | + OperationMode: "g", |
2054 | + }, |
2055 | + Portal: &PortalConfig{ |
2056 | + Password: "the_password", |
2057 | + NoResetCredentials: true, |
2058 | + NoOperational: false, |
2059 | + }, |
2060 | + } |
2061 | + |
2062 | + c.Assert(MustSetConfig(), check.Equals, true) |
2063 | + |
2064 | + err := WriteConfig(cfg) |
2065 | + c.Assert(err, check.IsNil) |
2066 | + |
2067 | + c.Assert(MustSetConfig(), check.Equals, false) |
2068 | + |
2069 | + err = WriteConfig(cfg) |
2070 | + c.Assert(err, check.IsNil) |
2071 | + |
2072 | + c.Assert(MustSetConfig(), check.Equals, false) |
2073 | +} |
2074 | + |
2075 | +func (s *S) TestConfigDump(c *check.C) { |
2076 | + cfg := &Config{ |
2077 | + Wifi: &WifiConfig{ |
2078 | + Ssid: "Ubuntu", |
2079 | + Passphrase: "17Soj8/Sxh14lcpD", |
2080 | + Interface: "wlp2s0", |
2081 | + CountryCode: "0x31", |
2082 | + Channel: 6, |
2083 | + OperationMode: "g", |
2084 | + }, |
2085 | + Portal: &PortalConfig{ |
2086 | + Password: "the_password", |
2087 | + NoResetCredentials: true, |
2088 | + NoOperational: false, |
2089 | + }, |
2090 | + } |
2091 | + |
2092 | + expected := `--Wifi-- |
2093 | +Ssid: Ubuntu |
2094 | +Passphrase: 17Soj8/Sxh14lcpD |
2095 | +Interface: wlp2s0 |
2096 | +CountryCode: 0x31 |
2097 | +Channel: 6 |
2098 | +OperationMode: g |
2099 | +--Portal-- |
2100 | +Password: the_password |
2101 | +NoResetCredentials: true |
2102 | +NoOperational: false` |
2103 | + |
2104 | + c.Assert(cfg.String(), check.Equals, expected) |
2105 | +} |
2106 | + |
2107 | +func (s *S) TestRollbackConfigIfFailsWriting(c *check.C) { |
2108 | + wifiapClient = &wifiapClientMock{ |
2109 | + m: map[string]interface{}{ |
2110 | + "dhcp.lease-time": "12h", |
2111 | + "dhcp.range-start": "10.0.60.2", |
2112 | + "dhcp.range-stop": "10.0.60.199", |
2113 | + "disabled": true, |
2114 | + "share.disabled": false, |
2115 | + "share.network-interface": "wlp2s0", |
2116 | + "wifi.address": "10.0.60.1", |
2117 | + "wifi.channel": "8", // in real environment, channel is returned as string |
2118 | + "wifi.hostapd-driver": "nl80211", |
2119 | + "wifi.interface": "wlp2s0", |
2120 | + "wifi.interface-mode": "direct", |
2121 | + "wifi.country-code": "ES", |
2122 | + "wifi.netmask": "255.255.255.0", |
2123 | + "wifi.operation-mode": "ad", |
2124 | + "wifi.security": "wlan1", |
2125 | + "wifi.security-passphrase": "abc17Soj8/Sxh14lcpD", |
2126 | + "wifi.ssid": "mySSID", |
2127 | + }, |
2128 | + } |
2129 | + |
2130 | + HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName()) |
2131 | + defer os.Remove(HashFile) |
2132 | + |
2133 | + mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName()) |
2134 | + defer os.Remove(mustConfigFlagFile) |
2135 | + |
2136 | + err := WriteFlagFile(mustConfigFlagFile) |
2137 | + c.Assert(err, check.IsNil) |
2138 | + c.Assert(MustSetConfig(), check.Equals, false) |
2139 | + |
2140 | + // read previous remote config file |
2141 | + remotePreviousConfig, err := readRemoteConfig() |
2142 | + c.Assert(err, check.IsNil) |
2143 | + |
2144 | + // config to save |
2145 | + cfg := &Config{ |
2146 | + Wifi: &WifiConfig{ |
2147 | + Ssid: "Ubuntu", |
2148 | + Passphrase: "17Soj8/Sxh14lcpD", |
2149 | + Interface: "wlp2s0", |
2150 | + CountryCode: "XX", |
2151 | + Channel: 6, |
2152 | + OperationMode: "g", |
2153 | + }, |
2154 | + Portal: &PortalConfig{ |
2155 | + Password: "the_password", |
2156 | + NoResetCredentials: true, |
2157 | + NoOperational: false, |
2158 | + }, |
2159 | + } |
2160 | + |
2161 | + originalHashIt := HashIt |
2162 | + HashIt = func(s string) ([]byte, error) { return nil, fmt.Errorf("Error hashing") } |
2163 | + |
2164 | + // assert write fails |
2165 | + err = WriteConfig(cfg) |
2166 | + c.Assert(err, check.NotNil) |
2167 | + |
2168 | + HashIt = originalHashIt |
2169 | + |
2170 | + // read remote config after write file failing and verify it's the original one |
2171 | + remoteCfg, err := readRemoteConfig() |
2172 | + c.Assert(err, check.IsNil) |
2173 | + c.Assert(*remoteCfg, check.Equals, *remotePreviousConfig) |
2174 | + |
2175 | + // verify must set config flag is not changed in the process |
2176 | + c.Assert(MustSetConfig(), check.Equals, false) |
2177 | +} |
2178 | diff --git a/utils/utils.go b/utils/utils.go |
2179 | index 94850c0..9a77ce3 100644 |
2180 | --- a/utils/utils.go |
2181 | +++ b/utils/utils.go |
2182 | @@ -40,7 +40,7 @@ var HashFile = filepath.Join(os.Getenv("SNAP_COMMON"), "hash") |
2183 | |
2184 | // HashIt writes the hash of the passed password to the HashFile and |
2185 | // returns the hash and error |
2186 | -func HashIt(s string) ([]byte, error) { |
2187 | +var HashIt = func(s string) ([]byte, error) { |
2188 | var b []byte |
2189 | var err error |
2190 | b, err = bcrypt.GenerateFromPassword([]byte(s), 8) |
2191 | @@ -154,3 +154,12 @@ func RunningOn(address string) bool { |
2192 | } |
2193 | return true |
2194 | } |
2195 | + |
2196 | +// ParseFormParamSingleValue extracts single value from a http post request |
2197 | +func ParseFormParamSingleValue(form map[string][]string, key string) string { |
2198 | + vals := form[key] |
2199 | + if len(vals) > 0 { |
2200 | + return vals[0] |
2201 | + } |
2202 | + return "" |
2203 | +} |
2204 | diff --git a/wifiap/wifiap.go b/wifiap/wifiap.go |
2205 | index faf0c15..d81f883 100644 |
2206 | --- a/wifiap/wifiap.go |
2207 | +++ b/wifiap/wifiap.go |
2208 | @@ -45,7 +45,7 @@ func DefaultClient() *Client { |
2209 | return &Client{restClient: defaultRestClient()} |
2210 | } |
2211 | |
2212 | -// Operations interface enables mock testing |
2213 | +// Operations interface defining operations implemented by wifiap client |
2214 | type Operations interface { |
2215 | Show() (map[string]interface{}, error) |
2216 | Enabled() (bool, error) |
2217 | @@ -53,6 +53,7 @@ type Operations interface { |
2218 | Disable() error |
2219 | SetSsid(string) error |
2220 | SetPassphrase(string) error |
2221 | + Set(map[string]interface{}) error |
2222 | } |
2223 | |
2224 | func defaultServiceURI() string { |
2225 | @@ -205,3 +206,22 @@ func (client *Client) SetPassphrase(passphrase string) error { |
2226 | |
2227 | return nil |
2228 | } |
2229 | + |
2230 | +// Set sets a group of parameters in wifi-ap |
2231 | +func (client *Client) Set(params map[string]interface{}) error { |
2232 | + b, err := json.Marshal(params) |
2233 | + if err != nil { |
2234 | + return fmt.Errorf("Error when marshalling input parameters: %q", err) |
2235 | + } |
2236 | + |
2237 | + response, err := client.restClient.sendHTTPRequest(defaultServiceURI(), "POST", bytes.NewReader(b)) |
2238 | + if err != nil { |
2239 | + return fmt.Errorf("wifi-ap set operation failed: %q", err) |
2240 | + } |
2241 | + |
2242 | + if response.StatusCode != http.StatusOK || response.Status != http.StatusText(http.StatusOK) { |
2243 | + return fmt.Errorf("Failed to set configuration, service returned: %d (%s)", response.StatusCode, response.Status) |
2244 | + } |
2245 | + |
2246 | + return nil |
2247 | +} |
2248 | diff --git a/wifiap/wifiap_test.go b/wifiap/wifiap_test.go |
2249 | index 6fa4207..e1f4279 100644 |
2250 | --- a/wifiap/wifiap_test.go |
2251 | +++ b/wifiap/wifiap_test.go |
2252 | @@ -21,6 +21,7 @@ import ( |
2253 | "fmt" |
2254 | "io/ioutil" |
2255 | "net/http" |
2256 | + "reflect" |
2257 | "strings" |
2258 | "testing" |
2259 | ) |
2260 | @@ -148,7 +149,7 @@ func TestShow(t *testing.T) { |
2261 | } |
2262 | |
2263 | // Testing Enable() |
2264 | -func validateHeaders(m map[string]string, req *http.Request) error { |
2265 | +func validateHeaders(m map[string]interface{}, req *http.Request) error { |
2266 | buf, _ := ioutil.ReadAll(req.Body) |
2267 | var headers map[string]interface{} |
2268 | if err := json.Unmarshal(buf, &headers); err != nil { |
2269 | @@ -160,9 +161,20 @@ func validateHeaders(m map[string]string, req *http.Request) error { |
2270 | return fmt.Errorf("Expected %v headers", n) |
2271 | } |
2272 | |
2273 | - for key, value := range m { |
2274 | - if headers[key] != value { |
2275 | - return fmt.Errorf("Header '%v' has not valid value", key) |
2276 | + for key, mValue := range m { |
2277 | + hValue := headers[key] |
2278 | + if hValue != mValue { |
2279 | + // evaluate the special case of hValue unmarshalled as float64 |
2280 | + if reflect.TypeOf(hValue) != reflect.TypeOf(mValue) { |
2281 | + switch hValue := hValue.(type) { |
2282 | + case float64: |
2283 | + if int(hValue) != mValue.(int) { |
2284 | + return fmt.Errorf("Header '%v' has not valid value. Expected %v but has %v", key, mValue, hValue) |
2285 | + } |
2286 | + } |
2287 | + } else { |
2288 | + return fmt.Errorf("Header '%v' has not valid value. Expected %v but has %v", key, mValue, hValue) |
2289 | + } |
2290 | } |
2291 | } |
2292 | |
2293 | @@ -181,7 +193,7 @@ func (mock *mockTransportEnable) Do(req *http.Request) (*http.Response, error) { |
2294 | return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method) |
2295 | } |
2296 | |
2297 | - err := validateHeaders(map[string]string{"disabled": "false"}, req) |
2298 | + err := validateHeaders(map[string]interface{}{"disabled": "false"}, req) |
2299 | if err != nil { |
2300 | return nil, err |
2301 | } |
2302 | @@ -238,7 +250,7 @@ func (mock *mockTransportDisable) Do(req *http.Request) (*http.Response, error) |
2303 | return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method) |
2304 | } |
2305 | |
2306 | - err := validateHeaders(map[string]string{"disabled": "true"}, req) |
2307 | + err := validateHeaders(map[string]interface{}{"disabled": "true"}, req) |
2308 | if err != nil { |
2309 | return nil, err |
2310 | } |
2311 | @@ -363,7 +375,7 @@ func (mock *mockTransportSetSsid) Do(req *http.Request) (*http.Response, error) |
2312 | return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method) |
2313 | } |
2314 | |
2315 | - err := validateHeaders(map[string]string{"wifi.ssid": "MySsid"}, req) |
2316 | + err := validateHeaders(map[string]interface{}{"wifi.ssid": "MySsid"}, req) |
2317 | if err != nil { |
2318 | return nil, err |
2319 | } |
2320 | @@ -401,7 +413,7 @@ func (mock *mockTransportSetPassphrase) Do(req *http.Request) (*http.Response, e |
2321 | return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method) |
2322 | } |
2323 | |
2324 | - err := validateHeaders(map[string]string{"wifi.security": "wpa2", "wifi.security-passphrase": "passphrase123"}, req) |
2325 | + err := validateHeaders(map[string]interface{}{"wifi.security": "wpa2", "wifi.security-passphrase": "passphrase123"}, req) |
2326 | if err != nil { |
2327 | return nil, err |
2328 | } |
2329 | @@ -424,3 +436,79 @@ func TestSetPassphrase(t *testing.T) { |
2330 | t.Errorf("Failed to set passphrase: %v\n", err) |
2331 | } |
2332 | } |
2333 | + |
2334 | +type mockTransportSet struct{} |
2335 | + |
2336 | +func (mock *mockTransportSet) Do(req *http.Request) (*http.Response, error) { |
2337 | + |
2338 | + url := req.URL.String() |
2339 | + if url != "http://unix/v1/configuration" { |
2340 | + return nil, fmt.Errorf("Not valid request URL: %v", url) |
2341 | + } |
2342 | + |
2343 | + if req.Method != "POST" { |
2344 | + return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method) |
2345 | + } |
2346 | + |
2347 | + err := validateHeaders(map[string]interface{}{ |
2348 | + "dhcp.lease-time": "12h", |
2349 | + "dhcp.range-start": "10.0.60.2", |
2350 | + "dhcp.range-stop": "10.0.60.199", |
2351 | + "disabled": true, |
2352 | + "share.disabled": false, |
2353 | + "share.network-interface": "wlp2s0", |
2354 | + "wifi.address": "10.0.60.1", |
2355 | + "wifi.channel": 6, |
2356 | + "wifi.hostapd-driver": "nl80211", |
2357 | + "wifi.interface": "wlp2s0", |
2358 | + "wifi.interface-mode": "direct", |
2359 | + "wifi.country-code": "0x31", |
2360 | + "wifi.netmask": "255.255.255.0", |
2361 | + "wifi.operation-mode": "g", |
2362 | + "wifi.security": "wpa2", |
2363 | + "wifi.security-passphrase": "17Soj8/Sxh14lcpD", |
2364 | + "wifi.ssid": "Ubuntu", |
2365 | + }, req) |
2366 | + if err != nil { |
2367 | + return nil, err |
2368 | + } |
2369 | + |
2370 | + rawBody := `{"result":{},"status":"OK","status-code":200,"type":"sync"}` |
2371 | + |
2372 | + response := http.Response{ |
2373 | + StatusCode: 200, |
2374 | + Status: "200 OK", |
2375 | + Body: ioutil.NopCloser(strings.NewReader(rawBody)), |
2376 | + } |
2377 | + |
2378 | + return &response, nil |
2379 | +} |
2380 | + |
2381 | +func TestSet(t *testing.T) { |
2382 | + |
2383 | + params := map[string]interface{}{ |
2384 | + "dhcp.lease-time": "12h", |
2385 | + "dhcp.range-start": "10.0.60.2", |
2386 | + "dhcp.range-stop": "10.0.60.199", |
2387 | + "disabled": true, |
2388 | + "share.disabled": false, |
2389 | + "share.network-interface": "wlp2s0", |
2390 | + "wifi.address": "10.0.60.1", |
2391 | + "wifi.channel": 6, |
2392 | + "wifi.hostapd-driver": "nl80211", |
2393 | + "wifi.interface": "wlp2s0", |
2394 | + "wifi.interface-mode": "direct", |
2395 | + "wifi.country-code": "0x31", |
2396 | + "wifi.netmask": "255.255.255.0", |
2397 | + "wifi.operation-mode": "g", |
2398 | + "wifi.security": "wpa2", |
2399 | + "wifi.security-passphrase": "17Soj8/Sxh14lcpD", |
2400 | + "wifi.ssid": "Ubuntu", |
2401 | + } |
2402 | + |
2403 | + client := NewClient(&mockTransportSet{}) |
2404 | + err := client.Set(params) |
2405 | + if err != nil { |
2406 | + t.Errorf("Failed to set params: %v\n", err) |
2407 | + } |
2408 | +} |
PASSED: Continuous integration, rev:31fc06176cf 87bc086aaa68828 69dbf096ea0277 /jenkins. canonical. com/system- enablement/ job/generic- build-snap/ 1706/ /jenkins. canonical. com/system- enablement/ job/generic- build-snap- worker/ 2306 /jenkins. canonical. com/system- enablement/ job/generic- update- snap-mp/ 1614/console /jenkins. canonical. com/system- enablement/ job/generic- test-snap/ 2658 /jenkins. canonical. com/system- enablement/ job/generic- cleanup- snap/1829/ console
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/system- enablement/ job/generic- build-snap/ 1706/rebuild
https:/