Merge ~rmescandon/snappy-hwe-snaps/+git/wifi-connect:config-ui into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master

Proposed by Roberto Mier Escandon
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)
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/config.json
* 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

To post a comment you must log in.
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
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:

https://pastebin.canonical.com/191841/

Revision history for this message
Sheila Miguez (codersquid) wrote :

I only have a few minor comments, see below.

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

Replied inline

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
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://geotags.com/iso3166/countries.html in
> compilation time by a scriptlet in snap/scriptlets/fill_country_code.sh.
> 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
../../../snap/scriptlets/fill_country_codes.sh: curl: not found

Command '['/bin/sh', '/tmp/tmpwyb7k31n', '/tmp/tmp9f5mhfj1']' returned
non-zero exit status 127

--
<email address hidden>

Revision history for this message
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

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

@Sheila code updated

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Sheila Miguez (codersquid) wrote :

Once you fix the failing test (maybe you forgot to check in the file), I approve.

review: Approve
Revision history for this message
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 :)

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
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"

review: Needs Fixing
Revision history for this message
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.

review: Needs Fixing (code)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
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

Revision history for this message
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...

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :
Download full text (3.2 KiB)

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...

Read more...

Revision history for this message
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.

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.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.

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.

review: Needs Fixing
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :
Download full text (3.5 KiB)

>
>
> 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...

Read more...

Revision history for this message
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.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.no-operational, though it's not been used so far anywhere

>
> 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

Revision history for this message
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.no-operational, though it's not been used so far
> 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

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :
Download full text (4.4 KiB)

> > > 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.no-operational, though it's not been used so far
> > anywhere
>
> Can you drop the no-opera...

Read more...

Revision history for this message
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.)

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Also, the management page design has changed, and regressed I think. I filed this bug: https://bugs.launchpad.net/snappy-hwe-snaps/+bug/1701729

Here's the bug I mentioned above about saving config message being displayed too late:
https://bugs.launchpad.net/snappy-hwe-snaps/+bug/1701727

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Alfonso said the country will land and be published soon, so please merge this as is if possible.

