├── .gitignore ├── .svnignore ├── README.md ├── assets ├── config.json ├── css │ └── bootstrap.min.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 └── js │ ├── dashboard.js │ └── vendor │ ├── bootstrap.min.js │ ├── chart.min.js │ ├── underscore.min.js │ ├── vue.js │ └── vue.min.js ├── bin └── install-wp-tests.sh ├── classes ├── api_manager.php ├── dashboard_ajax_provider.php ├── dev_portal.php ├── portal_user.php ├── token.php ├── tyk_api.php ├── tyk_gateway.php └── tyk_interaction.php ├── languages ├── tyk-dev-portal-de_DE.mo ├── tyk-dev-portal-de_DE.po ├── tyk-dev-portal-fr_FR.mo ├── tyk-dev-portal-fr_FR.po ├── tyk-dev-portal-it_IT.mo └── tyk-dev-portal-it_IT.po ├── package.json ├── phpunit.xml ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── template_tags.php ├── templates ├── api_subscribe_form.php ├── tab_quota.php ├── tab_tokens.php └── tab_usage.php ├── tests ├── ApiManagerTest.php ├── PluginTest.php ├── PortalUserTest.php ├── TokenTest.php ├── TykDevPortalTestcase.php └── bootstrap.php └── tyk_dev_portal.php /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /.svnignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | tests 3 | Makefile 4 | wp_tyk_dev_portal.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tyk Dev Portal 2 | 3 | Contributors: teamamboss, chlab 4 | Tags: api, api-management, tyk, liip 5 | Requires at least: 4.6 6 | Tested up to: 4.6.1 7 | Stable tag: 4.6.1 8 | License: MIT 9 | License URI: https://opensource.org/licenses/MIT 10 | 11 | Integrates a developer portal of a Tyk API Gateway in your WordPress site 12 | 13 | ## Description 14 | 15 | If you are using the [Tyk API Gateway](http://www.tyk.io) and have a WordPress site you can use this plugin to integrate a developer portal into your site. This is handy when your API requires a complementary website with information e.g. about the service and you want the developer portal in the same place. It's main goal is to offer developer sign up and obtaining access tokens from your WordPress site. 16 | 17 | ![Screenshot 1](./screenshot-1.png) 18 | ![Screenshot 2](./screenshot-2.png) 19 | ![Screenshot 3](./screenshot-3.png) 20 | 21 | This plugin is a work in progress and currently offers the following features: 22 | 23 | * automatic developer registration on Tyk when developers sign up in WordPress 24 | * configuration of API policies available for token registration 25 | * developers may request an access token for the available API policies 26 | * automatic or manual approval of key requests 27 | * storage of token (references) by name and API policy 28 | * revoking of tokens by developer 29 | * display usage statistics per key 30 | * request quota usage per key 31 | 32 | What this plugin does not offer: 33 | 34 | * Management of Tyk API Gateway (the Tyk Dashboard is best suited for that) 35 | * WordPress user registration (there are enough plugins that do that quite well) 36 | 37 | ## Support 38 | 39 | Please note that we, the plugin authors, cannot offer support for this plugin. The code is on [GitHub](https://github.com/liip/wp-tyk-dev-portal) however and we are happy to accept pull requests fixing bugs or adding functionality. Also feel free to report any [issues](https://github.com/liip/wp-tyk-dev-portal/issues) although we cannot promise when and if they will be fixed. 40 | 41 | ## Installation 42 | 43 | * Upload the plugin zip file or install the plugin through the WordPress plugins screen directly 44 | * Optional: choose and install a plugin that offers a better registration experience for WordPress users. This plugin was tested with [ProfilePress](https://wordpress.org/plugins/ppress/) and [Profile Builder](https://wordpress.org/plugins/profile-builder/), it should work with most or any registration/profile plugin though. 45 | * Activate the plugins through the 'Plugins' screen in WordPress. Activation of this plugin should have triggered the creation of the user role "Developer" an the page "Developer Dashboard". Note that the "Developer Dashboard" page must have the slug "dev-dashboard" otherwise the scripts won't be included. 46 | * Setup your Tyk Gateway in the Tyk Dashboard (assuming you already configured an API and policy): 47 | * Go to *System Management > Policies* 48 | * Tag policies that developers may register for with `allow_registration` 49 | * Tag policies where accepted terms and conditions are required with `requires_tac` 50 | * Name your policies accordingly, these will be shown to user for access token registration 51 | * Create a dedicated management user at *System Management > Users* (does not have to be an Admin), save, then generate an access 52 | token for this user on the same page (*Tyk Dashboard API Access Credentials*) 53 | * Go to *Portal Management > Settings* and make sure *Disable developer signup* is not active 54 | * Go to *Portal Management > Catalogue*, this is required because it does some setup work in the background, no need to do anything 55 | * Add the following configuration to your wp-config.php file: 56 | 57 | 58 | ` 59 | define( 'TYK_CONFIGURATION', 'cloud' ); // the tyk setup you're using ('cloud', 'on-premise' or 'hybrid') 60 | define( 'TYK_API_ENDPOINT', 'https://your.dashboardurl.tyk.io/api/' ); // or the url to your Tyk installation 61 | define( 'TYK_API_KEY', 'the access token' ); // access token you created for the management user 62 | define( 'TYK_AUTO_APPROVE_KEY_REQUESTS', true ); // read more below 63 | // these are only needed when TYK_CONFIGURATION is 'on-premise' or 'hybrid' 64 | define( 'TYK_GATEWAY_URL', 'https://your.tykgateway.com/tyk' ); // the url of your tyk gateway 65 | define( 'TYK_GATEWAY_SECRET', 'the gateway secret' ); // the gateway secret 66 | // optional 67 | define( 'TYK_FORCE_DISABLE_BOOTSTRAP', false ); // do not include bootstrap styles 68 | ` 69 | 70 | ## Screenshots 71 | 72 | 1. Developer Dashboard with token request in WordPress frontend 73 | 2. Usage graph per token and time period 74 | 3. Token usage quota (used / remaining requests) 75 | 76 | ## Changelog 77 | 78 | ### Version 1.3 79 | 80 | * Add terms and condition checkbox for policies with the tag `requires_tac` 81 | * Disable tyk policy paging, so instead of only 10, all policies can now be loaded in one request 82 | 83 | ### Version 1.2 84 | 85 | * Usage & Quota tabs 86 | * Improved overview of tokens and their policy 87 | * Support for Tyk in hybrid configuration 88 | * FR and IT locales 89 | 90 | ### Version 1.1 91 | 92 | * fixed language loading 93 | * only allow 1 token per policy 94 | * added support to force-disable bootstrap styles 95 | 96 | ### Version 1.0 97 | 98 | * Initial release offering the features mentioned in the description 99 | 100 | ## Todos 101 | 102 | The following changes are in planning: 103 | 104 | * use catalogues on Tyk portal to group policies instead of tags 105 | * make chart colors configurable 106 | * split up template and dashboard.js into multiple components 107 | * build js to vendor/ dir 108 | * ~~refactor dashboard a bit considering that there can't be more than 1 token per API anyway~~ 109 | * ~~add option to not include bootstrap altogether~~ 110 | * ~~do not allow registering for multiple API policies (as it's not supported by Tyk)~~ 111 | 112 | ## Further reading 113 | 114 | ### Automatic key approval 115 | 116 | When `TYK_AUTO_APPROVE_KEY_REQUESTS` is set to `true` in wp-config.php, key requests will be approved automatically by this plugin. That means a developer will request an access token on the Dashboard page of your WordPress (where this plugin is running) and they will be processed automatically. For this to work, make sure *Require key approval* is not active in your Tyk Dashboard at *Portal Management > Settings*. 117 | 118 | If you wish to approve key requests manually, set `TYK_AUTO_APPROVE_KEY_REQUESTS` to false and activate *Require key approval* mentioned above. When a developer requests an access token, he will get a message with his key request ID. The further process is up to you. Key requests can be approved in the Tyk Dashboard at *Portal Management > Key Requests*. You could configure an email to be sent out in the Portal Management Settings or do it manually. 119 | 120 | ### Custom dashboard page 121 | 122 | The developer dashboard will be displayed in the same layout as your other pages. If you wish to further customize the page, you can create a [custom page template](https://developer.wordpress.org/themes/template-files-section/page-template-files/page-templates/) and embed the dashboard by using this template tag: ``. 123 | 124 | ### Styling 125 | 126 | You may need to fix some styles as this plugin includes a subset of the [bootstrap](http://www.getbootstrap.com) styles with it and the styles of your theme may conflict with some of them. Bootstrap is only included when your theme doesn't already include it. You can set `TYK_FORCE_DISABLE_BOOTSTRAP` to `true` to force disable the bootstrap styles in case you need to. 127 | 128 | ### i18n 129 | 130 | This plugin is ready for translation and currently supports the following languages: 131 | 132 | * German 133 | * French 134 | * Italian 135 | 136 | ## Tyk API config 137 | 138 | There are two endpoints configured, one via the dashboard api (see TYK_API_ENDPOINT and tyk_api.php): 139 | 140 | `$this->api->get_url_for_path` 141 | 142 | For Authorization you need the Authorization header: 143 | 144 | `$api_response = wp_remote_post($this->get_url_for_path($path), array( 145 | 'headers' => array( 146 | 'Authorization' => TYK_API_KEY, 147 | ), 148 | ));` 149 | 150 | and one for the gateway (see TYK_GATEWAY_URL and tyk_gateway.php): 151 | 152 | `$this->gateway->get_url_for_path` 153 | 154 | For Authorization you need the x-tyk-authorization header: 155 | 156 | `$api_response = wp_remote_get($this->get_url_for_path($path, $args), array( 157 | 'headers' => array( 158 | 'x-tyk-authorization' => TYK_GATEWAY_SECRET 159 | ), 160 | ));` 161 | 162 | -------------------------------------------------------------------------------- /assets/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": { 3 | "@gray-base": "#000", 4 | "@gray-darker": "lighten(@gray-base, 13.5%)", 5 | "@gray-dark": "lighten(@gray-base, 20%)", 6 | "@gray": "lighten(@gray-base, 33.5%)", 7 | "@gray-light": "lighten(@gray-base, 46.7%)", 8 | "@gray-lighter": "lighten(@gray-base, 93.5%)", 9 | "@brand-primary": "darken(#428bca, 6.5%)", 10 | "@brand-success": "#5cb85c", 11 | "@brand-info": "#5bc0de", 12 | "@brand-warning": "#f0ad4e", 13 | "@brand-danger": "#d9534f", 14 | "@body-bg": "#fff", 15 | "@text-color": "@gray-dark", 16 | "@link-color": "@brand-primary", 17 | "@link-hover-color": "darken(@link-color, 15%)", 18 | "@link-hover-decoration": "underline", 19 | "@font-family-sans-serif": "\"Helvetica Neue\", Helvetica, Arial, sans-serif", 20 | "@font-family-serif": "Georgia, \"Times New Roman\", Times, serif", 21 | "@font-family-monospace": "Menlo, Monaco, Consolas, \"Courier New\", monospace", 22 | "@font-family-base": "@font-family-sans-serif", 23 | "@font-size-base": "14px", 24 | "@font-size-large": "ceil((@font-size-base * 1.25))", 25 | "@font-size-small": "ceil((@font-size-base * 0.85))", 26 | "@font-size-h1": "floor((@font-size-base * 2.6))", 27 | "@font-size-h2": "floor((@font-size-base * 2.15))", 28 | "@font-size-h3": "ceil((@font-size-base * 1.7))", 29 | "@font-size-h4": "ceil((@font-size-base * 1.25))", 30 | "@font-size-h5": "@font-size-base", 31 | "@font-size-h6": "ceil((@font-size-base * 0.85))", 32 | "@line-height-base": "1.428571429", 33 | "@line-height-computed": "floor((@font-size-base * @line-height-base))", 34 | "@headings-font-family": "inherit", 35 | "@headings-font-weight": "500", 36 | "@headings-line-height": "1.1", 37 | "@headings-color": "inherit", 38 | "@icon-font-path": "\"../fonts/\"", 39 | "@icon-font-name": "\"glyphicons-halflings-regular\"", 40 | "@icon-font-svg-id": "\"glyphicons_halflingsregular\"", 41 | "@padding-base-vertical": "6px", 42 | "@padding-base-horizontal": "12px", 43 | "@padding-large-vertical": "10px", 44 | "@padding-large-horizontal": "16px", 45 | "@padding-small-vertical": "5px", 46 | "@padding-small-horizontal": "10px", 47 | "@padding-xs-vertical": "1px", 48 | "@padding-xs-horizontal": "5px", 49 | "@line-height-large": "1.3333333", 50 | "@line-height-small": "1.5", 51 | "@border-radius-base": "4px", 52 | "@border-radius-large": "6px", 53 | "@border-radius-small": "3px", 54 | "@component-active-color": "#fff", 55 | "@component-active-bg": "@brand-primary", 56 | "@caret-width-base": "4px", 57 | "@caret-width-large": "5px", 58 | "@table-cell-padding": "8px", 59 | "@table-condensed-cell-padding": "5px", 60 | "@table-bg": "transparent", 61 | "@table-bg-accent": "#f9f9f9", 62 | "@table-bg-hover": "#f5f5f5", 63 | "@table-bg-active": "@table-bg-hover", 64 | "@table-border-color": "#ddd", 65 | "@btn-font-weight": "normal", 66 | "@btn-default-color": "#333", 67 | "@btn-default-bg": "#fff", 68 | "@btn-default-border": "#ccc", 69 | "@btn-primary-color": "#fff", 70 | "@btn-primary-bg": "@brand-primary", 71 | "@btn-primary-border": "darken(@btn-primary-bg, 5%)", 72 | "@btn-success-color": "#fff", 73 | "@btn-success-bg": "@brand-success", 74 | "@btn-success-border": "darken(@btn-success-bg, 5%)", 75 | "@btn-info-color": "#fff", 76 | "@btn-info-bg": "@brand-info", 77 | "@btn-info-border": "darken(@btn-info-bg, 5%)", 78 | "@btn-warning-color": "#fff", 79 | "@btn-warning-bg": "@brand-warning", 80 | "@btn-warning-border": "darken(@btn-warning-bg, 5%)", 81 | "@btn-danger-color": "#fff", 82 | "@btn-danger-bg": "@brand-danger", 83 | "@btn-danger-border": "darken(@btn-danger-bg, 5%)", 84 | "@btn-link-disabled-color": "@gray-light", 85 | "@btn-border-radius-base": "@border-radius-base", 86 | "@btn-border-radius-large": "@border-radius-large", 87 | "@btn-border-radius-small": "@border-radius-small", 88 | "@input-bg": "#fff", 89 | "@input-bg-disabled": "@gray-lighter", 90 | "@input-color": "@gray", 91 | "@input-border": "#ccc", 92 | "@input-border-radius": "@border-radius-base", 93 | "@input-border-radius-large": "@border-radius-large", 94 | "@input-border-radius-small": "@border-radius-small", 95 | "@input-border-focus": "#66afe9", 96 | "@input-color-placeholder": "#999", 97 | "@input-height-base": "(@line-height-computed + (@padding-base-vertical * 2) + 2)", 98 | "@input-height-large": "(ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2)", 99 | "@input-height-small": "(floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2)", 100 | "@form-group-margin-bottom": "15px", 101 | "@legend-color": "@gray-dark", 102 | "@legend-border-color": "#e5e5e5", 103 | "@input-group-addon-bg": "@gray-lighter", 104 | "@input-group-addon-border-color": "@input-border", 105 | "@cursor-disabled": "not-allowed", 106 | "@dropdown-bg": "#fff", 107 | "@dropdown-border": "rgba(0,0,0,.15)", 108 | "@dropdown-fallback-border": "#ccc", 109 | "@dropdown-divider-bg": "#e5e5e5", 110 | "@dropdown-link-color": "@gray-dark", 111 | "@dropdown-link-hover-color": "darken(@gray-dark, 5%)", 112 | "@dropdown-link-hover-bg": "#f5f5f5", 113 | "@dropdown-link-active-color": "@component-active-color", 114 | "@dropdown-link-active-bg": "@component-active-bg", 115 | "@dropdown-link-disabled-color": "@gray-light", 116 | "@dropdown-header-color": "@gray-light", 117 | "@dropdown-caret-color": "#000", 118 | "@screen-xs": "480px", 119 | "@screen-xs-min": "@screen-xs", 120 | "@screen-phone": "@screen-xs-min", 121 | "@screen-sm": "768px", 122 | "@screen-sm-min": "@screen-sm", 123 | "@screen-tablet": "@screen-sm-min", 124 | "@screen-md": "992px", 125 | "@screen-md-min": "@screen-md", 126 | "@screen-desktop": "@screen-md-min", 127 | "@screen-lg": "1200px", 128 | "@screen-lg-min": "@screen-lg", 129 | "@screen-lg-desktop": "@screen-lg-min", 130 | "@screen-xs-max": "(@screen-sm-min - 1)", 131 | "@screen-sm-max": "(@screen-md-min - 1)", 132 | "@screen-md-max": "(@screen-lg-min - 1)", 133 | "@grid-columns": "12", 134 | "@grid-gutter-width": "30px", 135 | "@grid-float-breakpoint": "@screen-sm-min", 136 | "@grid-float-breakpoint-max": "(@grid-float-breakpoint - 1)", 137 | "@container-tablet": "(720px + @grid-gutter-width)", 138 | "@container-sm": "@container-tablet", 139 | "@container-desktop": "(940px + @grid-gutter-width)", 140 | "@container-md": "@container-desktop", 141 | "@container-large-desktop": "(1140px + @grid-gutter-width)", 142 | "@container-lg": "@container-large-desktop", 143 | "@navbar-height": "50px", 144 | "@navbar-margin-bottom": "@line-height-computed", 145 | "@navbar-border-radius": "@border-radius-base", 146 | "@navbar-padding-horizontal": "floor((@grid-gutter-width / 2))", 147 | "@navbar-padding-vertical": "((@navbar-height - @line-height-computed) / 2)", 148 | "@navbar-collapse-max-height": "340px", 149 | "@navbar-default-color": "#777", 150 | "@navbar-default-bg": "#f8f8f8", 151 | "@navbar-default-border": "darken(@navbar-default-bg, 6.5%)", 152 | "@navbar-default-link-color": "#777", 153 | "@navbar-default-link-hover-color": "#333", 154 | "@navbar-default-link-hover-bg": "transparent", 155 | "@navbar-default-link-active-color": "#555", 156 | "@navbar-default-link-active-bg": "darken(@navbar-default-bg, 6.5%)", 157 | "@navbar-default-link-disabled-color": "#ccc", 158 | "@navbar-default-link-disabled-bg": "transparent", 159 | "@navbar-default-brand-color": "@navbar-default-link-color", 160 | "@navbar-default-brand-hover-color": "darken(@navbar-default-brand-color, 10%)", 161 | "@navbar-default-brand-hover-bg": "transparent", 162 | "@navbar-default-toggle-hover-bg": "#ddd", 163 | "@navbar-default-toggle-icon-bar-bg": "#888", 164 | "@navbar-default-toggle-border-color": "#ddd", 165 | "@navbar-inverse-color": "lighten(@gray-light, 15%)", 166 | "@navbar-inverse-bg": "#222", 167 | "@navbar-inverse-border": "darken(@navbar-inverse-bg, 10%)", 168 | "@navbar-inverse-link-color": "lighten(@gray-light, 15%)", 169 | "@navbar-inverse-link-hover-color": "#fff", 170 | "@navbar-inverse-link-hover-bg": "transparent", 171 | "@navbar-inverse-link-active-color": "@navbar-inverse-link-hover-color", 172 | "@navbar-inverse-link-active-bg": "darken(@navbar-inverse-bg, 10%)", 173 | "@navbar-inverse-link-disabled-color": "#444", 174 | "@navbar-inverse-link-disabled-bg": "transparent", 175 | "@navbar-inverse-brand-color": "@navbar-inverse-link-color", 176 | "@navbar-inverse-brand-hover-color": "#fff", 177 | "@navbar-inverse-brand-hover-bg": "transparent", 178 | "@navbar-inverse-toggle-hover-bg": "#333", 179 | "@navbar-inverse-toggle-icon-bar-bg": "#fff", 180 | "@navbar-inverse-toggle-border-color": "#333", 181 | "@nav-link-padding": "10px 15px", 182 | "@nav-link-hover-bg": "@gray-lighter", 183 | "@nav-disabled-link-color": "@gray-light", 184 | "@nav-disabled-link-hover-color": "@gray-light", 185 | "@nav-tabs-border-color": "#ddd", 186 | "@nav-tabs-link-hover-border-color": "@gray-lighter", 187 | "@nav-tabs-active-link-hover-bg": "@body-bg", 188 | "@nav-tabs-active-link-hover-color": "@gray", 189 | "@nav-tabs-active-link-hover-border-color": "#ddd", 190 | "@nav-tabs-justified-link-border-color": "#ddd", 191 | "@nav-tabs-justified-active-link-border-color": "@body-bg", 192 | "@nav-pills-border-radius": "@border-radius-base", 193 | "@nav-pills-active-link-hover-bg": "@component-active-bg", 194 | "@nav-pills-active-link-hover-color": "@component-active-color", 195 | "@pagination-color": "@link-color", 196 | "@pagination-bg": "#fff", 197 | "@pagination-border": "#ddd", 198 | "@pagination-hover-color": "@link-hover-color", 199 | "@pagination-hover-bg": "@gray-lighter", 200 | "@pagination-hover-border": "#ddd", 201 | "@pagination-active-color": "#fff", 202 | "@pagination-active-bg": "@brand-primary", 203 | "@pagination-active-border": "@brand-primary", 204 | "@pagination-disabled-color": "@gray-light", 205 | "@pagination-disabled-bg": "#fff", 206 | "@pagination-disabled-border": "#ddd", 207 | "@pager-bg": "@pagination-bg", 208 | "@pager-border": "@pagination-border", 209 | "@pager-border-radius": "15px", 210 | "@pager-hover-bg": "@pagination-hover-bg", 211 | "@pager-active-bg": "@pagination-active-bg", 212 | "@pager-active-color": "@pagination-active-color", 213 | "@pager-disabled-color": "@pagination-disabled-color", 214 | "@jumbotron-padding": "30px", 215 | "@jumbotron-color": "inherit", 216 | "@jumbotron-bg": "@gray-lighter", 217 | "@jumbotron-heading-color": "inherit", 218 | "@jumbotron-font-size": "ceil((@font-size-base * 1.5))", 219 | "@jumbotron-heading-font-size": "ceil((@font-size-base * 4.5))", 220 | "@state-success-text": "#3c763d", 221 | "@state-success-bg": "#dff0d8", 222 | "@state-success-border": "darken(spin(@state-success-bg, -10), 5%)", 223 | "@state-info-text": "#31708f", 224 | "@state-info-bg": "#d9edf7", 225 | "@state-info-border": "darken(spin(@state-info-bg, -10), 7%)", 226 | "@state-warning-text": "#8a6d3b", 227 | "@state-warning-bg": "#fcf8e3", 228 | "@state-warning-border": "darken(spin(@state-warning-bg, -10), 5%)", 229 | "@state-danger-text": "#a94442", 230 | "@state-danger-bg": "#f2dede", 231 | "@state-danger-border": "darken(spin(@state-danger-bg, -10), 5%)", 232 | "@tooltip-max-width": "200px", 233 | "@tooltip-color": "#fff", 234 | "@tooltip-bg": "#000", 235 | "@tooltip-opacity": ".9", 236 | "@tooltip-arrow-width": "5px", 237 | "@tooltip-arrow-color": "@tooltip-bg", 238 | "@popover-bg": "#fff", 239 | "@popover-max-width": "276px", 240 | "@popover-border-color": "rgba(0,0,0,.2)", 241 | "@popover-fallback-border-color": "#ccc", 242 | "@popover-title-bg": "darken(@popover-bg, 3%)", 243 | "@popover-arrow-width": "10px", 244 | "@popover-arrow-color": "@popover-bg", 245 | "@popover-arrow-outer-width": "(@popover-arrow-width + 1)", 246 | "@popover-arrow-outer-color": "fadein(@popover-border-color, 5%)", 247 | "@popover-arrow-outer-fallback-color": "darken(@popover-fallback-border-color, 20%)", 248 | "@label-default-bg": "@gray-light", 249 | "@label-primary-bg": "@brand-primary", 250 | "@label-success-bg": "@brand-success", 251 | "@label-info-bg": "@brand-info", 252 | "@label-warning-bg": "@brand-warning", 253 | "@label-danger-bg": "@brand-danger", 254 | "@label-color": "#fff", 255 | "@label-link-hover-color": "#fff", 256 | "@modal-inner-padding": "15px", 257 | "@modal-title-padding": "15px", 258 | "@modal-title-line-height": "@line-height-base", 259 | "@modal-content-bg": "#fff", 260 | "@modal-content-border-color": "rgba(0,0,0,.2)", 261 | "@modal-content-fallback-border-color": "#999", 262 | "@modal-backdrop-bg": "#000", 263 | "@modal-backdrop-opacity": ".5", 264 | "@modal-header-border-color": "#e5e5e5", 265 | "@modal-footer-border-color": "@modal-header-border-color", 266 | "@modal-lg": "900px", 267 | "@modal-md": "600px", 268 | "@modal-sm": "300px", 269 | "@alert-padding": "15px", 270 | "@alert-border-radius": "@border-radius-base", 271 | "@alert-link-font-weight": "bold", 272 | "@alert-success-bg": "@state-success-bg", 273 | "@alert-success-text": "@state-success-text", 274 | "@alert-success-border": "@state-success-border", 275 | "@alert-info-bg": "@state-info-bg", 276 | "@alert-info-text": "@state-info-text", 277 | "@alert-info-border": "@state-info-border", 278 | "@alert-warning-bg": "@state-warning-bg", 279 | "@alert-warning-text": "@state-warning-text", 280 | "@alert-warning-border": "@state-warning-border", 281 | "@alert-danger-bg": "@state-danger-bg", 282 | "@alert-danger-text": "@state-danger-text", 283 | "@alert-danger-border": "@state-danger-border", 284 | "@progress-bg": "#f5f5f5", 285 | "@progress-bar-color": "#fff", 286 | "@progress-border-radius": "@border-radius-base", 287 | "@progress-bar-bg": "@brand-primary", 288 | "@progress-bar-success-bg": "@brand-success", 289 | "@progress-bar-warning-bg": "@brand-warning", 290 | "@progress-bar-danger-bg": "@brand-danger", 291 | "@progress-bar-info-bg": "@brand-info", 292 | "@list-group-bg": "#fff", 293 | "@list-group-border": "#ddd", 294 | "@list-group-border-radius": "@border-radius-base", 295 | "@list-group-hover-bg": "#f5f5f5", 296 | "@list-group-active-color": "@component-active-color", 297 | "@list-group-active-bg": "@component-active-bg", 298 | "@list-group-active-border": "@list-group-active-bg", 299 | "@list-group-active-text-color": "lighten(@list-group-active-bg, 40%)", 300 | "@list-group-disabled-color": "@gray-light", 301 | "@list-group-disabled-bg": "@gray-lighter", 302 | "@list-group-disabled-text-color": "@list-group-disabled-color", 303 | "@list-group-link-color": "#555", 304 | "@list-group-link-hover-color": "@list-group-link-color", 305 | "@list-group-link-heading-color": "#333", 306 | "@panel-bg": "#fff", 307 | "@panel-body-padding": "15px", 308 | "@panel-heading-padding": "10px 15px", 309 | "@panel-footer-padding": "@panel-heading-padding", 310 | "@panel-border-radius": "@border-radius-base", 311 | "@panel-inner-border": "#ddd", 312 | "@panel-footer-bg": "#f5f5f5", 313 | "@panel-default-text": "@gray-dark", 314 | "@panel-default-border": "#ddd", 315 | "@panel-default-heading-bg": "#f5f5f5", 316 | "@panel-primary-text": "#fff", 317 | "@panel-primary-border": "@brand-primary", 318 | "@panel-primary-heading-bg": "@brand-primary", 319 | "@panel-success-text": "@state-success-text", 320 | "@panel-success-border": "@state-success-border", 321 | "@panel-success-heading-bg": "@state-success-bg", 322 | "@panel-info-text": "@state-info-text", 323 | "@panel-info-border": "@state-info-border", 324 | "@panel-info-heading-bg": "@state-info-bg", 325 | "@panel-warning-text": "@state-warning-text", 326 | "@panel-warning-border": "@state-warning-border", 327 | "@panel-warning-heading-bg": "@state-warning-bg", 328 | "@panel-danger-text": "@state-danger-text", 329 | "@panel-danger-border": "@state-danger-border", 330 | "@panel-danger-heading-bg": "@state-danger-bg", 331 | "@thumbnail-padding": "4px", 332 | "@thumbnail-bg": "@body-bg", 333 | "@thumbnail-border": "#ddd", 334 | "@thumbnail-border-radius": "@border-radius-base", 335 | "@thumbnail-caption-color": "@text-color", 336 | "@thumbnail-caption-padding": "9px", 337 | "@well-bg": "#f5f5f5", 338 | "@well-border": "darken(@well-bg, 7%)", 339 | "@badge-color": "#fff", 340 | "@badge-link-hover-color": "#fff", 341 | "@badge-bg": "@gray-light", 342 | "@badge-active-color": "@link-color", 343 | "@badge-active-bg": "#fff", 344 | "@badge-font-weight": "bold", 345 | "@badge-line-height": "1", 346 | "@badge-border-radius": "10px", 347 | "@breadcrumb-padding-vertical": "8px", 348 | "@breadcrumb-padding-horizontal": "15px", 349 | "@breadcrumb-bg": "#f5f5f5", 350 | "@breadcrumb-color": "#ccc", 351 | "@breadcrumb-active-color": "@gray-light", 352 | "@breadcrumb-separator": "\"/\"", 353 | "@carousel-text-shadow": "0 1px 2px rgba(0,0,0,.6)", 354 | "@carousel-control-color": "#fff", 355 | "@carousel-control-width": "15%", 356 | "@carousel-control-opacity": ".5", 357 | "@carousel-control-font-size": "20px", 358 | "@carousel-indicator-active-bg": "#fff", 359 | "@carousel-indicator-border-color": "#fff", 360 | "@carousel-caption-color": "#fff", 361 | "@close-font-weight": "bold", 362 | "@close-color": "#000", 363 | "@close-text-shadow": "0 1px 0 #fff", 364 | "@code-color": "#c7254e", 365 | "@code-bg": "#f9f2f4", 366 | "@kbd-color": "#fff", 367 | "@kbd-bg": "#333", 368 | "@pre-bg": "#f5f5f5", 369 | "@pre-color": "@gray-dark", 370 | "@pre-border-color": "#ccc", 371 | "@pre-scrollable-max-height": "340px", 372 | "@component-offset-horizontal": "180px", 373 | "@text-muted": "@gray-light", 374 | "@abbr-border-color": "@gray-light", 375 | "@headings-small-color": "@gray-light", 376 | "@blockquote-small-color": "@gray-light", 377 | "@blockquote-font-size": "(@font-size-base * 1.25)", 378 | "@blockquote-border-color": "@gray-lighter", 379 | "@page-header-border-color": "@gray-lighter", 380 | "@dl-horizontal-offset": "@component-offset-horizontal", 381 | "@dl-horizontal-breakpoint": "@grid-float-breakpoint", 382 | "@hr-border": "@gray-lighter" 383 | }, 384 | "css": [ 385 | "type.less", 386 | "grid.less", 387 | "tables.less", 388 | "forms.less", 389 | "buttons.less", 390 | "glyphicons.less", 391 | "navs.less", 392 | "alerts.less", 393 | "list-group.less", 394 | "component-animations.less" 395 | ], 396 | "js": [ 397 | "tab.js" 398 | ], 399 | "customizerUrl": "https://getbootstrap.com/customize/?id=38e82de47bacb1a273e8a2f31a1edcbf" 400 | } -------------------------------------------------------------------------------- /assets/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (https://getbootstrap.com/customize/?id=38e82de47bacb1a273e8a2f31a1edcbf) 9 | * Config saved to config.json and https://gist.github.com/38e82de47bacb1a273e8a2f31a1edcbf 10 | *//*! 11 | * Bootstrap v3.3.7 (http://getbootstrap.com) 12 | * Copyright 2011-2016 Twitter, Inc. 13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.nav:before,.nav:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.nav:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed} -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /assets/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/assets/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /assets/js/dashboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tyk Dev Portal Dashbaord Components 3 | * 4 | * To keep things simple, we're keeping everything in one file for now. 5 | * When our js codebase grows, we should consider spilitting it up into modules 6 | * and adding a build step. 7 | */ 8 | ;(function($, Vue, Chart, _) { 9 | /** 10 | * Add a leading zero to a number if it's not there yet 11 | * @param {int} nr 12 | * @return {string} 13 | */ 14 | function leadingZero(nr) { 15 | return ('0' + nr).slice(-2); 16 | } 17 | 18 | /** 19 | * Add two numbers 20 | * @param {number} a 21 | * @param {number} b 22 | * @return {number} Sum of a and b 23 | */ 24 | function add(a, b) { 25 | return a+b 26 | } 27 | 28 | /** 29 | * Request token form component 30 | */ 31 | var RequestTokenForm = Vue.extend({ 32 | props: ['apis', 'subscribedApis'], 33 | 34 | data: function() { 35 | return { 36 | token_name: '', 37 | api: '', 38 | tac_accepted: false, 39 | message: '', 40 | hasError: false, 41 | busy: false 42 | }; 43 | }, 44 | 45 | events: { 46 | /** 47 | * Reset form fields after token was created 48 | */ 49 | 'new-token': function() { 50 | this.token_name = ''; 51 | this.api = ''; 52 | this.tac_accepted = false; 53 | } 54 | }, 55 | 56 | computed: { 57 | /** 58 | * Check if register button should be shown 59 | * @return {boolean} 60 | */ 61 | formValid: function() { 62 | return ( 63 | this.token_name != '' && 64 | this.api != '' 65 | && (!this.requiresTAC || this.tac_accepted) 66 | ); 67 | }, 68 | requiresTAC: function() { 69 | // hide the tac field by default 70 | if (!this.api) { 71 | return false; 72 | } 73 | let api = this.apis.find(o => o.id === this.api); 74 | return api.requires_tac; 75 | } 76 | }, 77 | 78 | methods: { 79 | /** 80 | * Check if user is already subscribed to an api 81 | * @return {boolean} 82 | */ 83 | hasTokenForAPI: function(apiId) { 84 | return ($.inArray(apiId, this.subscribedApis) >= 0); 85 | }, 86 | 87 | /** 88 | * Request a token 89 | */ 90 | register: function() { 91 | this.busy = true; 92 | var self = this; 93 | 94 | var data = { 95 | action: 'get_token', 96 | api: this.api, 97 | token_name: this.token_name 98 | }; 99 | 100 | // post to server 101 | $.post(scriptParams.actionUrl, data) 102 | .done(function(result) { 103 | if (result && result.success) { 104 | self.message = result.data.message; 105 | self.$dispatch('new-token'); 106 | } 107 | else { 108 | self.hasError = true; 109 | if (console && console.error) { 110 | console.error(result); 111 | } 112 | } 113 | self.busy = false; 114 | }); 115 | }, 116 | 117 | /** 118 | * Reset the message 119 | */ 120 | closeMessage: function() { 121 | this.message = ''; 122 | } 123 | } 124 | }); 125 | 126 | 127 | /** 128 | * Usage tab component 129 | */ 130 | var UsageTab = Vue.extend({ 131 | props: ['tokens'], 132 | 133 | data: function() { 134 | // look at all this code! all just to format two dates to set initial values for the date fields :0 135 | var now = new Date(), 136 | nextWeek = new Date(); 137 | nextWeek.setDate(now.getDate() - 7); 138 | var fromDate = [nextWeek.getFullYear(), leadingZero(nextWeek.getMonth()+1), leadingZero(nextWeek.getDate())].join('-'), 139 | toDate = [now.getFullYear(), leadingZero(now.getMonth()+1), leadingZero(now.getDate())].join('-'); 140 | 141 | return { 142 | // instance of chartist line chart 143 | lineChart: null, 144 | error: null, 145 | form: { 146 | token: null, 147 | fromDate: fromDate, 148 | toDate: toDate, 149 | // is the form busy? 150 | busy: false, 151 | } 152 | } 153 | }, 154 | 155 | watch: { 156 | 'form.token': function() { 157 | this.fetchUsage(); 158 | } 159 | }, 160 | 161 | events: { 162 | // list for an event from parent to show data 163 | showUsage: function(token) { 164 | // this will trigger a reload of the data 165 | this.form.token = token.hash; 166 | } 167 | }, 168 | 169 | methods: { 170 | /** 171 | * Fetch usage data from server and display it 172 | * Uses this.form params to filter data 173 | */ 174 | fetchUsage: function() { 175 | var self = this; 176 | var data = { 177 | action: 'get_token_usage', 178 | token: this.form.token 179 | }; 180 | 181 | // add from date 182 | if (this.form.fromDate) { 183 | data.from = this.form.fromDate; 184 | } 185 | // add to date 186 | if (this.form.toDate) { 187 | data.to = this.form.toDate; 188 | } 189 | 190 | $.get(scriptParams.actionUrl, data) 191 | .done(function(response) { 192 | if (response.data) { 193 | self.showUsage(response.data); 194 | } 195 | }); 196 | }, 197 | 198 | /** 199 | * Show usage data 200 | * @return {[type]} 201 | */ 202 | showUsage: function(data) { 203 | var chartData = this.crunchUsageNumbers(data); 204 | 205 | // setup the chart data 206 | var chartConfig = { 207 | labels: chartData.labels, 208 | datasets: [ 209 | { 210 | data: chartData.success, 211 | backgroundColor: 'rgba(5,52,139,.6)', 212 | label: scriptParams.label_success, 213 | }, 214 | { 215 | data: chartData.errors, 216 | backgroundColor: 'rgba(255,193,21,.6)', 217 | label: scriptParams.label_errors, 218 | }, 219 | ] 220 | }; 221 | 222 | // build a line chart on next view render with this data 223 | this.$nextTick(function() { 224 | // init chart from scratch 225 | if (!this.lineChart) { 226 | this.lineChart = new Chart(this.$els.usage, { 227 | type: 'line', 228 | data: chartConfig, 229 | options: { 230 | spanGaps: true, 231 | scales: { 232 | yAxes: [{ ticks: { min: 0 }}] 233 | } 234 | } 235 | }); 236 | } 237 | // just update the data 238 | else { 239 | this.lineChart.config.data = chartConfig; 240 | } 241 | 242 | // start y scale at 0 and increment in steps of 1 243 | if (chartData.sum.total < 1) { 244 | this.lineChart.config.options.scales.yAxes[0].ticks.stepSize = 1; 245 | } 246 | this.lineChart.update(); 247 | }); 248 | }, 249 | 250 | /** 251 | * Crunch the numbers from usage data and gain insightful information :) 252 | * @param {object} data 253 | * @return {object} 254 | */ 255 | crunchUsageNumbers: function(data) { 256 | var chartData = { 257 | success: [], 258 | errors: [], 259 | labels: [], 260 | sum: {} 261 | }; 262 | 263 | // get each "success" and "error" stat from the stack 264 | _.chain(data) 265 | // sort by timestamp first 266 | .sortBy(function(item) { 267 | return new Date(item.id.year, item.id.month - 1, item.id.day).getTime(); 268 | }) 269 | // get number for each entry 270 | .each(function(stat) { 271 | chartData.success.push(stat.success); 272 | chartData.errors.push(stat.error); 273 | if (stat.id) { 274 | // @todo localize this? 275 | chartData.labels.push(stat.id.day + '.' + stat.id.month + '.'); 276 | } 277 | }); 278 | 279 | chartData.sum.success = _.reduce(chartData.success, add), 280 | chartData.sum.errors = _.reduce(chartData.errors, add); 281 | chartData.sum.total = add(chartData.sum.success, chartData.sum.errors); 282 | 283 | return chartData; 284 | } 285 | } 286 | }); 287 | 288 | 289 | /** 290 | * Quota tab component 291 | */ 292 | var QuotaTab = Vue.extend({ 293 | data: function() { 294 | return { 295 | // the selected token key 296 | key: null, 297 | busy: false, 298 | error: null, 299 | message: null, 300 | chart: null 301 | } 302 | }, 303 | 304 | watch: { 305 | 'key': function() { 306 | if (this.key.length > 0) { 307 | this.getQuotas(); 308 | } 309 | } 310 | }, 311 | 312 | methods: { 313 | /** 314 | * Get token usage data from server 315 | */ 316 | getQuotas: function() { 317 | var self = this; 318 | this.busy = true; 319 | this.error = null; 320 | 321 | var data = { 322 | action: 'get_token_quota', 323 | token: this.key 324 | }; 325 | 326 | // use a post request so the key doesn't show up in server logs 327 | $.post(scriptParams.actionUrl, data) 328 | .done(function(response) { 329 | if (response.data && response.success) { 330 | self.showQuotas(response.data); 331 | } 332 | else if (response.success === false) { 333 | self.setError(response.data) 334 | } 335 | }); 336 | }, 337 | 338 | /** 339 | * Set an error in gui 340 | * @param {string} error 341 | */ 342 | setError: function(error) { 343 | this.error = error; 344 | this.busy = false; 345 | }, 346 | 347 | /** 348 | * Set a message in gui 349 | * @param {string} message 350 | */ 351 | setMessage: function(message) { 352 | this.message = message; 353 | this.busy = false; 354 | }, 355 | 356 | /** 357 | * Show usage quota 358 | * @param {object} data 359 | */ 360 | showQuotas: function(data) { 361 | // catch invalid data 362 | if (data.quota_max === -1) { 363 | this.setMessage(scriptParams.msg_unlimited_quota); 364 | return; 365 | } 366 | else if (!data.quota_max || data.quota_max < data.quota_remaining) { 367 | this.setError(scriptParams.error_invalid_data) 368 | return; 369 | } 370 | 371 | this.$nextTick(function() { 372 | var chartData = { 373 | labels: [scriptParams.label_used, scriptParams.label_remaining], 374 | datasets: [{ 375 | data: [(data.quota_max - data.quota_remaining), data.quota_remaining], 376 | backgroundColor: ['#ffc115', '#05348B'] 377 | }] 378 | }; 379 | if (!this.chart) { 380 | this.chart = new Chart(this.$els.chart, { 381 | type: 'doughnut', 382 | data: chartData 383 | }); 384 | } else { 385 | this.chart.config.data = chartData; 386 | this.chart.update(); 387 | } 388 | this.busy = false; 389 | }); 390 | } 391 | } 392 | }); 393 | 394 | 395 | 396 | /** 397 | * List of user tokens component 398 | */ 399 | var Dashboard = new Vue({ 400 | el: '#tyk-dashboard', 401 | 402 | components: { 403 | 'request-token-form': RequestTokenForm, 404 | 'usage-tab': UsageTab, 405 | 'quota-tab': QuotaTab 406 | }, 407 | 408 | data: { 409 | tokens: [], 410 | message: '', 411 | hasError: false, 412 | loading: false, 413 | availableApis: [] 414 | }, 415 | 416 | computed: { 417 | /** 418 | * Get a list of subscribed APIs 419 | * @return {array} 420 | */ 421 | subscribedApis: function() { 422 | return _.pluck(this.tokens, 'api_id'); 423 | } 424 | }, 425 | 426 | events: { 427 | /** 428 | * Request token form got a new token: refresh token list 429 | */ 430 | 'new-token': function() { 431 | this.updateFromServer(); 432 | }, 433 | 434 | /** 435 | * A token was deleted: refresh token list 436 | */ 437 | 'deleted-token': function() { 438 | this.updateFromServer(); 439 | } 440 | }, 441 | 442 | beforeCompile: function() { 443 | this.updateFromServer(); 444 | }, 445 | 446 | methods: { 447 | /** 448 | * Update all data from server 449 | * @return {void} 450 | */ 451 | updateFromServer: function() { 452 | var self = this; 453 | this.loading = true; 454 | $.when( this.fetchTokens(), this.fetchApis() ).then(function() { 455 | self.loading = false; 456 | }); 457 | }, 458 | 459 | /** 460 | * Get api name from id 461 | * @return {string} 462 | */ 463 | getApiName: function(apiId) { 464 | var api = _.findWhere(this.availableApis, { id: apiId }); 465 | return _.isObject(api) 466 | ? api.name 467 | : ''; 468 | }, 469 | 470 | /** 471 | * Returns the state of a token 472 | * @param {object} token 473 | * @return {string} 474 | */ 475 | getState: function(token) { 476 | return token.is_valid 477 | ? scriptParams.label_valid 478 | : scriptParams.label_invalid; 479 | }, 480 | 481 | /** 482 | * Fetch tokens from server 483 | * @return {object} jQuery Promise 484 | */ 485 | fetchTokens: function() { 486 | var self = this; 487 | return $.getJSON(scriptParams.actionUrl, {action: 'get_tokens'}).done(function(result) { 488 | if (typeof(result) == 'object' && result.success) { 489 | self.tokens = result.data; 490 | } 491 | else { 492 | self.hasError = true; 493 | if (console && console.error) { 494 | console.error(result); 495 | } 496 | } 497 | }); 498 | }, 499 | 500 | /** 501 | * Fetch available apis from server 502 | * @return {object} jQuery Promise 503 | */ 504 | fetchApis: function() { 505 | var self = this; 506 | return $.getJSON(scriptParams.actionUrl, {action: 'get_available_apis'}).done(function(result) { 507 | if (typeof(result) == 'object' && result.success) { 508 | self.availableApis = result.data; 509 | } 510 | else { 511 | self.hasError = true; 512 | if (console && console.error) { 513 | console.error(result); 514 | } 515 | } 516 | }); 517 | }, 518 | 519 | /** 520 | * Revoke a token on tyk api 521 | * @param {object} token 522 | */ 523 | revokeToken: function(token) { 524 | var data = { 525 | action: 'revoke_token', 526 | token: token.hash 527 | }; 528 | var self = this; 529 | $.post(scriptParams.actionUrl, data) 530 | .done(function(result) { 531 | if (result && result.success) { 532 | self.$emit('deleted-token'); 533 | self.message = result.data.message; 534 | } 535 | else { 536 | self.hasError = true; 537 | if (console && console.error) { 538 | console.error(result); 539 | } 540 | } 541 | }); 542 | }, 543 | 544 | /** 545 | * Activate usage tab 546 | */ 547 | showUsageTab: function(token) { 548 | this.$broadcast('showUsage', token); 549 | $(this.$els.usageTab).tab('show'); 550 | }, 551 | } 552 | }); 553 | })(jQuery, Vue, Chart, _); 554 | -------------------------------------------------------------------------------- /assets/js/vendor/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (https://getbootstrap.com/customize/?id=38e82de47bacb1a273e8a2f31a1edcbf) 9 | * Config saved to config.json and https://gist.github.com/38e82de47bacb1a273e8a2f31a1edcbf 10 | */ 11 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||e[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(t){"use strict";function e(e){return this.each(function(){var n=t(this),r=n.data("bs.tab");r||n.data("bs.tab",r=new a(this)),"string"==typeof e&&r[e]()})}var a=function(e){this.element=t(e)};a.VERSION="3.3.7",a.TRANSITION_DURATION=150,a.prototype.show=function(){var e=this.element,a=e.closest("ul:not(.dropdown-menu)"),n=e.data("target");if(n||(n=e.attr("href"),n=n&&n.replace(/.*(?=#[^\s]*$)/,"")),!e.parent("li").hasClass("active")){var r=a.find(".active:last a"),i=t.Event("hide.bs.tab",{relatedTarget:e[0]}),s=t.Event("show.bs.tab",{relatedTarget:r[0]});if(r.trigger(i),e.trigger(s),!s.isDefaultPrevented()&&!i.isDefaultPrevented()){var o=t(n);this.activate(e.closest("li"),a),this.activate(o,o.parent(),function(){r.trigger({type:"hidden.bs.tab",relatedTarget:e[0]}),e.trigger({type:"shown.bs.tab",relatedTarget:r[0]})})}}},a.prototype.activate=function(e,n,r){function i(){s.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),e.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),o?(e[0].offsetWidth,e.addClass("in")):e.removeClass("fade"),e.parent(".dropdown-menu").length&&e.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),r&&r()}var s=n.find("> .active"),o=r&&t.support.transition&&(s.length&&s.hasClass("fade")||!!n.find("> .fade").length);s.length&&o?s.one("bsTransitionEnd",i).emulateTransitionEnd(a.TRANSITION_DURATION):i(),s.removeClass("in")};var n=t.fn.tab;t.fn.tab=e,t.fn.tab.Constructor=a,t.fn.tab.noConflict=function(){return t.fn.tab=n,this};var r=function(a){a.preventDefault(),e.call(t(this),"show")};t(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',r).on("click.bs.tab.data-api",'[data-toggle="pill"]',r)}(jQuery); -------------------------------------------------------------------------------- /assets/js/vendor/underscore.min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 16 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 17 | 18 | download() { 19 | if [ `which curl` ]; then 20 | curl -s "$1" > "$2"; 21 | elif [ `which wget` ]; then 22 | wget -nv -O "$2" "$1" 23 | fi 24 | } 25 | 26 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 27 | WP_TESTS_TAG="tags/$WP_VERSION" 28 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 29 | WP_TESTS_TAG="trunk" 30 | else 31 | # http serves a single offer, whereas https serves multiple. we only want one 32 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 33 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 34 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 35 | if [[ -z "$LATEST_VERSION" ]]; then 36 | echo "Latest WordPress version could not be found" 37 | exit 1 38 | fi 39 | WP_TESTS_TAG="tags/$LATEST_VERSION" 40 | fi 41 | 42 | set -ex 43 | 44 | install_wp() { 45 | 46 | if [ -d $WP_CORE_DIR ]; then 47 | return; 48 | fi 49 | 50 | mkdir -p $WP_CORE_DIR 51 | 52 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 53 | mkdir -p /tmp/wordpress-nightly 54 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 55 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 56 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 57 | else 58 | if [ $WP_VERSION == 'latest' ]; then 59 | local ARCHIVE_NAME='latest' 60 | else 61 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 62 | fi 63 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 64 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 65 | fi 66 | 67 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 68 | } 69 | 70 | install_test_suite() { 71 | # portable in-place argument for both GNU sed and Mac OSX sed 72 | if [[ $(uname -s) == 'Darwin' ]]; then 73 | local ioption='-i .bak' 74 | else 75 | local ioption='-i' 76 | fi 77 | 78 | # set up testing suite if it doesn't yet exist 79 | if [ ! -d $WP_TESTS_DIR ]; then 80 | # set up testing suite 81 | mkdir -p $WP_TESTS_DIR 82 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 83 | fi 84 | 85 | if [ ! -f wp-tests-config.php ]; then 86 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 87 | # remove all forward slashes in the end 88 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 89 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 90 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 91 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 92 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 93 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 94 | fi 95 | 96 | } 97 | 98 | install_db() { 99 | 100 | if [ ${SKIP_DB_CREATE} = "true" ]; then 101 | return 0 102 | fi 103 | 104 | # parse DB_HOST for port or socket references 105 | local PARTS=(${DB_HOST//\:/ }) 106 | local DB_HOSTNAME=${PARTS[0]}; 107 | local DB_SOCK_OR_PORT=${PARTS[1]}; 108 | local EXTRA="" 109 | 110 | if ! [ -z $DB_HOSTNAME ] ; then 111 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 112 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 113 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 114 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 115 | elif ! [ -z $DB_HOSTNAME ] ; then 116 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 117 | fi 118 | fi 119 | 120 | # create database 121 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 122 | } 123 | 124 | install_wp 125 | install_test_suite 126 | install_db 127 | -------------------------------------------------------------------------------- /classes/api_manager.php: -------------------------------------------------------------------------------- 1 | get('/portal/policies', array('p' => -1)); 32 | // a generator would be nice here but alas, php 5.4 is still very common 33 | $active_apis = array(); 34 | if (is_object($response) && isset($response->Data)) { 35 | foreach ($response->Data as $policy) { 36 | // only return active apis 37 | if ($policy->active && !$policy->is_inactive) { 38 | if (isset($policy->tags) && is_array($policy->tags) && in_array(self::POLICY_TAG, $policy->tags)) { 39 | $active_apis[] = array( 40 | 'id' => $policy->_id, 41 | 'name' => $policy->name, 42 | 'requires_tac' => in_array(self::TAC_TAG, $policy->tags), 43 | ); 44 | } 45 | } 46 | } 47 | } 48 | 49 | return $active_apis; 50 | } 51 | 52 | /** 53 | * Get APIs defined on Tyk. 54 | * 55 | * @return array 56 | * 57 | * @throws Exception When no apis are found in response 58 | */ 59 | public static function available_apis() 60 | { 61 | $tyk = new Tyk_API(); 62 | $response = $tyk->get('/apis'); 63 | // a generator would be nice here but alas, php 5.4 is still very common 64 | $apis = array(); 65 | if (is_object($response) && isset($response->apis)) { 66 | return $response->apis; 67 | } 68 | 69 | throw new Exception('No apis defined'); 70 | 71 | return $apis; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /classes/dashboard_ajax_provider.php: -------------------------------------------------------------------------------- 1 | request(); 38 | 39 | // when keys are approved automatically 40 | if (TYK_AUTO_APPROVE_KEY_REQUESTS) { 41 | $token->approve(); 42 | // save the access token information 43 | $user->save_access_token($_POST['api'], $_POST['token_name'], $token->get_hash()); 44 | 45 | // we removed the dot for User Experience, 46 | // since the User copies the dot with the key and therefore the key is corupted. 47 | $message = sprintf( 48 | __('Your token for this API is: %s and we will only show this once. Please save it somewhere now.', Tyk_Dev_Portal::TEXT_DOMAIN), 49 | $token->get_key(), 50 | ); 51 | } 52 | // when keys await manual approval 53 | else { 54 | $message = sprintf( 55 | __( 56 | 'Your key request is pending review. You will receive an Email when your request is processed. Your request id is %s. This is not your access token. Please refer to the request ID when contacting us by Email.', 57 | Tyk_Dev_Portal::TEXT_DOMAIN, 58 | ), 59 | $token->get_id(), 60 | ); 61 | } 62 | 63 | wp_send_json_success(array( 64 | 'message' => $message, 65 | 'key' => $token->get_key(), 66 | 'approved' => TYK_AUTO_APPROVE_KEY_REQUESTS, 67 | )); 68 | } catch (Exception $e) { 69 | wp_send_json_error($e->getMessage()); 70 | } 71 | } 72 | wp_send_json_error(__('Invalid request')); 73 | } 74 | 75 | /** 76 | * Get user tokens. 77 | */ 78 | public function get_user_tokens(): void 79 | { 80 | try { 81 | $user = new Tyk_Portal_User(); 82 | wp_send_json_success($user->get_access_tokens()); 83 | } catch (Exception $e) { 84 | wp_send_json_error(sprintf('An error occured: %s', $e->getMessage())); 85 | } 86 | } 87 | 88 | /** 89 | * Get available policies 90 | * These are communicated as APIs to the user because that's what they're interested in. 91 | */ 92 | public function get_available_policies(): void 93 | { 94 | try { 95 | wp_send_json_success(Tyk_API_Manager::available_policies()); 96 | } catch (Exception $e) { 97 | wp_send_json_error(sprintf('An error occured: %s', $e->getMessage())); 98 | } 99 | } 100 | 101 | /** 102 | * Revoke a user token. 103 | */ 104 | public function revoke_token(): void 105 | { 106 | if (isset($_POST['token'])) { 107 | try { 108 | $user = new Tyk_Portal_User(); 109 | // revoke token on tyk api 110 | // this will throw an exception if token is invalid or revoking fails 111 | $token = $user->get_access_token($_POST['token']); 112 | $token->revoke(); 113 | } catch (Exception $e) { 114 | // treat everything as an error except when tyk can't find the token 115 | // in which case we'll assume it's gone on their side and delete it on our side as well 116 | if (false === strpos(strtolower($e->getMessage()), 'not found')) { 117 | wp_send_json_error($e->getMessage()); 118 | } 119 | } 120 | // we can't assume PHP 5.5 otherwise we could use the finally directive 121 | 122 | // delete local storage of hashed token 123 | $user->delete_access_token($_POST['token']); 124 | $message = sprintf(__('Your token "%s" was revoked permanently and is invalidated effective immediately.', Tyk_Dev_Portal::TEXT_DOMAIN), $token->get_name()); 125 | wp_send_json_success(array( 126 | 'message' => $message, 127 | )); 128 | } 129 | wp_send_json_error(__('Invalid request')); 130 | } 131 | 132 | /** 133 | * Get usage quota. 134 | */ 135 | public function get_token_quota(): void 136 | { 137 | if (isset($_POST['token'])) { 138 | try { 139 | $user = new Tyk_Portal_User(); 140 | $token = Tyk_Token::init_from_key(sanitize_text_field($_POST['token']), $user); 141 | wp_send_json_success($token->get_usage_quota()); 142 | } catch (Exception $e) { 143 | if ('not found' != strtolower($e->getMessage())) { 144 | wp_send_json_error($e->getMessage()); 145 | } else { 146 | wp_send_json_error(__('Token could not be found. Please note that you must use the token at least once before you can request the remaining quota.', Tyk_Dev_Portal::TEXT_DOMAIN)); 147 | } 148 | } 149 | } 150 | wp_send_json_error(__('Invalid request')); 151 | } 152 | 153 | /** 154 | * Get token usage. 155 | */ 156 | public function get_token_usage(): void 157 | { 158 | if (isset($_GET['token'])) { 159 | try { 160 | $from_date = $_GET['from'] 161 | ?? null; 162 | $to_date = $_GET['to'] 163 | ?? null; 164 | $user = new Tyk_Portal_User(); 165 | $token = $user->get_access_token($_GET['token']); 166 | wp_send_json_success($token->get_usage($from_date, $to_date)); 167 | } catch (Exception $e) { 168 | wp_send_json_error($e->getMessage()); 169 | } 170 | } 171 | wp_send_json_error(__('Invalid request')); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /classes/dev_portal.php: -------------------------------------------------------------------------------- 1 | register_actions(); 72 | 73 | add_action('init', array($this, 'register_scripts')); 74 | add_action('init', array($this, 'register_styles')); 75 | add_action('wp', array($this, 'enqueue_assets')); 76 | add_action('wp', array($this, 'environment_is_ready')); 77 | add_action('plugins_loaded', array($this, 'load_plugin_textdomain')); 78 | } 79 | 80 | /** 81 | * Register any shortcodes. 82 | */ 83 | public function register_shortcodes(): void 84 | { 85 | add_shortcode('tyk_dev_dashboard', 'tyk_dev_portal_dashboard'); 86 | } 87 | 88 | /** 89 | * Make sure environment is ready for our plugin. 90 | */ 91 | public function environment_is_ready(): void 92 | { 93 | // make sure we have a tyk user in case registration failed 94 | $this->register_user_with_tyk(); 95 | } 96 | 97 | /** 98 | * Enqueue scripts and styles for the appropriate page. 99 | * 100 | * We do this in 'wp' hook because it's after the theme has setup 101 | * and enqueued it's styles so we can react accordingly. 102 | */ 103 | public function enqueue_assets(): void 104 | { 105 | $user = new Tyk_Portal_User(); 106 | // if this is our dashboard page, enqueue our assets 107 | if ($user->is_logged_in() && is_page(self::DASHBOARD_SLUG)) { 108 | // enqueue and localize our dashboard script 109 | wp_enqueue_script('tyk-dev-portal-dashboard'); 110 | $params = array( 111 | 'actionUrl' => esc_url(admin_url('admin-ajax.php')), 112 | 'label_used' => __('Used', self::TEXT_DOMAIN), 113 | 'label_remaining' => __('Remaining', self::TEXT_DOMAIN), 114 | 'label_success' => __('Success', self::TEXT_DOMAIN), 115 | 'label_errors' => __('Errors', self::TEXT_DOMAIN), 116 | 'error_invalid_data' => __('Invalid or insufficient data', self::TEXT_DOMAIN), 117 | 'label_valid' => __('valid', Tyk_Dev_Portal::TEXT_DOMAIN), 118 | 'label_invalid' => __('invalid', Tyk_Dev_Portal::TEXT_DOMAIN), 119 | 'msg_unlimited_quota' => __('This token has unlimited quota', Tyk_Dev_Portal::TEXT_DOMAIN), 120 | ); 121 | wp_localize_script('tyk-dev-portal-dashboard', 'scriptParams', $params); 122 | 123 | wp_enqueue_script('chart.js'); 124 | 125 | // only enqueue our bootstrap styles if the current theme isn't using bootstrap 126 | if (!wp_style_is('bootstrap', 'enqueued')) { 127 | if (!defined('TYK_FORCE_DISABLE_BOOTSTRAP') || TYK_FORCE_DISABLE_BOOTSTRAP !== true) { 128 | wp_enqueue_style('tyk-dev-portal-bootstrap'); 129 | } 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * Register javascript files. 136 | */ 137 | public function register_scripts(): void 138 | { 139 | // enqueue vue.js 140 | $vue_file = (WP_DEBUG === true) 141 | ? 'vue.js' 142 | : 'vue.min.js'; 143 | $vendor_version = (WP_DEBUG === true) 144 | ? time() 145 | : self::PLUGIN_VERSION; 146 | wp_register_script('vue', tyk_dev_portal_plugin_url('assets/js/vendor/' . $vue_file), array(), $vendor_version, true); 147 | wp_register_script('underscore', tyk_dev_portal_plugin_url('assets/js/vendor/underscore.min.js'), array(), $vendor_version, true); 148 | 149 | wp_register_script('chart.js', tyk_dev_portal_plugin_url('assets/js/vendor/chart.min.js'), array(), $vendor_version, false); 150 | 151 | // enqueue dashboard.js 152 | $dashboard_ver = (WP_DEBUG === true) 153 | ? time() 154 | : self::PLUGIN_VERSION; 155 | wp_register_script('tyk-dev-portal-dashboard', tyk_dev_portal_plugin_url('assets/js/dashboard.js'), array('jquery', 'vue', 'underscore'), $dashboard_ver, true); 156 | } 157 | 158 | /** 159 | * Register styles. 160 | * 161 | * Registers a minimal bootstrap theme that only contains the stuff we need 162 | * Note: this style is only enqueued if the activate theme does not enqueue a bootstrap file 163 | */ 164 | public function register_styles(): void 165 | { 166 | $style_ver = (WP_DEBUG === true) 167 | ? time() 168 | : self::PLUGIN_VERSION; 169 | wp_register_style('tyk-dev-portal-bootstrap', tyk_dev_portal_plugin_url('assets/css/bootstrap.min.css'), null, $style_ver); 170 | } 171 | 172 | /** 173 | * load the localized strings. 174 | */ 175 | public function load_plugin_textdomain(): void 176 | { 177 | load_plugin_textdomain(self::TEXT_DOMAIN, false, basename(TYK_DEV_PORTAL_PLUGIN_PATH) . '/languages'); 178 | } 179 | 180 | /** 181 | * Register a user as a developer with Tyk. 182 | * 183 | * @param int $user_id 184 | */ 185 | public function register_user_with_tyk($user_id = null): void 186 | { 187 | $user = new Tyk_Portal_User($user_id); 188 | if ($user->is_logged_in() && !$user->has_tyk_id()) { 189 | $user->register_with_tyk(); 190 | } 191 | } 192 | 193 | /** 194 | * Hook that is fired when this plugin is activated. 195 | */ 196 | public function activate_plugin(): void 197 | { 198 | $this->create_developer_role(); 199 | $this->create_dashboard_page(); 200 | } 201 | 202 | /** 203 | * Create a role "developer". 204 | */ 205 | public function create_developer_role(): void 206 | { 207 | $result = add_role(self::DEVELOPER_ROLE_NAME, __('Developer', self::TEXT_DOMAIN)); 208 | if (false === $result) { 209 | trigger_error(sprintf( 210 | 'Could not create role "%s" for plugin %s', 211 | self::DEVELOPER_ROLE_NAME, 212 | self::PLUGIN_NAME, 213 | )); 214 | } 215 | } 216 | 217 | /** 218 | * Create a page for the developer dashboard. 219 | */ 220 | public function create_dashboard_page(): void 221 | { 222 | // @todo check if the page already exists 223 | $page = array( 224 | 'post_title' => __('Developer Dashboard', self::TEXT_DOMAIN), 225 | 'post_name' => self::DASHBOARD_SLUG, 226 | 'post_content' => '[tyk_dev_dashboard]', 227 | 'post_status' => 'publish', 228 | 'post_type' => 'page', 229 | 'post_author' => 1, // @todo get an admin user id here 230 | ); 231 | $post_id = wp_insert_post($page); 232 | // @todo we should probably save the slug of the created page here 233 | } 234 | 235 | /** 236 | * Are we using Tyk Cloud? 237 | * 238 | * @return bool 239 | */ 240 | public static function is_cloud() 241 | { 242 | return self::CONFIGURATION_CLOUD == strtolower(TYK_CONFIGURATION); 243 | } 244 | 245 | /** 246 | * Are we using Tyk Hybrid? 247 | * 248 | * @return bool 249 | */ 250 | public static function is_hybrid() 251 | { 252 | return self::CONFIGURATION_HYBRID == strtolower(TYK_CONFIGURATION); 253 | } 254 | 255 | /** 256 | * Are we using Tyk On-Premise? 257 | * 258 | * @return bool 259 | */ 260 | public static function is_on_premise() 261 | { 262 | return self::CONFIGURATION_ON_PREMISE == strtolower(TYK_CONFIGURATION); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /classes/portal_user.php: -------------------------------------------------------------------------------- 1 | user = get_userdata($user_id); 55 | } 56 | 57 | /** 58 | * Check if we're dealing with a logged in user. 59 | * 60 | * @return bool 61 | */ 62 | public function is_logged_in() 63 | { 64 | return is_a($this->user, 'WP_User'); 65 | } 66 | 67 | /** 68 | * Check if current user is logged in and is a developer. 69 | * 70 | * @return bool 71 | */ 72 | public function is_developer() 73 | { 74 | return $this->user->exists() && in_array(self::DEVELOPER_ROLE_NAME, $this->user->roles); 75 | } 76 | 77 | /** 78 | * Save a tyk access token 79 | * Note that the actual tokens are not stored, just the name and id so we can manage them. 80 | * 81 | * @param string $api_id ID of tyk API policy 82 | * @param string $token_name User-given name of token 83 | * @param string $hash Token hash 84 | */ 85 | public function save_access_token($api_id, $token_name, $hash): void 86 | { 87 | $data = array( 88 | 'api_id' => sanitize_text_field($api_id), 89 | 'token_name' => sanitize_text_field($token_name), 90 | 'hash' => sanitize_text_field($hash), 91 | ); 92 | 93 | // check if token already exists 94 | $tokens = $this->get_access_tokens(); 95 | 96 | $key = false; 97 | if (count($tokens)) { 98 | $ids = wp_list_pluck($tokens, 'hash'); 99 | $key = array_search($hash, $ids); 100 | } 101 | 102 | // this is a new token 103 | if (false === $key) { 104 | $tokens[] = $data; 105 | } 106 | // this is an existing token 107 | else { 108 | $tokens[$key] = $data; 109 | } 110 | 111 | // re-index array and update key in user meta storage 112 | update_user_meta($this->user->ID, self::META_TYK_ACCESS_TOKENS_KEY, array_values($tokens)); 113 | } 114 | 115 | /** 116 | * Get a single access token. 117 | * 118 | * @param string $hash 119 | * 120 | * @return Tyk_Token 121 | * 122 | * @throws OutOfBoundsException When token id is invalid 123 | */ 124 | public function get_access_token($hash) 125 | { 126 | $tokens = $this->get_access_tokens(); 127 | foreach ($tokens as $token) { 128 | if ($token['hash'] == $hash) { 129 | return Tyk_Token::init($token, $this); 130 | } 131 | } 132 | 133 | // if we get here, we didn't find the token 134 | throw new OutOfBoundsException('Invalid token id'); 135 | } 136 | 137 | /** 138 | * Get user's access tokens. 139 | * 140 | * @return array 141 | */ 142 | public function get_access_tokens() 143 | { 144 | $tokens = get_user_meta($this->user->ID, self::META_TYK_ACCESS_TOKENS_KEY, true); 145 | if (!is_array($tokens)) { 146 | return array(); 147 | } 148 | 149 | // lambda to add an 'is_valid' attribute to each token 150 | $map_is_valid = function ($token) { 151 | return array_merge($token, array( 152 | 'is_valid' => $this->has_token($token['hash']), 153 | )); 154 | }; 155 | 156 | return array_map($map_is_valid, $tokens); 157 | } 158 | 159 | /** 160 | * Delete an access token by it's ID. 161 | * 162 | * @param string $hash 163 | */ 164 | public function delete_access_token($hash): void 165 | { 166 | $tokens = $this->get_access_tokens(); 167 | $found = false; 168 | foreach ($tokens as $i => $token) { 169 | if ($token['hash'] == $hash) { 170 | $found = true; 171 | 172 | break; 173 | } 174 | } 175 | if (true === $found) { 176 | unset($tokens[$i]); 177 | // re-index array update the dataset 178 | update_user_meta($this->user->ID, self::META_TYK_ACCESS_TOKENS_KEY, array_values($tokens)); 179 | } 180 | } 181 | 182 | /** 183 | * Check if we already stored a tyk id for this developer. 184 | * 185 | * @return bool 186 | */ 187 | public function has_tyk_id() 188 | { 189 | $tyk_user_id = get_user_meta($this->user->ID, self::META_TYK_USER_ID_KEY, true); 190 | 191 | return !empty($tyk_user_id); 192 | } 193 | 194 | /** 195 | * Set the user's tyk user id. 196 | * 197 | * @param string $id 198 | */ 199 | public function set_tyk_id($id): void 200 | { 201 | update_user_meta($this->user->ID, self::META_TYK_USER_ID_KEY, $id); 202 | } 203 | 204 | /** 205 | * Get the user's tyk user id. 206 | * 207 | * @return string 208 | */ 209 | public function get_tyk_id() 210 | { 211 | return get_user_meta($this->user->ID, self::META_TYK_USER_ID_KEY, true); 212 | } 213 | 214 | /** 215 | * Get tyk access token. 216 | * 217 | * @return array 218 | */ 219 | public function register_with_tyk() 220 | { 221 | try { 222 | $tyk = new Tyk_API(); 223 | $user_id = $tyk->post('/portal/developers', array( 224 | 'email' => $this->user->user_email, 225 | )); 226 | $this->set_tyk_id($user_id); 227 | } catch (Exception $e) { 228 | trigger_error(sprintf('Could not register user for API: %s', $e->getMessage()), E_USER_WARNING); 229 | } 230 | } 231 | 232 | /** 233 | * Check if the user still has according to tyk. 234 | * 235 | * @param string $hash 236 | * 237 | * @return bool 238 | * 239 | * @throws Exception If subscriptions info is missing in Tyk user data 240 | */ 241 | private function has_token($hash) 242 | { 243 | if (!isset($this->tyk_subscriptions) || !is_array($this->tyk_subscriptions)) { 244 | $user_data = $this->fetch_from_tyk(); 245 | if (!isset($user_data->subscriptions)) { 246 | throw new Exception('Missing policy subscriptions'); 247 | } 248 | $this->tyk_subscriptions = array_flip((array) $user_data->subscriptions); 249 | } 250 | 251 | return isset($this->tyk_subscriptions[$hash]); 252 | } 253 | 254 | /** 255 | * Fetch developer data from Tyk. 256 | * 257 | * @return stdClass 258 | */ 259 | public function fetch_from_tyk() 260 | { 261 | try { 262 | $tyk = new Tyk_API(); 263 | $developer = $tyk->get(sprintf('/portal/developers/%s', $this->get_tyk_id())); 264 | if (!is_object($developer) || !isset($developer->id)) { 265 | throw new Exception('Received invalid response'); 266 | } 267 | 268 | return $developer; 269 | } catch (Exception $e) { 270 | trigger_error(sprintf('Could not fetch developer from API: %s', $e->getMessage()), E_USER_WARNING); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /classes/token.php: -------------------------------------------------------------------------------- 1 | api = new Tyk_API(); 73 | $this->gateway = new Tyk_Gateway(); 74 | $this->user = $user; 75 | if (!is_null($policy)) { 76 | $this->policy = $policy; 77 | } 78 | } 79 | 80 | /** 81 | * Set an existing token. 82 | * 83 | * @param Portal_User $user 84 | * 85 | * @return Tyk_Token 86 | */ 87 | public static function init(array $token, Tyk_Portal_User $user) 88 | { 89 | if (isset($token['api_id'], $token['hash'])) { 90 | $instance = new Tyk_Token($user, $token['api_id']); 91 | $instance->set_hash($token['hash']); 92 | $instance->set_name($token['token_name']); 93 | 94 | return $instance; 95 | } 96 | 97 | throw new InvalidArgumentException('Invalid token specified'); 98 | } 99 | 100 | /** 101 | * Setup token from the key. 102 | * 103 | * @param string $key 104 | * 105 | * @return Tyk_Token 106 | */ 107 | public static function init_from_key($key, Tyk_Portal_User $user) 108 | { 109 | $token = new Tyk_Token($user); 110 | $token->set_key($key); 111 | 112 | return $token; 113 | } 114 | 115 | /** 116 | * Get the key request id (not the actual token). 117 | * 118 | * @return string 119 | */ 120 | public function get_id() 121 | { 122 | return $this->id; 123 | } 124 | 125 | /** 126 | * Set the key request id 127 | * You shouldn't use this, this is for testing. 128 | * 129 | * @param mixed $id 130 | * 131 | * @return string 132 | */ 133 | public function set_id($id) 134 | { 135 | $this->id = $id; 136 | } 137 | 138 | /** 139 | * Get the key/access token. 140 | * 141 | * @return string 142 | */ 143 | public function get_key() 144 | { 145 | return $this->key; 146 | } 147 | 148 | /** 149 | * Set the unhashed key. 150 | * 151 | * @param string $key 152 | */ 153 | public function set_key($key): void 154 | { 155 | $this->key = $key; 156 | } 157 | 158 | /** 159 | * Set the hashed token. 160 | * 161 | * @param string $hash 162 | */ 163 | public function set_hash($hash): void 164 | { 165 | $this->hash = $hash; 166 | } 167 | 168 | /** 169 | * Get the hashed token. 170 | * 171 | * @return string 172 | */ 173 | public function get_hash() 174 | { 175 | return $this->hash; 176 | } 177 | 178 | /** 179 | * Set the name. 180 | * 181 | * @param string $name 182 | */ 183 | public function set_name($name): void 184 | { 185 | $this->name = $name; 186 | } 187 | 188 | /** 189 | * Get the name 190 | * Note: the name isn't always set, only when token is setup with self::init(). 191 | * 192 | * @return string 193 | */ 194 | public function get_name() 195 | { 196 | return $this->name; 197 | } 198 | 199 | /** 200 | * Get the api policy ID. 201 | * 202 | * @return string 203 | */ 204 | public function get_policy() 205 | { 206 | return $this->policy; 207 | } 208 | 209 | /** 210 | * Make a key request for a tyk api plan/policy. 211 | * 212 | * @return string 213 | * 214 | * @throws UnexpectedValueException When we get an invalid response from API 215 | */ 216 | public function request() 217 | { 218 | $request_id = $this->api->post('/portal/requests', array( 219 | 'by_user' => $this->user->get_tyk_id(), 220 | 'for_plan' => $this->policy, 221 | // as of Tyk Dashboard v1.8 this always needs to be set to false 222 | // otherwise the subsequent approval call fails because the request 223 | // is already considered as approved. 224 | 'approved' => false, 225 | // this is a bit absurd but tyk api doesn't set this by itself 226 | 'date_created' => date('c'), 227 | 'version' => 'v2', 228 | )); 229 | 230 | // save key request id 231 | if (is_string($request_id)) { 232 | $this->id = $request_id; 233 | } else { 234 | throw new UnexpectedValueException('Received an invalid response for key request'); 235 | } 236 | } 237 | 238 | /** 239 | * Approve a key request. 240 | * 241 | * Unfortunately, tyk api doesn't support making and approving a key 242 | * request in the same request, so this method must be invoked after 243 | * issuing {@link this::request()}. 244 | * 245 | * @throws InvalidArgumentException When the key request ID is missing 246 | * @throws UnexpectedValueException When we don't get a token back from API 247 | */ 248 | public function approve(): void 249 | { 250 | if (!is_string($this->id) || empty($this->id)) { 251 | throw new InvalidArgumentException('Invalid key request'); 252 | } 253 | 254 | try { 255 | $token = $this->api->put('/portal/requests/approve', $this->id); 256 | $developer = $this->user->fetch_from_tyk(); 257 | if (is_object($token) && isset($token->RawKey) && !empty($token->RawKey)) { 258 | $this->key = $token->RawKey; 259 | 260 | if (is_object($developer) && isset($developer->subscriptions)) { 261 | if (isset($developer->subscriptions->{$this->policy})) { 262 | $this->hash = $developer->subscriptions->{$this->policy}; 263 | } 264 | } 265 | } else { 266 | throw new Exception('Could not approve token request'); 267 | } 268 | } catch (Exception $e) { 269 | throw new UnexpectedValueException($e->getMessage()); 270 | } 271 | } 272 | 273 | /** 274 | * Revoke (delete!) a token. 275 | * 276 | * @throws InvalidArgumentException When this class doesn't have all the data it needs 277 | * @throws UnexpectedValueException When API does not respond as expected 278 | */ 279 | public function revoke(): void 280 | { 281 | if (!is_string($this->hash) || !is_a($this->user, 'Tyk_Portal_User')) { 282 | throw new InvalidArgumentException('Missing token information'); 283 | } 284 | 285 | try { 286 | $this->api->delete(sprintf( 287 | '/api/apis/%s/keys/%s', 288 | null, 289 | $this->hash, 290 | )); 291 | } catch (Exception $e) { 292 | throw new UnexpectedValueException($e->getMessage()); 293 | } 294 | } 295 | 296 | /** 297 | * Get usage quota of this token 298 | * Requires the unhashed key of the token, unfortunately. 299 | * 300 | * @return object Usage quota 301 | * 302 | * @throws InvalidArgumentException When the key isn't set 303 | * @throws UnexpectedValueException When API does not respond as expected 304 | */ 305 | public function get_usage_quota() 306 | { 307 | if (!is_string($this->key)) { 308 | throw new InvalidArgumentException('Missing token key'); 309 | } 310 | if (56 === strlen($this->key)) { 311 | $this->key = TYK_ORG_ID . $this->key; 312 | } 313 | 314 | try { 315 | /* 316 | * Hybrid Tyk 317 | * Get usage quota from gateways, as this info isn't synced back to cloud 318 | */ 319 | if (Tyk_Dev_Portal::is_hybrid()) { 320 | $response = $this->gateway->get(sprintf('/keys/%s', $this->key)); 321 | if (is_object($response) && isset($response->quota_remaining)) { 322 | return (object) array( 323 | 'quota_remaining' => $response->quota_remaining, 324 | 'quota_max' => $response->quota_max, 325 | ); 326 | } 327 | 328 | throw new Exception('Received invalid response from Gateway'); 329 | } /* 330 | * Cloud and on-premise Tyk 331 | * Get usage quota from API 332 | */ 333 | else { 334 | // first: we need an api id on which to request the tokens 335 | // sounds weird I know, here's the explanation: https://community.tyk.io/t/several-questions/1041/3 336 | $apiManager = new Tyk_API_Manager(); 337 | $apis = $apiManager->available_apis(); 338 | if (is_array($apis)) { 339 | $firstApi = array_shift($apis); 340 | $response = $this->api->get(sprintf( 341 | '/apis/%s/keys/%s', 342 | $firstApi->api_definition->api_id, 343 | $this->key, 344 | )); 345 | } 346 | if (is_object($response) && isset($response->data)) { 347 | return $response->data; 348 | } 349 | 350 | throw new Exception('Received invalid response from API'); 351 | } 352 | } catch (Exception $e) { 353 | throw new UnexpectedValueException($e->getMessage()); 354 | } 355 | } 356 | 357 | /** 358 | * Get usage stats for this token. 359 | * 360 | * @param string $from_date From this date forward 361 | * @param string $to_date To this date 362 | * 363 | * @return object Usage data 364 | * 365 | * @throws InvalidArgumentException When the hash isn't set 366 | * @throws UnexpectedValueException When API does not respond as expected 367 | */ 368 | public function get_usage($from_date = null, $to_date = null) 369 | { 370 | if (!is_string($this->hash)) { 371 | throw new InvalidArgumentException('Missing token hash'); 372 | } 373 | 374 | // use from_date or today-1 month 375 | $from = is_null($from_date) 376 | ? strtotime('-1 week') 377 | : strtotime($from_date); 378 | // use to_date or 379 | $to = is_null($to_date) 380 | ? time() 381 | : strtotime($to_date); 382 | 383 | try { 384 | $response = $this->api->get(sprintf( 385 | '/activity/keys/%s/%s/%s', 386 | $this->hash, 387 | date('j/n/Y', $from), 388 | date('j/n/Y', $to), 389 | ), array( 390 | 'res' => 'day', 391 | 'p' => -1, 392 | )); 393 | if (is_object($response) && property_exists($response, 'data')) { 394 | return $response->data; 395 | } 396 | 397 | throw new Exception('Received invalid response from API'); 398 | } catch (Exception $e) { 399 | throw new UnexpectedValueException($e->getMessage()); 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /classes/tyk_api.php: -------------------------------------------------------------------------------- 1 | get_url_for_path($path), array( 26 | 'headers' => array( 27 | 'Authorization' => TYK_API_KEY, 28 | ), 29 | 'body' => json_encode($body), 30 | )); 31 | 32 | // analyse response 33 | $response = $this->parse_response($api_response); 34 | if (is_object($response) && isset($response->Status, $response->Message)) { 35 | if ('OK' == $response->Status) { 36 | return $response->Message; 37 | } 38 | 39 | throw new Exception($response->Message); 40 | } else { 41 | throw new Exception('Received invalid response from API'); 42 | } 43 | } 44 | 45 | /** 46 | * Send a get request to Tyk API. 47 | * 48 | * @param string $path 49 | * @param array $args Query string args 50 | * 51 | * @return array 52 | * 53 | * @throws Exception When API sends invalid response 54 | */ 55 | public function get($path, ?array $args = null) 56 | { 57 | $api_response = wp_remote_get($this->get_url_for_path($path, $args), array( 58 | 'headers' => array( 59 | 'Authorization' => TYK_API_KEY, 60 | ))); 61 | 62 | $response = $this->parse_response($api_response); 63 | if (is_object($response)) { 64 | return $response; 65 | } 66 | 67 | throw new Exception('Received invalid response from API'); 68 | } 69 | 70 | /** 71 | * Send a put request to Tyk API. 72 | * 73 | * @param string $path 74 | * @param string $location 75 | * 76 | * @return array 77 | * 78 | * @throws Exception When API sends invalid response 79 | */ 80 | public function put($path, $location) 81 | { 82 | $base_url = $this->get_url_for_path($path); 83 | $url = $base_url . '/' . $location; 84 | 85 | $api_response = wp_remote_request($url, array( 86 | 'method' => 'PUT', 87 | 'headers' => array( 88 | 'Authorization' => TYK_API_KEY, 89 | ))); 90 | $response = $this->parse_response($api_response); 91 | if (is_object($response)) { 92 | return $response; 93 | } 94 | 95 | throw new Exception('Received invalid response from API'); 96 | } 97 | 98 | /** 99 | * Send a delete request to Tyk API. 100 | * 101 | * @param string $path 102 | * 103 | * @return stdClass 104 | * 105 | * @throws Exception When API sends invalid response 106 | */ 107 | public function delete($path) 108 | { 109 | $url = $this->get_url_for_path($path); 110 | $api_response = wp_remote_request($url, array( 111 | 'method' => 'DELETE', 112 | 'headers' => array( 113 | 'Authorization' => TYK_API_KEY, 114 | ), 115 | )); 116 | 117 | $response = $this->parse_response($api_response); 118 | if (is_object($response)) { 119 | return $response; 120 | } 121 | 122 | throw new Exception('Received invalid response from API'); 123 | } 124 | 125 | /** 126 | * Get absolute url to api endpoint for a path. 127 | * 128 | * @param string $path 129 | * 130 | * @return string 131 | */ 132 | protected function get_url_for_path($path, ?array $args = null) 133 | { 134 | // build query string out of args if they're set 135 | $qs = ''; 136 | if (is_array($args)) { 137 | $qs = '?' . http_build_query($args); 138 | } 139 | 140 | return sprintf( 141 | '%s/%s%s', 142 | rtrim(TYK_API_ENDPOINT, '/'), 143 | ltrim($path, '/'), 144 | $qs, 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /classes/tyk_gateway.php: -------------------------------------------------------------------------------- 1 | get_url_for_path($path, $args), array( 28 | 'headers' => array( 29 | 'x-tyk-authorization' => TYK_GATEWAY_SECRET, 30 | ), 31 | )); 32 | 33 | $response = $this->parse_response($api_response); 34 | if (is_object($response)) { 35 | return $response; 36 | } 37 | 38 | throw new Exception('Received invalid response from Gateway'); 39 | } 40 | 41 | /** 42 | * Send a delete request to Tyk gateway. 43 | * 44 | * @param string $path 45 | * 46 | * @return stdClass 47 | * 48 | * @throws Exception when the gateway sends an invalid response 49 | */ 50 | public function delete($path) 51 | { 52 | $url = $this->get_url_for_path($path); 53 | 54 | $api_response = wp_remote_request($url, array( 55 | 'method' => 'DELETE', 56 | 'headers' => array( 57 | 'x-tyk-authorization' => TYK_GATEWAY_SECRET, 58 | ))); 59 | 60 | $response = $this->parse_response($api_response); 61 | if (is_object($response)) { 62 | return $response; 63 | } 64 | 65 | throw new Exception('Received invalid response from API'); 66 | } 67 | 68 | /** 69 | * Get absolute url to api endpoint for a path. 70 | * 71 | * @param string $path 72 | * 73 | * @return string 74 | */ 75 | protected function get_url_for_path($path, ?array $args = null) 76 | { 77 | // build query string out of args if they're set 78 | $qs = ''; 79 | if (is_array($args)) { 80 | $qs = '?' . http_build_query($args); 81 | } 82 | 83 | return sprintf( 84 | '%s/%s%s', 85 | rtrim(TYK_GATEWAY_URL, '/'), 86 | ltrim($path, '/'), 87 | $qs, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /classes/tyk_interaction.php: -------------------------------------------------------------------------------- 1 | errors as $name => $errors) { 37 | $error_list[] = is_array($errors) 38 | ? join('. ', $errors) 39 | : $errors; 40 | } 41 | if (count($error_list) > 0) { 42 | throw new Exception(sprintf('API error: %s', join('. ', $error_list))); 43 | } 44 | 45 | throw new Exception('An unknown error occured when connecting to API'); 46 | } 47 | // parse response 48 | else { 49 | $response = json_decode(wp_remote_retrieve_body($api_response)); 50 | $http_code = wp_remote_retrieve_response_code($api_response); 51 | $message = wp_remote_retrieve_response_message($api_response); 52 | if (200 != $http_code) { 53 | // see if we have more information 54 | if (is_object($response) && isset($response->Message)) { 55 | $message .= sprintf(': %s', $response->Message); 56 | } 57 | 58 | throw new Exception($message); 59 | } 60 | } 61 | 62 | return $response; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /languages/tyk-dev-portal-de_DE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/languages/tyk-dev-portal-de_DE.mo -------------------------------------------------------------------------------- /languages/tyk-dev-portal-de_DE.po: -------------------------------------------------------------------------------- 1 | # This file was generated from translations/tyk_de.xlsx 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: \n" 5 | "POT-Creation-Date: 2016-12-06 16:21+0100\n" 6 | "PO-Revision-Date: 2016-12-06 16:22+0100\n" 7 | "Last-Translator: Team Amboss \n" 8 | "Language-Team: \n" 9 | "Language: de_CH\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Generated-By: xls-to-po 1.0\n" 14 | "X-Generator: Poedit 1.8.11\n" 15 | "X-Poedit-Basepath: ..\n" 16 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 17 | "X-Poedit-KeywordsList: __;_e\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPathExcluded-0: node_modules\n" 20 | "X-Poedit-SearchPathExcluded-1: assets\n" 21 | 22 | #: classes/dashboard_ajax_provider.php:46 23 | #, php-format 24 | msgid "" 25 | "Your token for this API is: %s. We will only show this once. Please save it " 26 | "somewhere now." 27 | msgstr "" 28 | "Dein Schlüssel für diese API ist: %s. Dieser Schlüssel kann nur einmal " 29 | "angezeigt werden, leg ihn bitte jetzt an einem sicheren Ort ab." 30 | 31 | #: classes/dashboard_ajax_provider.php:53 32 | #, php-format 33 | msgid "" 34 | "Your key request is pending review. You will receive an Email when your " 35 | "request is processed. Your request id is %s. This is not your access token. " 36 | "Please refer to the request ID when contacting us by Email." 37 | msgstr "" 38 | "Ihre Anfrage wird geprüft. Sie erhalten ein Email wenn Ihre Anfrage " 39 | "bearbeitet wurde. Ihre Anfrage ID ist: %s (dies ist kein Zugangs-Token für " 40 | "die API). Bitte geben Sie diese ID an falls Sie uns per Email kontaktieren." 41 | 42 | #: classes/dashboard_ajax_provider.php:69 43 | #: classes/dashboard_ajax_provider.php:132 44 | #: classes/dashboard_ajax_provider.php:156 45 | #: classes/dashboard_ajax_provider.php:181 46 | msgid "Invalid request" 47 | msgstr "Ungültiger request" 48 | 49 | #: classes/dashboard_ajax_provider.php:127 50 | #, php-format 51 | msgid "" 52 | "Your token \"%s\" was revoked permanently and is invalidated effective " 53 | "immediately." 54 | msgstr "" 55 | "Dein Schlüssel “%s” wurde unwiderruflich annuliert und ist ab sofort " 56 | "ungültig." 57 | 58 | #: classes/dashboard_ajax_provider.php:152 59 | msgid "" 60 | "Token could not be found. Please note that you must use the token at least " 61 | "once before you can request the remaining quota." 62 | msgstr "" 63 | "Der Schlüssel konnte nicht gefunden werden . Bitte beachte, dass der " 64 | "Schlüssel mindestens einmal verwendet werden muss, bevor du das Kontingent " 65 | "abrufen kannst." 66 | 67 | #: classes/dev_portal.php:117 68 | msgid "Used" 69 | msgstr "Gebraucht" 70 | 71 | #: classes/dev_portal.php:118 72 | msgid "Remaining" 73 | msgstr "Verbleibend" 74 | 75 | #: classes/dev_portal.php:119 76 | msgid "Success" 77 | msgstr "Erfolgreich" 78 | 79 | #: classes/dev_portal.php:120 80 | msgid "Errors" 81 | msgstr "Fehler" 82 | 83 | #: classes/dev_portal.php:121 84 | msgid "Invalid or insufficient data" 85 | msgstr "Ungültige oder ungenügende Daten" 86 | 87 | #: classes/dev_portal.php:122 88 | msgid "valid" 89 | msgstr "gültig" 90 | 91 | #: classes/dev_portal.php:123 92 | msgid "invalid" 93 | msgstr "ungültig" 94 | 95 | #: classes/dev_portal.php:124 96 | msgid "This token has unlimited quota" 97 | msgstr "Dieser Schlüssel hat ein unlimitiertes Kontingent" 98 | 99 | #: classes/dev_portal.php:217 100 | msgid "Developer" 101 | msgstr "Entwickler" 102 | 103 | #: classes/dev_portal.php:234 104 | msgid "Developer Dashboard" 105 | msgstr "Entwickler Dashboard" 106 | 107 | #: template_tags.php:18 108 | #, php-format 109 | msgid "" 110 | "This page is only available when you are logged in." 111 | msgstr "" 112 | "Diese Seite ist nur verfügbar wenn Sie eingeloggt sind." 113 | 114 | #: templates/api_subscribe_form.php:10 115 | msgid "Tokens" 116 | msgstr "Schlüssel" 117 | 118 | #: templates/api_subscribe_form.php:13 119 | msgid "Usage" 120 | msgstr "Nutzung" 121 | 122 | #: templates/api_subscribe_form.php:16 123 | msgid "Quota" 124 | msgstr "Kontingent" 125 | 126 | #: templates/tab_quota.php:7 127 | msgid "Remaining quota" 128 | msgstr "Offenes Kontingent" 129 | 130 | #: templates/tab_quota.php:12 131 | msgid "Please enter your token here" 132 | msgstr "Bitte Schlüssel eingeben" 133 | 134 | #: templates/tab_quota.php:13 templates/tab_tokens.php:44 135 | #: templates/tab_tokens.php:92 136 | msgid "loading" 137 | msgstr "laden" 138 | 139 | #: templates/tab_tokens.php:4 140 | msgid "My tokens" 141 | msgstr "Meine Schlüssel" 142 | 143 | #: templates/tab_tokens.php:12 templates/tab_tokens.php:67 144 | msgid "An error occurred. Please try again." 145 | msgstr "Ein Fehler ist aufgetreten. Bitte versuche es nochmal." 146 | 147 | #: templates/tab_tokens.php:20 templates/tab_tokens.php:72 148 | msgid "Name" 149 | msgstr "Name" 150 | 151 | #: templates/tab_tokens.php:21 templates/tab_tokens.php:79 152 | msgid "API" 153 | msgstr "API" 154 | 155 | #: templates/tab_tokens.php:22 156 | msgid "State" 157 | msgstr "Zustand" 158 | 159 | #: templates/tab_tokens.php:32 160 | msgid "Show usage" 161 | msgstr "Nutzung anzeigen" 162 | 163 | #: templates/tab_tokens.php:35 164 | msgid "Revoke this token" 165 | msgstr "Schlüssel annullieren" 166 | 167 | #: templates/tab_tokens.php:47 168 | msgid "You don't have any tokens yet" 169 | msgstr "Du hast noch keine Schlüssel" 170 | 171 | #: templates/tab_tokens.php:82 templates/tab_tokens.php:94 172 | msgid "I accept the general terms and conditions" 173 | msgstr "Ich akzeptiere die allgemeinen Geschäftsbedingungen" 174 | 175 | #: templates/tab_tokens.php:58 templates/tab_tokens.php:105 176 | msgid "Request a token" 177 | msgstr "Schlüssel anfordern" 178 | 179 | #: templates/tab_tokens.php:74 180 | msgid "Give this token a name" 181 | msgstr "Gib diesem Schlüssel einen Namen" 182 | 183 | #: templates/tab_tokens.php:82 templates/tab_usage.php:9 184 | msgid "-- please choose" 185 | msgstr "-- bitte wählen " 186 | 187 | #: templates/tab_usage.php:7 188 | msgid "Token" 189 | msgstr "Schlüssel" 190 | 191 | #: templates/tab_usage.php:15 192 | msgid "From" 193 | msgstr "Von" 194 | 195 | #: templates/tab_usage.php:20 196 | msgid "To" 197 | msgstr "Bis" 198 | 199 | #~ msgid "" 200 | #~ "Token could not be found. Please not that you have to use the token at " 201 | #~ "least once before you can request the remaining quota." 202 | #~ msgstr "" 203 | #~ "Der Schlüssel konnte nicht gefunden werden . Bitte beachte, dass der " 204 | #~ "Schlüssel mindestens einmal verwendet werden muss, bevor du das " 205 | #~ "Kontingent abrufen kannst." 206 | -------------------------------------------------------------------------------- /languages/tyk-dev-portal-fr_FR.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/languages/tyk-dev-portal-fr_FR.mo -------------------------------------------------------------------------------- /languages/tyk-dev-portal-fr_FR.po: -------------------------------------------------------------------------------- 1 | # This file was generated from translations/tyk_fr.xlsx 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: \n" 5 | "POT-Creation-Date: 2016-12-06 16:22+0100\n" 6 | "PO-Revision-Date: 2016-12-06 16:22+0100\n" 7 | "Last-Translator: Team Amboss \n" 8 | "Language-Team: \n" 9 | "Language: fr_FR\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Generated-By: xls-to-po 1.0\n" 14 | "X-Generator: Poedit 1.8.11\n" 15 | "X-Poedit-Basepath: ..\n" 16 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 17 | "X-Poedit-KeywordsList: __;_e\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPathExcluded-0: node_modules\n" 20 | "X-Poedit-SearchPathExcluded-1: assets\n" 21 | 22 | #: classes/dashboard_ajax_provider.php:46 23 | #, php-format 24 | msgid "" 25 | "Your token for this API is: %s. We will only show this once. Please save it " 26 | "somewhere now." 27 | msgstr "" 28 | "Votre jeton pour cet API est: %s. Nous n’afficherons cet élément qu’une " 29 | "seule fois. Veuillez le sauvegarder quelque part." 30 | 31 | #: classes/dashboard_ajax_provider.php:53 32 | #, php-format 33 | msgid "" 34 | "Your key request is pending review. You will receive an Email when your " 35 | "request is processed. Your request id is %s. This is not your access token. " 36 | "Please refer to the request ID when contacting us by Email." 37 | msgstr "" 38 | "Votre demande de clé est en attente d’examen. Vous recevrez un e-mail " 39 | "lorsque votre demande aura été traitée. L’ID de votre demande est %s. Il ne " 40 | "s’agit pas de votre jeton d’authentification. Veuillez faire référence à " 41 | "l’ID de la demande si vous nous contactez par e-mail." 42 | 43 | #: classes/dashboard_ajax_provider.php:69 44 | #: classes/dashboard_ajax_provider.php:132 45 | #: classes/dashboard_ajax_provider.php:156 46 | #: classes/dashboard_ajax_provider.php:181 47 | msgid "Invalid request" 48 | msgstr "Demande invalide" 49 | 50 | #: classes/dashboard_ajax_provider.php:127 51 | #, php-format 52 | msgid "" 53 | "Your token \"%s\" was revoked permanently and is invalidated effective " 54 | "immediately." 55 | msgstr "" 56 | "Votre jeton \"%s\" a été annulé de manière permanente et invalidé avec effet " 57 | "immédiat." 58 | 59 | #: classes/dashboard_ajax_provider.php:152 60 | msgid "" 61 | "Token could not be found. Please note that you must use the token at least " 62 | "once before you can request the remaining quota." 63 | msgstr "" 64 | "Votre jeton n'a pas été trouvé. Notez que le jeton doit être utilisé au " 65 | "minimum une fois avant que vous pouvez voir le quota restant." 66 | 67 | #: classes/dev_portal.php:117 68 | msgid "Used" 69 | msgstr "Utilisé" 70 | 71 | #: classes/dev_portal.php:118 72 | msgid "Remaining" 73 | msgstr "Restant" 74 | 75 | #: classes/dev_portal.php:119 76 | msgid "Success" 77 | msgstr "Succès" 78 | 79 | #: classes/dev_portal.php:120 80 | msgid "Errors" 81 | msgstr "Erreurs" 82 | 83 | #: classes/dev_portal.php:121 84 | msgid "Invalid or insufficient data" 85 | msgstr "Données invalides ou insuffisantes" 86 | 87 | #: classes/dev_portal.php:122 88 | msgid "valid" 89 | msgstr "valide" 90 | 91 | #: classes/dev_portal.php:123 92 | msgid "invalid" 93 | msgstr "invalide" 94 | 95 | #: classes/dev_portal.php:124 96 | msgid "This token has unlimited quota" 97 | msgstr "Ce jeton a un quota illimité" 98 | 99 | #: classes/dev_portal.php:217 100 | msgid "Developer" 101 | msgstr "Développeur" 102 | 103 | #: classes/dev_portal.php:234 104 | msgid "Developer Dashboard" 105 | msgstr "Tableau de bord du développeur" 106 | 107 | #: template_tags.php:18 108 | #, php-format 109 | msgid "" 110 | "This page is only available when you are logged in." 111 | msgstr "" 112 | "Cette page est uniquement disponible si vous êtes connecté(e) à." 114 | 115 | #: templates/api_subscribe_form.php:10 116 | msgid "Tokens" 117 | msgstr "Jetons" 118 | 119 | #: templates/api_subscribe_form.php:13 120 | msgid "Usage" 121 | msgstr "Utilisation" 122 | 123 | #: templates/api_subscribe_form.php:16 124 | msgid "Quota" 125 | msgstr "Quota" 126 | 127 | #: templates/tab_quota.php:7 128 | msgid "Remaining quota" 129 | msgstr "Quota restant" 130 | 131 | #: templates/tab_quota.php:12 132 | msgid "Please enter your token here" 133 | msgstr "Veuillez saisir votre jeton ici." 134 | 135 | #: templates/tab_quota.php:13 templates/tab_tokens.php:44 136 | #: templates/tab_tokens.php:92 137 | msgid "loading" 138 | msgstr "chargement" 139 | 140 | #: templates/tab_tokens.php:4 141 | msgid "My tokens" 142 | msgstr "Mes jetons" 143 | 144 | #: templates/tab_tokens.php:12 templates/tab_tokens.php:67 145 | msgid "An error occurred. Please try again." 146 | msgstr "Une erreur s’est produite. Veuillez réessayer." 147 | 148 | #: templates/tab_tokens.php:20 templates/tab_tokens.php:72 149 | msgid "Name" 150 | msgstr "Nom" 151 | 152 | #: templates/tab_tokens.php:21 templates/tab_tokens.php:79 153 | msgid "API" 154 | msgstr "API" 155 | 156 | #: templates/tab_tokens.php:22 157 | msgid "State" 158 | msgstr "État" 159 | 160 | #: templates/tab_tokens.php:32 161 | msgid "Show usage" 162 | msgstr "Montrer utilisation" 163 | 164 | #: templates/tab_tokens.php:35 165 | msgid "Revoke this token" 166 | msgstr "Annuler ce jeton" 167 | 168 | #: templates/tab_tokens.php:47 169 | msgid "You don't have any tokens yet" 170 | msgstr "Vous n’avez pas encore de jetons." 171 | 172 | #: templates/tab_tokens.php:58 templates/tab_tokens.php:95 173 | msgid "Request a token" 174 | msgstr "Demander un jeton" 175 | 176 | #: templates/tab_tokens.php:74 177 | msgid "Give this token a name" 178 | msgstr "Donner un nom à ce jeton" 179 | 180 | #: templates/tab_tokens.php:82 templates/tab_usage.php:9 181 | msgid "-- please choose" 182 | msgstr "-- Veuillez sélectionner" 183 | 184 | #: templates/tab_tokens.php:82 templates/tab_tokens.php:94 185 | msgid "I accept the general terms and conditions" 186 | msgstr "J’accepte les conditions générales" 187 | 188 | #: templates/tab_usage.php:7 189 | msgid "Token" 190 | msgstr "Jeton" 191 | 192 | #: templates/tab_usage.php:15 193 | msgid "From" 194 | msgstr "De" 195 | 196 | #: templates/tab_usage.php:20 197 | msgid "To" 198 | msgstr "À" 199 | 200 | #~ msgid "" 201 | #~ "Token could not be found. Please not that you have to use the token at " 202 | #~ "least once before you can request the remaining quota." 203 | #~ msgstr "" 204 | #~ "Votre jeton n'a pas été trouvé. Notez que le jeton doit être utilisé au " 205 | #~ "minimum une fois avant que vous pouvez voir le quota restant." 206 | -------------------------------------------------------------------------------- /languages/tyk-dev-portal-it_IT.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/languages/tyk-dev-portal-it_IT.mo -------------------------------------------------------------------------------- /languages/tyk-dev-portal-it_IT.po: -------------------------------------------------------------------------------- 1 | # This file was generated from translations/tyk_it.xlsx 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: \n" 5 | "POT-Creation-Date: 2016-12-06 16:23+0100\n" 6 | "PO-Revision-Date: 2016-12-06 16:24+0100\n" 7 | "Last-Translator: Team Amboss \n" 8 | "Language-Team: \n" 9 | "Language: it_IT\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Generated-By: xls-to-po 1.0\n" 14 | "X-Generator: Poedit 1.8.11\n" 15 | "X-Poedit-Basepath: ..\n" 16 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 17 | "X-Poedit-KeywordsList: __;_e\n" 18 | "X-Poedit-SearchPath-0: .\n" 19 | "X-Poedit-SearchPathExcluded-0: node_modules\n" 20 | "X-Poedit-SearchPathExcluded-1: assets\n" 21 | 22 | #: classes/dashboard_ajax_provider.php:46 23 | #, php-format 24 | msgid "" 25 | "Your token for this API is: %s. We will only show this once. Please save it " 26 | "somewhere now." 27 | msgstr "" 28 | "Il token per questa API è: %s. Sarà mostrato una sola vola. Salvalo da " 29 | "qualche parte ora." 30 | 31 | #: classes/dashboard_ajax_provider.php:53 32 | #, php-format 33 | msgid "" 34 | "Your key request is pending review. You will receive an Email when your " 35 | "request is processed. Your request id is %s. This is not your access token. " 36 | "Please refer to the request ID when contacting us by Email." 37 | msgstr "" 38 | "La richiesta della chiave è in attesa di revisione. Sarà inviata un’e-mail " 39 | "al termine dell'elaborazione della richiesta. L’ID della richiesta è %s. " 40 | "Questo non è il token di accesso. Fare riferimento all’ID della richiesta in " 41 | "caso di contatto via e-mail." 42 | 43 | #: classes/dashboard_ajax_provider.php:69 44 | #: classes/dashboard_ajax_provider.php:132 45 | #: classes/dashboard_ajax_provider.php:156 46 | #: classes/dashboard_ajax_provider.php:181 47 | msgid "Invalid request" 48 | msgstr "Richiesta non valida" 49 | 50 | #: classes/dashboard_ajax_provider.php:127 51 | #, php-format 52 | msgid "" 53 | "Your token \"%s\" was revoked permanently and is invalidated effective " 54 | "immediately." 55 | msgstr "" 56 | "Il token \"%s\" è stato revocato in modo permanente e non è più valido con " 57 | "effetto immediato." 58 | 59 | #: classes/dashboard_ajax_provider.php:152 60 | msgid "" 61 | "Token could not be found. Please note that you must use the token at least " 62 | "once before you can request the remaining quota." 63 | msgstr "" 64 | "La chiave non è stata trovata. Nota, la chiave deve almeno essere stata " 65 | "usata una volta, prima di potere prelevare il contingente." 66 | 67 | #: classes/dev_portal.php:117 68 | msgid "Used" 69 | msgstr "Usato" 70 | 71 | #: classes/dev_portal.php:118 72 | msgid "Remaining" 73 | msgstr "Rimanente" 74 | 75 | #: classes/dev_portal.php:119 76 | msgid "Success" 77 | msgstr "Successo" 78 | 79 | #: classes/dev_portal.php:120 80 | msgid "Errors" 81 | msgstr "Errori" 82 | 83 | #: classes/dev_portal.php:121 84 | msgid "Invalid or insufficient data" 85 | msgstr "Dati non validi o insufficienti" 86 | 87 | #: classes/dev_portal.php:122 88 | msgid "valid" 89 | msgstr "valido" 90 | 91 | #: classes/dev_portal.php:123 92 | msgid "invalid" 93 | msgstr "invalido" 94 | 95 | #: classes/dev_portal.php:124 96 | msgid "This token has unlimited quota" 97 | msgstr "Questa token ha una quota illimitata" 98 | 99 | #: classes/dev_portal.php:217 100 | msgid "Developer" 101 | msgstr "Sviluppatore" 102 | 103 | #: classes/dev_portal.php:234 104 | msgid "Developer Dashboard" 105 | msgstr "Sviluppatore Dashboard" 106 | 107 | #: template_tags.php:18 108 | #, php-format 109 | msgid "" 110 | "This page is only available when you are logged in." 111 | msgstr "" 112 | "Questa pagina è disponibile solo se è stato effettuato il " 113 | "login." 114 | 115 | #: templates/api_subscribe_form.php:10 116 | msgid "Tokens" 117 | msgstr "Token" 118 | 119 | #: templates/api_subscribe_form.php:13 120 | msgid "Usage" 121 | msgstr "Utilizzo" 122 | 123 | #: templates/api_subscribe_form.php:16 124 | msgid "Quota" 125 | msgstr "Quota" 126 | 127 | #: templates/tab_quota.php:7 128 | msgid "Remaining quota" 129 | msgstr "Quota rimanente" 130 | 131 | #: templates/tab_quota.php:12 132 | msgid "Please enter your token here" 133 | msgstr "Inserire qui il proprio token" 134 | 135 | #: templates/tab_quota.php:13 templates/tab_tokens.php:44 136 | #: templates/tab_tokens.php:92 137 | msgid "loading" 138 | msgstr "Caricamento" 139 | 140 | #: templates/tab_tokens.php:4 141 | msgid "My tokens" 142 | msgstr "Token personali" 143 | 144 | #: templates/tab_tokens.php:12 templates/tab_tokens.php:67 145 | msgid "An error occurred. Please try again." 146 | msgstr "Si è verificato un errore. Riprovare." 147 | 148 | #: templates/tab_tokens.php:20 templates/tab_tokens.php:72 149 | msgid "Name" 150 | msgstr "Nome" 151 | 152 | #: templates/tab_tokens.php:21 templates/tab_tokens.php:79 153 | msgid "API" 154 | msgstr "API" 155 | 156 | #: templates/tab_tokens.php:22 157 | msgid "State" 158 | msgstr "Stato" 159 | 160 | #: templates/tab_tokens.php:32 161 | msgid "Show usage" 162 | msgstr "Mostrare l'utilizzo" 163 | 164 | #: templates/tab_tokens.php:35 165 | msgid "Revoke this token" 166 | msgstr "Revocare questo token" 167 | 168 | #: templates/tab_tokens.php:47 169 | msgid "You don't have any tokens yet" 170 | msgstr "Non si è ancora in possesso di un token" 171 | 172 | #: templates/tab_tokens.php:58 templates/tab_tokens.php:95 173 | msgid "Request a token" 174 | msgstr "Richiedere un token" 175 | 176 | #: templates/tab_tokens.php:74 177 | msgid "Give this token a name" 178 | msgstr "Assegnare un nome al token" 179 | 180 | #: templates/tab_tokens.php:82 templates/tab_usage.php:9 181 | msgid "-- please choose" 182 | msgstr "-- scegliere" 183 | 184 | #: templates/tab_tokens.php:82 templates/tab_tokens.php:94 185 | msgid "I accept the general terms and conditions" 186 | msgstr "Accetto le condizioni generali di contratto " 187 | 188 | #: templates/tab_usage.php:7 189 | msgid "Token" 190 | msgstr "Token" 191 | 192 | #: templates/tab_usage.php:15 193 | msgid "From" 194 | msgstr "Da" 195 | 196 | #: templates/tab_usage.php:20 197 | msgid "To" 198 | msgstr "A" 199 | 200 | #~ msgid "" 201 | #~ "Token could not be found. Please not that you have to use the token at " 202 | #~ "least once before you can request the remaining quota." 203 | #~ msgstr "" 204 | #~ "La chiave non è stata trovata. Nota, la chiave deve almeno essere stata " 205 | #~ "usata una volta, prima di potere prelevare il contingente." 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-tyk-dev-portal", 3 | "version": "1.2.0", 4 | "description": "WordPress plugin that integrates a developer portal of a Tyk API Gateway in your WordPress site", 5 | "directories": { 6 | "test": "tests" 7 | }, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/liip/wp-tyk-dev-portal.git" 14 | }, 15 | "keywords": [ 16 | "wordpress", 17 | "plugin", 18 | "tyk" 19 | ], 20 | "author": "teamamboss", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/liip/wp-tyk-dev-portal/issues" 24 | }, 25 | "homepage": "https://github.com/liip/wp-tyk-dev-portal#readme", 26 | "dependencies": { 27 | "bootstrap": "^3.3.7", 28 | "chart.js": "^2.3.0", 29 | "underscore": "^1.8.3", 30 | "vue": "^2.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | -------------------------------------------------------------------------------- /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/screenshot-1.png -------------------------------------------------------------------------------- /screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/screenshot-2.png -------------------------------------------------------------------------------- /screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liip/wp-tyk-dev-portal/26f6cd0ceeec680a2cbffb73834cd8669522ab42/screenshot-3.png -------------------------------------------------------------------------------- /template_tags.php: -------------------------------------------------------------------------------- 1 | is_logged_in()) { 15 | ob_start(); 16 | 17 | include_once TYK_DEV_PORTAL_PLUGIN_PATH . '/templates/api_subscribe_form.php'; 18 | 19 | return ob_get_clean(); 20 | } 21 | 22 | $login_page = get_page_by_path('log-in'); 23 | 24 | return sprintf(__('This page is only available when you are logged in.', Tyk_Dev_Portal::TEXT_DOMAIN), get_permalink($login_page)); 25 | } 26 | -------------------------------------------------------------------------------- /templates/api_subscribe_form.php: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
-------------------------------------------------------------------------------- /templates/tab_quota.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |
7 |