review: Approve
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
index b923a21..41bf8bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
1!/.gitignore
2*.snap
3*.tar.bz2
1# testing files4# testing files
2.coverage5.coverage
3.tests-extras6.tests-extras
@@ -11,4 +14,3 @@ parts/
11prime/14prime/
12snap/.snapcraft/15snap/.snapcraft/
13stage/16stage/
14*.snap
diff --git a/README.md b/README.md
index 1759797..e30f47d 100644
--- a/README.md
+++ b/README.md
@@ -265,6 +265,46 @@ Restart the daemon normal loop cleanly with:
265sudo wifi-connect start265sudo wifi-connect start
266```266```
267267
268# Configuration
269
270First time one access management portal a configuration page is shown. Once there, settings for Wi-Fi access point and portals password can be saved.
271That configuration is composed of one first section for Wi-Fi ones and another second for portal
272
273*Wi-Fi config*:
274| Config Key | Description |
275|----------------|-------------|
276| SSID | of the access point |
277| Passphrase | password to connect to access point |
278| Interface | network interface where access point must be brought up |
279| Country Code | ISO-3166 two digits code to use in Wi-Fi |
280| Channel | Wi-Fi channel |
281| Operation Mode | Wi-Fi operation mode |
282
283*Portal config*:
284| Config Key | Description |
285|----------------|-------------|
286| Password | to access any of management or operational portals |
287
288There is a button to apply configuration just below the input fields. While configuration
289is being saved a different page will inform about changes are in process to be saved. After small delay one
290should be able to reconnect to AP in order to see regular management page showing available ssids.
291
292NOTE: If there is any previous config file at $SNAP_COMMON/config.json containining 'portal.no-reset-creds' key with value true, first time accessing
293management portal won't show config page to modify it.
294
295## Country codes
296
297There is a combo box in config page to select the country code to use in the access point.
298Its values are injected in compilation time from a file stored at snap/scriptlets/country-codes.
299This file is generated dynamically when executed by hand this script:
300
301```
302snap/scriptlets/fetch_country_codes.sh
303```
304
305So, every time you want to update country codes to most recent ones, you have to execute
306that script by hand and, after success, rebuild and install snap
307
268# Tests308# Tests
269## Unit Tests309## Unit Tests
270310
diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
index 177c26b..b7d0199 100644
--- a/daemon/daemon_test.go
+++ b/daemon/daemon_test.go
@@ -163,6 +163,10 @@ func (mock *mockWifiap) SetPassphrase(p string) error {
163 return nil163 return nil
164}164}
165165
166func (mock *mockWifiap) Set(map[string]interface{}) error {
167 return nil
168}
169
166func TestLoadPreConfig(t *testing.T) {170func TestLoadPreConfig(t *testing.T) {
167 PreConfigFile = "../static/tests/pre-config0.json"171 PreConfigFile = "../static/tests/pre-config0.json"
168 config, err := LoadPreConfig()172 config, err := LoadPreConfig()
diff --git a/gulpfile.js b/gulpfile.js
index e33a752..904f3d1 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -9,4 +9,8 @@ gulp.task('sass', function() {
9 }).on('error', sass.logError))9 }).on('error', sass.logError))
10 .pipe(uglifycss())10 .pipe(uglifycss())
11 .pipe(gulp.dest('static/css'));11 .pipe(gulp.dest('static/css'));
12});
13\ No newline at end of file12\ No newline at end of file
13});
14
15// Default: remember that these tasks get run asynchronously
16gulp.task('default', ['sass']);
17
diff --git a/netman/dbus.go b/netman/dbus.go
index 2db7107..8f36ac9 100644
--- a/netman/dbus.go
+++ b/netman/dbus.go
@@ -20,16 +20,32 @@ package netman
20import (20import (
21 "errors"21 "errors"
22 "fmt"22 "fmt"
23 "io"
23 "log"24 "log"
24 "os"25 "os"
25 "strings"26 "strings"
26 "time"27 "time"
2728
28 "io"
29
30 "github.com/godbus/dbus"29 "github.com/godbus/dbus"
31)30)
3231
32// Operations defines operations for this package client
33type Operations interface {
34 GetDevices() []string
35 GetWifiDevices(devices []string) []string
36 GetAccessPoints(devices []string, ap2device map[string]string) []string
37 ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error
38 Ssids() ([]SSID, map[string]string, map[string]string)
39 Connected(devices []string) bool
40 ConnectedWifi(wifiDevices []string) bool
41 DisconnectWifi(wifiDevices []string) int
42 SetIfaceManaged(iface string, state bool, devices []string) string
43 WifisManaged(wifiDevices []string) (map[string]string, error)
44 Unmanage() error
45 Manage() error
46 ScanAndWriteSsidsToFile(filepath string) bool
47}
48
33// Client type to support unit test mock and runtime execution49// Client type to support unit test mock and runtime execution
34type Client struct {50type Client struct {
35 dbusClient DbusClient51 dbusClient DbusClient
diff --git a/package.json b/package.json
index d3e2671..baf164b 100644
--- a/package.json
+++ b/package.json
@@ -2,15 +2,15 @@
2 "name": "wifi-connect",2 "name": "wifi-connect",
3 "version": "1.0.0",3 "version": "1.0.0",
4 "description": "Management UI to be able to switch a wireless card of a UC device into AP mode and use it to configure wireless",4 "description": "Management UI to be able to switch a wireless card of a UC device into AP mode and use it to configure wireless",
5 "main": "index.js",
6 "scripts": {5 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"6 "test": "echo \"Error: no test specified\" && exit 1",
7 "build": "gulp"
8 },8 },
9 "repository": {9 "repository": {
10 "type": "git",10 "type": "git",
11 "url": "git+ssh://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect"11 "url": "git+ssh://git.launchpad.net/~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect"
12 },12 },
13 "author": "",13 "author": "Canonical Ltd.",
14 "license": "GPL-3.0",14 "license": "GPL-3.0",
15 "bugs": {15 "bugs": {
16 "url": "https://bugs.launchpad.net/snappy-hwe-snaps"16 "url": "https://bugs.launchpad.net/snappy-hwe-snaps"
@@ -20,6 +20,6 @@
20 "gulp": "^3.9.1",20 "gulp": "^3.9.1",
21 "gulp-sass": "^3.1.0",21 "gulp-sass": "^3.1.0",
22 "gulp-uglifycss": "^1.0.8",22 "gulp-uglifycss": "^1.0.8",
23 "ubuntu-vanilla-theme": "0.0.35"23 "vanilla-framework": "^1.2.2"
24 }24 }
25}25}
diff --git a/server/handlers.go b/server/handlers.go
index f28c2d8..c14189f 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -27,6 +27,8 @@ import (
27 "text/template"27 "text/template"
28 "time"28 "time"
2929
30 "strconv"
31
30 "launchpad.net/wifi-connect/utils"32 "launchpad.net/wifi-connect/utils"
31)33)
3234
@@ -34,19 +36,29 @@ const (
34 managementTemplatePath = "/templates/management.html"36 managementTemplatePath = "/templates/management.html"
35 connectingTemplatePath = "/templates/connecting.html"37 connectingTemplatePath = "/templates/connecting.html"
36 operationalTemplatePath = "/templates/operational.html"38 operationalTemplatePath = "/templates/operational.html"
39 firstConfigTemplatePath = "/templates/config.html"
37)40)
3841
39// ResourcesPath absolute path to web static resources42// ResourcesPath absolute path to web static resources
40var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static")43var ResourcesPath = filepath.Join(os.Getenv("SNAP"), "static")
4144
45// first time management portal is accessed this file is created.
46var firstConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".first_config")
47
42var cw interface{}48var cw interface{}
4349
44// Data interface representing any data included in a template50// Data interface representing any data included in a template
45type Data interface{}51type Data interface{}
4652
47// SsidsData dynamic data to fulfill the SSIDs page template53// ManagementData dynamic data to fulfill the management page.
48type SsidsData struct {54// It can contain SSIDs list or snap configuration
49 Ssids []string55// NOTE: page is a workaround to render proper grid depending on its value (ssids or config).
56// In case authentication mechanism is modified for not to be so intrusive, we could have
57// different pages for this
58type ManagementData struct {
59 Ssids []string
60 Config *utils.Config
61 Page string
50}62}
5163
52// ConnectingData dynamic data to fulfill the connect result page template64// ConnectingData dynamic data to fulfill the connect result page template
@@ -61,22 +73,41 @@ func execTemplate(w http.ResponseWriter, templatePath string, data Data) {
61 templateAbsPath := filepath.Join(ResourcesPath, templatePath)73 templateAbsPath := filepath.Join(ResourcesPath, templatePath)
62 t, err := template.ParseFiles(templateAbsPath)74 t, err := template.ParseFiles(templateAbsPath)
63 if err != nil {75 if err != nil {
64 log.Printf("Error loading the template at %v: %v", templatePath, err)76 msg := fmt.Sprintf("Error loading the template at %v: %v", templatePath, err)
65 http.Error(w, err.Error(), http.StatusInternalServerError)77 log.Println(msg)
78 http.Error(w, msg, http.StatusInternalServerError)
66 return79 return
67 }80 }
6881
69 err = t.Execute(w, data)82 err = t.Execute(w, data)
70 if err != nil {83 if err != nil {
71 log.Printf("Error executing the template at %v: %v", templatePath, err)84 msg := fmt.Sprintf("Error executing the template at %v : %v", templatePath, err)
72 http.Error(w, err.Error(), http.StatusInternalServerError)85 log.Print(msg)
86 http.Error(w, msg, http.StatusInternalServerError)
73 return87 return
74 }88 }
75}89}
7690
77// ManagementHandler lists the current available SSIDs91// ManagementHandler handles management portal
78func ManagementHandler(w http.ResponseWriter, r *http.Request) {92func ManagementHandler(w http.ResponseWriter, r *http.Request) {
79 // daemon stores current available ssids in a file93
94 if utils.MustSetConfig() {
95
96 config, err := utils.ReadConfig()
97 if err != nil {
98 msg := fmt.Sprintf("Error reading configuration: %v", err)
99 log.Println(msg)
100 http.Error(w, msg, http.StatusInternalServerError)
101 return
102 }
103
104 if !config.Portal.NoResetCredentials {
105 execTemplate(w, managementTemplatePath, ManagementData{Config: config, Page: "config"})
106 return
107 }
108
109 }
110
80 ssids, err := utils.ReadSsidsFile()111 ssids, err := utils.ReadSsidsFile()
81 if err != nil {112 if err != nil {
82 log.Printf("Error reading SSIDs file: %v", err)113 log.Printf("Error reading SSIDs file: %v", err)
@@ -84,15 +115,58 @@ func ManagementHandler(w http.ResponseWriter, r *http.Request) {
84 return115 return
85 }116 }
86117
87 data := SsidsData{Ssids: ssids}118 execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
119}
120
121// SaveConfigHandler saves config received as form post parameters
122func SaveConfigHandler(w http.ResponseWriter, r *http.Request) {
123 // read previous config
124 config, err := utils.ReadConfig()
125 if err != nil {
126 msg := fmt.Sprintf("Error reading previous stored config: %v", err)
127 log.Println(msg)
128 http.Error(w, msg, http.StatusInternalServerError)
129 return
130 }
131
132 r.ParseForm()
133
134 config.Wifi.Ssid = utils.ParseFormParamSingleValue(r.Form, "Ssid")
135 config.Wifi.Passphrase = utils.ParseFormParamSingleValue(r.Form, "Passphrase")
136 config.Wifi.Interface = utils.ParseFormParamSingleValue(r.Form, "Interface")
137 config.Wifi.CountryCode = utils.ParseFormParamSingleValue(r.Form, "CountryCode")
138 config.Wifi.Channel, err = strconv.Atoi(utils.ParseFormParamSingleValue(r.Form, "Channel"))
139 if err != nil {
140 msg := fmt.Sprintf("Error parsing channel form value: %v", err)
141 log.Println(msg)
142 http.Error(w, msg, http.StatusInternalServerError)
143 return
144 }
145 config.Wifi.OperationMode = utils.ParseFormParamSingleValue(r.Form, "OperationMode")
146 config.Portal.Password = utils.ParseFormParamSingleValue(r.Form, "PortalPassword")
147
148 err = utils.WriteConfig(config)
149 if err != nil {
150 msg := fmt.Sprintf("Error saving config: %v", err)
151 log.Println(msg)
152 http.Error(w, msg, http.StatusInternalServerError)
153 return
154 }
88155
89 // parse template156 //after saving config, redirect to management portal, showing available ssids
90 execTemplate(w, managementTemplatePath, data)157 ssids, err := utils.ReadSsidsFile()
158 if err != nil {
159 msg := fmt.Sprintf("== wifi-connect/handler: Error reading SSIDs file: %v\n", err)
160 log.Println(msg)
161 http.Error(w, msg, http.StatusInternalServerError)
162 return
163 }
164
165 execTemplate(w, managementTemplatePath, ManagementData{Ssids: ssids, Page: "ssids"})
91}166}
92167
93// ConnectHandler reads form got ssid and password and tries to connect to that network168// ConnectHandler reads form got ssid and password and tries to connect to that network
94func ConnectHandler(w http.ResponseWriter, r *http.Request) {169func ConnectHandler(w http.ResponseWriter, r *http.Request) {
95
96 r.ParseForm()170 r.ParseForm()
97171
98 pwd := ""172 pwd := ""
@@ -103,7 +177,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
103177
104 ssids := r.Form["ssid"]178 ssids := r.Form["ssid"]
105 if len(ssids) == 0 {179 if len(ssids) == 0 {
106 log.Print("SSID not available")180 msg := "SSID not available"
181 log.Print(msg)
182 http.Error(w, msg, http.StatusBadRequest)
107 return183 return
108 }184 }
109 ssid := ssids[0]185 ssid := ssids[0]
@@ -116,7 +192,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
116192
117 err := wifiapClient.Disable()193 err := wifiapClient.Disable()
118 if err != nil {194 if err != nil {
119 log.Printf("Error disabling AP: %v", err)195 msg := fmt.Sprintf("Error disabling AP: %v", err)
196 log.Print(msg)
197 http.Error(w, msg, http.StatusInternalServerError)
120 return198 return
121 }199 }
122200
@@ -127,7 +205,9 @@ func ConnectHandler(w http.ResponseWriter, r *http.Request) {
127 err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap)205 err = netmanClient.ConnectAp(ssid, pwd, ap2device, ssid2ap)
128 //TODO signal user in portal on failure to connect206 //TODO signal user in portal on failure to connect
129 if err != nil {207 if err != nil {
130 log.Printf("Failed connecting to %v.", ssid)208 msg := fmt.Sprintf("Failed connecting to %v", ssid)
209 log.Println(msg)
210 http.Error(w, msg, http.StatusInternalServerError)
131 return211 return
132 }212 }
133213
diff --git a/server/handlers_test.go b/server/handlers_test.go
index ad8eda7..336d242 100644
--- a/server/handlers_test.go
+++ b/server/handlers_test.go
@@ -56,6 +56,10 @@ func (c *wifiapClientMock) SetPassphrase(string) error {
56 return nil56 return nil
57}57}
5858
59func (c *wifiapClientMock) Set(map[string]interface{}) error {
60 return nil
61}
62
59type netmanClientMock struct{}63type netmanClientMock struct{}
6064
61func (c *netmanClientMock) GetDevices() []string {65func (c *netmanClientMock) GetDevices() []string {
@@ -111,6 +115,26 @@ func (c *netmanClientMock) ScanAndWriteSsidsToFile(filepath string) bool {
111115
112func TestManagementHandler(t *testing.T) {116func TestManagementHandler(t *testing.T) {
113117
118 // mock settings to not to ask for saving a first configuration
119 utils.ReadConfig = func() (*utils.Config, error) {
120 return &utils.Config{
121 Wifi: &utils.WifiConfig{
122 Ssid: "Ubuntu",
123 Passphrase: "17Soj8/Sxh14lcpD",
124 Interface: "wlp2s0",
125 CountryCode: "0x31",
126 Channel: 6,
127 OperationMode: "g",
128 },
129 Portal: &utils.PortalConfig{
130 Password: "the_password",
131 NoResetCredentials: true,
132 NoOperational: false,
133 },
134 }, nil
135 }
136 utils.MustSetConfig = func() bool { return false }
137
114 ResourcesPath = "../static"138 ResourcesPath = "../static"
115 SsidsFile := "../static/tests/ssids"139 SsidsFile := "../static/tests/ssids"
116 utils.SetSsidsFile(SsidsFile)140 utils.SetSsidsFile(SsidsFile)
diff --git a/server/middleware.go b/server/middleware.go
index a63c717..99bd6f9 100644
--- a/server/middleware.go
+++ b/server/middleware.go
@@ -26,34 +26,8 @@ import (
26 "launchpad.net/wifi-connect/wifiap"26 "launchpad.net/wifi-connect/wifiap"
27)27)
2828
29// Operations interface defining operations implemented by wifiap client29var wifiapClient wifiap.Operations
30type wifiapOperations interface {30var netmanClient netman.Operations
31 Show() (map[string]interface{}, error)
32 Enabled() (bool, error)
33 Enable() error
34 Disable() error
35 SetSsid(string) error
36 SetPassphrase(string) error
37}
38
39type netmanOperations interface {
40 GetDevices() []string
41 GetWifiDevices(devices []string) []string
42 GetAccessPoints(devices []string, ap2device map[string]string) []string
43 ConnectAp(ssid string, p string, ap2device map[string]string, ssid2ap map[string]string) error
44 Ssids() ([]netman.SSID, map[string]string, map[string]string)
45 Connected(devices []string) bool
46 ConnectedWifi(wifiDevices []string) bool
47 DisconnectWifi(wifiDevices []string) int
48 SetIfaceManaged(iface string, state bool, devices []string) string
49 WifisManaged(wifiDevices []string) (map[string]string, error)
50 Unmanage() error
51 Manage() error
52 ScanAndWriteSsidsToFile(filepath string) bool
53}
54
55var wifiapClient wifiapOperations
56var netmanClient netmanOperations
5731
58// Middleware to pre-process web service requests32// Middleware to pre-process web service requests
59func Middleware(inner http.Handler) http.Handler {33func Middleware(inner http.Handler) http.Handler {
diff --git a/server/router.go b/server/router.go
index 47e63f3..20a3716 100644
--- a/server/router.go
+++ b/server/router.go
@@ -28,7 +28,8 @@ func managementHandler() *mux.Router {
28 router := mux.NewRouter()28 router := mux.NewRouter()
2929
30 // Pages routes30 // Pages routes
31 router.HandleFunc("/", ManagementHandler).Methods("GET")31 router.Handle("/", Middleware(http.HandlerFunc(ManagementHandler))).Methods("GET")
32 router.Handle("/config", Middleware(http.HandlerFunc(SaveConfigHandler))).Methods("POST")
32 router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST")33 router.Handle("/connect", Middleware(http.HandlerFunc(ConnectHandler))).Methods("POST")
33 router.HandleFunc("/hashit", HashItHandler).Methods("POST")34 router.HandleFunc("/hashit", HashItHandler).Methods("POST")
34 router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET")35 router.Handle("/refresh", Middleware(http.HandlerFunc(RefreshHandler))).Methods("GET")
diff --git a/snap/scriptlets/country-codes b/snap/scriptlets/country-codes
35new file mode 10064436new file mode 100644
index 0000000..d4e8a97
--- /dev/null
+++ b/snap/scriptlets/country-codes
@@ -0,0 +1,243 @@
1<html><head><title>ISO-3166-1 Country Names</title></head>
2<body bgcolor=#ffffff>
3<h2>ISO-3166-1 Country Names</h2>
4<a href="/iso3166/">Reproduced with permission from ISO</a>
5<p>
6Follow links for ISO-3166-2 subdivision codes
7<p>
8<a href=iso.AD.html>AD</a> : ANDORRA<br>
9<a href=iso.AE.html>AE</a> : UNITED ARAB EMIRATES<br>
10<a href=iso.AF.html>AF</a> : AFGHANISTAN<br>
11<a href=iso.AG.html>AG</a> : ANTIGUA AND BARBUDA<br>
12<a href=iso.AI.html>AI</a> : ANGUILLA<br>
13<a href=iso.AL.html>AL</a> : ALBANIA<br>
14<a href=iso.AM.html>AM</a> : ARMENIA<br>
15<a href=iso.AN.html>AN</a> : NETHERLANDS ANTILLES<br>
16<a href=iso.AO.html>AO</a> : ANGOLA<br>
17<a href=iso.AQ.html>AQ</a> : ANTARCTICA<br>
18<a href=iso.AR.html>AR</a> : ARGENTINA<br>
19<a href=iso.AS.html>AS</a> : AMERICAN SAMOA<br>
20<a href=iso.AT.html>AT</a> : AUSTRIA<br>
21<a href=iso.AU.html>AU</a> : AUSTRALIA<br>
22<a href=iso.AW.html>AW</a> : ARUBA<br>
23<a href=iso.AZ.html>AZ</a> : AZERBAIJAN<br>
24<a href=iso.BA.html>BA</a> : BOSNIA AND HERZEGOVINA<br>
25<a href=iso.BB.html>BB</a> : BARBADOS<br>
26<a href=iso.BD.html>BD</a> : BANGLADESH<br>
27<a href=iso.BE.html>BE</a> : BELGIUM<br>
28<a href=iso.BF.html>BF</a> : BURKINA FASO<br>
29<a href=iso.BG.html>BG</a> : BULGARIA<br>
30<a href=iso.BH.html>BH</a> : BAHRAIN<br>
31<a href=iso.BI.html>BI</a> : BURUNDI<br>
32<a href=iso.BJ.html>BJ</a> : BENIN<br>
33<a href=iso.BM.html>BM</a> : BERMUDA<br>
34<a href=iso.BN.html>BN</a> : BRUNEI DARUSSALAM<br>
35<a href=iso.BO.html>BO</a> : BOLIVIA<br>
36<a href=iso.BR.html>BR</a> : BRAZIL<br>
37<a href=iso.BS.html>BS</a> : BAHAMAS<br>
38<a href=iso.BT.html>BT</a> : BHUTAN<br>
39<a href=iso.BV.html>BV</a> : BOUVET ISLAND<br>
40<a href=iso.BW.html>BW</a> : BOTSWANA<br>
41<a href=iso.BY.html>BY</a> : BELARUS<br>
42<a href=iso.BZ.html>BZ</a> : BELIZE<br>
43<a href=iso.CA.html>CA</a> : CANADA<br>
44<a href=iso.CC.html>CC</a> : COCOS (KEELING) ISLANDS<br>
45<a href=iso.CD.html>CD</a> : CONGO, THE DEMOCRATIC REPUBLIC OF THE<br>
46<a href=iso.CF.html>CF</a> : CENTRAL AFRICAN REPUBLIC<br>
47<a href=iso.CG.html>CG</a> : CONGO<br>
48<a href=iso.CH.html>CH</a> : SWITZERLAND<br>
49<a href=iso.CI.html>CI</a> : C�TE D'IVOIRE<br>
50<a href=iso.CK.html>CK</a> : COOK ISLANDS<br>
51<a href=iso.CL.html>CL</a> : CHILE<br>
52<a href=iso.CM.html>CM</a> : CAMEROON<br>
53<a href=iso.CN.html>CN</a> : CHINA<br>
54<a href=iso.CO.html>CO</a> : COLOMBIA<br>
55<a href=iso.CR.html>CR</a> : COSTA RICA<br>
56<a href=iso.CU.html>CU</a> : CUBA<br>
57<a href=iso.CV.html>CV</a> : CAPE VERDE<br>
58<a href=iso.CX.html>CX</a> : CHRISTMAS ISLAND<br>
59<a href=iso.CY.html>CY</a> : CYPRUS<br>
60<a href=iso.CZ.html>CZ</a> : CZECH REPUBLIC<br>
61<a href=iso.DE.html>DE</a> : GERMANY<br>
62<a href=iso.DJ.html>DJ</a> : DJIBOUTI<br>
63<a href=iso.DK.html>DK</a> : DENMARK<br>
64<a href=iso.DM.html>DM</a> : DOMINICA<br>
65<a href=iso.DO.html>DO</a> : DOMINICAN REPUBLIC<br>
66<a href=iso.DZ.html>DZ</a> : ALGERIA<br>
67<a href=iso.EC.html>EC</a> : ECUADOR<br>
68<a href=iso.EE.html>EE</a> : ESTONIA<br>
69<a href=iso.EG.html>EG</a> : EGYPT<br>
70<a href=iso.EH.html>EH</a> : WESTERN SARARA<br>
71<a href=iso.ER.html>ER</a> : ERITREA<br>
72<a href=iso.ES.html>ES</a> : SPAIN<br>
73<a href=iso.ET.html>ET</a> : ETHIOPIA<br>
74<a href=iso.FI.html>FI</a> : FINLAND<br>
75<a href=iso.FJ.html>FJ</a> : FIJI<br>
76<a href=iso.FK.html>FK</a> : FALKLAND ISLANDS (MALVINAS)<br>
77<a href=iso.FM.html>FM</a> : MICRONESIA, FEDERATED STATES OF<br>
78<a href=iso.FO.html>FO</a> : FAROE ISLANDS<br>
79<a href=iso.FR.html>FR</a> : FRANCE<br>
80<a href=iso.GA.html>GA</a> : GABON<br>
81<a href=iso.GB.html>GB</a> : UNITED KINGDOM<br>
82<a href=iso.GD.html>GD</a> : GRENADA<br>
83<a href=iso.GE.html>GE</a> : GEORGIA<br>
84<a href=iso.GF.html>GF</a> : FRENCH GUIANA<br>
85<a href=iso.GH.html>GH</a> : GHANA<br>
86<a href=iso.GI.html>GI</a> : GIBRALTAR<br>
87<a href=iso.GL.html>GL</a> : GREENLAND<br>
88<a href=iso.GM.html>GM</a> : GAMBIA<br>
89<a href=iso.GN.html>GN</a> : GUINEA<br>
90<a href=iso.GP.html>GP</a> : GUADELOUPE<br>
91<a href=iso.GQ.html>GQ</a> : EQUATORIAL GUINEA<br>
92<a href=iso.GR.html>GR</a> : GREECE<br>
93<a href=iso.GS.html>GS</a> : SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS<br>
94<a href=iso.GT.html>GT</a> : GUATEMALA<br>
95<a href=iso.GU.html>GU</a> : GUAM<br>
96<a href=iso.GW.html>GW</a> : GUINEA-BISSAU<br>
97<a href=iso.GY.html>GY</a> : GUYANA<br>
98<a href=iso.HK.html>HK</a> : HONG KONG<br>
99<a href=iso.HM.html>HM</a> : HEARD ISLAND AND MCDONALD ISLANDS<br>
100<a href=iso.HN.html>HN</a> : HONDURAS<br>
101<a href=iso.HR.html>HR</a> : CROATIA<br>
102<a href=iso.HT.html>HT</a> : HAITI<br>
103<a href=iso.HU.html>HU</a> : HUNGARY<br>
104<a href=iso.ID.html>ID</a> : INDONESIA<br>
105<a href=iso.IE.html>IE</a> : IRELAND<br>
106<a href=iso.IL.html>IL</a> : ISRAEL<br>
107<a href=iso.IN.html>IN</a> : INDIA<br>
108<a href=iso.IO.html>IO</a> : BRITISH INDIAN OCEAN TERRITORY<br>
109<a href=iso.IQ.html>IQ</a> : IRAQ<br>
110<a href=iso.IR.html>IR</a> : IRAN, ISLAMIC REPUBLIC OF<br>
111<a href=iso.IS.html>IS</a> : ICELAND<br>
112<a href=iso.IT.html>IT</a> : ITALY<br>
113<a href=iso.JM.html>JM</a> : JAMAICA<br>
114<a href=iso.JO.html>JO</a> : JORDAN<br>
115<a href=iso.JP.html>JP</a> : JAPAN<br>
116<a href=iso.KE.html>KE</a> : KENYA<br>
117<a href=iso.KG.html>KG</a> : KYRGYZSTAN<br>
118<a href=iso.KH.html>KH</a> : CAMBODIA<br>
119<a href=iso.KI.html>KI</a> : KIRIBATI<br>
120<a href=iso.KM.html>KM</a> : COMOROS<br>
121<a href=iso.KN.html>KN</a> : SAINT KITTS AND NEVIS<br>
122<a href=iso.KP.html>KP</a> : KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF<br>
123<a href=iso.KR.html>KR</a> : KOREA, REPUBLIC OF<br>
124<a href=iso.KW.html>KW</a> : KUWAIT<br>
125<a href=iso.KY.html>KY</a> : CAYMAN ISLANDS<br>
126<a href=iso.KZ.html>KZ</a> : KAZAKHSTAN<br>
127<a href=iso.LA.html>LA</a> : LAO PEOPLE'S DEMOCRATIC REPUBLIC<br>
128<a href=iso.LB.html>LB</a> : LEBANON<br>
129<a href=iso.LC.html>LC</a> : SAINT LUCIA<br>
130<a href=iso.LI.html>LI</a> : LIECHTENSTEIN<br>
131<a href=iso.LK.html>LK</a> : SRI LANKA<br>
132<a href=iso.LR.html>LR</a> : LIBERIA<br>
133<a href=iso.LS.html>LS</a> : LESOTHO<br>
134<a href=iso.LT.html>LT</a> : LITHUANIA<br>
135<a href=iso.LU.html>LU</a> : LUXEMBOURG<br>
136<a href=iso.LV.html>LV</a> : LATVIA<br>
137<a href=iso.LY.html>LY</a> : LIBYAN ARAB JAMABIRIYA<br>
138<a href=iso.MA.html>MA</a> : MOROCCO<br>
139<a href=iso.MC.html>MC</a> : MONACO<br>
140<a href=iso.MD.html>MD</a> : MOLDOVA, REPUBLIC OF<br>
141<a href=iso.MG.html>MG</a> : MADAGASCAR<br>
142<a href=iso.MH.html>MH</a> : MARSHALL ISLANDS<br>
143<a href=iso.MK.html>MK</a> : MACEDONIA, THE FORMER YUGOSLAV REPU8LIC OF<br>
144<a href=iso.ML.html>ML</a> : MALI<br>
145<a href=iso.MM.html>MM</a> : MYANMAR<br>
146<a href=iso.MN.html>MN</a> : MONGOLIA<br>
147<a href=iso.MO.html>MO</a> : MACAU<br>
148<a href=iso.MP.html>MP</a> : NORTHERN MARIANA ISLANDS<br>
149<a href=iso.MQ.html>MQ</a> : MARTINIQUE<br>
150<a href=iso.MR.html>MR</a> : MAURITANIA<br>
151<a href=iso.MS.html>MS</a> : MONTSERRAT<br>
152<a href=iso.MT.html>MT</a> : MALTA<br>
153<a href=iso.MU.html>MU</a> : MAURITIUS<br>
154<a href=iso.MV.html>MV</a> : MALDIVES<br>
155<a href=iso.MW.html>MW</a> : MALAWI<br>
156<a href=iso.MX.html>MX</a> : MEXICO<br>
157<a href=iso.MY.html>MY</a> : MALAYSIA<br>
158<a href=iso.MZ.html>MZ</a> : MOZAMBIQUE<br>
159<a href=iso.NA.html>NA</a> : NAMIBIA<br>
160<a href=iso.NC.html>NC</a> : NEW CALEDONIA<br>
161<a href=iso.NE.html>NE</a> : NIGER<br>
162<a href=iso.NF.html>NF</a> : NORFOLK ISLAND<br>
163<a href=iso.NG.html>NG</a> : NIGERIA<br>
164<a href=iso.NI.html>NI</a> : NICARAGUA<br>
165<a href=iso.NL.html>NL</a> : NETHERLANDS<br>
166<a href=iso.NO.html>NO</a> : NORWAY<br>
167<a href=iso.NP.html>NP</a> : NEPAL<br>
168<a href=iso.NU.html>NU</a> : NIUE<br>
169<a href=iso.NZ.html>NZ</a> : NEW ZEALAND<br>
170<a href=iso.OM.html>OM</a> : OMAN<br>
171<a href=iso.PA.html>PA</a> : PANAMA<br>
172<a href=iso.PE.html>PE</a> : PERU<br>
173<a href=iso.PF.html>PF</a> : FRENCH POLYNESIA<br>
174<a href=iso.PG.html>PG</a> : PAPUA NEW GUINEA<br>
175<a href=iso.PH.html>PH</a> : PHILIPPINES<br>
176<a href=iso.PK.html>PK</a> : PAKISTAN<br>
177<a href=iso.PL.html>PL</a> : POLAND<br>
178<a href=iso.PM.html>PM</a> : SAINT PIERRE AND MIQUELON<br>
179<a href=iso.PN.html>PN</a> : PITCAIRN<br>
180<a href=iso.PR.html>PR</a> : PUERTO RICO<br>
181<a href=iso.PT.html>PT</a> : PORTUGAL<br>
182<a href=iso.PW.html>PW</a> : PALAU<br>
183<a href=iso.PY.html>PY</a> : PARAGUAY<br>
184<a href=iso.QA.html>QA</a> : QATAR<br>
185<a href=iso.RE.html>RE</a> : R�UNION<br>
186<a href=iso.RO.html>RO</a> : ROMANIA<br>
187<a href=iso.RU.html>RU</a> : RUSSIAN FEDERATION<br>
188<a href=iso.RW.html>RW</a> : RWANDA<br>
189<a href=iso.SA.html>SA</a> : SAUDI ARABIA<br>
190<a href=iso.SB.html>SB</a> : SOLOMON ISLANDS<br>
191<a href=iso.SC.html>SC</a> : SEYCHELLES<br>
192<a href=iso.SD.html>SD</a> : SUDAN<br>
193<a href=iso.SE.html>SE</a> : SWEDEN<br>
194<a href=iso.SG.html>SG</a> : SINGAPORE<br>
195<a href=iso.SH.html>SH</a> : SAINT HELENA<br>
196<a href=iso.SI.html>SI</a> : SLOVENIA<br>
197<a href=iso.SJ.html>SJ</a> : SVALBARD AND JAN MAYEN<br>
198<a href=iso.SK.html>SK</a> : SLOVAKIA<br>
199<a href=iso.SL.html>SL</a> : SIERRA LEONE<br>
200<a href=iso.SM.html>SM</a> : SAN MARINO<br>
201<a href=iso.SN.html>SN</a> : SENEGAL<br>
202<a href=iso.SO.html>SO</a> : SOMALIA<br>
203<a href=iso.SR.html>SR</a> : SURINAME<br>
204<a href=iso.ST.html>ST</a> : SAO TOME AND PRINCIPE<br>
205<a href=iso.SV.html>SV</a> : EL SALVADOR<br>
206<a href=iso.SY.html>SY</a> : SYRIAN ARAB REPUBLIC<br>
207<a href=iso.SZ.html>SZ</a> : SWAZILAND<br>
208<a href=iso.TC.html>TC</a> : TURKS AND CAICOS ISLANDS<br>
209<a href=iso.TD.html>TD</a> : CHAD<br>
210<a href=iso.TF.html>TF</a> : FRENCH SOUTHERN TERRITORIES<br>
211<a href=iso.TG.html>TG</a> : TOGO<br>
212<a href=iso.TH.html>TH</a> : THAILAND<br>
213<a href=iso.TJ.html>TJ</a> : TAJIKISTAN<br>
214<a href=iso.TK.html>TK</a> : TOKELAU<br>
215<a href=iso.TM.html>TM</a> : TURKMENISTAN<br>
216<a href=iso.TN.html>TN</a> : TUNISIA<br>
217<a href=iso.TO.html>TO</a> : TONGA<br>
218<a href=iso.TP.html>TP</a> : EAST TIMOR<br>
219<a href=iso.TR.html>TR</a> : TURKEY<br>
220<a href=iso.TT.html>TT</a> : TRINIDAD AND TOBAGO<br>
221<a href=iso.TV.html>TV</a> : TUVALU<br>
222<a href=iso.TW.html>TW</a> : TAIWAN, PROVINCE OF CHINA<br>
223<a href=iso.TZ.html>TZ</a> : TANZANIA, UNITED REPUBLIC OF<br>
224<a href=iso.UA.html>UA</a> : UKRAINE<br>
225<a href=iso.UG.html>UG</a> : UGANDA<br>
226<a href=iso.UM.html>UM</a> : UNITED STATES MINOR OUTLYING ISLANDS<br>
227<a href=iso.US.html>US</a> : UNITED STATES<br>
228<a href=iso.UY.html>UY</a> : URUGUAY<br>
229<a href=iso.UZ.html>UZ</a> : UZBEKISTAN<br>
230<a href=iso.VE.html>VE</a> : VENEZUELA<br>
231<a href=iso.VG.html>VG</a> : VIRGIN ISLANDS, BRITISH<br>
232<a href=iso.VI.html>VI</a> : VIRGIN ISLANDS, U.S.<br>
233<a href=iso.VN.html>VN</a> : VIET NAM<br>
234<a href=iso.VU.html>VU</a> : VANUATU<br>
235<a href=iso.WF.html>WF</a> : WALLIS AND FUTUNA<br>
236<a href=iso.WS.html>WS</a> : SAMOA<br>
237<a href=iso.YE.html>YE</a> : YEMEN<br>
238<a href=iso.YT.html>YT</a> : MAYOTTE<br>
239<a href=iso.YU.html>YU</a> : YUGOSLAVIA<br>
240<a href=iso.ZA.html>ZA</a> : SOUTH AFRICA<br>
241<a href=iso.ZM.html>ZM</a> : ZAMBIA<br>
242<a href=iso.ZW.html>ZW</a> : ZIMBABWE<br>
243</body></html>
diff --git a/snap/scriptlets/fetch_country_codes.sh b/snap/scriptlets/fetch_country_codes.sh
0new file mode 100755244new file mode 100755
index 0000000..8dae525
--- /dev/null
+++ b/snap/scriptlets/fetch_country_codes.sh
@@ -0,0 +1,10 @@
1#!/bin/sh
2# The purpose of this script is taking ISO-3166 country codes and
3# store them locally to be used in compilation time.
4# This operation must be executed by hand when wanted to update
5# current ones shown in config page
6set -e
7
8cd "$(dirname "$0")"
9
10curl -X GET http://geotags.com/iso3166/countries.html > country-codes
0\ No newline at end of file11\ No newline at end of file
diff --git a/snap/scriptlets/fill_country_codes.sh b/snap/scriptlets/fill_country_codes.sh
1new file mode 10075512new file mode 100755
index 0000000..451c61b
--- /dev/null
+++ b/snap/scriptlets/fill_country_codes.sh
@@ -0,0 +1,39 @@
1#!/bin/sh
2# The purpose of this script is taking ISO-3166 country codes from country_code file
3# so that they can be updated in management portal when snap is built
4# Process is easy, get it from remote path, format using sed, and replace
5# html page were they will be used
6
7set -e
8
9COUNTRY_CODES=../../../snap/scriptlets/country-codes
10
11if [ ! -e $COUNTRY_CODES ]; then
12 echo "======================================================="
13 echo "Could not find country_codes needed file for compiling."
14 echo "Please, before building snap for the first time execute by hand:"
15 echo ""
16 echo "snap/scriptlets/fetch_country_codes.sh"
17 echo ""
18 echo "======================================================="
19 exit 1
20fi
21
22# think that this is a scriptlet, executed in parts/<the_part>/build folder
23cp $COUNTRY_CODES .
24
25# remove non processable lines
26sed -i '/^<a href=iso/!d' country-codes
27
28# process lines to change its format to be html select options
29sed -i '/<a href=iso/ s/<a href=iso.*html>/<option value="/
30s/<\/a>\ :\ /">/
31s/<br>/<\/option>/' country-codes
32
33# add world wide default option at beginning
34sed -i '1s;^;<option value="XX">\-WORLD WIDE\-<\/option>\n;' country-codes
35
36# replace mark in management.html template with real country-codes
37sed -e '/\[COUNTRY_CODE_OPTIONS\]/ {' -e 'r country-codes' -e 'd' -e '}' -i static/templates/management.html
38
39echo "country codes filled ok"
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 12f6cbc..3a7cf44 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -56,5 +56,6 @@ parts:
56 assets:56 assets:
57 plugin: dump57 plugin: dump
58 source: .58 source: .
59 prepare: ../../../snap/scriptlets/fill_country_codes.sh
59 stage:60 stage:
60 - static61 - static
diff --git a/static/css/application.css b/static/css/application.css
index ab1e598..b0edc63 100644
--- a/static/css/application.css
+++ b/static/css/application.css
@@ -1 +1 @@
1@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}
2\ No newline at end of file1\ No newline at end of file
2[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}
3\ No newline at end of file3\ No newline at end of file
diff --git a/static/sass/application.scss b/static/sass/application.scss
index fc671d2..7a3bd95 100644
--- a/static/sass/application.scss
+++ b/static/sass/application.scss
@@ -1,7 +1,4 @@
1// Import the theme1@import 'node_modules/vanilla-framework/scss/build';
2@import '../../node_modules/ubuntu-vanilla-theme/scss/theme';
3// Run the theme
4@include ubuntu-vanilla-theme;
52
6// Customise theme3// Customise theme
7.collapse{4.collapse{
@@ -21,4 +18,11 @@
2118
22.cheshire {19.cheshire {
23 display: none;20 display: none;
24}
25\ No newline at end of file21\ No newline at end of file
22}
23
24.required label:after {
25 color: #e32;
26 content: ' *';
27 display:inline;
28}
29
diff --git a/static/templates/connecting.html b/static/templates/connecting.html
index d21a0c1..e0f8fb3 100644
--- a/static/templates/connecting.html
+++ b/static/templates/connecting.html
@@ -12,11 +12,14 @@
12<body>12<body>
13 <div class="wrapper">13 <div class="wrapper">
14 <div id="main-content" class="inner-wrapper">14 <div id="main-content" class="inner-wrapper">
15 <br/>
15 <div class="row no-border" id="grid">16 <div class="row no-border" id="grid">
16 <h3>Connecting...</h3>17 <div class="p-notification" id="grid">
17 <div class="twelve-col box" style="background-color: #eee">18 <p class="p-notification__response">
18 <p>Device is now attempting to connect to <b>{{.Ssid}}</b>. <br/>19 <span class="p-notification__status">Connecting...</span>
20 Device is now attempting to connect to <b>{{.Ssid}}</b>. <br/>
19 You may try to connect to the device through the new connection.</p>21 You may try to connect to the device through the new connection.</p>
22 </p>
20 </div>23 </div>
21 </div>24 </div>
22 </div>25 </div>
diff --git a/static/templates/management.html b/static/templates/management.html
index 6a95224..ed554fb 100644
--- a/static/templates/management.html
+++ b/static/templates/management.html
@@ -12,73 +12,185 @@
12<body>12<body>
13 <div class="wrapper">13 <div class="wrapper">
14 <div id="main-content" class="inner-wrapper">14 <div id="main-content" class="inner-wrapper">
15 <br/>
15 <div class="row no-border" id="login">16 <div class="row no-border" id="login">
16 <ul class="no-bullets">17 <fieldset>
17 <li><p>Password:</p></li>18 <div class="p-form-validation" id="passphrasePage_form">
18 <li><input type="password" id="passphrasePage"/></li>19 <label for="passphrasePage">Password:</label>
19 <li>20 <input class="p-form-validation__input" type="password" id="passphrasePage"/>
20 <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>21 <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>
21 <label for="showpassphrasePage">show passphrase</label>22 <label for="showpassphrasePage">show passphrase</label>
22 </li>23 </div>
23 <li><input type="button" value="Continue" onclick="authenticate()"/></li>24 <div class="p-notification--negative cheshire" id="passphrasePage_error">
24 </ul>25 <p class="p-notification__response">
26 <span class="p-notification__status">Error:</span> <text id="passphrasePage_error_msg"></text>
27 </p>
28 </div>
29 <button class="p-button--positive" onclick="authenticate()"/>Continue</button>
30 </fieldset>
25 </div>31 </div>
2632
27 <div class="row no-border" id="grid">33 {{if eq .Page "ssids"}}
28 <h2>Select Wi-Fi to connect to:</h2>34 <div class="row no-border" id="grid">
29 <fieldset>35 <h2>Select Wi-Fi to connect to:</h2>
30 <br/>36 <ul class="p-list--divided">
31 <table>
32 {{range $i, $ssid := .Ssids}}37 {{range $i, $ssid := .Ssids}}
33 <tr>38 <li class="p-list__item">
34 <th scope="row">39 <label class="collapse" for="radio{{$i}}">{{$ssid}}</label>
35 <label class="collapse" for="radio{{$i}}">{{$ssid}}</label>40 <input id="radio{{$i}}" type="radio" name="c1">
36 <input id="radio{{$i}}" type="radio" name="c1">41 <div>
37 <div class="six-col">42 <br/>
38 <fieldset>43 <fieldset>
39 <ul class="no-bullets">44 <input type="hidden" id="ssid{{$i}}" value="{{$ssid}}"/>
40 <li>45 <label for="passphrase">Enter passphrase:</label>
41 <input type="hidden" id="ssid{{$i}}" value="{{$ssid}}"/>46 <input type="password" id="passphrase{{$i}}"/>
42 <label for="passphrase">Enter passphrase:</label>47 <input type="checkbox" id="showpassphrase{{$i}}" onchange="show_passphrase({{$i}})"/>
43 <input type="password" id="passphrase{{$i}}"/>48 <label for="showpassphrase">show passphrase</label>
44 </li>49 <br/>
45 <li>50 <div id="alert{{$i}}" class="p-notification--caution cheshire">
46 <input type="checkbox" id="showpassphrase{{$i}}" onchange="show_passphrase({{$i}})"/>51 <p class="p-notification__response">
47 <label for="showpassphrase">show passphrase</label>52 <span class="p-notification__status">Warning:</span>
48 </li>53 Continuing will disconnect you from the device by taking down its Wi-Fi network.
49 <li>54 This page will be unavailable. After continuing, you may connect to the device
50 <div id="alert{{$i}}" class="cheshire box" style="background-color: #eee">55 through its new Wi-Fi connection. Proceed?.
51 <h3>Caution!</h3>56 </p>
52 <p>Continuing will disconnect you from the device by taking down its Wi-Fi network.
53 This page will be unavailable. After continuing, you may connect to the device
54 through its new Wi-Fi connection. Proceed?.</p>
55 </div>
56 </li>
57 <li>
58 <input type="button" value="Cancel" class="button--primary" onclick="clear_row({{$i}})"/>
59 <input type="button" id="connect{{$i}}" value="Connect" class="button--primary" onclick="do_connect({{$i}})"/>
60 </li>
61 </ul>
62 </fieldset>
63 </div>57 </div>
64 </th>58 <button class="p-button--neutral" onclick="clear_row({{$i}})">Cancel</button>
65 </tr>59 <input type="button" class="p-button--positive" id="connect{{$i}}" value="Connect" onclick="do_connect({{$i}})"/>
60 </fieldset>
61 </div>
62 </li>
66 {{end}}63 {{end}}
67 </table>
6864
69 <div class="box" style="background-color: #eee">65 </ul>
70 <h3>Warning</h3>66
71 <p>This takes down the device AP and disconnects you. Please reconnect in a minute or two</p>67 <div class="p-notification--caution">
72 <input type="button" value="Refresh Wi-Fi Access Points" class="button--primary" onclick="do_refresh()"/>68 <p class="p-notification__response">
69 <span class="p-notification__status">Warning:</span>
70 This takes down the device AP and disconnects you. Please reconnect in a minute or two
71 </p>
72 <br/>
73 <button class="p-button--neutral" onclick="do_refresh()">Refresh Wi-Fi Access Points</button>
73 </div>74 </div>
75 </div>
76
77 {{else if eq .Page "config"}}
78 <div class="row no-border" id="grid">
79 <h2>First configuration</h2>
80 <br/>
81 <fieldset>
82 <h4>Wi-Fi Access Point</h4>
83 <div class="p-form-validation" id="ssid_form">
84 <label for="ssid">SSID:</label>
85 <input class="p-form-validation__input" type="text" id="ssid" required="required" value="{{.Config.Wifi.Ssid}}" placeholder="Enter SSID"/>
86 </div>
87 <div class="p-notification--negative cheshire" id="ssid_error">
88 <p class="p-notification__response">
89 <span class="p-notification__status">Error:</span> <text id="ssid_error_msg"></text>
90 </p>
91 </div>
92 <div class="p-form-validation required" id="passphrase_form">
93 <label for="passphrase">Passphrase:</label>
94 <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"/>
95 <label for="passphrase_confirm">Confirm passphrase:</label>
96 <input class="p-form-validation__input" type="password" id="passphrase_confirm" required="required" onblur="validate_password('passphrase','Passphrase',8,63)" placeholder="Confirm passphrase"/>
97 </div>
98 <div class="p-notification--negative cheshire" id="passphrase_error">
99 <p class="p-notification__response">
100 <span class="p-notification__status">Error:</span> <text id="passphrase_error_msg"></text>
101 </p>
102 </div>
103 <div class="p-form-validation" id="interface_form">
104 <label for="interface">Interface:</label>
105 <input type="text" id="interface" required="required" value="{{.Config.Wifi.Interface}}" placeholder="Enter network interface"/>
106 </div>
107 <div class="p-notification--negative cheshire" id="interface_error">
108 <p class="p-notification__response">
109 <span class="p-notification__status">Error:</span> <text id="interface_error_msg"></text>
110 </p>
111 </div>
112 <label for="country_code">Country Code:</label>
113 <select id="country_code">
114 [COUNTRY_CODE_OPTIONS]
115 </select>
116 <label for="channel">Channel:</label>
117 <select id="channel">
118 <option value="1">1</option>
119 <option value="2">2</option>
120 <option value="3">3</option>
121 <option value="4">4</option>
122 <option value="5">5</option>
123 <option value="6">6</option>
124 <option value="7">7</option>
125 <option value="8">8</option>
126 <option value="9">9</option>
127 <option value="10">10</option>
128 <option value="11">11</option>
129 <option value="12">12</option>
130 <option value="13">13</option>
131 <option value="14">14</option>
132 </select>
133 <label for="operation_mode">Operation mode:</label>
134 <select id="operation_mode">
135 <option value="a">IEEE 802.11a (5 GHz)</option>
136 <option value="b">IEEE 802.11b (2.4 GHz)</option>
137 <option value="g">IEEE 802.11g (2.4 GHz)</option>
138 <option value="ad">IEEE 802.11ad (60 GHz)</option>
139 </select>
140 </fieldset>
74141
142 <br/>
143
144 <fieldset>
145 <h4>Web Portal</h4>
146 <div class="p-form-validation required" id="portal_pwd_form">
147 <label for="portal_pwd">Portal password:</label>
148 <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)"/>
149 <label for="portal_pwd_confirm">Confirm portal password:</label>
150 <input class="p-form-validation__input" type="password" id="portal_pwd_confirm" required="required" onblur="validate_password('portal_pwd','Portal password',8)"/>
151 </div>
152 <div class="p-notification--negative cheshire" id="portal_pwd_error">
153 <p class="p-notification__response">
154 <span class="p-notification__status">Error:</span> <text id="portal_pwd_error_msg"></text>
155 </p>
156 </div>
75 </fieldset>157 </fieldset>
158
159 <br/>
160
161 <div class="p-notification--caution">
162 <p class="p-notification__response">
163 <span class="p-notification__status">Warning:</span>
164 This may take down the device Wi-Fi AP and disconnect you. Please reconnect in a minute or two
165 </p>
166 <br/>
167 <button class="p-button--positive" onclick="saveConfig()">Apply configuration</button>
168 </div>
169 <div class="p-notification--negative cheshire" id="saveConfig_error">
170 <p class="p-notification__response">
171 <span class="p-notification__status">Error:</span><text id="saveConfig_error_msg">Could not save configuration. Server error.</text>
172 </p>
173 </div>
76 </div>174 </div>
77 175 {{end}}
78 <div class="row no-border" id="refreshingAlert">176
79 <h3>Refreshing Wi-Fi access points...</h3>177 <div class="row no-border">
80 <div class="twelve-col box" style="background-color: #eee">178 <div class="p-notification cheshire" id="refreshingAlert">
81 <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>179 <p class="p-notification__response">
180 <span class="p-notification__status">Refreshing Wi-Fi access points...</span>
181 You have been disconnected. The device is refreshing Wi-Fi access points.
182 Please rejoin the device access point and reload this page in a minute or two.
183 </p>
184 </div>
185 </div>
186
187 <div class="row no-border">
188 <div class="p-notification cheshire" id="savingConfigAlert">
189 <p class="p-notification__response">
190 <span class="p-notification__status">Saving configuration...</span>
191 You have been disconnected. The device is saving configuration.
192 This takes down the device AP and disconnects you. Please reconnect in a minute or two
193 </p>
82 </div>194 </div>
83 </div>195 </div>
84196
@@ -95,7 +207,37 @@
95 $(document).ready(function() {207 $(document).ready(function() {
96 $('#grid').css('display', 'none')208 $('#grid').css('display', 'none')
97 $('#refreshingAlert').css('display', 'none')209 $('#refreshingAlert').css('display', 'none')
210 $('#savingConfigAlert').css('display', 'none')
211
212 // enable submit password when enter is pressed for login div
213 $('#passphrasePage').keydown(function(event) {
214 if (event.keyCode == 13) {
215 authenticate()
216 return false;
217 }
218 });
219
220 {{if eq .Page "config"}}
221 // config specific initial values for combo boxes
222 $('#channel').val('{{.Config.Wifi.Channel}}')
223 $('#country_code').val('{{.Config.Wifi.CountryCode}}')
224 $('#operation_mode').val('{{.Config.Wifi.OperationMode}}')
225 {{end}}
226
227 // country codes select must be sorted as originally it is, but by value, not by text
228 sortCountryCodes();
98 })229 })
230 function sortCountryCodes() {
231 var cc_options = $("#country_code option");
232 var selected = $("#country_code").val();
233
234 cc_options.sort(function(a,b) {
235 return a.text.localeCompare(b.text)
236 })
237
238 $("#country_code").empty().append(cc_options);
239 $("#country_code").val(selected);
240 }
99 function showSsids() {241 function showSsids() {
100 $('#login').css('display', 'none'); 242 $('#login').css('display', 'none');
101 $('#grid').css('display', 'block');243 $('#grid').css('display', 'block');
@@ -105,26 +247,113 @@
105 $('#refreshingAlert').css('display', 'block');247 $('#refreshingAlert').css('display', 'block');
106 }248 }
107 function authenticate() {249 function authenticate() {
108 var pw = $('#passphrasePage').val();250 $.ajax({
109 if ( pw.length < 8) {251 type: "POST",
110 alert("Password must be at least 8 characters long");252 url: "/hashit",
111 } else {253 data: {Hash: $('#passphrasePage').val()}
112 $.ajax({254 }).done(function (hashRet) {
113 type: "POST",255 console.log("in ajax done.", hashRet);
114 url: "/hashit",256 hash = JSON.parse(hashRet);
115 data: {Hash: pw}257 console.log(hash)
116 }).done(function (hashRet) {258 if (hash.HashMatch) {
117 console.log("in ajax done.", hashRet);259 showSsids();
118 hash = JSON.parse(hashRet);260 } else {
119 console.log(hash)261 $('#passphrasePage_form').addClass('is-error')
120 if (hash.HashMatch) {262 $('#passphrasePage_error_msg').text('Your password does not match, please try again')
121 showSsids();263 $('#passphrasePage_error').css('display', 'block')
122 } else {264 }
123 alert("Your password does not match, please try again");265 })
124 }
125 })
126 }
127 } 266 }
267 function saveConfig() {
268 if ($('#ssid').val().length == 0) {
269 $('#ssid_form').addClass('is-error')
270 $('#ssid_error_msg').text('SSID cannot be empty')
271 $('#ssid_error').css('display', 'block')
272 $('#saveConfig_error').css('display', 'block')
273 $('#saveConfig_error_msg').text('Please, correct SSID field error before applying configuration')
274 return
275 } else {
276 $('#ssid_form').removeClass('is-error')
277 $('#ssid_error').css('display', 'none')
278 }
279
280 if (!validate_password('passphrase', 'Passphrase', 8, 63)) {
281 $('#saveConfig_error').css('display', 'block')
282 $('#saveConfig_error_msg').text('Please, correct Wi-Fi passphrase error before applying configuration')
283 return
284 }
285
286 if ($('#interface').val().length == 0) {
287 $('#interface_form').addClass('is-error')
288 $('#interface_error_msg').text('Interface cannot be empty')
289 $('#interface_error').css('display', 'block')
290 $('#saveConfig_error').css('display', 'block')
291 $('#saveConfig_error_msg').text('Please, correct Wi-Fi interface error before applying configuration')
292 return
293 } else {
294 $('#interface_form').removeClass('is-error')
295 $('#interface_error').css('display', 'none')
296 }
297
298 if (!validate_password('portal_pwd', 'Portal password', 8)) {
299 $('#saveConfig_error').css('display', 'block')
300 $('#saveConfig_error_msg').text('Please, correct Portal password error before applying configuration')
301 return
302 }
303 $.ajax({
304 type: "POST",
305 url: "/config",
306 data: {
307 Ssid: $('#ssid').val(),
308 Passphrase: $('#passphrase').val(),
309 Interface:$('#interface').val(),
310 CountryCode: $('#country_code').val(),
311 Channel: $('#channel').val(),
312 OperationMode: $('#operation_mode').val(),
313 PortalPassword: $('#portal_pwd').val()
314 }
315 }).done(function (response) {
316 $('#grid').css('display', 'none');
317 $('#saveConfig_error').css('display', 'none')
318 $('#savingConfigAlert').css('display', 'block')
319 }).fail(function(xhr, status, error) {
320 $('#saveConfig_error').css('display', 'block')
321 $('#saveConfig_error_msg').text(xhr.responseText)
322 });
323 }
324 // function to confirm password, and confirmation are equals.
325 // tag param is the name of primary input field for password.
326 // title human readable name of the field validating here
327 // n param is the min length to consider
328 // m param is the max length allowed
329 function validate_password(tag, title, n, m) {
330 var pwd = document.getElementById(tag)
331 var pwd_confirm = document.getElementById(tag + '_confirm')
332 //validate length
333 if (n !== undefined && pwd.value.length < n) {
334 $('#' + tag + "_form").addClass('is-error')
335 $('#' + tag + '_error_msg').text( title + ' is too short. Must have ' + n + ' characters at least')
336 $('#' + tag + '_error').css('display', 'block')
337 return false
338 }
339 if (m !== undefined && pwd.value.length > m) {
340 $('#' + tag + "_form").addClass('is-error')
341 $('#' + tag + '_error_msg').text( title + ' is too long. Must have ' + m + ' characters at most')
342 $('#' + tag + '_error').css('display', 'block')
343 return false
344 }
345 //validate confirm password field matches original
346 if (pwd.value != pwd_confirm.value) {
347 $('#' + tag + "_form").addClass('is-error')
348 $('#' + tag + '_error_msg').text('Passwords don\'t match')
349 $('#' + tag + '_error').css('display', 'block')
350 return false
351 }
352 // default
353 $('#' + tag + "_form").removeClass('is-error')
354 $('#' + tag + '_error').css('display', 'none')
355 return true
356 }
128</script>357</script>
129358
130359
diff --git a/static/templates/operational.html b/static/templates/operational.html
index 1d859c5..27d4f94 100644
--- a/static/templates/operational.html
+++ b/static/templates/operational.html
@@ -11,30 +11,52 @@
11<body>11<body>
12 <div class="wrapper">12 <div class="wrapper">
13 <div id="main-content" class="inner-wrapper">13 <div id="main-content" class="inner-wrapper">
14 <div class="row no-border" id="login">14 <br/>
15 <ul class="no-bullets">15 <div class="row no-border" id="login">
16 <li><p>Password:</p></li>16 <fieldset>
17 <li><input type="password" id="passphrasePage"/></li>17 <div class="p-form-validation" id="passphrasePage_form">
18 <li>18 <label for="passphrasePage">Password:</label>
19 <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>19 <input class="p-form-validation__input" type="password" id="passphrasePage"/>
20 <label for="showpassphrasePage">show passphrase</label>20 <input type="checkbox" id="showpassphrasePage" onchange="show_passphrase('Page')"/>
21 </li>21 <label for="showpassphrasePage">show passphrase</label>
22 <li><input type="button" value="Continue" onclick="authenticate()"/></li>22 </div>
23 </ul>23 <div class="p-notification--negative cheshire" id="passphrasePage_error">
24 </div>24 <p class="p-notification__response">
25 <div class="row no-border" id="grid">25 <span class="p-notification__status">Error:</span> <text id="passphrasePage_error_msg"></text>
26 <h2>Connected!</h2>26 </p>
27 <p>The device is connected to an external WiFi AP</p>27 </div>
28 <p>Click below to disconnect. Then, join the device Wifi AP, where you can select a new external AP to connect to.</p>28 <button class="p-button--positive" onclick="authenticate()"/>Continue</button>
29 <input type="button" id="disconnect" value="Disconnect from Wifi" class="button--primary" onclick="disconnect()"/>29 </fieldset>
30 </div>30 </div>
31 </div>31
32 <div class="row no-border">
33 <div class="p-notification--positive" id="grid">
34 <p class="p-notification__response">
35 <span class="p-notification__status">Connected!</span>
36 The device is connected to an external WiFi AP.<br/>
37 Click below to disconnect. Then, join the device Wifi AP,
38 where you can select a new external AP to connect to.
39 </p>
40 <br/>
41 <button class="p-button--negative" id="disconnect" onclick="disconnect()">Disconnect from Wi-Fi</button>
42 </div>
43 </div>
44
32 </div>45 </div>
3346
34<script>47<script>
35 $(document).ready(function(){48 $(document).ready(function(){
36 $('#grid').css('display', 'none') 49 $('#grid').css('display', 'none')
50
51 // enable submit password when enter is pressed for login div
52 $('#passphrasePage').keydown(function(event) {
53 if (event.keyCode == 13) {
54 authenticate()
55 return false;
56 }
57 });
37 })58 })
59
38 function showOper() {60 function showOper() {
39 $('#login').css('display', 'none'); 61 $('#login').css('display', 'none');
40 $('#grid').css('display', 'block'); 62 $('#grid').css('display', 'block');
@@ -45,37 +67,33 @@
45 document.getElementById('passphrase'+i).type = type67 document.getElementById('passphrase'+i).type = type
46 }68 }
4769
48 function authenticate() {70 function authenticate() {
49 var pw = $('#passphrasePage').val();71 $.ajax({
50 if ( pw.length < 8) {72 type: "POST",
51 alert("Password must be at least 8 characters long");73 url: "/hashit",
52 } else {74 data: {Hash: $('#passphrasePage').val()}
53 $.ajax({75 }).done(function (hashRet) {
54 type: "POST",76 console.log("in ajax done.", hashRet);
55 url: "/hashit",77 hash = JSON.parse(hashRet);
56 data: {Hash: pw}78 console.log(hash)
57 }).done(function (hashRet) {79 if (hash.HashMatch) {
58 console.log("in ajax done.", hashRet);80 showOper();
59 hash = JSON.parse(hashRet);81 } else {
60 console.log(hash)82 $('#passphrasePage_form').addClass('is-error')
61 if (hash.HashMatch) {83 $('#passphrasePage_error_msg').text('Your password does not match, please try again')
62 showOper();84 $('#passphrasePage_error').css('display', 'block')
63 } else {
64 alert("Your password does not match, please try again");
65 }
66 })
67 }85 }
86 })
68 } 87 }
6988
70function disconnect() {89 function disconnect() {
71 $.ajax({90 $.ajax({
72 url: "/disconnect"91 url: "/disconnect"
73 }).done(function () {92 }).done(function () {
74 console.log("in ajax done.");93 console.log("in ajax done.");
75
76 })
77}
7894
95 })
96 }
79</script>97</script>
8098
81</body>99</body>
diff --git a/utils/config.go b/utils/config.go
82new file mode 100644100new file mode 100644
index 0000000..6aac0f3
--- /dev/null
+++ b/utils/config.go
@@ -0,0 +1,235 @@
1package utils
2
3import (
4 "encoding/json"
5 "fmt"
6 "io/ioutil"
7 "log"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12
13 "launchpad.net/wifi-connect/wifiap"
14)
15
16var configFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")
17var mustConfigFlagFile = filepath.Join(os.Getenv("SNAP_COMMON"), ".config_done.flag")
18
19var wifiapClient wifiap.Operations = wifiap.DefaultClient()
20
21// Config this project config got from wifi-ap + custom wifi-connect params
22type Config struct {
23 Wifi *WifiConfig
24 Portal *PortalConfig
25}
26
27// WifiConfig config specific parameters for wifi configuration
28type WifiConfig struct {
29 Ssid string `json:"wifi.ssid"`
30 Passphrase string `json:"wifi.security-passphrase"`
31 Interface string `json:"wifi.interface"`
32 CountryCode string `json:"wifi.country-code"`
33 Channel int `json:"wifi.channel"`
34 OperationMode string `json:"wifi.operation-mode"`
35}
36
37// PortalConfig config specific parameters for portals configuration
38type PortalConfig struct {
39 Password string //`json:"portal.password"`
40 NoResetCredentials bool `json:"portal.no-reset-creds"`
41 NoOperational bool `json:"portal.no-operational"`
42}
43
44func (c *Config) String() string {
45 s := []string{
46 strings.Join([]string{"--Wifi--", c.Wifi.String()}, "\n"),
47 strings.Join([]string{"--Portal--", c.Portal.String()}, "\n"),
48 }
49 return fmt.Sprintf(strings.Join(s, "\n"))
50}
51
52func (c *PortalConfig) String() string {
53 s := []string{
54 strings.Join([]string{"Password: ", c.Password}, " "),
55 strings.Join([]string{"NoResetCredentials:", strconv.FormatBool(c.NoResetCredentials)}, " "),
56 strings.Join([]string{"NoOperational:", strconv.FormatBool(c.NoOperational)}, " "),
57 }
58 return fmt.Sprintf(strings.Join(s, "\n"))
59}
60
61func (c *WifiConfig) String() string {
62 s := []string{
63 strings.Join([]string{"Ssid: ", c.Ssid}, " "),
64 strings.Join([]string{"Passphrase:", c.Passphrase}, " "),
65 strings.Join([]string{"Interface:", c.Interface}, " "),
66 strings.Join([]string{"CountryCode:", c.CountryCode}, " "),
67 strings.Join([]string{"Channel:", strconv.Itoa(c.Channel)}, " "),
68 strings.Join([]string{"OperationMode:", c.OperationMode}, " "),
69 }
70 return fmt.Sprintf(strings.Join(s, "\n"))
71}
72
73func defaultPortalConfig() *PortalConfig {
74 return &PortalConfig{
75 Password: "",
76 NoResetCredentials: false,
77 NoOperational: false,
78 }
79}
80
81// config currently stored in local json file is completely storable in PortalConfig
82// If needed to scale, we could rewrite this method to support a more generic type
83func readLocalConfig() (*PortalConfig, error) {
84 if _, err := os.Stat(configFile); os.IsNotExist(err) {
85 log.Printf("Warn: not found local config file at %v\n", configFile)
86 // in case there is no local config file, return a null pointer
87 return nil, nil
88 }
89
90 fileContents, err := ioutil.ReadFile(configFile)
91 if err != nil {
92 return nil, fmt.Errorf("Error reading json config file: %v", err)
93 }
94
95 // parameters not available in config file will be se to default value
96 portalConfig := defaultPortalConfig()
97 err = json.Unmarshal(fileContents, portalConfig)
98 if err != nil {
99 return nil, fmt.Errorf("Error unmarshalling json config file contents: %v", err)
100 }
101
102 return portalConfig, nil
103}
104
105func writeLocalConfig(p *PortalConfig) error {
106 // the only writable local config param is the password, stored as a hash
107 _, err := HashIt(p.Password)
108 if err != nil {
109 return fmt.Errorf("Could not hash portal password to file: %v", err)
110 }
111
112 return nil
113}
114
115func readRemoteParam(m map[string]interface{}, key string, defaultValue interface{}) interface{} {
116 val, ok := m[key]
117 if !ok {
118 val = defaultValue
119 log.Printf("Warning: %v key was not found in remote config", key)
120 }
121
122 return val
123}
124
125func readRemoteConfig() (*WifiConfig, error) {
126 settings, err := wifiapClient.Show()
127 if err != nil {
128 return nil, fmt.Errorf("Error reading wifi-ap remote configuration: %v", err)
129 }
130
131 // NOTE: Preprocessing for the case of wifi.channel.
132 // In the case of the channel, it is returned as string from rest api, but we have to convert it
133 // as it is handled as int internally.
134 // In case wifi-ap provides this in future as int, this could be replaced by
135 //
136 // readRemoteParam(settings, "wifi.channel", 0).(int)
137 channel, err := strconv.Atoi(readRemoteParam(settings, "wifi.channel", "0").(string))
138 if err != nil {
139 return nil, fmt.Errorf("Could not parse wifi.channel parameter: %v", err)
140 }
141
142 return &WifiConfig{
143 Ssid: readRemoteParam(settings, "wifi.ssid", "").(string),
144 Passphrase: readRemoteParam(settings, "wifi.security-passphrase", "").(string),
145 Interface: readRemoteParam(settings, "wifi.interface", "").(string),
146 CountryCode: readRemoteParam(settings, "wifi.country-code", "").(string),
147 Channel: channel,
148 OperationMode: readRemoteParam(settings, "wifi.operation-mode", "").(string),
149 }, nil
150}
151
152func writeRemoteConfig(wc *WifiConfig) error {
153 params := make(map[string]interface{})
154 params["wifi.ssid"] = wc.Ssid
155 params["wifi.security-passphrase"] = wc.Passphrase
156 params["wifi.interface"] = wc.Interface
157 params["wifi.country-code"] = wc.CountryCode
158 params["wifi.channel"] = wc.Channel
159 params["wifi.operation-mode"] = wc.OperationMode
160
161 err := wifiapClient.Set(params)
162 if err != nil {
163 return fmt.Errorf("Error writing remote configuration: %v", err)
164 }
165
166 return nil
167}
168
169// ReadConfig reads all config, remote and local, at the same time
170var ReadConfig = func() (*Config, error) {
171 wifiConfig, err := readRemoteConfig()
172 if err != nil {
173 return nil, err
174 }
175
176 portalConfig, err := readLocalConfig()
177 if err != nil {
178 return nil, err
179 }
180
181 // if local config is nil, fill returning object with default values
182 if portalConfig == nil {
183 portalConfig = defaultPortalConfig()
184 }
185
186 return &Config{Wifi: wifiConfig, Portal: portalConfig}, nil
187}
188
189// WriteConfig writes all remote and local config at the same time
190var WriteConfig = func(c *Config) error {
191 previousRemoteConfig, err := readRemoteConfig()
192 if err != nil {
193 return fmt.Errorf("Error reading current remote config before applying new one: %v", err)
194 }
195
196 // only write remote config if it's different from current
197 if *previousRemoteConfig != *c.Wifi {
198 err = writeRemoteConfig(c.Wifi)
199 if err != nil {
200 // if an error happens writing remote config there is no need to restore
201 // backup, as nothing shouldn't have been written
202 return err
203 }
204 }
205
206 err = writeLocalConfig(c.Portal)
207 if err != nil {
208 // rollback
209 if previousRemoteConfig != nil {
210 backupErr := writeRemoteConfig(previousRemoteConfig)
211 if backupErr != nil {
212 return fmt.Errorf("Could not restore previous remote configuration: %v\n after error: %v", backupErr, err)
213 }
214 }
215 return err
216 }
217
218 // write flag file for not asking more times for configuring snap before first use
219 if MustSetConfig() {
220 err = WriteFlagFile(mustConfigFlagFile)
221 if err != nil {
222 return fmt.Errorf("Error writing flag file after configuring for a first time")
223 }
224 }
225
226 return nil
227}
228
229// MustSetConfig true if one needs to configure snap before continuing
230var MustSetConfig = func() bool {
231 if _, err := os.Stat(mustConfigFlagFile); os.IsNotExist(err) {
232 return true
233 }
234 return false
235}
diff --git a/utils/config_test.go b/utils/config_test.go
0new file mode 100644236new file mode 100644
index 0000000..1802a60
--- /dev/null
+++ b/utils/config_test.go
@@ -0,0 +1,543 @@
1package utils
2
3import (
4 "fmt"
5 "io/ioutil"
6 "os"
7 "path/filepath"
8 "strconv"
9 "sync"
10 "testing"
11 "time"
12
13 "gopkg.in/check.v1"
14)
15
16const testLocalConfig = `
17{
18 "portal.no-reset-creds": true,
19 "portal.no-operational": false
20}
21`
22
23const testLocalConfigBadEntry = `
24{
25 "portal.password": "the_password",
26 "portal.no-reset-creds": true,
27 "bad.parameter": "bad.value",
28 "portal.no-operational": false
29}
30`
31
32const testLocalEmptyConfig = `
33{
34}
35`
36
37var testPortalConfig = &PortalConfig{"the_password", true, false}
38
39var rand uint32
40var randmu sync.Mutex
41
42func Test(t *testing.T) { check.TestingT(t) }
43
44type S struct{}
45
46var _ = check.Suite(&S{})
47
48// ####################
49// Testing local config
50// ####################
51func randomName() string {
52 randmu.Lock()
53 r := rand
54 if r == 0 {
55 r = uint32(time.Now().UnixNano() + int64(os.Getpid()))
56 }
57 r = r*1664525 + 1013904223 // constants from Numerical Recipes
58 rand = r
59 randmu.Unlock()
60 return strconv.Itoa(int(1e9 + r%1e9))[1:]
61}
62
63func createTempFile(content string) (*os.File, error) {
64 contentAsBytes := []byte(content)
65
66 tmpfile, err := ioutil.TempFile("", "config")
67 if err != nil {
68 return nil, fmt.Errorf("Could not create temp file: %v", err)
69 }
70
71 if _, err := tmpfile.Write(contentAsBytes); err != nil {
72 return nil, fmt.Errorf("Could not write contents to temp file: %v", err)
73 }
74
75 if err := tmpfile.Close(); err != nil {
76 return nil, fmt.Errorf("Could not close tempfile properly: %v", err)
77 }
78
79 return tmpfile, nil
80}
81
82func verifyLocalConfig(c *check.C, cfg *PortalConfig, expectedPwd string, expectedNoResetCredentials bool, expectedNoOperational bool) {
83 c.Assert(cfg.Password, check.Equals, expectedPwd)
84 c.Assert(cfg.NoResetCredentials, check.Equals, expectedNoResetCredentials)
85 c.Assert(cfg.NoOperational, check.Equals, expectedNoOperational)
86}
87
88func verifyDefaultLocalConfig(c *check.C, cfg *PortalConfig) {
89 verifyLocalConfig(c, cfg, "", false, false)
90}
91
92func (s *S) TestReadLocalConfig(c *check.C) {
93 f, err := createTempFile(testLocalConfig)
94 c.Assert(err, check.IsNil)
95
96 defer os.Remove(f.Name())
97 configFile = f.Name()
98
99 cfg, err := readLocalConfig()
100 c.Assert(err, check.IsNil)
101
102 verifyLocalConfig(c, cfg, "", true, false)
103}
104
105func (s *S) TestReadLocalConfigBadEntry(c *check.C) {
106 // No matter if there are additional not recognized params, only known should be marshalled
107 f, err := createTempFile(testLocalConfigBadEntry)
108 c.Assert(err, check.IsNil)
109
110 defer os.Remove(f.Name())
111 configFile = f.Name()
112
113 cfg, err := readLocalConfig()
114 c.Assert(err, check.IsNil)
115
116 verifyLocalConfig(c, cfg, "", true, false)
117}
118
119func (s *S) TestReadLocalEmptyConfig(c *check.C) {
120 // No matter if there are additional not recognized params, only known should be marshalled
121 f, err := createTempFile(testLocalEmptyConfig)
122 c.Assert(err, check.IsNil)
123
124 defer os.Remove(f.Name())
125 configFile = f.Name()
126
127 cfg, err := readLocalConfig()
128 c.Assert(err, check.IsNil)
129
130 verifyDefaultLocalConfig(c, cfg)
131}
132
133func (s *S) TestReadLocalNotExistingConfig(c *check.C) {
134 configFile = "does/not/exists/config.json"
135
136 cfg, err := readLocalConfig()
137 c.Assert(err, check.IsNil)
138 c.Assert(cfg, check.IsNil)
139}
140
141func (s *S) TestWriteLocalConfigFileDoesNotExists(c *check.C) {
142 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
143 defer os.Remove(mustConfigFlagFile)
144 HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
145 defer os.Remove(HashFile)
146
147 err := writeLocalConfig(testPortalConfig)
148 c.Assert(err, check.IsNil)
149
150 ok, err := MatchingHash(testPortalConfig.Password)
151 c.Assert(err, check.IsNil)
152 c.Assert(ok, check.Equals, true)
153}
154
155func (s *S) TestWriteLocalConfigFiletExists(c *check.C) {
156 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
157 defer os.Remove(mustConfigFlagFile)
158
159 f, err := createTempFile("whateverbadpasswordhash")
160 c.Assert(err, check.IsNil)
161
162 defer os.Remove(f.Name())
163 HashFile = f.Name()
164
165 err = writeLocalConfig(testPortalConfig)
166 c.Assert(err, check.IsNil)
167
168 ok, err := MatchingHash(testPortalConfig.Password)
169 c.Assert(err, check.IsNil)
170 c.Assert(ok, check.Equals, true)
171}
172
173// #####################
174// Testing remote config
175// #####################
176type wifiapClientMock struct {
177 m map[string]interface{}
178}
179
180func (c *wifiapClientMock) Show() (map[string]interface{}, error) {
181 return c.m, nil
182}
183
184func (c *wifiapClientMock) Enable() error {
185 return nil
186}
187
188func (c *wifiapClientMock) Disable() error {
189 return nil
190}
191
192func (c *wifiapClientMock) Enabled() (bool, error) {
193 return true, nil
194}
195
196func (c *wifiapClientMock) SetSsid(string) error {
197 return nil
198}
199
200func (c *wifiapClientMock) SetPassphrase(string) error {
201 return nil
202}
203
204func (c *wifiapClientMock) Set(map[string]interface{}) error {
205 return nil
206}
207
208func (s *S) TestReadRemoteConfig(c *check.C) {
209 wifiapClient = &wifiapClientMock{
210 m: map[string]interface{}{
211 "dhcp.lease-time": "12h",
212 "dhcp.range-start": "10.0.60.2",
213 "dhcp.range-stop": "10.0.60.199",
214 "disabled": true,
215 "share.disabled": false,
216 "share.network-interface": "wlp2s0",
217 "wifi.address": "10.0.60.1",
218 "wifi.channel": "6", // in real environment, channel is returned as string
219 "wifi.hostapd-driver": "nl80211",
220 "wifi.interface": "wlp2s0",
221 "wifi.interface-mode": "direct",
222 "wifi.country-code": "0x31",
223 "wifi.netmask": "255.255.255.0",
224 "wifi.operation-mode": "g",
225 "wifi.security": "wpa2",
226 "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
227 "wifi.ssid": "Ubuntu",
228 },
229 }
230
231 cfg, err := readRemoteConfig()
232 c.Assert(err, check.IsNil)
233
234 expectedCfg := &WifiConfig{
235 Ssid: "Ubuntu",
236 Passphrase: "17Soj8/Sxh14lcpD",
237 Interface: "wlp2s0",
238 CountryCode: "0x31",
239 Channel: 6,
240 OperationMode: "g",
241 }
242
243 c.Assert(*cfg, check.Equals, *expectedCfg)
244}
245
246func (s *S) TestReadRemoteConfigNotAllParams(c *check.C) {
247 wifiapClient = &wifiapClientMock{
248 m: map[string]interface{}{
249 "dhcp.lease-time": "12h",
250 "dhcp.range-start": "10.0.60.2",
251 "dhcp.range-stop": "10.0.60.199",
252 "share.disabled": false,
253 "share.network-interface": "wlp2s0",
254 "wifi.address": "10.0.60.1",
255 "wifi.hostapd-driver": "nl80211",
256 "wifi.interface": "wlp2s0",
257 "wifi.interface-mode": "direct",
258 "wifi.country-code": "0x31",
259 "wifi.netmask": "255.255.255.0",
260 "wifi.security": "wpa2",
261 "wifi.ssid": "Ubuntu",
262 },
263 }
264
265 cfg, err := readRemoteConfig()
266 c.Assert(err, check.IsNil)
267
268 expectedCfg := &WifiConfig{
269 Ssid: "Ubuntu",
270 Passphrase: "",
271 Interface: "wlp2s0",
272 CountryCode: "0x31",
273 Channel: 0,
274 OperationMode: "",
275 }
276
277 c.Assert(*cfg, check.Equals, *expectedCfg)
278}
279
280func (s *S) TestReadEmptyRemoteConfig(c *check.C) {
281 wifiapClient = &wifiapClientMock{}
282
283 cfg, err := readRemoteConfig()
284 c.Assert(err, check.IsNil)
285
286 expectedCfg := &WifiConfig{
287 Ssid: "",
288 Passphrase: "",
289 Interface: "",
290 CountryCode: "",
291 Channel: 0,
292 OperationMode: "",
293 }
294
295 c.Assert(*cfg, check.Equals, *expectedCfg)
296}
297
298func (s *S) TestWriteRemoteConfig(c *check.C) {
299 wifiapClient = &wifiapClientMock{}
300
301 err := writeRemoteConfig(&WifiConfig{
302 Ssid: "Ubuntu",
303 Passphrase: "17Soj8/Sxh14lcpD",
304 Interface: "wlp2s0",
305 CountryCode: "0x31",
306 Channel: 6,
307 OperationMode: "g",
308 })
309
310 c.Assert(err, check.IsNil)
311}
312
313// ####################
314// Testing whole config
315// ####################
316func (s *S) TestWriteConfig(c *check.C) {
317 wifiapClient = &wifiapClientMock{}
318
319 HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
320 defer os.Remove(HashFile)
321
322 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
323 defer os.Remove(mustConfigFlagFile)
324
325 c.Assert(MustSetConfig(), check.Equals, true)
326
327 err := WriteConfig(&Config{
328 Wifi: &WifiConfig{
329 Ssid: "Ubuntu",
330 Passphrase: "17Soj8/Sxh14lcpD",
331 Interface: "wlp2s0",
332 CountryCode: "0x31",
333 Channel: 6,
334 OperationMode: "g",
335 },
336 Portal: &PortalConfig{
337 Password: "thepassword",
338 NoResetCredentials: true,
339 NoOperational: false,
340 },
341 })
342
343 c.Assert(err, check.IsNil)
344
345 ok, err := MatchingHash("thepassword")
346 c.Assert(err, check.IsNil)
347 c.Assert(ok, check.Equals, true)
348
349 c.Assert(MustSetConfig(), check.Equals, false)
350}
351
352func (s *S) TestReadConfig(c *check.C) {
353 wifiapClient = &wifiapClientMock{
354 m: map[string]interface{}{
355 "dhcp.lease-time": "12h",
356 "dhcp.range-start": "10.0.60.2",
357 "dhcp.range-stop": "10.0.60.199",
358 "disabled": true,
359 "share.disabled": false,
360 "share.network-interface": "wlp2s0",
361 "wifi.address": "10.0.60.1",
362 "wifi.channel": "6", // in real environment, channel is returned as string
363 "wifi.hostapd-driver": "nl80211",
364 "wifi.interface": "wlp2s0",
365 "wifi.interface-mode": "direct",
366 "wifi.country-code": "0x31",
367 "wifi.netmask": "255.255.255.0",
368 "wifi.operation-mode": "g",
369 "wifi.security": "wpa2",
370 "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
371 "wifi.ssid": "Ubuntu",
372 },
373 }
374
375 f, err := createTempFile(testLocalConfig)
376 c.Assert(err, check.IsNil)
377
378 defer os.Remove(f.Name())
379 configFile = f.Name()
380
381 expectedWifiConfig := &WifiConfig{
382 Ssid: "Ubuntu",
383 Passphrase: "17Soj8/Sxh14lcpD",
384 Interface: "wlp2s0",
385 CountryCode: "0x31",
386 Channel: 6,
387 OperationMode: "g",
388 }
389
390 expectedPortalConfig := &PortalConfig{
391 Password: "",
392 NoResetCredentials: true,
393 NoOperational: false,
394 }
395
396 cfg, err := ReadConfig()
397 c.Assert(err, check.IsNil)
398 c.Assert(*cfg.Wifi, check.Equals, *expectedWifiConfig)
399 c.Assert(*cfg.Portal, check.Equals, *expectedPortalConfig)
400}
401
402func (s *S) TestMustSetConfig(c *check.C) {
403 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
404 defer os.Remove(mustConfigFlagFile)
405
406 HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
407 defer os.Remove(HashFile)
408
409 wifiapClient = &wifiapClientMock{}
410
411 // config to save
412 cfg := &Config{
413 Wifi: &WifiConfig{
414 Ssid: "Ubuntu",
415 Passphrase: "17Soj8/Sxh14lcpD",
416 Interface: "wlp2s0",
417 CountryCode: "0x31",
418 Channel: 6,
419 OperationMode: "g",
420 },
421 Portal: &PortalConfig{
422 Password: "the_password",
423 NoResetCredentials: true,
424 NoOperational: false,
425 },
426 }
427
428 c.Assert(MustSetConfig(), check.Equals, true)
429
430 err := WriteConfig(cfg)
431 c.Assert(err, check.IsNil)
432
433 c.Assert(MustSetConfig(), check.Equals, false)
434
435 err = WriteConfig(cfg)
436 c.Assert(err, check.IsNil)
437
438 c.Assert(MustSetConfig(), check.Equals, false)
439}
440
441func (s *S) TestConfigDump(c *check.C) {
442 cfg := &Config{
443 Wifi: &WifiConfig{
444 Ssid: "Ubuntu",
445 Passphrase: "17Soj8/Sxh14lcpD",
446 Interface: "wlp2s0",
447 CountryCode: "0x31",
448 Channel: 6,
449 OperationMode: "g",
450 },
451 Portal: &PortalConfig{
452 Password: "the_password",
453 NoResetCredentials: true,
454 NoOperational: false,
455 },
456 }
457
458 expected := `--Wifi--
459Ssid: Ubuntu
460Passphrase: 17Soj8/Sxh14lcpD
461Interface: wlp2s0
462CountryCode: 0x31
463Channel: 6
464OperationMode: g
465--Portal--
466Password: the_password
467NoResetCredentials: true
468NoOperational: false`
469
470 c.Assert(cfg.String(), check.Equals, expected)
471}
472
473func (s *S) TestRollbackConfigIfFailsWriting(c *check.C) {
474 wifiapClient = &wifiapClientMock{
475 m: map[string]interface{}{
476 "dhcp.lease-time": "12h",
477 "dhcp.range-start": "10.0.60.2",
478 "dhcp.range-stop": "10.0.60.199",
479 "disabled": true,
480 "share.disabled": false,
481 "share.network-interface": "wlp2s0",
482 "wifi.address": "10.0.60.1",
483 "wifi.channel": "8", // in real environment, channel is returned as string
484 "wifi.hostapd-driver": "nl80211",
485 "wifi.interface": "wlp2s0",
486 "wifi.interface-mode": "direct",
487 "wifi.country-code": "ES",
488 "wifi.netmask": "255.255.255.0",
489 "wifi.operation-mode": "ad",
490 "wifi.security": "wlan1",
491 "wifi.security-passphrase": "abc17Soj8/Sxh14lcpD",
492 "wifi.ssid": "mySSID",
493 },
494 }
495
496 HashFile = filepath.Join(os.TempDir(), "portalpwd"+randomName())
497 defer os.Remove(HashFile)
498
499 mustConfigFlagFile = filepath.Join(os.TempDir(), "config_done"+randomName())
500 defer os.Remove(mustConfigFlagFile)
501
502 err := WriteFlagFile(mustConfigFlagFile)
503 c.Assert(err, check.IsNil)
504 c.Assert(MustSetConfig(), check.Equals, false)
505
506 // read previous remote config file
507 remotePreviousConfig, err := readRemoteConfig()
508 c.Assert(err, check.IsNil)
509
510 // config to save
511 cfg := &Config{
512 Wifi: &WifiConfig{
513 Ssid: "Ubuntu",
514 Passphrase: "17Soj8/Sxh14lcpD",
515 Interface: "wlp2s0",
516 CountryCode: "XX",
517 Channel: 6,
518 OperationMode: "g",
519 },
520 Portal: &PortalConfig{
521 Password: "the_password",
522 NoResetCredentials: true,
523 NoOperational: false,
524 },
525 }
526
527 originalHashIt := HashIt
528 HashIt = func(s string) ([]byte, error) { return nil, fmt.Errorf("Error hashing") }
529
530 // assert write fails
531 err = WriteConfig(cfg)
532 c.Assert(err, check.NotNil)
533
534 HashIt = originalHashIt
535
536 // read remote config after write file failing and verify it's the original one
537 remoteCfg, err := readRemoteConfig()
538 c.Assert(err, check.IsNil)
539 c.Assert(*remoteCfg, check.Equals, *remotePreviousConfig)
540
541 // verify must set config flag is not changed in the process
542 c.Assert(MustSetConfig(), check.Equals, false)
543}
diff --git a/utils/utils.go b/utils/utils.go
index 94850c0..9a77ce3 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -40,7 +40,7 @@ var HashFile = filepath.Join(os.Getenv("SNAP_COMMON"), "hash")
4040
41// HashIt writes the hash of the passed password to the HashFile and41// HashIt writes the hash of the passed password to the HashFile and
42// returns the hash and error42// returns the hash and error
43func HashIt(s string) ([]byte, error) {43var HashIt = func(s string) ([]byte, error) {
44 var b []byte44 var b []byte
45 var err error45 var err error
46 b, err = bcrypt.GenerateFromPassword([]byte(s), 8)46 b, err = bcrypt.GenerateFromPassword([]byte(s), 8)
@@ -154,3 +154,12 @@ func RunningOn(address string) bool {
154 }154 }
155 return true155 return true
156}156}
157
158// ParseFormParamSingleValue extracts single value from a http post request
159func ParseFormParamSingleValue(form map[string][]string, key string) string {
160 vals := form[key]
161 if len(vals) > 0 {
162 return vals[0]
163 }
164 return ""
165}
diff --git a/wifiap/wifiap.go b/wifiap/wifiap.go
index faf0c15..d81f883 100644
--- a/wifiap/wifiap.go
+++ b/wifiap/wifiap.go
@@ -45,7 +45,7 @@ func DefaultClient() *Client {
45 return &Client{restClient: defaultRestClient()}45 return &Client{restClient: defaultRestClient()}
46}46}
4747
48// Operations interface enables mock testing48// Operations interface defining operations implemented by wifiap client
49type Operations interface {49type Operations interface {
50 Show() (map[string]interface{}, error)50 Show() (map[string]interface{}, error)
51 Enabled() (bool, error)51 Enabled() (bool, error)
@@ -53,6 +53,7 @@ type Operations interface {
53 Disable() error53 Disable() error
54 SetSsid(string) error54 SetSsid(string) error
55 SetPassphrase(string) error55 SetPassphrase(string) error
56 Set(map[string]interface{}) error
56}57}
5758
58func defaultServiceURI() string {59func defaultServiceURI() string {
@@ -205,3 +206,22 @@ func (client *Client) SetPassphrase(passphrase string) error {
205206
206 return nil207 return nil
207}208}
209
210// Set sets a group of parameters in wifi-ap
211func (client *Client) Set(params map[string]interface{}) error {
212 b, err := json.Marshal(params)
213 if err != nil {
214 return fmt.Errorf("Error when marshalling input parameters: %q", err)
215 }
216
217 response, err := client.restClient.sendHTTPRequest(defaultServiceURI(), "POST", bytes.NewReader(b))
218 if err != nil {
219 return fmt.Errorf("wifi-ap set operation failed: %q", err)
220 }
221
222 if response.StatusCode != http.StatusOK || response.Status != http.StatusText(http.StatusOK) {
223 return fmt.Errorf("Failed to set configuration, service returned: %d (%s)", response.StatusCode, response.Status)
224 }
225
226 return nil
227}
diff --git a/wifiap/wifiap_test.go b/wifiap/wifiap_test.go
index 6fa4207..e1f4279 100644
--- a/wifiap/wifiap_test.go
+++ b/wifiap/wifiap_test.go
@@ -21,6 +21,7 @@ import (
21 "fmt"21 "fmt"
22 "io/ioutil"22 "io/ioutil"
23 "net/http"23 "net/http"
24 "reflect"
24 "strings"25 "strings"
25 "testing"26 "testing"
26)27)
@@ -148,7 +149,7 @@ func TestShow(t *testing.T) {
148}149}
149150
150// Testing Enable()151// Testing Enable()
151func validateHeaders(m map[string]string, req *http.Request) error {152func validateHeaders(m map[string]interface{}, req *http.Request) error {
152 buf, _ := ioutil.ReadAll(req.Body)153 buf, _ := ioutil.ReadAll(req.Body)
153 var headers map[string]interface{}154 var headers map[string]interface{}
154 if err := json.Unmarshal(buf, &headers); err != nil {155 if err := json.Unmarshal(buf, &headers); err != nil {
@@ -160,9 +161,20 @@ func validateHeaders(m map[string]string, req *http.Request) error {
160 return fmt.Errorf("Expected %v headers", n)161 return fmt.Errorf("Expected %v headers", n)
161 }162 }
162163
163 for key, value := range m {164 for key, mValue := range m {
164 if headers[key] != value {165 hValue := headers[key]
165 return fmt.Errorf("Header '%v' has not valid value", key)166 if hValue != mValue {
167 // evaluate the special case of hValue unmarshalled as float64
168 if reflect.TypeOf(hValue) != reflect.TypeOf(mValue) {
169 switch hValue := hValue.(type) {
170 case float64:
171 if int(hValue) != mValue.(int) {
172 return fmt.Errorf("Header '%v' has not valid value. Expected %v but has %v", key, mValue, hValue)
173 }
174 }
175 } else {
176 return fmt.Errorf("Header '%v' has not valid value. Expected %v but has %v", key, mValue, hValue)
177 }
166 }178 }
167 }179 }
168180
@@ -181,7 +193,7 @@ func (mock *mockTransportEnable) Do(req *http.Request) (*http.Response, error) {
181 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)193 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
182 }194 }
183195
184 err := validateHeaders(map[string]string{"disabled": "false"}, req)196 err := validateHeaders(map[string]interface{}{"disabled": "false"}, req)
185 if err != nil {197 if err != nil {
186 return nil, err198 return nil, err
187 }199 }
@@ -238,7 +250,7 @@ func (mock *mockTransportDisable) Do(req *http.Request) (*http.Response, error)
238 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)250 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
239 }251 }
240252
241 err := validateHeaders(map[string]string{"disabled": "true"}, req)253 err := validateHeaders(map[string]interface{}{"disabled": "true"}, req)
242 if err != nil {254 if err != nil {
243 return nil, err255 return nil, err
244 }256 }
@@ -363,7 +375,7 @@ func (mock *mockTransportSetSsid) Do(req *http.Request) (*http.Response, error)
363 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)375 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
364 }376 }
365377
366 err := validateHeaders(map[string]string{"wifi.ssid": "MySsid"}, req)378 err := validateHeaders(map[string]interface{}{"wifi.ssid": "MySsid"}, req)
367 if err != nil {379 if err != nil {
368 return nil, err380 return nil, err
369 }381 }
@@ -401,7 +413,7 @@ func (mock *mockTransportSetPassphrase) Do(req *http.Request) (*http.Response, e
401 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)413 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
402 }414 }
403415
404 err := validateHeaders(map[string]string{"wifi.security": "wpa2", "wifi.security-passphrase": "passphrase123"}, req)416 err := validateHeaders(map[string]interface{}{"wifi.security": "wpa2", "wifi.security-passphrase": "passphrase123"}, req)
405 if err != nil {417 if err != nil {
406 return nil, err418 return nil, err
407 }419 }
@@ -424,3 +436,79 @@ func TestSetPassphrase(t *testing.T) {
424 t.Errorf("Failed to set passphrase: %v\n", err)436 t.Errorf("Failed to set passphrase: %v\n", err)
425 }437 }
426}438}
439
440type mockTransportSet struct{}
441
442func (mock *mockTransportSet) Do(req *http.Request) (*http.Response, error) {
443
444 url := req.URL.String()
445 if url != "http://unix/v1/configuration" {
446 return nil, fmt.Errorf("Not valid request URL: %v", url)
447 }
448
449 if req.Method != "POST" {
450 return nil, fmt.Errorf("Method is not valid. Expected POST, got %v", req.Method)
451 }
452
453 err := validateHeaders(map[string]interface{}{
454 "dhcp.lease-time": "12h",
455 "dhcp.range-start": "10.0.60.2",
456 "dhcp.range-stop": "10.0.60.199",
457 "disabled": true,
458 "share.disabled": false,
459 "share.network-interface": "wlp2s0",
460 "wifi.address": "10.0.60.1",
461 "wifi.channel": 6,
462 "wifi.hostapd-driver": "nl80211",
463 "wifi.interface": "wlp2s0",
464 "wifi.interface-mode": "direct",
465 "wifi.country-code": "0x31",
466 "wifi.netmask": "255.255.255.0",
467 "wifi.operation-mode": "g",
468 "wifi.security": "wpa2",
469 "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
470 "wifi.ssid": "Ubuntu",
471 }, req)
472 if err != nil {
473 return nil, err
474 }
475
476 rawBody := `{"result":{},"status":"OK","status-code":200,"type":"sync"}`
477
478 response := http.Response{
479 StatusCode: 200,
480 Status: "200 OK",
481 Body: ioutil.NopCloser(strings.NewReader(rawBody)),
482 }
483
484 return &response, nil
485}
486
487func TestSet(t *testing.T) {
488
489 params := map[string]interface{}{
490 "dhcp.lease-time": "12h",
491 "dhcp.range-start": "10.0.60.2",
492 "dhcp.range-stop": "10.0.60.199",
493 "disabled": true,
494 "share.disabled": false,
495 "share.network-interface": "wlp2s0",
496 "wifi.address": "10.0.60.1",
497 "wifi.channel": 6,
498 "wifi.hostapd-driver": "nl80211",
499 "wifi.interface": "wlp2s0",
500 "wifi.interface-mode": "direct",
501 "wifi.country-code": "0x31",
502 "wifi.netmask": "255.255.255.0",
503 "wifi.operation-mode": "g",
504 "wifi.security": "wpa2",
505 "wifi.security-passphrase": "17Soj8/Sxh14lcpD",
506 "wifi.ssid": "Ubuntu",
507 }
508
509 client := NewClient(&mockTransportSet{})
510 err := client.Set(params)
511 if err != nil {
512 t.Errorf("Failed to set params: %v\n", err)
513 }
514}

Subscribers

People subscribed via source and target branches