8 | 13 | 14 | 15 |
16 |
{{error}}
17 |
{{message}}
18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 |
-------------------------------------------------------------------------------- /templates/tab_tokens.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

5 | 6 | 7 |
8 | 11 | 14 |
15 | 16 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 |
55 |
56 | 57 | 58 | 59 |
60 |

61 | 62 | 63 |
64 | 68 | 71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 |
83 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 |
96 |
97 | 98 |
99 |
100 | 108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | -------------------------------------------------------------------------------- /templates/tab_usage.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |
7 |

8 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 |
30 | 31 |
-------------------------------------------------------------------------------- /tests/ApiManagerTest.php: -------------------------------------------------------------------------------- 1 | available_policies(); 14 | $this->assertIsArray($apis); 15 | $this->assertTrue(sizeof($apis) > 0); 16 | } 17 | 18 | // test getting apis 19 | public function testAvailableApis(): void 20 | { 21 | $apiManager = new Tyk_API_Manager(); 22 | $apis = $apiManager->available_apis(); 23 | $this->assertIsArray($apis); 24 | $this->assertTrue(sizeof($apis) > 0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/PluginTest.php: -------------------------------------------------------------------------------- 1 | create_developer_role(); 14 | 15 | $role = get_role($plugin::DEVELOPER_ROLE_NAME); 16 | $this->assertInstanceOf('WP_Role', $role); 17 | } 18 | 19 | // test that dashboard page can be created 20 | public function testDashboardPageCreation(): void 21 | { 22 | $plugin = new Tyk_Dev_Portal(); 23 | $plugin->create_dashboard_page(); 24 | 25 | $page = get_page_by_path($plugin::DASHBOARD_SLUG); 26 | $this->assertInstanceOf('WP_Post', $page); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/PortalUserTest.php: -------------------------------------------------------------------------------- 1 | createTestUser(); 14 | $this->assertGreaterThanOrEqual(1, $user_id); 15 | 16 | // make sure our check gets it right 17 | $user = new Tyk_Portal_User($user_id); 18 | $this->assertTrue($user->is_developer()); 19 | } 20 | 21 | // test developer registration with tyk 22 | public function testTykRegistration(): void 23 | { 24 | // create a new user with developer role 25 | $user_id = $this->createTestUser(); 26 | $this->assertGreaterThanOrEqual(1, $user_id); 27 | 28 | $user = new Tyk_Portal_User($user_id); 29 | $user->register_with_tyk(); 30 | $tyk_id = $user->get_tyk_id(); 31 | 32 | // it's hard to check if the id is valid, but let's make sure 33 | // it's not empty and is at leaast 5 chars long 34 | $this->assertNotEmpty($tyk_id); 35 | $this->assertTrue(strlen($tyk_id) > 5); 36 | } 37 | 38 | // test adding an access token 39 | // @todo test adding token that already exists (same token_id) 40 | // @todo test adding multiple tokens and make sure they all get saved 41 | public function testAddAccessToken(): void 42 | { 43 | $user = $this->createPortalUser(); 44 | // save a token 45 | $testToken = array( 46 | 'api_id' => 'api-id', 47 | 'token_name' => 'Unittest Token', 48 | 'hash' => 'token-id', 49 | ); 50 | $user->save_access_token($testToken['api_id'], $testToken['token_name'], $testToken['hash']); 51 | 52 | // save it again to check that it updates and doesn't duplicate 53 | $user->save_access_token($testToken['api_id'], $testToken['token_name'], $testToken['hash']); 54 | 55 | // get all tokens 56 | $tokens = $user->get_access_tokens(); 57 | 58 | // make sure it didn't duplicate 59 | $this->assertEquals(count($tokens), 1); 60 | $this->assertArraySubset(array($testToken), $tokens); 61 | } 62 | 63 | // make sure we always get an array of access tokens 64 | public function testGetAccessTokens(): void 65 | { 66 | $user = $this->createPortalUser(); 67 | 68 | $this->assertIsArray($user->get_access_tokens()); 69 | } 70 | 71 | // test that we can delete tokens 72 | public function testDeleteAccessToken(): void 73 | { 74 | $user = $this->createPortalUser(); 75 | 76 | // let's add three tokens 77 | $user->save_access_token('api-id', 'My favorite token', 'token-id-1'); 78 | $user->save_access_token('api-id', 'My 2nd-favorite token', 'token-id-2'); 79 | $user->save_access_token('api-id', 'My 3rd-favorite token', 'token-id-3'); 80 | 81 | $tokens = $user->get_access_tokens(); 82 | $this->assertEquals(count($tokens), 3); 83 | 84 | // delete the 2nd one 85 | $user->delete_access_token('token-id-2'); 86 | 87 | $tokens = $user->get_access_tokens(); 88 | $found = false; 89 | foreach ($tokens as $token) { 90 | if ('token-id-2' == $token['hash']) { 91 | $found = true; 92 | } 93 | } 94 | $this->assertFalse($found); 95 | $this->assertEquals(count($tokens), 2); 96 | } 97 | 98 | // test getting a token 99 | public function testGetExistingToken(): void 100 | { 101 | $user = $this->createPortalUser(); 102 | // save a token 103 | $testToken = array( 104 | 'api_id' => 'api-id', 105 | 'token_name' => 'Unittest Token', 106 | 'hash' => 'token-id-4', 107 | ); 108 | $user->save_access_token($testToken['api_id'], $testToken['token_name'], $testToken['hash']); 109 | 110 | $token = $user->get_access_token($testToken['hash']); 111 | 112 | $this->assertInstanceOf('Tyk_Token', $token); 113 | // note: the token name isn't relevant for the Tyk_Token class 114 | $this->assertEquals($token->get_hash(), $testToken['hash']); 115 | $this->assertEquals($token->get_policy(), $testToken['api_id']); 116 | } 117 | 118 | /** 119 | * test that you can't get a on existent token. 120 | * 121 | * @expectedException \OutOfBoundsException 122 | */ 123 | public function testGetNonExistentToken(): void 124 | { 125 | $user = $this->createPortalUser(); 126 | $user->get_access_token("surely this won't work"); 127 | } 128 | 129 | // test if we can pull user data from tyk 130 | public function testFetchUserFromTyk(): void 131 | { 132 | $user = $this->createPortalUser(); 133 | 134 | $developer = $user->fetch_from_tyk(); 135 | $this->assertInstanceOf('stdClass', $developer); 136 | $this->assertEquals($developer->id, $user->get_tyk_id()); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/TokenTest.php: -------------------------------------------------------------------------------- 1 | createPortalUser(); 13 | 14 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 15 | $token->request(); 16 | 17 | // it's hard to check if the key is valid, but let's make sure 18 | // it's not empty and is at leaast 5 chars long 19 | $this->assertNotEmpty($token->get_id()); 20 | $this->assertTrue(strlen($token->get_id()) > 5); 21 | } 22 | 23 | /** 24 | * Disabled because Tyk API doesn't care if the policy exists or not. 25 | * 26 | * @see https://github.com/TykTechnologies/tyk/issues/272 27 | * expectedException UnexpectedValueException 28 | * 29 | * function testInvalidKeyRequest() { 30 | * $user = $this->createPortalUser(); 31 | * 32 | * $token = new Tyk_Token($user, 'invalid api'); 33 | * $token->request(); 34 | * print $token->get_id(); 35 | * }*/ 36 | 37 | // test making and approving a key request to get an access token 38 | // @todo test failure when using an invalid key 39 | public function testKeyApproval(): void 40 | { 41 | $user = $this->createPortalUser(); 42 | 43 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 44 | $token->request(); 45 | $token->approve(); 46 | 47 | // it's hard to check if the token is valid, but let's make sure 48 | // it's not empty and is at leaast 5 chars long 49 | $this->assertNotEmpty($token->get_key()); 50 | $this->assertTrue(strlen($token->get_key()) > 5); 51 | $this->assertNotEmpty($token->get_hash()); 52 | $this->assertTrue(strlen($token->get_hash()) > 5); 53 | } 54 | 55 | /** 56 | * test that you can't approve a key without an id. 57 | * 58 | * @expectedException \InvalidArgumentException 59 | */ 60 | public function testEmptyKeyApproval(): void 61 | { 62 | $user = $this->createPortalUser(); 63 | 64 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 65 | $token->request(); 66 | // let's set the internal id to something invalid 67 | $token->set_id(null); 68 | $token->approve(); 69 | } 70 | 71 | /** 72 | * test that you can't approve an invalid key. 73 | * 74 | * @expectedException \UnexpectedValueException 75 | */ 76 | public function testInvalidKeyApproval(): void 77 | { 78 | $user = $this->createPortalUser(); 79 | 80 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 81 | $token->request(); 82 | // let's set the internal id to an id that isn't on tyk 83 | $token->set_id('not an actual id'); 84 | $token->approve(); 85 | } 86 | 87 | /** 88 | * test that we can't instantiate a token with invalid values. 89 | * 90 | * @expectedException \InvalidArgumentException 91 | */ 92 | public function testInvalidTokenInstantiation(): void 93 | { 94 | $user = $this->createPortalUser(); 95 | $token = Tyk_Token::init(array('foo' => 'bar'), $user); 96 | } 97 | 98 | // test revoking a key 99 | public function testRevokeKey(): void 100 | { 101 | $user = $this->createPortalUser(); 102 | 103 | // request a token 104 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 105 | $token->request(); 106 | 107 | // approve it 108 | $token->approve(); 109 | $this->assertNotEmpty($token->get_key()); 110 | $this->assertTrue(strlen($token->get_key()) > 5); 111 | 112 | // revoke it 113 | $this->assertTrue($token->revoke()); 114 | } 115 | 116 | // test getting usage quota of a token 117 | public function testUsageQuota(): void 118 | { 119 | $user = $this->createPortalUser(); 120 | 121 | // create a token first 122 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 123 | $token->request(); 124 | $token->approve(); 125 | 126 | $token = Tyk_Token::init_from_key($token->get_key(), $user); 127 | $data = $token->get_usage_quota(); 128 | 129 | $this->assertIsObject($data); 130 | } 131 | 132 | // test getting usage stats of a token 133 | public function testUsageStats(): void 134 | { 135 | $user = $this->createPortalUser(); 136 | 137 | // create a token first 138 | $token = new Tyk_Token($user, TYK_TEST_API_POLICY); 139 | $token->request(); 140 | $token->approve(); 141 | 142 | $data = $token->get_usage(); 143 | 144 | // this doesn't make a lot of sense like this but $data will be null if we don't use the token 145 | // but at least we didn't get an exception if we got this far 146 | $this->assertTrue(is_object($data) || is_null($data)); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /tests/TykDevPortalTestcase.php: -------------------------------------------------------------------------------- 1 | createTestUser(); 22 | $this->assertGreaterThanOrEqual(1, $user_id); 23 | 24 | $user = new Tyk_Portal_User($user_id); 25 | $user->register_with_tyk(); 26 | $this->assertTrue($user->has_tyk_id()); 27 | 28 | return $user; 29 | } 30 | 31 | /** 32 | * Create a test wordpress user. 33 | * 34 | * @return int User ID 35 | */ 36 | protected function createTestUser() 37 | { 38 | return wp_insert_user(array( 39 | 'user_login' => 'test_developer', 40 | 'user_pass' => '123456789', 41 | 'user_email' => 'unittest@example.org', 42 | 'role' => 'developer', 43 | )); 44 | } 45 | 46 | /** 47 | * Get access to a protected method of a class. 48 | * 49 | * @param string $class 50 | * @param string $method 51 | * 52 | * @return ReflectionMethod 53 | */ 54 | protected function getProtectedMethod($class, $method) 55 | { 56 | $class = new ReflectionClass($class); 57 | $method = $class->getMethod($method); 58 | $method->setAccessible(true); 59 | 60 | return $method; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 9 | * Version: 1.3.0 10 | * Date: 20.02.2020 11 | * Text Domain: tyk-dev-portal. 12 | */ 13 | defined('ABSPATH') or exit('No script kiddies please!'); 14 | 15 | define('TYK_DEV_PORTAL_PLUGIN_PATH', dirname(__FILE__)); 16 | define('TYK_DEV_PORTAL_TPL_PATH', TYK_DEV_PORTAL_PLUGIN_PATH . '/templates'); 17 | define('TYK_DEV_PORTAL_PLUGIN_FILE', __FILE__); 18 | 19 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/dev_portal.php'; 20 | 21 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/portal_user.php'; 22 | 23 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/api_manager.php'; 24 | 25 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/dashboard_ajax_provider.php'; 26 | 27 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/token.php'; 28 | 29 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/tyk_interaction.php'; 30 | 31 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/tyk_api.php'; 32 | 33 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/classes/tyk_gateway.php'; 34 | 35 | require_once TYK_DEV_PORTAL_PLUGIN_PATH . '/template_tags.php'; 36 | 37 | $plugin = new Tyk_Dev_Portal(); 38 | $plugin->register_hooks(); 39 | $plugin->register_actions(); 40 | $plugin->register_shortcodes(); 41 | 42 | /** 43 | * Get url to this plugin's dir. 44 | * 45 | * @param string $path Path to the plugin file you want the url for 46 | * 47 | * @return string 48 | */ 49 | function tyk_dev_portal_plugin_url($path) 50 | { 51 | return plugin_dir_url(__FILE__) . $path; 52 | } 53 | --------------------------------------------------------------------------------