commit c679f8e500e94d4b687f017cd7c7cf63b219133c Author: Gowtham M Date: Mon Jun 30 17:01:07 2025 +0530 Initial commit - Gowtham diff --git a/.env b/.env new file mode 100755 index 0000000..29535e4 --- /dev/null +++ b/.env @@ -0,0 +1,44 @@ + +#-------------------------------------------------------------------- +# ENVIRONMENT +#-------------------------------------------------------------------- + +CI_ENVIRONMENT = development + +#-------------------------------------------------------------------- +# APP +#-------------------------------------------------------------------- + +app.baseURL = 'http://localhost/ts_tat/' + +ORG_id = 1 +FE_URL = 'https://apitest.tripapprovaltool.com/tstat/index.html' +encryption.key = 'e3bf95f0ef4a5d2707e9a84d44d50b33' + +# IdP (Identity Provider) +# IDP_EntityId = 'https://sts.windows.net/84750c4c-1e54-4d6d-9d5f-c395e8b1faf2/'; // Identity Provider Entity ID from the XML +# IDP_SSOUrl = 'https://login.microsoftonline.com/84750c4c-1e54-4d6d-9d5f-c395e8b1faf2/saml2'; // Single Sign-On URL +# IDP_SLOUrl = 'https://login.microsoftonline.com/84750c4c-1e54-4d6d-9d5f-c395e8b1faf2/saml2'; // Single Logout URL +# IDP_X509Cert = 'MIIC8DCCAdigAwIBAgIQWleLECyLTpdMmXTERG7FojANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0yNTAxMjEwNTQyMTZaFw0yODAxMjEwNTQyMTZaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQgU1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJwYcfg/Fy3vuQ1LMWDKO4lNhnJenUZGDtE5KNjfAmR6ihCklQffrHF5DcTg5C72H62q1Me+Og2TnzOvUCepTUi88eoFh3rNfKyTcaKpcp7cx3lBy51iZIw5WYUqLz/547bA/m585bIVHMD8A3WiqOLUCaYoP8zbVb51uqa5+lPPscRj0DMAKuENf0FLvLbVLzFvWXH2VUbLJcErqumIb1eBg31rppcU+sS+wzNiZ8CIPPz0GpebQvcQpMR+OBbqG+jtDneyT9e20M9BHg303DTXsxxkAVHJiB8cUnFKHsDsfTuDtGvFXk/yUz9QP89SKDvxe6TlmrpeXyapPdZT/QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQACOvBUZ+d0BHNXPB0DsekCciK0wXlhPZVTNzCKtCyIcmQWXd2vOhxUEt7qm90R92Gnb5r6BDpLD8ng2AkqYg5dSfgk8xq8oSSxbiQVLypFwOelH7B0bNbF4OdRAQnovFH78ZZG8agT73xYJpcod5dyNwHGJXi/X+A0mHtYNFjZb5Nj5DVPfaXbvp440i0mGrK1MX9pgoQnY5F2749dyZiedXFTpL0S6r4LOYtUUlxnI1CI+f7Zy2oaijh22yZp2PS5cTo9BDx+tJbv2jEhF4L1ITrJnCRlvdxeJD9lGnSE9CfsN6kRNzxuUNiUVM+/s1j0nB3k303OE5y2F/fCDzjx'; // Identity Provider's X.509 certificate + + +IDP_EntityId = 'https://sts.windows.net/284bf2b6-6dd8-4b46-881c-de45fde35736/'; // Identity Provider Entity ID from the XML +IDP_SSOUrl = 'https://login.microsoftonline.com/284bf2b6-6dd8-4b46-881c-de45fde35736/saml2'; // Single Sign-On URL +IDP_SLOUrl = 'https://login.microsoftonline.com/284bf2b6-6dd8-4b46-881c-de45fde35736/saml2'; // Single Logout URL +IDP_X509Cert = 'MIIC8DCCAdigAwIBAgIQZBsZqgPgSLRAMuQzWQDkDTANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0yNTAxMjMxMjM4MTdaFw0yODAxMjMxMjM4MTdaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQgU1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApzTls3aSwJi8mYI78WsJRGg6cBdLkatWIYUqYPw8x1iTJ1GYPOuQdqk5PM34ybwHRTPcDjfHsIOjksvIrcOOtfXQ8MIqMaCYFcACqOAEM/Qb1pIvQBUT4TtiprzM01GzqFq5K7fYEftqKuQ+53yZFFb/xP/3CCWM8fRZOxoI9igexlaJT0MY0ics7Pi/kjPQt8OGncZMJhsiMmIzCJxDaj0dKAPaxZzpFZYkediw3SYbNa8Q18oA/zi32tT/0hC8DqWJkR+1i3DRwnp82wfH4BZDqPt6bgBlcs4V1WkVo+z6bX+HXIq1PGdIX6nPeKxMPupx3za+CIKVE8Jf3qX+zQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCUILtMUKA6u36R5FtgTT0KmxdIDRax+LJyzOW4GpS/cM2YJye2HR1HTaOmMLvthEp67hkseJjIfqDrT0ZZ56iiKblDUTDhvs2dHt1+gly3lBtL4AG0pvn/D7vSvCyN6cjUguQ6upwG/jhP5b6wbYH3hSkPq2cbq6GMU1JlGjVK2BhyseJ9dxsqetDkE1nWi4maPyjXisTM+QC9Y6VIMvfn+ZfSdZJsm6jeIlpXFDyefputXdtfyjlB9fcHYi0Udy15KGA9FOFBTrCnvuScF1tFBOJwvbx8jKJSNfHXim7laMhVmO8/RTrnyKxnXy5KO7mGcD8wQJy4nZreZTEACubu'; // Identity Provider's X.509 certificate + +JWT_SECRET = b5f9d3a16e92e0f8e3c0f2b8e4d6f9a12234b5c6d7e8f9a0b1c2d3e4f5a6b7c8 + + +#-------------------------------------------------------------------- +# DATABASE +#-------------------------------------------------------------------- + +database.default.hostname = '119.18.54.85' +database.default.database = 'venbaehn_ts' +database.default.username = 'venbaehn_ts' +database.default.password = '9[b~~Ge9[f&;' + +database.default.DBDriver = MySQLi +database.default.DBPrefix = +database.default.port = 3306 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec806cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Ignore vendor folder +/vendor/ + +# Ignore logs folder +/writable/logs/ +/writable/debugbar/ +/writable/session/ \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100755 index 0000000..c44eac3 --- /dev/null +++ b/.htaccess @@ -0,0 +1,55 @@ +# Disable directory browsing +Options -Indexes + +# ---------------------------------------------------------------------- +# Rewrite engine +# ---------------------------------------------------------------------- + +# Turning on the rewrite engine is necessary for the following rules and features. +# FollowSymLinks must be enabled for this to work. + + Options +FollowSymlinks + RewriteEngine On + + # If you installed CodeIgniter in a subfolder, you will need to + # change the following line to match the subfolder you need. + # http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase + # RewriteBase / + + # Redirect Trailing Slashes... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Rewrite "www.example.com -> example.com" + RewriteCond %{HTTPS} !=on + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + + # Checks to see if the user is attempting to access a valid file, + # such as an image or css document, if this isn't true it sends the + # request to the front controller, index.php + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA] + + # Ensure Authorization header is passed along + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + + + # If we don't have mod_rewrite installed, all 404's + # can be sent to index.php, and everything works as normal. + ErrorDocument 404 index.php + + + + Header set Access-Control-Allow-Origin "*" + Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" + Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" + + +# Disable server signature start +ServerSignature Off +# Disable server signature end diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..24728f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 British Columbia Institute of Technology +Copyright (c) 2019-present CodeIgniter Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..a23783a --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# CodeIgniter 4 Framework + +## What is CodeIgniter? + +CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. +More information can be found at the [official site](https://codeigniter.com). + +This repository holds the distributable version of the framework. +It has been built from the +[development repository](https://github.com/codeigniter4/CodeIgniter4). + +More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. + +You can read the [user guide](https://codeigniter.com/user_guide/) +corresponding to the latest version of the framework. + +## Important Change with index.php + +`index.php` is no longer in the root of the project! It has been moved inside the *public* folder, +for better security and separation of components. + +This means that you should configure your web server to "point" to your project's *public* folder, and +not to the project root. A better practice would be to configure a virtual host to point there. A poor practice would be to point your web server to the project root and expect to enter *public/...*, as the rest of your logic and the +framework are exposed. + +**Please** read the user guide for a better explanation of how CI4 works! + +## Repository Management + +We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. +We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss +FEATURE REQUESTS. + +This repository is a "distribution" one, built by our release preparation script. +Problems with it can be raised on our forum, or as issues in the main repository. + +## Contributing + +We welcome contributions from the community. + +Please read the [*Contributing to CodeIgniter*](https://github.com/codeigniter4/CodeIgniter4/blob/develop/CONTRIBUTING.md) section in the development repository. + +## Server Requirements + +PHP version 8.1 or higher is required, with the following extensions installed: + +- [intl](http://php.net/manual/en/intl.requirements.php) +- [mbstring](http://php.net/manual/en/mbstring.installation.php) + +> [!WARNING] +> - The end of life date for PHP 7.4 was November 28, 2022. +> - The end of life date for PHP 8.0 was November 26, 2023. +> - If you are still using PHP 7.4 or 8.0, you should upgrade immediately. +> - The end of life date for PHP 8.1 will be December 31, 2025. + +Additionally, make sure that the following extensions are enabled in your PHP: + +- json (enabled by default - don't turn it off) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library diff --git a/app/.htaccess b/app/.htaccess new file mode 100755 index 0000000..3462048 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/app/Common.php b/app/Common.php new file mode 100755 index 0000000..95f5544 --- /dev/null +++ b/app/Common.php @@ -0,0 +1,15 @@ + + */ + public array $allowedHostnames = []; + + /** + * -------------------------------------------------------------------------- + * Index File + * -------------------------------------------------------------------------- + * + * Typically, this will be your `index.php` file, unless you've renamed it to + * something else. If you have configured your web server to remove this file + * from your site URIs, set this variable to an empty string. + */ + public string $indexPage = 'index.php'; + + /** + * -------------------------------------------------------------------------- + * URI PROTOCOL + * -------------------------------------------------------------------------- + * + * This item determines which server global should be used to retrieve the + * URI string. The default setting of 'REQUEST_URI' works for most servers. + * If your links do not seem to work, try one of the other delicious flavors: + * + * 'REQUEST_URI': Uses $_SERVER['REQUEST_URI'] + * 'QUERY_STRING': Uses $_SERVER['QUERY_STRING'] + * 'PATH_INFO': Uses $_SERVER['PATH_INFO'] + * + * WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded! + */ + public string $uriProtocol = 'REQUEST_URI'; + + /* + |-------------------------------------------------------------------------- + | Allowed URL Characters + |-------------------------------------------------------------------------- + | + | This lets you specify which characters are permitted within your URLs. + | When someone tries to submit a URL with disallowed characters they will + | get a warning message. + | + | As a security measure you are STRONGLY encouraged to restrict URLs to + | as few characters as possible. + | + | By default, only these are allowed: `a-z 0-9~%.:_-` + | + | Set an empty string to allow all characters -- but only if you are insane. + | + | The configured value is actually a regular expression character group + | and it will be used as: '/\A[]+\z/iu' + | + | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! + | + */ + public string $permittedURIChars = 'a-z 0-9~%.:_\-'; + + /** + * -------------------------------------------------------------------------- + * Default Locale + * -------------------------------------------------------------------------- + * + * The Locale roughly represents the language and location that your visitor + * is viewing the site from. It affects the language strings and other + * strings (like currency markers, numbers, etc), that your program + * should run under for this request. + */ + public string $defaultLocale = 'en'; + + /** + * -------------------------------------------------------------------------- + * Negotiate Locale + * -------------------------------------------------------------------------- + * + * If true, the current Request object will automatically determine the + * language to use based on the value of the Accept-Language header. + * + * If false, no automatic detection will be performed. + */ + public bool $negotiateLocale = false; + + /** + * -------------------------------------------------------------------------- + * Supported Locales + * -------------------------------------------------------------------------- + * + * If $negotiateLocale is true, this array lists the locales supported + * by the application in descending order of priority. If no match is + * found, the first locale will be used. + * + * IncomingRequest::setLocale() also uses this list. + * + * @var list + */ + public array $supportedLocales = ['en']; + + /** + * -------------------------------------------------------------------------- + * Application Timezone + * -------------------------------------------------------------------------- + * + * The default timezone that will be used in your application to display + * dates with the date helper, and can be retrieved through app_timezone() + * + * @see https://www.php.net/manual/en/timezones.php for list of timezones + * supported by PHP. + */ + public string $appTimezone = 'UTC'; + + /** + * -------------------------------------------------------------------------- + * Default Character Set + * -------------------------------------------------------------------------- + * + * This determines which character set is used by default in various methods + * that require a character set to be provided. + * + * @see http://php.net/htmlspecialchars for a list of supported charsets. + */ + public string $charset = 'UTF-8'; + + /** + * -------------------------------------------------------------------------- + * Force Global Secure Requests + * -------------------------------------------------------------------------- + * + * If true, this will force every request made to this application to be + * made via a secure connection (HTTPS). If the incoming request is not + * secure, the user will be redirected to a secure version of the page + * and the HTTP Strict Transport Security (HSTS) header will be set. + */ + public bool $forceGlobalSecureRequests = false; + + /** + * -------------------------------------------------------------------------- + * Reverse Proxy IPs + * -------------------------------------------------------------------------- + * + * If your server is behind a reverse proxy, you must whitelist the proxy + * IP addresses from which CodeIgniter should trust headers such as + * X-Forwarded-For or Client-IP in order to properly identify + * the visitor's IP address. + * + * You need to set a proxy IP address or IP address with subnets and + * the HTTP header for the client IP address. + * + * Here are some examples: + * [ + * '10.0.1.200' => 'X-Forwarded-For', + * '192.168.5.0/24' => 'X-Real-IP', + * ] + * + * @var array + */ + public array $proxyIPs = []; + + /** + * -------------------------------------------------------------------------- + * Content Security Policy + * -------------------------------------------------------------------------- + * + * Enables the Response's Content Secure Policy to restrict the sources that + * can be used for images, scripts, CSS files, audio, video, etc. If enabled, + * the Response object will populate default values for the policy from the + * `ContentSecurityPolicy.php` file. Controllers can always add to those + * restrictions at run time. + * + * For a better understanding of CSP, see these documents: + * + * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + * @see http://www.w3.org/TR/CSP/ + */ + public bool $CSPEnabled = false; +} diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php new file mode 100755 index 0000000..7bafd98 --- /dev/null +++ b/app/Config/Autoload.php @@ -0,0 +1,94 @@ +|string> + */ + public $psr4 = [ + APP_NAMESPACE => APPPATH, + ]; + + /** + * ------------------------------------------------------------------- + * Class Map + * ------------------------------------------------------------------- + * The class map provides a map of class names and their exact + * location on the drive. Classes loaded in this manner will have + * slightly faster performance because they will not have to be + * searched for within one or more directories as they would if they + * were being autoloaded through a namespace. + * + * Prototype: + * $classmap = [ + * 'MyClass' => '/path/to/class/file.php' + * ]; + * + * @var array + */ + public $classmap = []; + + /** + * ------------------------------------------------------------------- + * Files + * ------------------------------------------------------------------- + * The files array provides a list of paths to __non-class__ files + * that will be autoloaded. This can be useful for bootstrap operations + * or for loading functions. + * + * Prototype: + * $files = [ + * '/path/to/my/file.php', + * ]; + * + * @var list + */ + public $files = []; + + /** + * ------------------------------------------------------------------- + * Helpers + * ------------------------------------------------------------------- + * Prototype: + * $helpers = [ + * 'form', + * ]; + * + * @var list + */ + public $helpers = ['JwtHelper','common_helper','mail_helper','url_helper','status_helper']; +} diff --git a/app/Config/Boot/development.php b/app/Config/Boot/development.php new file mode 100755 index 0000000..a868447 --- /dev/null +++ b/app/Config/Boot/development.php @@ -0,0 +1,34 @@ + + */ + public array $file = [ + 'storePath' => WRITEPATH . 'cache/', + 'mode' => 0640, + ]; + + /** + * ------------------------------------------------------------------------- + * Memcached settings + * ------------------------------------------------------------------------- + * + * Your Memcached servers can be specified below, if you are using + * the Memcached drivers. + * + * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached + * + * @var array + */ + public array $memcached = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'weight' => 1, + 'raw' => false, + ]; + + /** + * ------------------------------------------------------------------------- + * Redis settings + * ------------------------------------------------------------------------- + * Your Redis server can be specified below, if you are using + * the Redis or Predis drivers. + * + * @var array + */ + public array $redis = [ + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + 'database' => 0, + ]; + + /** + * -------------------------------------------------------------------------- + * Available Cache Handlers + * -------------------------------------------------------------------------- + * + * This is an array of cache engine alias' and class names. Only engines + * that are listed here are allowed to be used. + * + * @var array> + */ + public array $validHandlers = [ + 'dummy' => DummyHandler::class, + 'file' => FileHandler::class, + 'memcached' => MemcachedHandler::class, + 'predis' => PredisHandler::class, + 'redis' => RedisHandler::class, + 'wincache' => WincacheHandler::class, + ]; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Include Query String + * -------------------------------------------------------------------------- + * + * Whether to take the URL query string into consideration when generating + * output cache files. Valid options are: + * + * false = Disabled + * true = Enabled, take all query parameters into account. + * Please be aware that this may result in numerous cache + * files generated for the same page over and over again. + * ['q'] = Enabled, but only take into account the specified list + * of query parameters. + * + * @var bool|list + */ + public $cacheQueryString = false; +} diff --git a/app/Config/Constants.php b/app/Config/Constants.php new file mode 100755 index 0000000..fb56bb1 --- /dev/null +++ b/app/Config/Constants.php @@ -0,0 +1,79 @@ +|string|null + */ + public $defaultSrc; + + /** + * Lists allowed scripts' URLs. + * + * @var list|string + */ + public $scriptSrc = 'self'; + + /** + * Lists allowed stylesheets' URLs. + * + * @var list|string + */ + public $styleSrc = 'self'; + + /** + * Defines the origins from which images can be loaded. + * + * @var list|string + */ + public $imageSrc = 'self'; + + /** + * Restricts the URLs that can appear in a page's `` element. + * + * Will default to self if not overridden + * + * @var list|string|null + */ + public $baseURI; + + /** + * Lists the URLs for workers and embedded frame contents + * + * @var list|string + */ + public $childSrc = 'self'; + + /** + * Limits the origins that you can connect to (via XHR, + * WebSockets, and EventSource). + * + * @var list|string + */ + public $connectSrc = 'self'; + + /** + * Specifies the origins that can serve web fonts. + * + * @var list|string + */ + public $fontSrc; + + /** + * Lists valid endpoints for submission from `
` tags. + * + * @var list|string + */ + public $formAction = 'self'; + + /** + * Specifies the sources that can embed the current page. + * This directive applies to ``, `\n * \n *\n * */\n\n.aspect-ratio {\n height: 0;\n position: relative;\n}\n\n.aspect-ratio--16x9 { padding-bottom: 56.25%; }\n.aspect-ratio--9x16 { padding-bottom: 177.77%; }\n\n.aspect-ratio--4x3 { padding-bottom: 75%; }\n.aspect-ratio--3x4 { padding-bottom: 133.33%; }\n\n.aspect-ratio--6x4 { padding-bottom: 66.6%; }\n.aspect-ratio--4x6 { padding-bottom: 150%; }\n\n.aspect-ratio--8x5 { padding-bottom: 62.5%; }\n.aspect-ratio--5x8 { padding-bottom: 160%; }\n\n.aspect-ratio--7x5 { padding-bottom: 71.42%; }\n.aspect-ratio--5x7 { padding-bottom: 140%; }\n\n.aspect-ratio--1x1 { padding-bottom: 100%; }\n\n.aspect-ratio--object {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 100;\n}\n\n@media #{$breakpoint-not-small}{\n .aspect-ratio-ns {\n height: 0;\n position: relative;\n }\n .aspect-ratio--16x9-ns { padding-bottom: 56.25%; }\n .aspect-ratio--9x16-ns { padding-bottom: 177.77%; }\n .aspect-ratio--4x3-ns { padding-bottom: 75%; }\n .aspect-ratio--3x4-ns { padding-bottom: 133.33%; }\n .aspect-ratio--6x4-ns { padding-bottom: 66.6%; }\n .aspect-ratio--4x6-ns { padding-bottom: 150%; }\n .aspect-ratio--8x5-ns { padding-bottom: 62.5%; }\n .aspect-ratio--5x8-ns { padding-bottom: 160%; }\n .aspect-ratio--7x5-ns { padding-bottom: 71.42%; }\n .aspect-ratio--5x7-ns { padding-bottom: 140%; }\n .aspect-ratio--1x1-ns { padding-bottom: 100%; }\n .aspect-ratio--object-ns {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 100;\n }\n}\n\n@media #{$breakpoint-medium}{\n .aspect-ratio-m {\n height: 0;\n position: relative;\n }\n .aspect-ratio--16x9-m { padding-bottom: 56.25%; }\n .aspect-ratio--9x16-m { padding-bottom: 177.77%; }\n .aspect-ratio--4x3-m { padding-bottom: 75%; }\n .aspect-ratio--3x4-m { padding-bottom: 133.33%; }\n .aspect-ratio--6x4-m { padding-bottom: 66.6%; }\n .aspect-ratio--4x6-m { padding-bottom: 150%; }\n .aspect-ratio--8x5-m { padding-bottom: 62.5%; }\n .aspect-ratio--5x8-m { padding-bottom: 160%; }\n .aspect-ratio--7x5-m { padding-bottom: 71.42%; }\n .aspect-ratio--5x7-m { padding-bottom: 140%; }\n .aspect-ratio--1x1-m { padding-bottom: 100%; }\n .aspect-ratio--object-m {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 100;\n }\n}\n\n@media #{$breakpoint-large}{\n .aspect-ratio-l {\n height: 0;\n position: relative;\n }\n .aspect-ratio--16x9-l { padding-bottom: 56.25%; }\n .aspect-ratio--9x16-l { padding-bottom: 177.77%; }\n .aspect-ratio--4x3-l { padding-bottom: 75%; }\n .aspect-ratio--3x4-l { padding-bottom: 133.33%; }\n .aspect-ratio--6x4-l { padding-bottom: 66.6%; }\n .aspect-ratio--4x6-l { padding-bottom: 150%; }\n .aspect-ratio--8x5-l { padding-bottom: 62.5%; }\n .aspect-ratio--5x8-l { padding-bottom: 160%; }\n .aspect-ratio--7x5-l { padding-bottom: 71.42%; }\n .aspect-ratio--5x7-l { padding-bottom: 140%; }\n .aspect-ratio--1x1-l { padding-bottom: 100%; }\n .aspect-ratio--object-l {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 100;\n }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n IMAGES\n Docs: http://tachyons.io/docs/elements/images/\n\n*/\n\n/* Responsive images! */\n\nimg { max-width: 100%; }\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BACKGROUND SIZE\n Docs: http://tachyons.io/docs/themes/background-size/\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n/*\n Often used in combination with background image set as an inline style\n on an html element.\n*/\n\n .cover { background-size: cover!important; }\n .contain { background-size: contain!important; }\n\n@media #{$breakpoint-not-small} {\n .cover-ns { background-size: cover!important; }\n .contain-ns { background-size: contain!important; }\n}\n\n@media #{$breakpoint-medium} {\n .cover-m { background-size: cover!important; }\n .contain-m { background-size: contain!important; }\n}\n\n@media #{$breakpoint-large} {\n .cover-l { background-size: cover!important; }\n .contain-l { background-size: contain!important; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BACKGROUND POSITION\n\n Base:\n bg = background\n\n Modifiers:\n -center = center center\n -top = top center\n -right = center right\n -bottom = bottom center\n -left = center left\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n */\n\n.bg-center { \n background-repeat: no-repeat;\n background-position: center center; \n}\n\n.bg-top { \n background-repeat: no-repeat; \n background-position: top center; \n}\n\n.bg-right { \n background-repeat: no-repeat; \n background-position: center right; \n}\n\n.bg-bottom { \n background-repeat: no-repeat; \n background-position: bottom center; \n}\n\n.bg-left { \n background-repeat: no-repeat; \n background-position: center left; \n}\n\n@media #{$breakpoint-not-small} {\n .bg-center-ns { \n background-repeat: no-repeat;\n background-position: center center; \n }\n\n .bg-top-ns { \n background-repeat: no-repeat; \n background-position: top center; \n }\n\n .bg-right-ns { \n background-repeat: no-repeat; \n background-position: center right; \n }\n\n .bg-bottom-ns { \n background-repeat: no-repeat; \n background-position: bottom center; \n }\n\n .bg-left-ns { \n background-repeat: no-repeat; \n background-position: center left; \n }\n}\n\n@media #{$breakpoint-medium} {\n .bg-center-m { \n background-repeat: no-repeat;\n background-position: center center; \n }\n\n .bg-top-m { \n background-repeat: no-repeat; \n background-position: top center; \n }\n\n .bg-right-m { \n background-repeat: no-repeat; \n background-position: center right; \n }\n\n .bg-bottom-m { \n background-repeat: no-repeat; \n background-position: bottom center; \n }\n\n .bg-left-m { \n background-repeat: no-repeat; \n background-position: center left; \n }\n}\n\n@media #{$breakpoint-large} {\n .bg-center-l { \n background-repeat: no-repeat;\n background-position: center center; \n }\n\n .bg-top-l { \n background-repeat: no-repeat; \n background-position: top center; \n }\n\n .bg-right-l { \n background-repeat: no-repeat; \n background-position: center right; \n }\n\n .bg-bottom-l { \n background-repeat: no-repeat; \n background-position: bottom center; \n }\n\n .bg-left-l { \n background-repeat: no-repeat; \n background-position: center left; \n }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n OUTLINES\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.outline { outline: 1px solid; }\n.outline-transparent { outline: 1px solid transparent; }\n.outline-0 { outline: 0; }\n\n@media #{$breakpoint-not-small} {\n .outline-ns { outline: 1px solid; }\n .outline-transparent-ns { outline: 1px solid transparent; }\n .outline-0-ns { outline: 0; }\n}\n\n@media #{$breakpoint-medium} {\n .outline-m { outline: 1px solid; }\n .outline-transparent-m { outline: 1px solid transparent; }\n .outline-0-m { outline: 0; }\n}\n\n@media #{$breakpoint-large} {\n .outline-l { outline: 1px solid; }\n .outline-transparent-l { outline: 1px solid transparent; }\n .outline-0-l { outline: 0; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BORDERS\n Docs: http://tachyons.io/docs/themes/borders/\n\n Base:\n b = border\n\n Modifiers:\n a = all\n t = top\n r = right\n b = bottom\n l = left\n n = none\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n .ba { border-style: solid; border-width: 1px; }\n .bt { border-top-style: solid; border-top-width: 1px; }\n .br { border-right-style: solid; border-right-width: 1px; }\n .bb { border-bottom-style: solid; border-bottom-width: 1px; }\n .bl { border-left-style: solid; border-left-width: 1px; }\n .bn { border-style: none; border-width: 0; }\n\n\n@media #{$breakpoint-not-small} {\n .ba-ns { border-style: solid; border-width: 1px; }\n .bt-ns { border-top-style: solid; border-top-width: 1px; }\n .br-ns { border-right-style: solid; border-right-width: 1px; }\n .bb-ns { border-bottom-style: solid; border-bottom-width: 1px; }\n .bl-ns { border-left-style: solid; border-left-width: 1px; }\n .bn-ns { border-style: none; border-width: 0; }\n}\n\n@media #{$breakpoint-medium} {\n .ba-m { border-style: solid; border-width: 1px; }\n .bt-m { border-top-style: solid; border-top-width: 1px; }\n .br-m { border-right-style: solid; border-right-width: 1px; }\n .bb-m { border-bottom-style: solid; border-bottom-width: 1px; }\n .bl-m { border-left-style: solid; border-left-width: 1px; }\n .bn-m { border-style: none; border-width: 0; }\n}\n\n@media #{$breakpoint-large} {\n .ba-l { border-style: solid; border-width: 1px; }\n .bt-l { border-top-style: solid; border-top-width: 1px; }\n .br-l { border-right-style: solid; border-right-width: 1px; }\n .bb-l { border-bottom-style: solid; border-bottom-width: 1px; }\n .bl-l { border-left-style: solid; border-left-width: 1px; }\n .bn-l { border-style: none; border-width: 0; }\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BORDER COLORS\n Docs: http://tachyons.io/docs/themes/borders/\n\n Border colors can be used to extend the base\n border classes ba,bt,bb,br,bl found in the _borders.css file.\n\n The base border class by default will set the color of the border\n to that of the current text color. These classes are for the cases\n where you desire for the text and border colors to be different.\n\n Base:\n b = border\n\n Modifiers:\n --color-name = each color variable name is also a border color name\n\n*/\n\n.b--black { border-color: $black; }\n.b--near-black { border-color: $near-black; }\n.b--dark-gray { border-color: $dark-gray; }\n.b--mid-gray { border-color: $mid-gray; }\n.b--gray { border-color: $gray; }\n.b--silver { border-color: $silver; }\n.b--light-silver { border-color: $light-silver; }\n.b--moon-gray { border-color: $moon-gray; }\n.b--light-gray { border-color: $light-gray; }\n.b--near-white { border-color: $near-white; }\n.b--white { border-color: $white; }\n\n.b--white-90 { border-color: $white-90; }\n.b--white-80 { border-color: $white-80; }\n.b--white-70 { border-color: $white-70; }\n.b--white-60 { border-color: $white-60; }\n.b--white-50 { border-color: $white-50; }\n.b--white-40 { border-color: $white-40; }\n.b--white-30 { border-color: $white-30; }\n.b--white-20 { border-color: $white-20; }\n.b--white-10 { border-color: $white-10; }\n.b--white-05 { border-color: $white-05; }\n.b--white-025 { border-color: $white-025; }\n.b--white-0125 { border-color: $white-0125; }\n\n.b--black-90 { border-color: $black-90; }\n.b--black-80 { border-color: $black-80; }\n.b--black-70 { border-color: $black-70; }\n.b--black-60 { border-color: $black-60; }\n.b--black-50 { border-color: $black-50; }\n.b--black-40 { border-color: $black-40; }\n.b--black-30 { border-color: $black-30; }\n.b--black-20 { border-color: $black-20; }\n.b--black-10 { border-color: $black-10; }\n.b--black-05 { border-color: $black-05; }\n.b--black-025 { border-color: $black-025; }\n.b--black-0125 { border-color: $black-0125; }\n\n.b--dark-red { border-color: $dark-red; }\n.b--red { border-color: $red; }\n.b--light-red { border-color: $light-red; }\n.b--orange { border-color: $orange; }\n.b--gold { border-color: $gold; }\n.b--yellow { border-color: $yellow; }\n.b--light-yellow { border-color: $light-yellow; }\n.b--purple { border-color: $purple; }\n.b--light-purple { border-color: $light-purple; }\n.b--dark-pink { border-color: $dark-pink; }\n.b--hot-pink { border-color: $hot-pink; }\n.b--pink { border-color: $pink; }\n.b--light-pink { border-color: $light-pink; }\n.b--dark-green { border-color: $dark-green; }\n.b--green { border-color: $green; }\n.b--light-green { border-color: $light-green; }\n.b--navy { border-color: $navy; }\n.b--dark-blue { border-color: $dark-blue; }\n.b--blue { border-color: $blue; }\n.b--light-blue { border-color: $light-blue; }\n.b--lightest-blue { border-color: $lightest-blue; }\n.b--washed-blue { border-color: $washed-blue; }\n.b--washed-green { border-color: $washed-green; }\n.b--washed-yellow { border-color: $washed-yellow; }\n.b--washed-red { border-color: $washed-red; }\n\n.b--transparent { border-color: $transparent; }\n.b--inherit { border-color: inherit; }\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BORDER RADIUS\n Docs: http://tachyons.io/docs/themes/border-radius/\n\n Base:\n br = border-radius\n\n Modifiers:\n 0 = 0/none\n 1 = 1st step in scale\n 2 = 2nd step in scale\n 3 = 3rd step in scale\n 4 = 4th step in scale\n\n Literal values:\n -100 = 100%\n -pill = 9999px\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n .br0 { border-radius: $border-radius-none }\n .br1 { border-radius: $border-radius-1; }\n .br2 { border-radius: $border-radius-2; }\n .br3 { border-radius: $border-radius-3; }\n .br4 { border-radius: $border-radius-4; }\n .br-100 { border-radius: $border-radius-circle; }\n .br-pill { border-radius: $border-radius-pill; }\n .br--bottom {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n .br--top {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n }\n .br--right {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .br--left {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n@media #{$breakpoint-not-small} {\n .br0-ns { border-radius: $border-radius-none }\n .br1-ns { border-radius: $border-radius-1; }\n .br2-ns { border-radius: $border-radius-2; }\n .br3-ns { border-radius: $border-radius-3; }\n .br4-ns { border-radius: $border-radius-4; }\n .br-100-ns { border-radius: $border-radius-circle; }\n .br-pill-ns { border-radius: $border-radius-pill; }\n .br--bottom-ns {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n .br--top-ns {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n }\n .br--right-ns {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .br--left-ns {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n}\n\n@media #{$breakpoint-medium} {\n .br0-m { border-radius: $border-radius-none }\n .br1-m { border-radius: $border-radius-1; }\n .br2-m { border-radius: $border-radius-2; }\n .br3-m { border-radius: $border-radius-3; }\n .br4-m { border-radius: $border-radius-4; }\n .br-100-m { border-radius: $border-radius-circle; }\n .br-pill-m { border-radius: $border-radius-pill; }\n .br--bottom-m {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n .br--top-m {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n }\n .br--right-m {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .br--left-m {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n}\n\n@media #{$breakpoint-large} {\n .br0-l { border-radius: $border-radius-none }\n .br1-l { border-radius: $border-radius-1; }\n .br2-l { border-radius: $border-radius-2; }\n .br3-l { border-radius: $border-radius-3; }\n .br4-l { border-radius: $border-radius-4; }\n .br-100-l { border-radius: $border-radius-circle; }\n .br-pill-l { border-radius: $border-radius-pill; }\n .br--bottom-l {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n .br--top-l {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n }\n .br--right-l {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .br--left-l {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BORDER STYLES\n Docs: http://tachyons.io/docs/themes/borders/\n\n Depends on base border module in _borders.css\n\n Base:\n b = border-style\n\n Modifiers:\n --none = none\n --dotted = dotted\n --dashed = dashed\n --solid = solid\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n */\n\n.b--dotted { border-style: dotted; }\n.b--dashed { border-style: dashed; }\n.b--solid { border-style: solid; }\n.b--none { border-style: none; }\n\n@media #{$breakpoint-not-small} {\n .b--dotted-ns { border-style: dotted; }\n .b--dashed-ns { border-style: dashed; }\n .b--solid-ns { border-style: solid; }\n .b--none-ns { border-style: none; }\n}\n\n@media #{$breakpoint-medium} {\n .b--dotted-m { border-style: dotted; }\n .b--dashed-m { border-style: dashed; }\n .b--solid-m { border-style: solid; }\n .b--none-m { border-style: none; }\n}\n\n@media #{$breakpoint-large} {\n .b--dotted-l { border-style: dotted; }\n .b--dashed-l { border-style: dashed; }\n .b--solid-l { border-style: solid; }\n .b--none-l { border-style: none; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BORDER WIDTHS\n Docs: http://tachyons.io/docs/themes/borders/\n\n Base:\n bw = border-width\n\n Modifiers:\n 0 = 0 width border\n 1 = 1st step in border-width scale\n 2 = 2nd step in border-width scale\n 3 = 3rd step in border-width scale\n 4 = 4th step in border-width scale\n 5 = 5th step in border-width scale\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.bw0 { border-width: $border-width-none; }\n.bw1 { border-width: $border-width-1; }\n.bw2 { border-width: $border-width-2; }\n.bw3 { border-width: $border-width-3; }\n.bw4 { border-width: $border-width-4; }\n.bw5 { border-width: $border-width-5; }\n\n/* Resets */\n.bt-0 { border-top-width: $border-width-none }\n.br-0 { border-right-width: $border-width-none }\n.bb-0 { border-bottom-width: $border-width-none }\n.bl-0 { border-left-width: $border-width-none }\n\n@media #{$breakpoint-not-small} {\n .bw0-ns { border-width: $border-width-none; }\n .bw1-ns { border-width: $border-width-1; }\n .bw2-ns { border-width: $border-width-2; }\n .bw3-ns { border-width: $border-width-3; }\n .bw4-ns { border-width: $border-width-4; }\n .bw5-ns { border-width: $border-width-5; }\n .bt-0-ns { border-top-width: $border-width-none }\n .br-0-ns { border-right-width: $border-width-none }\n .bb-0-ns { border-bottom-width: $border-width-none }\n .bl-0-ns { border-left-width: $border-width-none }\n}\n\n@media #{$breakpoint-medium} {\n .bw0-m { border-width: $border-width-none; }\n .bw1-m { border-width: $border-width-1; }\n .bw2-m { border-width: $border-width-2; }\n .bw3-m { border-width: $border-width-3; }\n .bw4-m { border-width: $border-width-4; }\n .bw5-m { border-width: $border-width-5; }\n .bt-0-m { border-top-width: $border-width-none }\n .br-0-m { border-right-width: $border-width-none }\n .bb-0-m { border-bottom-width: $border-width-none }\n .bl-0-m { border-left-width: $border-width-none }\n}\n\n@media #{$breakpoint-large} {\n .bw0-l { border-width: $border-width-none; }\n .bw1-l { border-width: $border-width-1; }\n .bw2-l { border-width: $border-width-2; }\n .bw3-l { border-width: $border-width-3; }\n .bw4-l { border-width: $border-width-4; }\n .bw5-l { border-width: $border-width-5; }\n .bt-0-l { border-top-width: $border-width-none }\n .br-0-l { border-right-width: $border-width-none }\n .bb-0-l { border-bottom-width: $border-width-none }\n .bl-0-l { border-left-width: $border-width-none }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n BOX-SHADOW\n Docs: http://tachyons.io/docs/themes/box-shadow/\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n */\n\n.shadow-1 { box-shadow: $box-shadow-1; }\n.shadow-2 { box-shadow: $box-shadow-2; }\n.shadow-3 { box-shadow: $box-shadow-3; }\n.shadow-4 { box-shadow: $box-shadow-4; }\n.shadow-5 { box-shadow: $box-shadow-5; }\n\n@media #{$breakpoint-not-small} {\n .shadow-1-ns { box-shadow: $box-shadow-1; }\n .shadow-2-ns { box-shadow: $box-shadow-2; }\n .shadow-3-ns { box-shadow: $box-shadow-3; }\n .shadow-4-ns { box-shadow: $box-shadow-4; }\n .shadow-5-ns { box-shadow: $box-shadow-5; }\n}\n\n@media #{$breakpoint-medium} {\n .shadow-1-m { box-shadow: $box-shadow-1; }\n .shadow-2-m { box-shadow: $box-shadow-2; }\n .shadow-3-m { box-shadow: $box-shadow-3; }\n .shadow-4-m { box-shadow: $box-shadow-4; }\n .shadow-5-m { box-shadow: $box-shadow-5; }\n}\n\n@media #{$breakpoint-large} {\n .shadow-1-l { box-shadow: $box-shadow-1; }\n .shadow-2-l { box-shadow: $box-shadow-2; }\n .shadow-3-l { box-shadow: $box-shadow-3; }\n .shadow-4-l { box-shadow: $box-shadow-4; }\n .shadow-5-l { box-shadow: $box-shadow-5; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n CODE\n\n*/\n\n.pre {\n overflow-x: auto;\n overflow-y: hidden;\n overflow: scroll;\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n COORDINATES\n Docs: http://tachyons.io/docs/layout/position/\n\n Use in combination with the position module.\n\n Base:\n top\n bottom\n right\n left\n\n Modifiers:\n -0 = literal value 0\n -1 = literal value 1\n -2 = literal value 2\n --1 = literal value -1\n --2 = literal value -2\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.top-0 { top: 0; }\n.right-0 { right: 0; }\n.bottom-0 { bottom: 0; }\n.left-0 { left: 0; }\n\n.top-1 { top: 1rem; }\n.right-1 { right: 1rem; }\n.bottom-1 { bottom: 1rem; }\n.left-1 { left: 1rem; }\n\n.top-2 { top: 2rem; }\n.right-2 { right: 2rem; }\n.bottom-2 { bottom: 2rem; }\n.left-2 { left: 2rem; }\n\n.top--1 { top: -1rem; }\n.right--1 { right: -1rem; }\n.bottom--1 { bottom: -1rem; }\n.left--1 { left: -1rem; }\n\n.top--2 { top: -2rem; }\n.right--2 { right: -2rem; }\n.bottom--2 { bottom: -2rem; }\n.left--2 { left: -2rem; }\n\n\n.absolute--fill {\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n}\n\n@media #{$breakpoint-not-small} {\n .top-0-ns { top: 0; }\n .left-0-ns { left: 0; }\n .right-0-ns { right: 0; }\n .bottom-0-ns { bottom: 0; }\n .top-1-ns { top: 1rem; }\n .left-1-ns { left: 1rem; }\n .right-1-ns { right: 1rem; }\n .bottom-1-ns { bottom: 1rem; }\n .top-2-ns { top: 2rem; }\n .left-2-ns { left: 2rem; }\n .right-2-ns { right: 2rem; }\n .bottom-2-ns { bottom: 2rem; }\n .top--1-ns { top: -1rem; }\n .right--1-ns { right: -1rem; }\n .bottom--1-ns { bottom: -1rem; }\n .left--1-ns { left: -1rem; }\n .top--2-ns { top: -2rem; }\n .right--2-ns { right: -2rem; }\n .bottom--2-ns { bottom: -2rem; }\n .left--2-ns { left: -2rem; }\n .absolute--fill-ns {\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n }\n}\n\n@media #{$breakpoint-medium} {\n .top-0-m { top: 0; }\n .left-0-m { left: 0; }\n .right-0-m { right: 0; }\n .bottom-0-m { bottom: 0; }\n .top-1-m { top: 1rem; }\n .left-1-m { left: 1rem; }\n .right-1-m { right: 1rem; }\n .bottom-1-m { bottom: 1rem; }\n .top-2-m { top: 2rem; }\n .left-2-m { left: 2rem; }\n .right-2-m { right: 2rem; }\n .bottom-2-m { bottom: 2rem; }\n .top--1-m { top: -1rem; }\n .right--1-m { right: -1rem; }\n .bottom--1-m { bottom: -1rem; }\n .left--1-m { left: -1rem; }\n .top--2-m { top: -2rem; }\n .right--2-m { right: -2rem; }\n .bottom--2-m { bottom: -2rem; }\n .left--2-m { left: -2rem; }\n .absolute--fill-m {\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n }\n}\n\n@media #{$breakpoint-large} {\n .top-0-l { top: 0; }\n .left-0-l { left: 0; }\n .right-0-l { right: 0; }\n .bottom-0-l { bottom: 0; }\n .top-1-l { top: 1rem; }\n .left-1-l { left: 1rem; }\n .right-1-l { right: 1rem; }\n .bottom-1-l { bottom: 1rem; }\n .top-2-l { top: 2rem; }\n .left-2-l { left: 2rem; }\n .right-2-l { right: 2rem; }\n .bottom-2-l { bottom: 2rem; }\n .top--1-l { top: -1rem; }\n .right--1-l { right: -1rem; }\n .bottom--1-l { bottom: -1rem; }\n .left--1-l { left: -1rem; }\n .top--2-l { top: -2rem; }\n .right--2-l { right: -2rem; }\n .bottom--2-l { bottom: -2rem; }\n .left--2-l { left: -2rem; }\n .absolute--fill-l {\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n CLEARFIX\n http://tachyons.io/docs/layout/clearfix/\n\n*/\n\n/* Nicolas Gallaghers Clearfix solution\n Ref: http://nicolasgallagher.com/micro-clearfix-hack/ */\n\n.cf:before,\n.cf:after { content: \" \"; display: table; }\n.cf:after { clear: both; }\n.cf { *zoom: 1; }\n\n.cl { clear: left; }\n.cr { clear: right; }\n.cb { clear: both; }\n.cn { clear: none; }\n\n@media #{$breakpoint-not-small} {\n .cl-ns { clear: left; }\n .cr-ns { clear: right; }\n .cb-ns { clear: both; }\n .cn-ns { clear: none; }\n}\n\n@media #{$breakpoint-medium} {\n .cl-m { clear: left; }\n .cr-m { clear: right; }\n .cb-m { clear: both; }\n .cn-m { clear: none; }\n}\n\n@media #{$breakpoint-large} {\n .cl-l { clear: left; }\n .cr-l { clear: right; }\n .cb-l { clear: both; }\n .cn-l { clear: none; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n FLEXBOX\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.flex { display: flex; }\n.inline-flex { display: inline-flex; }\n\n/* 1. Fix for Chrome 44 bug.\n * https://code.google.com/p/chromium/issues/detail?id=506893 */\n.flex-auto {\n flex: 1 1 auto;\n min-width: 0; /* 1 */\n min-height: 0; /* 1 */\n}\n\n.flex-none { flex: none; }\n\n.flex-column { flex-direction: column; }\n.flex-row { flex-direction: row; }\n.flex-wrap { flex-wrap: wrap; }\n.flex-nowrap { flex-wrap: nowrap; }\n.flex-wrap-reverse { flex-wrap: wrap-reverse; }\n.flex-column-reverse { flex-direction: column-reverse; }\n.flex-row-reverse { flex-direction: row-reverse; }\n\n.items-start { align-items: flex-start; }\n.items-end { align-items: flex-end; }\n.items-center { align-items: center; }\n.items-baseline { align-items: baseline; }\n.items-stretch { align-items: stretch; }\n\n.self-start { align-self: flex-start; }\n.self-end { align-self: flex-end; }\n.self-center { align-self: center; }\n.self-baseline { align-self: baseline; }\n.self-stretch { align-self: stretch; }\n\n.justify-start { justify-content: flex-start; }\n.justify-end { justify-content: flex-end; }\n.justify-center { justify-content: center; }\n.justify-between { justify-content: space-between; }\n.justify-around { justify-content: space-around; }\n\n.content-start { align-content: flex-start; }\n.content-end { align-content: flex-end; }\n.content-center { align-content: center; }\n.content-between { align-content: space-between; }\n.content-around { align-content: space-around; }\n.content-stretch { align-content: stretch; }\n\n.order-0 { order: 0; }\n.order-1 { order: 1; }\n.order-2 { order: 2; }\n.order-3 { order: 3; }\n.order-4 { order: 4; }\n.order-5 { order: 5; }\n.order-6 { order: 6; }\n.order-7 { order: 7; }\n.order-8 { order: 8; }\n.order-last { order: 99999; }\n\n.flex-grow-0 { flex-grow: 0; }\n.flex-grow-1 { flex-grow: 1; }\n\n.flex-shrink-0 { flex-shrink: 0; }\n.flex-shrink-1 { flex-shrink: 1; }\n\n@media #{$breakpoint-not-small} {\n .flex-ns { display: flex; }\n .inline-flex-ns { display: inline-flex; }\n .flex-auto-ns {\n flex: 1 1 auto;\n min-width: 0; /* 1 */\n min-height: 0; /* 1 */\n }\n .flex-none-ns { flex: none; }\n .flex-column-ns { flex-direction: column; }\n .flex-row-ns { flex-direction: row; }\n .flex-wrap-ns { flex-wrap: wrap; }\n .flex-nowrap-ns { flex-wrap: nowrap; }\n .flex-wrap-reverse-ns { flex-wrap: wrap-reverse; }\n .flex-column-reverse-ns { flex-direction: column-reverse; }\n .flex-row-reverse-ns { flex-direction: row-reverse; }\n .items-start-ns { align-items: flex-start; }\n .items-end-ns { align-items: flex-end; }\n .items-center-ns { align-items: center; }\n .items-baseline-ns { align-items: baseline; }\n .items-stretch-ns { align-items: stretch; }\n\n .self-start-ns { align-self: flex-start; }\n .self-end-ns { align-self: flex-end; }\n .self-center-ns { align-self: center; }\n .self-baseline-ns { align-self: baseline; }\n .self-stretch-ns { align-self: stretch; }\n\n .justify-start-ns { justify-content: flex-start; }\n .justify-end-ns { justify-content: flex-end; }\n .justify-center-ns { justify-content: center; }\n .justify-between-ns { justify-content: space-between; }\n .justify-around-ns { justify-content: space-around; }\n\n .content-start-ns { align-content: flex-start; }\n .content-end-ns { align-content: flex-end; }\n .content-center-ns { align-content: center; }\n .content-between-ns { align-content: space-between; }\n .content-around-ns { align-content: space-around; }\n .content-stretch-ns { align-content: stretch; }\n\n .order-0-ns { order: 0; }\n .order-1-ns { order: 1; }\n .order-2-ns { order: 2; }\n .order-3-ns { order: 3; }\n .order-4-ns { order: 4; }\n .order-5-ns { order: 5; }\n .order-6-ns { order: 6; }\n .order-7-ns { order: 7; }\n .order-8-ns { order: 8; }\n .order-last-ns { order: 99999; }\n\n .flex-grow-0-ns { flex-grow: 0; }\n .flex-grow-1-ns { flex-grow: 1; }\n\n .flex-shrink-0-ns { flex-shrink: 0; }\n .flex-shrink-1-ns { flex-shrink: 1; }\n}\n@media #{$breakpoint-medium} {\n .flex-m { display: flex; }\n .inline-flex-m { display: inline-flex; }\n .flex-auto-m {\n flex: 1 1 auto;\n min-width: 0; /* 1 */\n min-height: 0; /* 1 */\n }\n .flex-none-m { flex: none; }\n .flex-column-m { flex-direction: column; }\n .flex-row-m { flex-direction: row; }\n .flex-wrap-m { flex-wrap: wrap; }\n .flex-nowrap-m { flex-wrap: nowrap; }\n .flex-wrap-reverse-m { flex-wrap: wrap-reverse; }\n .flex-column-reverse-m { flex-direction: column-reverse; }\n .flex-row-reverse-m { flex-direction: row-reverse; }\n .items-start-m { align-items: flex-start; }\n .items-end-m { align-items: flex-end; }\n .items-center-m { align-items: center; }\n .items-baseline-m { align-items: baseline; }\n .items-stretch-m { align-items: stretch; }\n\n .self-start-m { align-self: flex-start; }\n .self-end-m { align-self: flex-end; }\n .self-center-m { align-self: center; }\n .self-baseline-m { align-self: baseline; }\n .self-stretch-m { align-self: stretch; }\n\n .justify-start-m { justify-content: flex-start; }\n .justify-end-m { justify-content: flex-end; }\n .justify-center-m { justify-content: center; }\n .justify-between-m { justify-content: space-between; }\n .justify-around-m { justify-content: space-around; }\n\n .content-start-m { align-content: flex-start; }\n .content-end-m { align-content: flex-end; }\n .content-center-m { align-content: center; }\n .content-between-m { align-content: space-between; }\n .content-around-m { align-content: space-around; }\n .content-stretch-m { align-content: stretch; }\n\n .order-0-m { order: 0; }\n .order-1-m { order: 1; }\n .order-2-m { order: 2; }\n .order-3-m { order: 3; }\n .order-4-m { order: 4; }\n .order-5-m { order: 5; }\n .order-6-m { order: 6; }\n .order-7-m { order: 7; }\n .order-8-m { order: 8; }\n .order-last-m { order: 99999; }\n\n .flex-grow-0-m { flex-grow: 0; }\n .flex-grow-1-m { flex-grow: 1; }\n\n .flex-shrink-0-m { flex-shrink: 0; }\n .flex-shrink-1-m { flex-shrink: 1; }\n}\n\n@media #{$breakpoint-large} {\n .flex-l { display: flex; }\n .inline-flex-l { display: inline-flex; }\n .flex-auto-l {\n flex: 1 1 auto;\n min-width: 0; /* 1 */\n min-height: 0; /* 1 */\n }\n .flex-none-l { flex: none; }\n .flex-column-l { flex-direction: column; }\n .flex-row-l { flex-direction: row; }\n .flex-wrap-l { flex-wrap: wrap; }\n .flex-nowrap-l { flex-wrap: nowrap; }\n .flex-wrap-reverse-l { flex-wrap: wrap-reverse; }\n .flex-column-reverse-l { flex-direction: column-reverse; }\n .flex-row-reverse-l { flex-direction: row-reverse; }\n\n .items-start-l { align-items: flex-start; }\n .items-end-l { align-items: flex-end; }\n .items-center-l { align-items: center; }\n .items-baseline-l { align-items: baseline; }\n .items-stretch-l { align-items: stretch; }\n\n .self-start-l { align-self: flex-start; }\n .self-end-l { align-self: flex-end; }\n .self-center-l { align-self: center; }\n .self-baseline-l { align-self: baseline; }\n .self-stretch-l { align-self: stretch; }\n\n .justify-start-l { justify-content: flex-start; }\n .justify-end-l { justify-content: flex-end; }\n .justify-center-l { justify-content: center; }\n .justify-between-l { justify-content: space-between; }\n .justify-around-l { justify-content: space-around; }\n\n .content-start-l { align-content: flex-start; }\n .content-end-l { align-content: flex-end; }\n .content-center-l { align-content: center; }\n .content-between-l { align-content: space-between; }\n .content-around-l { align-content: space-around; }\n .content-stretch-l { align-content: stretch; }\n\n .order-0-l { order: 0; }\n .order-1-l { order: 1; }\n .order-2-l { order: 2; }\n .order-3-l { order: 3; }\n .order-4-l { order: 4; }\n .order-5-l { order: 5; }\n .order-6-l { order: 6; }\n .order-7-l { order: 7; }\n .order-8-l { order: 8; }\n .order-last-l { order: 99999; }\n\n .flex-grow-0-l { flex-grow: 0; }\n .flex-grow-1-l { flex-grow: 1; }\n\n .flex-shrink-0-l { flex-shrink: 0; }\n .flex-shrink-1-l { flex-shrink: 1; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n DISPLAY\n Docs: http://tachyons.io/docs/layout/display\n\n Base:\n d = display\n\n Modifiers:\n n = none\n b = block\n ib = inline-block\n it = inline-table\n t = table\n tc = table-cell\n tr = table-row\n tcol = table-column\n tcolg = table-column-group\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.dn { display: none; }\n.di { display: inline; }\n.db { display: block; }\n.dib { display: inline-block; }\n.dit { display: inline-table; }\n.dt { display: table; }\n.dtc { display: table-cell; }\n.dt-row { display: table-row; }\n.dt-row-group { display: table-row-group; }\n.dt-column { display: table-column; }\n.dt-column-group { display: table-column-group; }\n\n/*\n This will set table to full width and then\n all cells will be equal width\n*/\n.dt--fixed {\n table-layout: fixed;\n width: 100%;\n}\n\n@media #{$breakpoint-not-small} {\n .dn-ns { display: none; }\n .di-ns { display: inline; }\n .db-ns { display: block; }\n .dib-ns { display: inline-block; }\n .dit-ns { display: inline-table; }\n .dt-ns { display: table; }\n .dtc-ns { display: table-cell; }\n .dt-row-ns { display: table-row; }\n .dt-row-group-ns { display: table-row-group; }\n .dt-column-ns { display: table-column; }\n .dt-column-group-ns { display: table-column-group; }\n\n .dt--fixed-ns {\n table-layout: fixed;\n width: 100%;\n }\n}\n\n@media #{$breakpoint-medium} {\n .dn-m { display: none; }\n .di-m { display: inline; }\n .db-m { display: block; }\n .dib-m { display: inline-block; }\n .dit-m { display: inline-table; }\n .dt-m { display: table; }\n .dtc-m { display: table-cell; }\n .dt-row-m { display: table-row; }\n .dt-row-group-m { display: table-row-group; }\n .dt-column-m { display: table-column; }\n .dt-column-group-m { display: table-column-group; }\n\n .dt--fixed-m {\n table-layout: fixed;\n width: 100%;\n }\n}\n\n@media #{$breakpoint-large} {\n .dn-l { display: none; }\n .di-l { display: inline; }\n .db-l { display: block; }\n .dib-l { display: inline-block; }\n .dit-l { display: inline-table; }\n .dt-l { display: table; }\n .dtc-l { display: table-cell; }\n .dt-row-l { display: table-row; }\n .dt-row-group-l { display: table-row-group; }\n .dt-column-l { display: table-column; }\n .dt-column-group-l { display: table-column-group; }\n\n .dt--fixed-l {\n table-layout: fixed;\n width: 100%;\n }\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n FLOATS\n http://tachyons.io/docs/layout/floats/\n\n 1. Floated elements are automatically rendered as block level elements.\n Setting floats to display inline will fix the double margin bug in\n ie6. You know... just in case.\n\n 2. Don't forget to clearfix your floats with .cf\n\n Base:\n f = float\n\n Modifiers:\n l = left\n r = right\n n = none\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n\n\n.fl { float: left; _display: inline; }\n.fr { float: right; _display: inline; }\n.fn { float: none; }\n\n@media #{$breakpoint-not-small} {\n .fl-ns { float: left; _display: inline; }\n .fr-ns { float: right; _display: inline; }\n .fn-ns { float: none; }\n}\n\n@media #{$breakpoint-medium} {\n .fl-m { float: left; _display: inline; }\n .fr-m { float: right; _display: inline; }\n .fn-m { float: none; }\n}\n\n@media #{$breakpoint-large} {\n .fl-l { float: left; _display: inline; }\n .fr-l { float: right; _display: inline; }\n .fn-l { float: none; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n FONT FAMILY GROUPS\n Docs: http://tachyons.io/docs/typography/font-family/\n\n*/\n\n\n.sans-serif {\n font-family: $sans-serif;\n}\n\n.serif {\n font-family: $serif;\n}\n\n.system-sans-serif {\n font-family: sans-serif;\n}\n\n.system-serif {\n font-family: serif;\n}\n\n\n/* Monospaced Typefaces (for code) */\n\n/* From http://cssfontstack.com */\ncode, .code {\n font-family: Consolas,\n monaco,\n monospace;\n}\n\n.courier {\n font-family: 'Courier Next',\n courier,\n monospace;\n}\n\n\n/* Sans-Serif Typefaces */\n\n.helvetica {\n font-family: 'helvetica neue', helvetica,\n sans-serif;\n}\n\n.avenir {\n font-family: 'avenir next', avenir,\n sans-serif;\n}\n\n\n/* Serif Typefaces */\n\n.athelas {\n font-family: athelas,\n georgia,\n serif;\n}\n\n.georgia {\n font-family: georgia,\n serif;\n}\n\n.times {\n font-family: times,\n serif;\n}\n\n.bodoni {\n font-family: \"Bodoni MT\",\n serif;\n}\n\n.calisto {\n font-family: \"Calisto MT\",\n serif;\n}\n\n.garamond {\n font-family: garamond,\n serif;\n}\n\n.baskerville {\n font-family: baskerville,\n serif;\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n FONT STYLE\n Docs: http://tachyons.io/docs/typography/font-style/\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.i { font-style: italic; }\n.fs-normal { font-style: normal; }\n\n@media #{$breakpoint-not-small} {\n .i-ns { font-style: italic; }\n .fs-normal-ns { font-style: normal; }\n}\n\n@media #{$breakpoint-medium} {\n .i-m { font-style: italic; }\n .fs-normal-m { font-style: normal; }\n}\n\n@media #{$breakpoint-large} {\n .i-l { font-style: italic; }\n .fs-normal-l { font-style: normal; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n FONT WEIGHT\n Docs: http://tachyons.io/docs/typography/font-weight/\n\n Base\n fw = font-weight\n\n Modifiers:\n 1 = literal value 100\n 2 = literal value 200\n 3 = literal value 300\n 4 = literal value 400\n 5 = literal value 500\n 6 = literal value 600\n 7 = literal value 700\n 8 = literal value 800\n 9 = literal value 900\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.normal { font-weight: normal; }\n.b { font-weight: bold; }\n.fw1 { font-weight: 100; }\n.fw2 { font-weight: 200; }\n.fw3 { font-weight: 300; }\n.fw4 { font-weight: 400; }\n.fw5 { font-weight: 500; }\n.fw6 { font-weight: 600; }\n.fw7 { font-weight: 700; }\n.fw8 { font-weight: 800; }\n.fw9 { font-weight: 900; }\n\n\n@media #{$breakpoint-not-small} {\n .normal-ns { font-weight: normal; }\n .b-ns { font-weight: bold; }\n .fw1-ns { font-weight: 100; }\n .fw2-ns { font-weight: 200; }\n .fw3-ns { font-weight: 300; }\n .fw4-ns { font-weight: 400; }\n .fw5-ns { font-weight: 500; }\n .fw6-ns { font-weight: 600; }\n .fw7-ns { font-weight: 700; }\n .fw8-ns { font-weight: 800; }\n .fw9-ns { font-weight: 900; }\n}\n\n@media #{$breakpoint-medium} {\n .normal-m { font-weight: normal; }\n .b-m { font-weight: bold; }\n .fw1-m { font-weight: 100; }\n .fw2-m { font-weight: 200; }\n .fw3-m { font-weight: 300; }\n .fw4-m { font-weight: 400; }\n .fw5-m { font-weight: 500; }\n .fw6-m { font-weight: 600; }\n .fw7-m { font-weight: 700; }\n .fw8-m { font-weight: 800; }\n .fw9-m { font-weight: 900; }\n}\n\n@media #{$breakpoint-large} {\n .normal-l { font-weight: normal; }\n .b-l { font-weight: bold; }\n .fw1-l { font-weight: 100; }\n .fw2-l { font-weight: 200; }\n .fw3-l { font-weight: 300; }\n .fw4-l { font-weight: 400; }\n .fw5-l { font-weight: 500; }\n .fw6-l { font-weight: 600; }\n .fw7-l { font-weight: 700; }\n .fw8-l { font-weight: 800; }\n .fw9-l { font-weight: 900; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n FORMS\n \n*/\n\n.input-reset {\n -webkit-appearance: none;\n -moz-appearance: none;\n}\n\n.button-reset::-moz-focus-inner,\n.input-reset::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n HEIGHTS\n Docs: http://tachyons.io/docs/layout/heights/\n\n Base:\n h = height\n min-h = min-height\n min-vh = min-height vertical screen height\n vh = vertical screen height\n\n Modifiers\n 1 = 1st step in height scale\n 2 = 2nd step in height scale\n 3 = 3rd step in height scale\n 4 = 4th step in height scale\n 5 = 5th step in height scale\n\n -25 = literal value 25%\n -50 = literal value 50%\n -75 = literal value 75%\n -100 = literal value 100%\n\n -auto = string value of auto\n -inherit = string value of inherit\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n/* Height Scale */\n\n.h1 { height: $height-1; }\n.h2 { height: $height-2; }\n.h3 { height: $height-3; }\n.h4 { height: $height-4; }\n.h5 { height: $height-5; }\n\n/* Height Percentages - Based off of height of parent */\n\n.h-25 { height: 25%; }\n.h-50 { height: 50%; }\n.h-75 { height: 75%; }\n.h-100 { height: 100%; }\n\n.min-h-100 { min-height: 100%; }\n\n/* Screen Height Percentage */\n\n.vh-25 { height: 25vh; }\n.vh-50 { height: 50vh; }\n.vh-75 { height: 75vh; }\n.vh-100 { height: 100vh; }\n\n.min-vh-100 { min-height: 100vh; }\n\n\n/* String Properties */\n\n.h-auto { height: auto; }\n.h-inherit { height: inherit; }\n\n@media #{$breakpoint-not-small} {\n .h1-ns { height: $height-1; }\n .h2-ns { height: $height-2; }\n .h3-ns { height: $height-3; }\n .h4-ns { height: $height-4; }\n .h5-ns { height: $height-5; }\n .h-25-ns { height: 25%; }\n .h-50-ns { height: 50%; }\n .h-75-ns { height: 75%; }\n .h-100-ns { height: 100%; }\n .min-h-100-ns { min-height: 100%; }\n .vh-25-ns { height: 25vh; }\n .vh-50-ns { height: 50vh; }\n .vh-75-ns { height: 75vh; }\n .vh-100-ns { height: 100vh; }\n .min-vh-100-ns { min-height: 100vh; }\n .h-auto-ns { height: auto; }\n .h-inherit-ns { height: inherit; }\n}\n\n@media #{$breakpoint-medium} {\n .h1-m { height: $height-1; }\n .h2-m { height: $height-2; }\n .h3-m { height: $height-3; }\n .h4-m { height: $height-4; }\n .h5-m { height: $height-5; }\n .h-25-m { height: 25%; }\n .h-50-m { height: 50%; }\n .h-75-m { height: 75%; }\n .h-100-m { height: 100%; }\n .min-h-100-m { min-height: 100%; }\n .vh-25-m { height: 25vh; }\n .vh-50-m { height: 50vh; }\n .vh-75-m { height: 75vh; }\n .vh-100-m { height: 100vh; }\n .min-vh-100-m { min-height: 100vh; }\n .h-auto-m { height: auto; }\n .h-inherit-m { height: inherit; }\n}\n\n@media #{$breakpoint-large} {\n .h1-l { height: $height-1; }\n .h2-l { height: $height-2; }\n .h3-l { height: $height-3; }\n .h4-l { height: $height-4; }\n .h5-l { height: $height-5; }\n .h-25-l { height: 25%; }\n .h-50-l { height: 50%; }\n .h-75-l { height: 75%; }\n .h-100-l { height: 100%; }\n .min-h-100-l { min-height: 100%; }\n .vh-25-l { height: 25vh; }\n .vh-50-l { height: 50vh; }\n .vh-75-l { height: 75vh; }\n .vh-100-l { height: 100vh; }\n .min-vh-100-l { min-height: 100vh; }\n .h-auto-l { height: auto; }\n .h-inherit-l { height: inherit; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n LETTER SPACING\n Docs: http://tachyons.io/docs/typography/tracking/\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.tracked { letter-spacing: $letter-spacing-1; }\n.tracked-tight { letter-spacing: $letter-spacing-tight; }\n.tracked-mega { letter-spacing: $letter-spacing-2; }\n\n@media #{$breakpoint-not-small} {\n .tracked-ns { letter-spacing: $letter-spacing-1; }\n .tracked-tight-ns { letter-spacing: $letter-spacing-tight; }\n .tracked-mega-ns { letter-spacing: $letter-spacing-2; }\n}\n\n@media #{$breakpoint-medium} {\n .tracked-m { letter-spacing: $letter-spacing-1; }\n .tracked-tight-m { letter-spacing: $letter-spacing-tight; }\n .tracked-mega-m { letter-spacing: $letter-spacing-2; }\n}\n\n@media #{$breakpoint-large} {\n .tracked-l { letter-spacing: $letter-spacing-1; }\n .tracked-tight-l { letter-spacing: $letter-spacing-tight; }\n .tracked-mega-l { letter-spacing: $letter-spacing-2; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n LINE HEIGHT / LEADING\n Docs: http://tachyons.io/docs/typography/line-height\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n .lh-solid { line-height: $line-height-solid; }\n .lh-title { line-height: $line-height-title; }\n .lh-copy { line-height: $line-height-copy; }\n\n@media #{$breakpoint-not-small} {\n .lh-solid-ns { line-height: $line-height-solid; }\n .lh-title-ns { line-height: $line-height-title; }\n .lh-copy-ns { line-height: $line-height-copy; }\n}\n\n@media #{$breakpoint-medium} {\n .lh-solid-m { line-height: $line-height-solid; }\n .lh-title-m { line-height: $line-height-title; }\n .lh-copy-m { line-height: $line-height-copy; }\n}\n\n@media #{$breakpoint-large} {\n .lh-solid-l { line-height: $line-height-solid; }\n .lh-title-l { line-height: $line-height-title; }\n .lh-copy-l { line-height: $line-height-copy; }\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n LINKS\n Docs: http://tachyons.io/docs/elements/links/\n\n*/\n\n.link {\n text-decoration: none;\n transition: color .15s ease-in;\n}\n\n.link:link,\n.link:visited {\n transition: color .15s ease-in;\n}\n.link:hover {\n transition: color .15s ease-in;\n}\n.link:active {\n transition: color .15s ease-in;\n}\n.link:focus {\n transition: color .15s ease-in;\n outline: 1px dotted currentColor;\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n LISTS\n http://tachyons.io/docs/elements/lists/\n\n*/\n\n.list { list-style-type: none; }\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n MAX WIDTHS\n Docs: http://tachyons.io/docs/layout/max-widths/\n\n Base:\n mw = max-width\n\n Modifiers\n 1 = 1st step in width scale\n 2 = 2nd step in width scale\n 3 = 3rd step in width scale\n 4 = 4th step in width scale\n 5 = 5th step in width scale\n 6 = 6st step in width scale\n 7 = 7nd step in width scale\n 8 = 8rd step in width scale\n 9 = 9th step in width scale\n\n -100 = literal value 100%\n\n -none = string value none\n\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n/* Max Width Percentages */\n\n.mw-100 { max-width: 100%; }\n\n/* Max Width Scale */\n\n.mw1 { max-width: $max-width-1; }\n.mw2 { max-width: $max-width-2; }\n.mw3 { max-width: $max-width-3; }\n.mw4 { max-width: $max-width-4; }\n.mw5 { max-width: $max-width-5; }\n.mw6 { max-width: $max-width-6; }\n.mw7 { max-width: $max-width-7; }\n.mw8 { max-width: $max-width-8; }\n.mw9 { max-width: $max-width-9; }\n\n/* Max Width String Properties */\n\n.mw-none { max-width: none; }\n\n@media #{$breakpoint-not-small} {\n .mw-100-ns { max-width: 100%; }\n\n .mw1-ns { max-width: $max-width-1; }\n .mw2-ns { max-width: $max-width-2; }\n .mw3-ns { max-width: $max-width-3; }\n .mw4-ns { max-width: $max-width-4; }\n .mw5-ns { max-width: $max-width-5; }\n .mw6-ns { max-width: $max-width-6; }\n .mw7-ns { max-width: $max-width-7; }\n .mw8-ns { max-width: $max-width-8; }\n .mw9-ns { max-width: $max-width-9; }\n\n .mw-none-ns { max-width: none; }\n}\n\n@media #{$breakpoint-medium} {\n .mw-100-m { max-width: 100%; }\n\n .mw1-m { max-width: $max-width-1; }\n .mw2-m { max-width: $max-width-2; }\n .mw3-m { max-width: $max-width-3; }\n .mw4-m { max-width: $max-width-4; }\n .mw5-m { max-width: $max-width-5; }\n .mw6-m { max-width: $max-width-6; }\n .mw7-m { max-width: $max-width-7; }\n .mw8-m { max-width: $max-width-8; }\n .mw9-m { max-width: $max-width-9; }\n\n .mw-none-m { max-width: none; }\n}\n\n@media #{$breakpoint-large} {\n .mw-100-l { max-width: 100%; }\n\n .mw1-l { max-width: $max-width-1; }\n .mw2-l { max-width: $max-width-2; }\n .mw3-l { max-width: $max-width-3; }\n .mw4-l { max-width: $max-width-4; }\n .mw5-l { max-width: $max-width-5; }\n .mw6-l { max-width: $max-width-6; }\n .mw7-l { max-width: $max-width-7; }\n .mw8-l { max-width: $max-width-8; }\n .mw9-l { max-width: $max-width-9; }\n\n .mw-none-l { max-width: none; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n WIDTHS\n Docs: http://tachyons.io/docs/layout/widths/\n\n Base:\n w = width\n\n Modifiers\n 1 = 1st step in width scale\n 2 = 2nd step in width scale\n 3 = 3rd step in width scale\n 4 = 4th step in width scale\n 5 = 5th step in width scale\n\n -10 = literal value 10%\n -20 = literal value 20%\n -25 = literal value 25%\n -30 = literal value 30%\n -33 = literal value 33%\n -34 = literal value 34%\n -40 = literal value 40%\n -50 = literal value 50%\n -60 = literal value 60%\n -70 = literal value 70%\n -75 = literal value 75%\n -80 = literal value 80%\n -90 = literal value 90%\n -100 = literal value 100%\n\n -third = 100% / 3 (Not supported in opera mini or IE8)\n -two-thirds = 100% / 1.5 (Not supported in opera mini or IE8)\n -auto = string value auto\n\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n */\n\n/* Width Scale */\n\n.w1 { width: $width-1; }\n.w2 { width: $width-2; }\n.w3 { width: $width-3; }\n.w4 { width: $width-4; }\n.w5 { width: $width-5; }\n\n.w-10 { width: 10%; }\n.w-20 { width: 20%; }\n.w-25 { width: 25%; }\n.w-30 { width: 30%; }\n.w-33 { width: 33%; }\n.w-34 { width: 34%; }\n.w-40 { width: 40%; }\n.w-50 { width: 50%; }\n.w-60 { width: 60%; }\n.w-70 { width: 70%; }\n.w-75 { width: 75%; }\n.w-80 { width: 80%; }\n.w-90 { width: 90%; }\n.w-100 { width: 100%; }\n\n.w-third { width: (100% / 3); }\n.w-two-thirds { width: (100% / 1.5); }\n.w-auto { width: auto; }\n\n@media #{$breakpoint-not-small} {\n .w1-ns { width: $width-1; }\n .w2-ns { width: $width-2; }\n .w3-ns { width: $width-3; }\n .w4-ns { width: $width-4; }\n .w5-ns { width: $width-5; }\n .w-10-ns { width: 10%; }\n .w-20-ns { width: 20%; }\n .w-25-ns { width: 25%; }\n .w-30-ns { width: 30%; }\n .w-33-ns { width: 33%; }\n .w-34-ns { width: 34%; }\n .w-40-ns { width: 40%; }\n .w-50-ns { width: 50%; }\n .w-60-ns { width: 60%; }\n .w-70-ns { width: 70%; }\n .w-75-ns { width: 75%; }\n .w-80-ns { width: 80%; }\n .w-90-ns { width: 90%; }\n .w-100-ns { width: 100%; }\n .w-third-ns { width: (100% / 3); }\n .w-two-thirds-ns { width: (100% / 1.5); }\n .w-auto-ns { width: auto; }\n}\n\n@media #{$breakpoint-medium} {\n .w1-m { width: $width-1; }\n .w2-m { width: $width-2; }\n .w3-m { width: $width-3; }\n .w4-m { width: $width-4; }\n .w5-m { width: $width-5; }\n .w-10-m { width: 10%; }\n .w-20-m { width: 20%; }\n .w-25-m { width: 25%; }\n .w-30-m { width: 30%; }\n .w-33-m { width: 33%; }\n .w-34-m { width: 34%; }\n .w-40-m { width: 40%; }\n .w-50-m { width: 50%; }\n .w-60-m { width: 60%; }\n .w-70-m { width: 70%; }\n .w-75-m { width: 75%; }\n .w-80-m { width: 80%; }\n .w-90-m { width: 90%; }\n .w-100-m { width: 100%; }\n .w-third-m { width: (100% / 3); }\n .w-two-thirds-m { width: (100% / 1.5); }\n .w-auto-m { width: auto; }\n}\n\n@media #{$breakpoint-large} {\n .w1-l { width: $width-1; }\n .w2-l { width: $width-2; }\n .w3-l { width: $width-3; }\n .w4-l { width: $width-4; }\n .w5-l { width: $width-5; }\n .w-10-l { width: 10%; }\n .w-20-l { width: 20%; }\n .w-25-l { width: 25%; }\n .w-30-l { width: 30%; }\n .w-33-l { width: 33%; }\n .w-34-l { width: 34%; }\n .w-40-l { width: 40%; }\n .w-50-l { width: 50%; }\n .w-60-l { width: 60%; }\n .w-70-l { width: 70%; }\n .w-75-l { width: 75%; }\n .w-80-l { width: 80%; }\n .w-90-l { width: 90%; }\n .w-100-l { width: 100%; }\n .w-third-l { width: (100% / 3); }\n .w-two-thirds-l { width: (100% / 1.5); }\n .w-auto-l { width: auto; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n OVERFLOW\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n */\n\n.overflow-visible { overflow: visible; }\n.overflow-hidden { overflow: hidden; }\n.overflow-scroll { overflow: scroll; }\n.overflow-auto { overflow: auto; }\n\n.overflow-x-visible { overflow-x: visible; }\n.overflow-x-hidden { overflow-x: hidden; }\n.overflow-x-scroll { overflow-x: scroll; }\n.overflow-x-auto { overflow-x: auto; }\n\n.overflow-y-visible { overflow-y: visible; }\n.overflow-y-hidden { overflow-y: hidden; }\n.overflow-y-scroll { overflow-y: scroll; }\n.overflow-y-auto { overflow-y: auto; }\n\n@media #{$breakpoint-not-small} {\n .overflow-visible-ns { overflow: visible; }\n .overflow-hidden-ns { overflow: hidden; }\n .overflow-scroll-ns { overflow: scroll; }\n .overflow-auto-ns { overflow: auto; }\n .overflow-x-visible-ns { overflow-x: visible; }\n .overflow-x-hidden-ns { overflow-x: hidden; }\n .overflow-x-scroll-ns { overflow-x: scroll; }\n .overflow-x-auto-ns { overflow-x: auto; }\n\n .overflow-y-visible-ns { overflow-y: visible; }\n .overflow-y-hidden-ns { overflow-y: hidden; }\n .overflow-y-scroll-ns { overflow-y: scroll; }\n .overflow-y-auto-ns { overflow-y: auto; }\n}\n\n@media #{$breakpoint-medium} {\n .overflow-visible-m { overflow: visible; }\n .overflow-hidden-m { overflow: hidden; }\n .overflow-scroll-m { overflow: scroll; }\n .overflow-auto-m { overflow: auto; }\n\n .overflow-x-visible-m { overflow-x: visible; }\n .overflow-x-hidden-m { overflow-x: hidden; }\n .overflow-x-scroll-m { overflow-x: scroll; }\n .overflow-x-auto-m { overflow-x: auto; }\n\n .overflow-y-visible-m { overflow-y: visible; }\n .overflow-y-hidden-m { overflow-y: hidden; }\n .overflow-y-scroll-m { overflow-y: scroll; }\n .overflow-y-auto-m { overflow-y: auto; }\n}\n\n@media #{$breakpoint-large} {\n .overflow-visible-l { overflow: visible; }\n .overflow-hidden-l { overflow: hidden; }\n .overflow-scroll-l { overflow: scroll; }\n .overflow-auto-l { overflow: auto; }\n\n .overflow-x-visible-l { overflow-x: visible; }\n .overflow-x-hidden-l { overflow-x: hidden; }\n .overflow-x-scroll-l { overflow-x: scroll; }\n .overflow-x-auto-l { overflow-x: auto; }\n\n .overflow-y-visible-l { overflow-y: visible; }\n .overflow-y-hidden-l { overflow-y: hidden; }\n .overflow-y-scroll-l { overflow-y: scroll; }\n .overflow-y-auto-l { overflow-y: auto; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n POSITIONING\n Docs: http://tachyons.io/docs/layout/position/\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.static { position: static; }\n.relative { position: relative; }\n.absolute { position: absolute; }\n.fixed { position: fixed; }\n\n@media #{$breakpoint-not-small} {\n .static-ns { position: static; }\n .relative-ns { position: relative; }\n .absolute-ns { position: absolute; }\n .fixed-ns { position: fixed; }\n}\n\n@media #{$breakpoint-medium} {\n .static-m { position: static; }\n .relative-m { position: relative; }\n .absolute-m { position: absolute; }\n .fixed-m { position: fixed; }\n}\n\n@media #{$breakpoint-large} {\n .static-l { position: static; }\n .relative-l { position: relative; }\n .absolute-l { position: absolute; }\n .fixed-l { position: fixed; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n OPACITY\n Docs: http://tachyons.io/docs/themes/opacity/\n\n*/\n\n.o-100 { opacity: 1; }\n.o-90 { opacity: .9; }\n.o-80 { opacity: .8; }\n.o-70 { opacity: .7; }\n.o-60 { opacity: .6; }\n.o-50 { opacity: .5; }\n.o-40 { opacity: .4; }\n.o-30 { opacity: .3; }\n.o-20 { opacity: .2; }\n.o-10 { opacity: .1; }\n.o-05 { opacity: .05; }\n.o-025 { opacity: .025; }\n.o-0 { opacity: 0; }\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n ROTATIONS\n\n*/\n\n.rotate-45 { transform: rotate(45deg); }\n.rotate-90 { transform: rotate(90deg); }\n.rotate-135 { transform: rotate(135deg); }\n.rotate-180 { transform: rotate(180deg); }\n.rotate-225 { transform: rotate(225deg); }\n.rotate-270 { transform: rotate(270deg); }\n.rotate-315 { transform: rotate(315deg); }\n\n@media #{$breakpoint-not-small}{\n .rotate-45-ns { transform: rotate(45deg); }\n .rotate-90-ns { transform: rotate(90deg); }\n .rotate-135-ns { transform: rotate(135deg); }\n .rotate-180-ns { transform: rotate(180deg); }\n .rotate-225-ns { transform: rotate(225deg); }\n .rotate-270-ns { transform: rotate(270deg); }\n .rotate-315-ns { transform: rotate(315deg); }\n}\n\n@media #{$breakpoint-medium}{\n .rotate-45-m { transform: rotate(45deg); }\n .rotate-90-m { transform: rotate(90deg); }\n .rotate-135-m { transform: rotate(135deg); }\n .rotate-180-m { transform: rotate(180deg); }\n .rotate-225-m { transform: rotate(225deg); }\n .rotate-270-m { transform: rotate(270deg); }\n .rotate-315-m { transform: rotate(315deg); }\n}\n\n@media #{$breakpoint-large}{\n .rotate-45-l { transform: rotate(45deg); }\n .rotate-90-l { transform: rotate(90deg); }\n .rotate-135-l { transform: rotate(135deg); }\n .rotate-180-l { transform: rotate(180deg); }\n .rotate-225-l { transform: rotate(225deg); }\n .rotate-270-l { transform: rotate(270deg); }\n .rotate-315-l { transform: rotate(315deg); }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n SKINS\n Docs: http://tachyons.io/docs/themes/skins/\n\n Classes for setting foreground and background colors on elements.\n If you haven't declared a border color, but set border on an element, it will\n be set to the current text color.\n\n*/\n\n/* Text colors */\n\n.black-90 { color: $black-90; }\n.black-80 { color: $black-80; }\n.black-70 { color: $black-70; }\n.black-60 { color: $black-60; }\n.black-50 { color: $black-50; }\n.black-40 { color: $black-40; }\n.black-30 { color: $black-30; }\n.black-20 { color: $black-20; }\n.black-10 { color: $black-10; }\n.black-05 { color: $black-05; }\n\n.white-90 { color: $white-90; }\n.white-80 { color: $white-80; }\n.white-70 { color: $white-70; }\n.white-60 { color: $white-60; }\n.white-50 { color: $white-50; }\n.white-40 { color: $white-40; }\n.white-30 { color: $white-30; }\n.white-20 { color: $white-20; }\n.white-10 { color: $white-10; }\n\n.black { color: $black; }\n.near-black { color: $near-black; }\n.dark-gray { color: $dark-gray; }\n.mid-gray { color: $mid-gray; }\n.gray { color: $gray; }\n.silver { color: $silver; }\n.light-silver { color: $light-silver; }\n.moon-gray { color: $moon-gray; }\n.light-gray { color: $light-gray; }\n.near-white { color: $near-white; }\n.white { color: $white; }\n\n.dark-red { color: $dark-red; }\n.red { color: $red; }\n.light-red { color: $light-red; }\n.orange { color: $orange; }\n.gold { color: $gold; }\n.yellow { color: $yellow; }\n.light-yellow { color: $light-yellow; }\n.purple { color: $purple; }\n.light-purple { color: $light-purple; }\n.dark-pink { color: $dark-pink; }\n.hot-pink { color: $hot-pink; }\n.pink { color: $pink; }\n.light-pink { color: $light-pink; }\n.dark-green { color: $dark-green; }\n.green { color: $green; }\n.light-green { color: $light-green; }\n.navy { color: $navy; }\n.dark-blue { color: $dark-blue; }\n.blue { color: $blue; }\n.light-blue { color: $light-blue; }\n.lightest-blue { color: $lightest-blue; }\n.washed-blue { color: $washed-blue; }\n.washed-green { color: $washed-green; }\n.washed-yellow { color: $washed-yellow; }\n.washed-red { color: $washed-red; }\n.color-inherit { color: inherit; }\n\n.bg-black-90 { background-color: $black-90; }\n.bg-black-80 { background-color: $black-80; }\n.bg-black-70 { background-color: $black-70; }\n.bg-black-60 { background-color: $black-60; }\n.bg-black-50 { background-color: $black-50; }\n.bg-black-40 { background-color: $black-40; }\n.bg-black-30 { background-color: $black-30; }\n.bg-black-20 { background-color: $black-20; }\n.bg-black-10 { background-color: $black-10; }\n.bg-black-05 { background-color: $black-05; }\n.bg-white-90 { background-color: $white-90; }\n.bg-white-80 { background-color: $white-80; }\n.bg-white-70 { background-color: $white-70; }\n.bg-white-60 { background-color: $white-60; }\n.bg-white-50 { background-color: $white-50; }\n.bg-white-40 { background-color: $white-40; }\n.bg-white-30 { background-color: $white-30; }\n.bg-white-20 { background-color: $white-20; }\n.bg-white-10 { background-color: $white-10; }\n\n\n\n/* Background colors */\n\n.bg-black { background-color: $black; }\n.bg-near-black { background-color: $near-black; }\n.bg-dark-gray { background-color: $dark-gray; }\n.bg-mid-gray { background-color: $mid-gray; }\n.bg-gray { background-color: $gray; }\n.bg-silver { background-color: $silver; }\n.bg-light-silver { background-color: $light-silver; }\n.bg-moon-gray { background-color: $moon-gray; }\n.bg-light-gray { background-color: $light-gray; }\n.bg-near-white { background-color: $near-white; }\n.bg-white { background-color: $white; }\n.bg-transparent { background-color: $transparent; }\n\n.bg-dark-red { background-color: $dark-red; }\n.bg-red { background-color: $red; }\n.bg-light-red { background-color: $light-red; }\n.bg-orange { background-color: $orange; }\n.bg-gold { background-color: $gold; }\n.bg-yellow { background-color: $yellow; }\n.bg-light-yellow { background-color: $light-yellow; }\n.bg-purple { background-color: $purple; }\n.bg-light-purple { background-color: $light-purple; }\n.bg-dark-pink { background-color: $dark-pink; }\n.bg-hot-pink { background-color: $hot-pink; }\n.bg-pink { background-color: $pink; }\n.bg-light-pink { background-color: $light-pink; }\n.bg-dark-green { background-color: $dark-green; }\n.bg-green { background-color: $green; }\n.bg-light-green { background-color: $light-green; }\n.bg-navy { background-color: $navy; }\n.bg-dark-blue { background-color: $dark-blue; }\n.bg-blue { background-color: $blue; }\n.bg-light-blue { background-color: $light-blue; }\n.bg-lightest-blue { background-color: $lightest-blue; }\n.bg-washed-blue { background-color: $washed-blue; }\n.bg-washed-green { background-color: $washed-green; }\n.bg-washed-yellow { background-color: $washed-yellow; }\n.bg-washed-red { background-color: $washed-red; }\n.bg-inherit { background-color: inherit; }\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n SKINS:PSEUDO\n\n Customize the color of an element when\n it is focused or hovered over.\n\n */\n\n.hover-black:hover,\n.hover-black:focus { color: $black; }\n.hover-near-black:hover,\n.hover-near-black:focus { color: $near-black; }\n.hover-dark-gray:hover,\n.hover-dark-gray:focus { color: $dark-gray; }\n.hover-mid-gray:hover,\n.hover-mid-gray:focus { color: $mid-gray; }\n.hover-gray:hover,\n.hover-gray:focus { color: $gray; }\n.hover-silver:hover,\n.hover-silver:focus { color: $silver; }\n.hover-light-silver:hover,\n.hover-light-silver:focus { color: $light-silver; }\n.hover-moon-gray:hover,\n.hover-moon-gray:focus { color: $moon-gray; }\n.hover-light-gray:hover,\n.hover-light-gray:focus { color: $light-gray; }\n.hover-near-white:hover,\n.hover-near-white:focus { color: $near-white; }\n.hover-white:hover,\n.hover-white:focus { color: $white; }\n\n.hover-black-90:hover,\n.hover-black-90:focus { color: $black-90; }\n.hover-black-80:hover,\n.hover-black-80:focus { color: $black-80; }\n.hover-black-70:hover,\n.hover-black-70:focus { color: $black-70; }\n.hover-black-60:hover,\n.hover-black-60:focus { color: $black-60; }\n.hover-black-50:hover,\n.hover-black-50:focus { color: $black-50; }\n.hover-black-40:hover,\n.hover-black-40:focus { color: $black-40; }\n.hover-black-30:hover,\n.hover-black-30:focus { color: $black-30; }\n.hover-black-20:hover,\n.hover-black-20:focus { color: $black-20; }\n.hover-black-10:hover,\n.hover-black-10:focus { color: $black-10; }\n.hover-white-90:hover,\n.hover-white-90:focus { color: $white-90; }\n.hover-white-80:hover,\n.hover-white-80:focus { color: $white-80; }\n.hover-white-70:hover,\n.hover-white-70:focus { color: $white-70; }\n.hover-white-60:hover,\n.hover-white-60:focus { color: $white-60; }\n.hover-white-50:hover,\n.hover-white-50:focus { color: $white-50; }\n.hover-white-40:hover,\n.hover-white-40:focus { color: $white-40; }\n.hover-white-30:hover,\n.hover-white-30:focus { color: $white-30; }\n.hover-white-20:hover,\n.hover-white-20:focus { color: $white-20; }\n.hover-white-10:hover,\n.hover-white-10:focus { color: $white-10; }\n.hover-inherit:hover,\n.hover-inherit:focus { color: inherit; }\n\n.hover-bg-black:hover,\n.hover-bg-black:focus { background-color: $black; }\n.hover-bg-near-black:hover,\n.hover-bg-near-black:focus { background-color: $near-black; }\n.hover-bg-dark-gray:hover,\n.hover-bg-dark-gray:focus { background-color: $dark-gray; }\n.hover-bg-mid-gray:hover,\n.hover-bg-mid-gray:focus { background-color: $mid-gray; }\n.hover-bg-gray:hover,\n.hover-bg-gray:focus { background-color: $gray; }\n.hover-bg-silver:hover,\n.hover-bg-silver:focus { background-color: $silver; }\n.hover-bg-light-silver:hover,\n.hover-bg-light-silver:focus { background-color: $light-silver; }\n.hover-bg-moon-gray:hover,\n.hover-bg-moon-gray:focus { background-color: $moon-gray; }\n.hover-bg-light-gray:hover,\n.hover-bg-light-gray:focus { background-color: $light-gray; }\n.hover-bg-near-white:hover,\n.hover-bg-near-white:focus { background-color: $near-white; }\n.hover-bg-white:hover,\n.hover-bg-white:focus { background-color: $white; }\n.hover-bg-transparent:hover,\n.hover-bg-transparent:focus { background-color: $transparent; }\n\n.hover-bg-black-90:hover,\n.hover-bg-black-90:focus { background-color: $black-90; }\n.hover-bg-black-80:hover,\n.hover-bg-black-80:focus { background-color: $black-80; }\n.hover-bg-black-70:hover,\n.hover-bg-black-70:focus { background-color: $black-70; }\n.hover-bg-black-60:hover,\n.hover-bg-black-60:focus { background-color: $black-60; }\n.hover-bg-black-50:hover,\n.hover-bg-black-50:focus { background-color: $black-50; }\n.hover-bg-black-40:hover,\n.hover-bg-black-40:focus { background-color: $black-40; }\n.hover-bg-black-30:hover,\n.hover-bg-black-30:focus { background-color: $black-30; }\n.hover-bg-black-20:hover,\n.hover-bg-black-20:focus { background-color: $black-20; }\n.hover-bg-black-10:hover,\n.hover-bg-black-10:focus { background-color: $black-10; }\n.hover-bg-white-90:hover,\n.hover-bg-white-90:focus { background-color: $white-90; }\n.hover-bg-white-80:hover,\n.hover-bg-white-80:focus { background-color: $white-80; }\n.hover-bg-white-70:hover,\n.hover-bg-white-70:focus { background-color: $white-70; }\n.hover-bg-white-60:hover,\n.hover-bg-white-60:focus { background-color: $white-60; }\n.hover-bg-white-50:hover,\n.hover-bg-white-50:focus { background-color: $white-50; }\n.hover-bg-white-40:hover,\n.hover-bg-white-40:focus { background-color: $white-40; }\n.hover-bg-white-30:hover,\n.hover-bg-white-30:focus { background-color: $white-30; }\n.hover-bg-white-20:hover,\n.hover-bg-white-20:focus { background-color: $white-20; }\n.hover-bg-white-10:hover,\n.hover-bg-white-10:focus { background-color: $white-10; }\n\n.hover-dark-red:hover,\n.hover-dark-red:focus { color: $dark-red; }\n.hover-red:hover,\n.hover-red:focus { color: $red; }\n.hover-light-red:hover,\n.hover-light-red:focus { color: $light-red; }\n.hover-orange:hover,\n.hover-orange:focus { color: $orange; }\n.hover-gold:hover,\n.hover-gold:focus { color: $gold; }\n.hover-yellow:hover,\n.hover-yellow:focus { color: $yellow; }\n.hover-light-yellow:hover,\n.hover-light-yellow:focus { color: $light-yellow; }\n.hover-purple:hover,\n.hover-purple:focus { color: $purple; }\n.hover-light-purple:hover,\n.hover-light-purple:focus { color: $light-purple; }\n.hover-dark-pink:hover,\n.hover-dark-pink:focus { color: $dark-pink; }\n.hover-hot-pink:hover,\n.hover-hot-pink:focus { color: $hot-pink; }\n.hover-pink:hover,\n.hover-pink:focus { color: $pink; }\n.hover-light-pink:hover,\n.hover-light-pink:focus { color: $light-pink; }\n.hover-dark-green:hover,\n.hover-dark-green:focus { color: $dark-green; }\n.hover-green:hover,\n.hover-green:focus { color: $green; }\n.hover-light-green:hover,\n.hover-light-green:focus { color: $light-green; }\n.hover-navy:hover,\n.hover-navy:focus { color: $navy; }\n.hover-dark-blue:hover,\n.hover-dark-blue:focus { color: $dark-blue; }\n.hover-blue:hover,\n.hover-blue:focus { color: $blue; }\n.hover-light-blue:hover,\n.hover-light-blue:focus { color: $light-blue; }\n.hover-lightest-blue:hover,\n.hover-lightest-blue:focus { color: $lightest-blue; }\n.hover-washed-blue:hover,\n.hover-washed-blue:focus { color: $washed-blue; }\n.hover-washed-green:hover,\n.hover-washed-green:focus { color: $washed-green; }\n.hover-washed-yellow:hover,\n.hover-washed-yellow:focus { color: $washed-yellow; }\n.hover-washed-red:hover,\n.hover-washed-red:focus { color: $washed-red; }\n\n.hover-bg-dark-red:hover,\n.hover-bg-dark-red:focus { background-color: $dark-red; }\n.hover-bg-red:hover,\n.hover-bg-red:focus { background-color: $red; }\n.hover-bg-light-red:hover,\n.hover-bg-light-red:focus { background-color: $light-red; }\n.hover-bg-orange:hover,\n.hover-bg-orange:focus { background-color: $orange; }\n.hover-bg-gold:hover,\n.hover-bg-gold:focus { background-color: $gold; }\n.hover-bg-yellow:hover,\n.hover-bg-yellow:focus { background-color: $yellow; }\n.hover-bg-light-yellow:hover,\n.hover-bg-light-yellow:focus { background-color: $light-yellow; }\n.hover-bg-purple:hover,\n.hover-bg-purple:focus { background-color: $purple; }\n.hover-bg-light-purple:hover,\n.hover-bg-light-purple:focus { background-color: $light-purple; }\n.hover-bg-dark-pink:hover,\n.hover-bg-dark-pink:focus { background-color: $dark-pink; }\n.hover-bg-hot-pink:hover,\n.hover-bg-hot-pink:focus { background-color: $hot-pink; }\n.hover-bg-pink:hover,\n.hover-bg-pink:focus { background-color: $pink; }\n.hover-bg-light-pink:hover,\n.hover-bg-light-pink:focus { background-color: $light-pink; }\n.hover-bg-dark-green:hover,\n.hover-bg-dark-green:focus { background-color: $dark-green; }\n.hover-bg-green:hover,\n.hover-bg-green:focus { background-color: $green; }\n.hover-bg-light-green:hover,\n.hover-bg-light-green:focus { background-color: $light-green; }\n.hover-bg-navy:hover,\n.hover-bg-navy:focus { background-color: $navy; }\n.hover-bg-dark-blue:hover,\n.hover-bg-dark-blue:focus { background-color: $dark-blue; }\n.hover-bg-blue:hover,\n.hover-bg-blue:focus { background-color: $blue; }\n.hover-bg-light-blue:hover,\n.hover-bg-light-blue:focus { background-color: $light-blue; }\n.hover-bg-lightest-blue:hover,\n.hover-bg-lightest-blue:focus { background-color: $lightest-blue; }\n.hover-bg-washed-blue:hover,\n.hover-bg-washed-blue:focus { background-color: $washed-blue; }\n.hover-bg-washed-green:hover,\n.hover-bg-washed-green:focus { background-color: $washed-green; }\n.hover-bg-washed-yellow:hover,\n.hover-bg-washed-yellow:focus { background-color: $washed-yellow; }\n.hover-bg-washed-red:hover,\n.hover-bg-washed-red:focus { background-color: $washed-red; }\n.hover-bg-inherit:hover,\n.hover-bg-inherit:focus { background-color: inherit; }\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/* Variables */\n\n/*\n SPACING\n Docs: http://tachyons.io/docs/layout/spacing/\n\n An eight step powers of two scale ranging from 0 to 16rem.\n\n Base:\n p = padding\n m = margin\n\n Modifiers:\n a = all\n h = horizontal\n v = vertical\n t = top\n r = right\n b = bottom\n l = left\n\n 0 = none\n 1 = 1st step in spacing scale\n 2 = 2nd step in spacing scale\n 3 = 3rd step in spacing scale\n 4 = 4th step in spacing scale\n 5 = 5th step in spacing scale\n 6 = 6th step in spacing scale\n 7 = 7th step in spacing scale\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n\n.pa0 { padding: $spacing-none; }\n.pa1 { padding: $spacing-extra-small; }\n.pa2 { padding: $spacing-small; }\n.pa3 { padding: $spacing-medium; }\n.pa4 { padding: $spacing-large; }\n.pa5 { padding: $spacing-extra-large; }\n.pa6 { padding: $spacing-extra-extra-large; }\n.pa7 { padding: $spacing-extra-extra-extra-large; }\n\n.pl0 { padding-left: $spacing-none; }\n.pl1 { padding-left: $spacing-extra-small; }\n.pl2 { padding-left: $spacing-small; }\n.pl3 { padding-left: $spacing-medium; }\n.pl4 { padding-left: $spacing-large; }\n.pl5 { padding-left: $spacing-extra-large; }\n.pl6 { padding-left: $spacing-extra-extra-large; }\n.pl7 { padding-left: $spacing-extra-extra-extra-large; }\n\n.pr0 { padding-right: $spacing-none; }\n.pr1 { padding-right: $spacing-extra-small; }\n.pr2 { padding-right: $spacing-small; }\n.pr3 { padding-right: $spacing-medium; }\n.pr4 { padding-right: $spacing-large; }\n.pr5 { padding-right: $spacing-extra-large; }\n.pr6 { padding-right: $spacing-extra-extra-large; }\n.pr7 { padding-right: $spacing-extra-extra-extra-large; }\n\n.pb0 { padding-bottom: $spacing-none; }\n.pb1 { padding-bottom: $spacing-extra-small; }\n.pb2 { padding-bottom: $spacing-small; }\n.pb3 { padding-bottom: $spacing-medium; }\n.pb4 { padding-bottom: $spacing-large; }\n.pb5 { padding-bottom: $spacing-extra-large; }\n.pb6 { padding-bottom: $spacing-extra-extra-large; }\n.pb7 { padding-bottom: $spacing-extra-extra-extra-large; }\n\n.pt0 { padding-top: $spacing-none; }\n.pt1 { padding-top: $spacing-extra-small; }\n.pt2 { padding-top: $spacing-small; }\n.pt3 { padding-top: $spacing-medium; }\n.pt4 { padding-top: $spacing-large; }\n.pt5 { padding-top: $spacing-extra-large; }\n.pt6 { padding-top: $spacing-extra-extra-large; }\n.pt7 { padding-top: $spacing-extra-extra-extra-large; }\n\n.pv0 {\n padding-top: $spacing-none;\n padding-bottom: $spacing-none;\n}\n.pv1 {\n padding-top: $spacing-extra-small;\n padding-bottom: $spacing-extra-small;\n}\n.pv2 {\n padding-top: $spacing-small;\n padding-bottom: $spacing-small;\n}\n.pv3 {\n padding-top: $spacing-medium;\n padding-bottom: $spacing-medium;\n}\n.pv4 {\n padding-top: $spacing-large;\n padding-bottom: $spacing-large;\n}\n.pv5 {\n padding-top: $spacing-extra-large;\n padding-bottom: $spacing-extra-large;\n}\n.pv6 {\n padding-top: $spacing-extra-extra-large;\n padding-bottom: $spacing-extra-extra-large;\n}\n\n.pv7 {\n padding-top: $spacing-extra-extra-extra-large;\n padding-bottom: $spacing-extra-extra-extra-large;\n}\n\n.ph0 {\n padding-left: $spacing-none;\n padding-right: $spacing-none;\n}\n\n.ph1 {\n padding-left: $spacing-extra-small;\n padding-right: $spacing-extra-small;\n}\n\n.ph2 {\n padding-left: $spacing-small;\n padding-right: $spacing-small;\n}\n\n.ph3 {\n padding-left: $spacing-medium;\n padding-right: $spacing-medium;\n}\n\n.ph4 {\n padding-left: $spacing-large;\n padding-right: $spacing-large;\n}\n\n.ph5 {\n padding-left: $spacing-extra-large;\n padding-right: $spacing-extra-large;\n}\n\n.ph6 {\n padding-left: $spacing-extra-extra-large;\n padding-right: $spacing-extra-extra-large;\n}\n\n.ph7 {\n padding-left: $spacing-extra-extra-extra-large;\n padding-right: $spacing-extra-extra-extra-large;\n}\n\n.ma0 { margin: $spacing-none; }\n.ma1 { margin: $spacing-extra-small; }\n.ma2 { margin: $spacing-small; }\n.ma3 { margin: $spacing-medium; }\n.ma4 { margin: $spacing-large; }\n.ma5 { margin: $spacing-extra-large; }\n.ma6 { margin: $spacing-extra-extra-large; }\n.ma7 { margin: $spacing-extra-extra-extra-large; }\n\n.ml0 { margin-left: $spacing-none; }\n.ml1 { margin-left: $spacing-extra-small; }\n.ml2 { margin-left: $spacing-small; }\n.ml3 { margin-left: $spacing-medium; }\n.ml4 { margin-left: $spacing-large; }\n.ml5 { margin-left: $spacing-extra-large; }\n.ml6 { margin-left: $spacing-extra-extra-large; }\n.ml7 { margin-left: $spacing-extra-extra-extra-large; }\n\n.mr0 { margin-right: $spacing-none; }\n.mr1 { margin-right: $spacing-extra-small; }\n.mr2 { margin-right: $spacing-small; }\n.mr3 { margin-right: $spacing-medium; }\n.mr4 { margin-right: $spacing-large; }\n.mr5 { margin-right: $spacing-extra-large; }\n.mr6 { margin-right: $spacing-extra-extra-large; }\n.mr7 { margin-right: $spacing-extra-extra-extra-large; }\n\n.mb0 { margin-bottom: $spacing-none; }\n.mb1 { margin-bottom: $spacing-extra-small; }\n.mb2 { margin-bottom: $spacing-small; }\n.mb3 { margin-bottom: $spacing-medium; }\n.mb4 { margin-bottom: $spacing-large; }\n.mb5 { margin-bottom: $spacing-extra-large; }\n.mb6 { margin-bottom: $spacing-extra-extra-large; }\n.mb7 { margin-bottom: $spacing-extra-extra-extra-large; }\n\n.mt0 { margin-top: $spacing-none; }\n.mt1 { margin-top: $spacing-extra-small; }\n.mt2 { margin-top: $spacing-small; }\n.mt3 { margin-top: $spacing-medium; }\n.mt4 { margin-top: $spacing-large; }\n.mt5 { margin-top: $spacing-extra-large; }\n.mt6 { margin-top: $spacing-extra-extra-large; }\n.mt7 { margin-top: $spacing-extra-extra-extra-large; }\n\n.mv0 {\n margin-top: $spacing-none;\n margin-bottom: $spacing-none;\n}\n.mv1 {\n margin-top: $spacing-extra-small;\n margin-bottom: $spacing-extra-small;\n}\n.mv2 {\n margin-top: $spacing-small;\n margin-bottom: $spacing-small;\n}\n.mv3 {\n margin-top: $spacing-medium;\n margin-bottom: $spacing-medium;\n}\n.mv4 {\n margin-top: $spacing-large;\n margin-bottom: $spacing-large;\n}\n.mv5 {\n margin-top: $spacing-extra-large;\n margin-bottom: $spacing-extra-large;\n}\n.mv6 {\n margin-top: $spacing-extra-extra-large;\n margin-bottom: $spacing-extra-extra-large;\n}\n.mv7 {\n margin-top: $spacing-extra-extra-extra-large;\n margin-bottom: $spacing-extra-extra-extra-large;\n}\n\n.mh0 {\n margin-left: $spacing-none;\n margin-right: $spacing-none;\n}\n.mh1 {\n margin-left: $spacing-extra-small;\n margin-right: $spacing-extra-small;\n}\n.mh2 {\n margin-left: $spacing-small;\n margin-right: $spacing-small;\n}\n.mh3 {\n margin-left: $spacing-medium;\n margin-right: $spacing-medium;\n}\n.mh4 {\n margin-left: $spacing-large;\n margin-right: $spacing-large;\n}\n.mh5 {\n margin-left: $spacing-extra-large;\n margin-right: $spacing-extra-large;\n}\n.mh6 {\n margin-left: $spacing-extra-extra-large;\n margin-right: $spacing-extra-extra-large;\n}\n.mh7 {\n margin-left: $spacing-extra-extra-extra-large;\n margin-right: $spacing-extra-extra-extra-large;\n}\n\n@media #{$breakpoint-not-small} {\n .pa0-ns { padding: $spacing-none; }\n .pa1-ns { padding: $spacing-extra-small; }\n .pa2-ns { padding: $spacing-small; }\n .pa3-ns { padding: $spacing-medium; }\n .pa4-ns { padding: $spacing-large; }\n .pa5-ns { padding: $spacing-extra-large; }\n .pa6-ns { padding: $spacing-extra-extra-large; }\n .pa7-ns { padding: $spacing-extra-extra-extra-large; }\n\n .pl0-ns { padding-left: $spacing-none; }\n .pl1-ns { padding-left: $spacing-extra-small; }\n .pl2-ns { padding-left: $spacing-small; }\n .pl3-ns { padding-left: $spacing-medium; }\n .pl4-ns { padding-left: $spacing-large; }\n .pl5-ns { padding-left: $spacing-extra-large; }\n .pl6-ns { padding-left: $spacing-extra-extra-large; }\n .pl7-ns { padding-left: $spacing-extra-extra-extra-large; }\n\n .pr0-ns { padding-right: $spacing-none; }\n .pr1-ns { padding-right: $spacing-extra-small; }\n .pr2-ns { padding-right: $spacing-small; }\n .pr3-ns { padding-right: $spacing-medium; }\n .pr4-ns { padding-right: $spacing-large; }\n .pr5-ns { padding-right: $spacing-extra-large; }\n .pr6-ns { padding-right: $spacing-extra-extra-large; }\n .pr7-ns { padding-right: $spacing-extra-extra-extra-large; }\n\n .pb0-ns { padding-bottom: $spacing-none; }\n .pb1-ns { padding-bottom: $spacing-extra-small; }\n .pb2-ns { padding-bottom: $spacing-small; }\n .pb3-ns { padding-bottom: $spacing-medium; }\n .pb4-ns { padding-bottom: $spacing-large; }\n .pb5-ns { padding-bottom: $spacing-extra-large; }\n .pb6-ns { padding-bottom: $spacing-extra-extra-large; }\n .pb7-ns { padding-bottom: $spacing-extra-extra-extra-large; }\n\n .pt0-ns { padding-top: $spacing-none; }\n .pt1-ns { padding-top: $spacing-extra-small; }\n .pt2-ns { padding-top: $spacing-small; }\n .pt3-ns { padding-top: $spacing-medium; }\n .pt4-ns { padding-top: $spacing-large; }\n .pt5-ns { padding-top: $spacing-extra-large; }\n .pt6-ns { padding-top: $spacing-extra-extra-large; }\n .pt7-ns { padding-top: $spacing-extra-extra-extra-large; }\n\n .pv0-ns {\n padding-top: $spacing-none;\n padding-bottom: $spacing-none;\n }\n .pv1-ns {\n padding-top: $spacing-extra-small;\n padding-bottom: $spacing-extra-small;\n }\n .pv2-ns {\n padding-top: $spacing-small;\n padding-bottom: $spacing-small;\n }\n .pv3-ns {\n padding-top: $spacing-medium;\n padding-bottom: $spacing-medium;\n }\n .pv4-ns {\n padding-top: $spacing-large;\n padding-bottom: $spacing-large;\n }\n .pv5-ns {\n padding-top: $spacing-extra-large;\n padding-bottom: $spacing-extra-large;\n }\n .pv6-ns {\n padding-top: $spacing-extra-extra-large;\n padding-bottom: $spacing-extra-extra-large;\n }\n .pv7-ns {\n padding-top: $spacing-extra-extra-extra-large;\n padding-bottom: $spacing-extra-extra-extra-large;\n }\n .ph0-ns {\n padding-left: $spacing-none;\n padding-right: $spacing-none;\n }\n .ph1-ns {\n padding-left: $spacing-extra-small;\n padding-right: $spacing-extra-small;\n }\n .ph2-ns {\n padding-left: $spacing-small;\n padding-right: $spacing-small;\n }\n .ph3-ns {\n padding-left: $spacing-medium;\n padding-right: $spacing-medium;\n }\n .ph4-ns {\n padding-left: $spacing-large;\n padding-right: $spacing-large;\n }\n .ph5-ns {\n padding-left: $spacing-extra-large;\n padding-right: $spacing-extra-large;\n }\n .ph6-ns {\n padding-left: $spacing-extra-extra-large;\n padding-right: $spacing-extra-extra-large;\n }\n .ph7-ns {\n padding-left: $spacing-extra-extra-extra-large;\n padding-right: $spacing-extra-extra-extra-large;\n }\n\n .ma0-ns { margin: $spacing-none; }\n .ma1-ns { margin: $spacing-extra-small; }\n .ma2-ns { margin: $spacing-small; }\n .ma3-ns { margin: $spacing-medium; }\n .ma4-ns { margin: $spacing-large; }\n .ma5-ns { margin: $spacing-extra-large; }\n .ma6-ns { margin: $spacing-extra-extra-large; }\n .ma7-ns { margin: $spacing-extra-extra-extra-large; }\n\n .ml0-ns { margin-left: $spacing-none; }\n .ml1-ns { margin-left: $spacing-extra-small; }\n .ml2-ns { margin-left: $spacing-small; }\n .ml3-ns { margin-left: $spacing-medium; }\n .ml4-ns { margin-left: $spacing-large; }\n .ml5-ns { margin-left: $spacing-extra-large; }\n .ml6-ns { margin-left: $spacing-extra-extra-large; }\n .ml7-ns { margin-left: $spacing-extra-extra-extra-large; }\n\n .mr0-ns { margin-right: $spacing-none; }\n .mr1-ns { margin-right: $spacing-extra-small; }\n .mr2-ns { margin-right: $spacing-small; }\n .mr3-ns { margin-right: $spacing-medium; }\n .mr4-ns { margin-right: $spacing-large; }\n .mr5-ns { margin-right: $spacing-extra-large; }\n .mr6-ns { margin-right: $spacing-extra-extra-large; }\n .mr7-ns { margin-right: $spacing-extra-extra-extra-large; }\n\n .mb0-ns { margin-bottom: $spacing-none; }\n .mb1-ns { margin-bottom: $spacing-extra-small; }\n .mb2-ns { margin-bottom: $spacing-small; }\n .mb3-ns { margin-bottom: $spacing-medium; }\n .mb4-ns { margin-bottom: $spacing-large; }\n .mb5-ns { margin-bottom: $spacing-extra-large; }\n .mb6-ns { margin-bottom: $spacing-extra-extra-large; }\n .mb7-ns { margin-bottom: $spacing-extra-extra-extra-large; }\n\n .mt0-ns { margin-top: $spacing-none; }\n .mt1-ns { margin-top: $spacing-extra-small; }\n .mt2-ns { margin-top: $spacing-small; }\n .mt3-ns { margin-top: $spacing-medium; }\n .mt4-ns { margin-top: $spacing-large; }\n .mt5-ns { margin-top: $spacing-extra-large; }\n .mt6-ns { margin-top: $spacing-extra-extra-large; }\n .mt7-ns { margin-top: $spacing-extra-extra-extra-large; }\n\n .mv0-ns {\n margin-top: $spacing-none;\n margin-bottom: $spacing-none;\n }\n .mv1-ns {\n margin-top: $spacing-extra-small;\n margin-bottom: $spacing-extra-small;\n }\n .mv2-ns {\n margin-top: $spacing-small;\n margin-bottom: $spacing-small;\n }\n .mv3-ns {\n margin-top: $spacing-medium;\n margin-bottom: $spacing-medium;\n }\n .mv4-ns {\n margin-top: $spacing-large;\n margin-bottom: $spacing-large;\n }\n .mv5-ns {\n margin-top: $spacing-extra-large;\n margin-bottom: $spacing-extra-large;\n }\n .mv6-ns {\n margin-top: $spacing-extra-extra-large;\n margin-bottom: $spacing-extra-extra-large;\n }\n .mv7-ns {\n margin-top: $spacing-extra-extra-extra-large;\n margin-bottom: $spacing-extra-extra-extra-large;\n }\n\n .mh0-ns {\n margin-left: $spacing-none;\n margin-right: $spacing-none;\n }\n .mh1-ns {\n margin-left: $spacing-extra-small;\n margin-right: $spacing-extra-small;\n }\n .mh2-ns {\n margin-left: $spacing-small;\n margin-right: $spacing-small;\n }\n .mh3-ns {\n margin-left: $spacing-medium;\n margin-right: $spacing-medium;\n }\n .mh4-ns {\n margin-left: $spacing-large;\n margin-right: $spacing-large;\n }\n .mh5-ns {\n margin-left: $spacing-extra-large;\n margin-right: $spacing-extra-large;\n }\n .mh6-ns {\n margin-left: $spacing-extra-extra-large;\n margin-right: $spacing-extra-extra-large;\n }\n .mh7-ns {\n margin-left: $spacing-extra-extra-extra-large;\n margin-right: $spacing-extra-extra-extra-large;\n }\n\n}\n\n@media #{$breakpoint-medium} {\n .pa0-m { padding: $spacing-none; }\n .pa1-m { padding: $spacing-extra-small; }\n .pa2-m { padding: $spacing-small; }\n .pa3-m { padding: $spacing-medium; }\n .pa4-m { padding: $spacing-large; }\n .pa5-m { padding: $spacing-extra-large; }\n .pa6-m { padding: $spacing-extra-extra-large; }\n .pa7-m { padding: $spacing-extra-extra-extra-large; }\n\n .pl0-m { padding-left: $spacing-none; }\n .pl1-m { padding-left: $spacing-extra-small; }\n .pl2-m { padding-left: $spacing-small; }\n .pl3-m { padding-left: $spacing-medium; }\n .pl4-m { padding-left: $spacing-large; }\n .pl5-m { padding-left: $spacing-extra-large; }\n .pl6-m { padding-left: $spacing-extra-extra-large; }\n .pl7-m { padding-left: $spacing-extra-extra-extra-large; }\n\n .pr0-m { padding-right: $spacing-none; }\n .pr1-m { padding-right: $spacing-extra-small; }\n .pr2-m { padding-right: $spacing-small; }\n .pr3-m { padding-right: $spacing-medium; }\n .pr4-m { padding-right: $spacing-large; }\n .pr5-m { padding-right: $spacing-extra-large; }\n .pr6-m { padding-right: $spacing-extra-extra-large; }\n .pr7-m { padding-right: $spacing-extra-extra-extra-large; }\n\n .pb0-m { padding-bottom: $spacing-none; }\n .pb1-m { padding-bottom: $spacing-extra-small; }\n .pb2-m { padding-bottom: $spacing-small; }\n .pb3-m { padding-bottom: $spacing-medium; }\n .pb4-m { padding-bottom: $spacing-large; }\n .pb5-m { padding-bottom: $spacing-extra-large; }\n .pb6-m { padding-bottom: $spacing-extra-extra-large; }\n .pb7-m { padding-bottom: $spacing-extra-extra-extra-large; }\n\n .pt0-m { padding-top: $spacing-none; }\n .pt1-m { padding-top: $spacing-extra-small; }\n .pt2-m { padding-top: $spacing-small; }\n .pt3-m { padding-top: $spacing-medium; }\n .pt4-m { padding-top: $spacing-large; }\n .pt5-m { padding-top: $spacing-extra-large; }\n .pt6-m { padding-top: $spacing-extra-extra-large; }\n .pt7-m { padding-top: $spacing-extra-extra-extra-large; }\n\n .pv0-m {\n padding-top: $spacing-none;\n padding-bottom: $spacing-none;\n }\n .pv1-m {\n padding-top: $spacing-extra-small;\n padding-bottom: $spacing-extra-small;\n }\n .pv2-m {\n padding-top: $spacing-small;\n padding-bottom: $spacing-small;\n }\n .pv3-m {\n padding-top: $spacing-medium;\n padding-bottom: $spacing-medium;\n }\n .pv4-m {\n padding-top: $spacing-large;\n padding-bottom: $spacing-large;\n }\n .pv5-m {\n padding-top: $spacing-extra-large;\n padding-bottom: $spacing-extra-large;\n }\n .pv6-m {\n padding-top: $spacing-extra-extra-large;\n padding-bottom: $spacing-extra-extra-large;\n }\n .pv7-m {\n padding-top: $spacing-extra-extra-extra-large;\n padding-bottom: $spacing-extra-extra-extra-large;\n }\n\n .ph0-m {\n padding-left: $spacing-none;\n padding-right: $spacing-none;\n }\n .ph1-m {\n padding-left: $spacing-extra-small;\n padding-right: $spacing-extra-small;\n }\n .ph2-m {\n padding-left: $spacing-small;\n padding-right: $spacing-small;\n }\n .ph3-m {\n padding-left: $spacing-medium;\n padding-right: $spacing-medium;\n }\n .ph4-m {\n padding-left: $spacing-large;\n padding-right: $spacing-large;\n }\n .ph5-m {\n padding-left: $spacing-extra-large;\n padding-right: $spacing-extra-large;\n }\n .ph6-m {\n padding-left: $spacing-extra-extra-large;\n padding-right: $spacing-extra-extra-large;\n }\n .ph7-m {\n padding-left: $spacing-extra-extra-extra-large;\n padding-right: $spacing-extra-extra-extra-large;\n }\n\n .ma0-m { margin: $spacing-none; }\n .ma1-m { margin: $spacing-extra-small; }\n .ma2-m { margin: $spacing-small; }\n .ma3-m { margin: $spacing-medium; }\n .ma4-m { margin: $spacing-large; }\n .ma5-m { margin: $spacing-extra-large; }\n .ma6-m { margin: $spacing-extra-extra-large; }\n .ma7-m { margin: $spacing-extra-extra-extra-large; }\n\n .ml0-m { margin-left: $spacing-none; }\n .ml1-m { margin-left: $spacing-extra-small; }\n .ml2-m { margin-left: $spacing-small; }\n .ml3-m { margin-left: $spacing-medium; }\n .ml4-m { margin-left: $spacing-large; }\n .ml5-m { margin-left: $spacing-extra-large; }\n .ml6-m { margin-left: $spacing-extra-extra-large; }\n .ml7-m { margin-left: $spacing-extra-extra-extra-large; }\n\n .mr0-m { margin-right: $spacing-none; }\n .mr1-m { margin-right: $spacing-extra-small; }\n .mr2-m { margin-right: $spacing-small; }\n .mr3-m { margin-right: $spacing-medium; }\n .mr4-m { margin-right: $spacing-large; }\n .mr5-m { margin-right: $spacing-extra-large; }\n .mr6-m { margin-right: $spacing-extra-extra-large; }\n .mr7-m { margin-right: $spacing-extra-extra-extra-large; }\n\n .mb0-m { margin-bottom: $spacing-none; }\n .mb1-m { margin-bottom: $spacing-extra-small; }\n .mb2-m { margin-bottom: $spacing-small; }\n .mb3-m { margin-bottom: $spacing-medium; }\n .mb4-m { margin-bottom: $spacing-large; }\n .mb5-m { margin-bottom: $spacing-extra-large; }\n .mb6-m { margin-bottom: $spacing-extra-extra-large; }\n .mb7-m { margin-bottom: $spacing-extra-extra-extra-large; }\n\n .mt0-m { margin-top: $spacing-none; }\n .mt1-m { margin-top: $spacing-extra-small; }\n .mt2-m { margin-top: $spacing-small; }\n .mt3-m { margin-top: $spacing-medium; }\n .mt4-m { margin-top: $spacing-large; }\n .mt5-m { margin-top: $spacing-extra-large; }\n .mt6-m { margin-top: $spacing-extra-extra-large; }\n .mt7-m { margin-top: $spacing-extra-extra-extra-large; }\n\n .mv0-m {\n margin-top: $spacing-none;\n margin-bottom: $spacing-none;\n }\n .mv1-m {\n margin-top: $spacing-extra-small;\n margin-bottom: $spacing-extra-small;\n }\n .mv2-m {\n margin-top: $spacing-small;\n margin-bottom: $spacing-small;\n }\n .mv3-m {\n margin-top: $spacing-medium;\n margin-bottom: $spacing-medium;\n }\n .mv4-m {\n margin-top: $spacing-large;\n margin-bottom: $spacing-large;\n }\n .mv5-m {\n margin-top: $spacing-extra-large;\n margin-bottom: $spacing-extra-large;\n }\n .mv6-m {\n margin-top: $spacing-extra-extra-large;\n margin-bottom: $spacing-extra-extra-large;\n }\n .mv7-m {\n margin-top: $spacing-extra-extra-extra-large;\n margin-bottom: $spacing-extra-extra-extra-large;\n }\n\n .mh0-m {\n margin-left: $spacing-none;\n margin-right: $spacing-none;\n }\n .mh1-m {\n margin-left: $spacing-extra-small;\n margin-right: $spacing-extra-small;\n }\n .mh2-m {\n margin-left: $spacing-small;\n margin-right: $spacing-small;\n }\n .mh3-m {\n margin-left: $spacing-medium;\n margin-right: $spacing-medium;\n }\n .mh4-m {\n margin-left: $spacing-large;\n margin-right: $spacing-large;\n }\n .mh5-m {\n margin-left: $spacing-extra-large;\n margin-right: $spacing-extra-large;\n }\n .mh6-m {\n margin-left: $spacing-extra-extra-large;\n margin-right: $spacing-extra-extra-large;\n }\n .mh7-m {\n margin-left: $spacing-extra-extra-extra-large;\n margin-right: $spacing-extra-extra-extra-large;\n }\n\n}\n\n@media #{$breakpoint-large} {\n .pa0-l { padding: $spacing-none; }\n .pa1-l { padding: $spacing-extra-small; }\n .pa2-l { padding: $spacing-small; }\n .pa3-l { padding: $spacing-medium; }\n .pa4-l { padding: $spacing-large; }\n .pa5-l { padding: $spacing-extra-large; }\n .pa6-l { padding: $spacing-extra-extra-large; }\n .pa7-l { padding: $spacing-extra-extra-extra-large; }\n\n .pl0-l { padding-left: $spacing-none; }\n .pl1-l { padding-left: $spacing-extra-small; }\n .pl2-l { padding-left: $spacing-small; }\n .pl3-l { padding-left: $spacing-medium; }\n .pl4-l { padding-left: $spacing-large; }\n .pl5-l { padding-left: $spacing-extra-large; }\n .pl6-l { padding-left: $spacing-extra-extra-large; }\n .pl7-l { padding-left: $spacing-extra-extra-extra-large; }\n\n .pr0-l { padding-right: $spacing-none; }\n .pr1-l { padding-right: $spacing-extra-small; }\n .pr2-l { padding-right: $spacing-small; }\n .pr3-l { padding-right: $spacing-medium; }\n .pr4-l { padding-right: $spacing-large; }\n .pr5-l { padding-right: $spacing-extra-large; }\n .pr6-l { padding-right: $spacing-extra-extra-large; }\n .pr7-l { padding-right: $spacing-extra-extra-extra-large; }\n\n .pb0-l { padding-bottom: $spacing-none; }\n .pb1-l { padding-bottom: $spacing-extra-small; }\n .pb2-l { padding-bottom: $spacing-small; }\n .pb3-l { padding-bottom: $spacing-medium; }\n .pb4-l { padding-bottom: $spacing-large; }\n .pb5-l { padding-bottom: $spacing-extra-large; }\n .pb6-l { padding-bottom: $spacing-extra-extra-large; }\n .pb7-l { padding-bottom: $spacing-extra-extra-extra-large; }\n\n .pt0-l { padding-top: $spacing-none; }\n .pt1-l { padding-top: $spacing-extra-small; }\n .pt2-l { padding-top: $spacing-small; }\n .pt3-l { padding-top: $spacing-medium; }\n .pt4-l { padding-top: $spacing-large; }\n .pt5-l { padding-top: $spacing-extra-large; }\n .pt6-l { padding-top: $spacing-extra-extra-large; }\n .pt7-l { padding-top: $spacing-extra-extra-extra-large; }\n\n .pv0-l {\n padding-top: $spacing-none;\n padding-bottom: $spacing-none;\n }\n .pv1-l {\n padding-top: $spacing-extra-small;\n padding-bottom: $spacing-extra-small;\n }\n .pv2-l {\n padding-top: $spacing-small;\n padding-bottom: $spacing-small;\n }\n .pv3-l {\n padding-top: $spacing-medium;\n padding-bottom: $spacing-medium;\n }\n .pv4-l {\n padding-top: $spacing-large;\n padding-bottom: $spacing-large;\n }\n .pv5-l {\n padding-top: $spacing-extra-large;\n padding-bottom: $spacing-extra-large;\n }\n .pv6-l {\n padding-top: $spacing-extra-extra-large;\n padding-bottom: $spacing-extra-extra-large;\n }\n .pv7-l {\n padding-top: $spacing-extra-extra-extra-large;\n padding-bottom: $spacing-extra-extra-extra-large;\n }\n\n .ph0-l {\n padding-left: $spacing-none;\n padding-right: $spacing-none;\n }\n .ph1-l {\n padding-left: $spacing-extra-small;\n padding-right: $spacing-extra-small;\n }\n .ph2-l {\n padding-left: $spacing-small;\n padding-right: $spacing-small;\n }\n .ph3-l {\n padding-left: $spacing-medium;\n padding-right: $spacing-medium;\n }\n .ph4-l {\n padding-left: $spacing-large;\n padding-right: $spacing-large;\n }\n .ph5-l {\n padding-left: $spacing-extra-large;\n padding-right: $spacing-extra-large;\n }\n .ph6-l {\n padding-left: $spacing-extra-extra-large;\n padding-right: $spacing-extra-extra-large;\n }\n .ph7-l {\n padding-left: $spacing-extra-extra-extra-large;\n padding-right: $spacing-extra-extra-extra-large;\n }\n\n .ma0-l { margin: $spacing-none; }\n .ma1-l { margin: $spacing-extra-small; }\n .ma2-l { margin: $spacing-small; }\n .ma3-l { margin: $spacing-medium; }\n .ma4-l { margin: $spacing-large; }\n .ma5-l { margin: $spacing-extra-large; }\n .ma6-l { margin: $spacing-extra-extra-large; }\n .ma7-l { margin: $spacing-extra-extra-extra-large; }\n\n .ml0-l { margin-left: $spacing-none; }\n .ml1-l { margin-left: $spacing-extra-small; }\n .ml2-l { margin-left: $spacing-small; }\n .ml3-l { margin-left: $spacing-medium; }\n .ml4-l { margin-left: $spacing-large; }\n .ml5-l { margin-left: $spacing-extra-large; }\n .ml6-l { margin-left: $spacing-extra-extra-large; }\n .ml7-l { margin-left: $spacing-extra-extra-extra-large; }\n\n .mr0-l { margin-right: $spacing-none; }\n .mr1-l { margin-right: $spacing-extra-small; }\n .mr2-l { margin-right: $spacing-small; }\n .mr3-l { margin-right: $spacing-medium; }\n .mr4-l { margin-right: $spacing-large; }\n .mr5-l { margin-right: $spacing-extra-large; }\n .mr6-l { margin-right: $spacing-extra-extra-large; }\n .mr7-l { margin-right: $spacing-extra-extra-extra-large; }\n\n .mb0-l { margin-bottom: $spacing-none; }\n .mb1-l { margin-bottom: $spacing-extra-small; }\n .mb2-l { margin-bottom: $spacing-small; }\n .mb3-l { margin-bottom: $spacing-medium; }\n .mb4-l { margin-bottom: $spacing-large; }\n .mb5-l { margin-bottom: $spacing-extra-large; }\n .mb6-l { margin-bottom: $spacing-extra-extra-large; }\n .mb7-l { margin-bottom: $spacing-extra-extra-extra-large; }\n\n .mt0-l { margin-top: $spacing-none; }\n .mt1-l { margin-top: $spacing-extra-small; }\n .mt2-l { margin-top: $spacing-small; }\n .mt3-l { margin-top: $spacing-medium; }\n .mt4-l { margin-top: $spacing-large; }\n .mt5-l { margin-top: $spacing-extra-large; }\n .mt6-l { margin-top: $spacing-extra-extra-large; }\n .mt7-l { margin-top: $spacing-extra-extra-extra-large; }\n\n .mv0-l {\n margin-top: $spacing-none;\n margin-bottom: $spacing-none;\n }\n .mv1-l {\n margin-top: $spacing-extra-small;\n margin-bottom: $spacing-extra-small;\n }\n .mv2-l {\n margin-top: $spacing-small;\n margin-bottom: $spacing-small;\n }\n .mv3-l {\n margin-top: $spacing-medium;\n margin-bottom: $spacing-medium;\n }\n .mv4-l {\n margin-top: $spacing-large;\n margin-bottom: $spacing-large;\n }\n .mv5-l {\n margin-top: $spacing-extra-large;\n margin-bottom: $spacing-extra-large;\n }\n .mv6-l {\n margin-top: $spacing-extra-extra-large;\n margin-bottom: $spacing-extra-extra-large;\n }\n .mv7-l {\n margin-top: $spacing-extra-extra-extra-large;\n margin-bottom: $spacing-extra-extra-extra-large;\n }\n\n .mh0-l {\n margin-left: $spacing-none;\n margin-right: $spacing-none;\n }\n .mh1-l {\n margin-left: $spacing-extra-small;\n margin-right: $spacing-extra-small;\n }\n .mh2-l {\n margin-left: $spacing-small;\n margin-right: $spacing-small;\n }\n .mh3-l {\n margin-left: $spacing-medium;\n margin-right: $spacing-medium;\n }\n .mh4-l {\n margin-left: $spacing-large;\n margin-right: $spacing-large;\n }\n .mh5-l {\n margin-left: $spacing-extra-large;\n margin-right: $spacing-extra-large;\n }\n .mh6-l {\n margin-left: $spacing-extra-extra-large;\n margin-right: $spacing-extra-extra-large;\n }\n .mh7-l {\n margin-left: $spacing-extra-extra-extra-large;\n margin-right: $spacing-extra-extra-extra-large;\n }\n}\n","\n// Converted Variables\n\n$sans-serif: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, helvetica, 'helvetica neue', ubuntu, roboto, noto, 'segoe ui', arial, sans-serif !default;\n$serif: georgia, serif !default;\n$code: consolas, monaco, monospace !default;\n$font-size-headline: 6rem !default;\n$font-size-subheadline: 5rem !default;\n$font-size-1: 3rem !default;\n$font-size-2: 2.25rem !default;\n$font-size-3: 1.5rem !default;\n$font-size-4: 1.25rem !default;\n$font-size-5: 1rem !default;\n$font-size-6: .875rem !default;\n$font-size-7: .75rem !default;\n$letter-spacing-tight: -.05em !default;\n$letter-spacing-1: .1em !default;\n$letter-spacing-2: .25em !default;\n$line-height-solid: 1 !default;\n$line-height-title: 1.25 !default;\n$line-height-copy: 1.5 !default;\n$measure: 30em !default;\n$measure-narrow: 20em !default;\n$measure-wide: 34em !default;\n$spacing-none: 0 !default;\n$spacing-extra-small: .25rem !default;\n$spacing-small: .5rem !default;\n$spacing-medium: 1rem !default;\n$spacing-large: 2rem !default;\n$spacing-extra-large: 4rem !default;\n$spacing-extra-extra-large: 8rem !default;\n$spacing-extra-extra-extra-large: 16rem !default;\n$spacing-copy-separator: 1.5em !default;\n$height-1: 1rem !default;\n$height-2: 2rem !default;\n$height-3: 4rem !default;\n$height-4: 8rem !default;\n$height-5: 16rem !default;\n$width-1: 1rem !default;\n$width-2: 2rem !default;\n$width-3: 4rem !default;\n$width-4: 8rem !default;\n$width-5: 16rem !default;\n$max-width-1: 1rem !default;\n$max-width-2: 2rem !default;\n$max-width-3: 4rem !default;\n$max-width-4: 8rem !default;\n$max-width-5: 16rem !default;\n$max-width-6: 32rem !default;\n$max-width-7: 48rem !default;\n$max-width-8: 64rem !default;\n$max-width-9: 96rem !default;\n$border-radius-none: 0 !default;\n$border-radius-1: .125rem !default;\n$border-radius-2: .25rem !default;\n$border-radius-3: .5rem !default;\n$border-radius-4: 1rem !default;\n$border-radius-circle: 100% !default;\n$border-radius-pill: 9999px !default;\n$border-width-none: 0 !default;\n$border-width-1: .125rem !default;\n$border-width-2: .25rem !default;\n$border-width-3: .5rem !default;\n$border-width-4: 1rem !default;\n$border-width-5: 2rem !default;\n$box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 ) !default;\n$box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 ) !default;\n$box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 ) !default;\n$box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 ) !default;\n$box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 ) !default;\n$black: #000 !default;\n$near-black: #111 !default;\n$dark-gray: #333 !default;\n$mid-gray: #555 !default;\n$gray: #777 !default;\n$silver: #999 !default;\n$light-silver: #aaa !default;\n$moon-gray: #ccc !default;\n$light-gray: #eee !default;\n$near-white: #f4f4f4 !default;\n$white: #fff !default;\n$transparent: transparent !default;\n$black-90: rgba(0,0,0,.9) !default;\n$black-80: rgba(0,0,0,.8) !default;\n$black-70: rgba(0,0,0,.7) !default;\n$black-60: rgba(0,0,0,.6) !default;\n$black-50: rgba(0,0,0,.5) !default;\n$black-40: rgba(0,0,0,.4) !default;\n$black-30: rgba(0,0,0,.3) !default;\n$black-20: rgba(0,0,0,.2) !default;\n$black-10: rgba(0,0,0,.1) !default;\n$black-05: rgba(0,0,0,.05) !default;\n$black-025: rgba(0,0,0,.025) !default;\n$black-0125: rgba(0,0,0,.0125) !default;\n$white-90: rgba(255,255,255,.9) !default;\n$white-80: rgba(255,255,255,.8) !default;\n$white-70: rgba(255,255,255,.7) !default;\n$white-60: rgba(255,255,255,.6) !default;\n$white-50: rgba(255,255,255,.5) !default;\n$white-40: rgba(255,255,255,.4) !default;\n$white-30: rgba(255,255,255,.3) !default;\n$white-20: rgba(255,255,255,.2) !default;\n$white-10: rgba(255,255,255,.1) !default;\n$white-05: rgba(255,255,255,.05) !default;\n$white-025: rgba(255,255,255,.025) !default;\n$white-0125: rgba(255,255,255,.0125) !default;\n$dark-red: #e7040f !default;\n$red: #ff4136 !default;\n$light-red: #ff725c !default;\n$orange: #ff6300 !default;\n$gold: #ffb700 !default;\n$yellow: #ffd700 !default;\n$light-yellow: #fbf1a9 !default;\n$purple: #5e2ca5 !default;\n$light-purple: #a463f2 !default;\n$dark-pink: #d5008f !default;\n$hot-pink: #ff41b4 !default;\n$pink: #ff80cc !default;\n$light-pink: #ffa3d7 !default;\n$dark-green: #137752 !default;\n$green: #19a974 !default;\n$light-green: #9eebcf !default;\n$navy: #001b44 !default;\n$dark-blue: #00449e !default;\n$blue: #357edd !default;\n$light-blue: #96ccff !default;\n$lightest-blue: #cdecff !default;\n$washed-blue: #f6fffe !default;\n$washed-green: #e8fdf5 !default;\n$washed-yellow: #fffceb !default;\n$washed-red: #ffdfdf !default;\n\n// Custom Media Query Variables\n\n$breakpoint-not-small: 'screen and (min-width: 30em)' !default;\n$breakpoint-medium: 'screen and (min-width: 30em) and (max-width: 60em)' !default;\n$breakpoint-large: 'screen and (min-width: 60em)' !default;\n\n/*\n\n VARIABLES\n\n*/\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n NEGATIVE MARGINS\n\n Base:\n n = negative\n\n Modifiers:\n a = all\n t = top\n r = right\n b = bottom\n l = left\n\n 1 = 1st step in spacing scale\n 2 = 2nd step in spacing scale\n 3 = 3rd step in spacing scale\n 4 = 4th step in spacing scale\n 5 = 5th step in spacing scale\n 6 = 6th step in spacing scale\n 7 = 7th step in spacing scale\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n\n\n.na1 { margin: -$spacing-extra-small; }\n.na2 { margin: -$spacing-small; }\n.na3 { margin: -$spacing-medium; }\n.na4 { margin: -$spacing-large; }\n.na5 { margin: -$spacing-extra-large; }\n.na6 { margin: -$spacing-extra-extra-large; }\n.na7 { margin: -$spacing-extra-extra-extra-large; }\n\n.nl1 { margin-left: -$spacing-extra-small; }\n.nl2 { margin-left: -$spacing-small; }\n.nl3 { margin-left: -$spacing-medium; }\n.nl4 { margin-left: -$spacing-large; }\n.nl5 { margin-left: -$spacing-extra-large; }\n.nl6 { margin-left: -$spacing-extra-extra-large; }\n.nl7 { margin-left: -$spacing-extra-extra-extra-large; }\n\n.nr1 { margin-right: -$spacing-extra-small; }\n.nr2 { margin-right: -$spacing-small; }\n.nr3 { margin-right: -$spacing-medium; }\n.nr4 { margin-right: -$spacing-large; }\n.nr5 { margin-right: -$spacing-extra-large; }\n.nr6 { margin-right: -$spacing-extra-extra-large; }\n.nr7 { margin-right: -$spacing-extra-extra-extra-large; }\n\n.nb1 { margin-bottom: -$spacing-extra-small; }\n.nb2 { margin-bottom: -$spacing-small; }\n.nb3 { margin-bottom: -$spacing-medium; }\n.nb4 { margin-bottom: -$spacing-large; }\n.nb5 { margin-bottom: -$spacing-extra-large; }\n.nb6 { margin-bottom: -$spacing-extra-extra-large; }\n.nb7 { margin-bottom: -$spacing-extra-extra-extra-large; }\n\n.nt1 { margin-top: -$spacing-extra-small; }\n.nt2 { margin-top: -$spacing-small; }\n.nt3 { margin-top: -$spacing-medium; }\n.nt4 { margin-top: -$spacing-large; }\n.nt5 { margin-top: -$spacing-extra-large; }\n.nt6 { margin-top: -$spacing-extra-extra-large; }\n.nt7 { margin-top: -$spacing-extra-extra-extra-large; }\n\n@media #{$breakpoint-not-small} {\n\n .na1-ns { margin: -$spacing-extra-small; }\n .na2-ns { margin: -$spacing-small; }\n .na3-ns { margin: -$spacing-medium; }\n .na4-ns { margin: -$spacing-large; }\n .na5-ns { margin: -$spacing-extra-large; }\n .na6-ns { margin: -$spacing-extra-extra-large; }\n .na7-ns { margin: -$spacing-extra-extra-extra-large; }\n\n .nl1-ns { margin-left: -$spacing-extra-small; }\n .nl2-ns { margin-left: -$spacing-small; }\n .nl3-ns { margin-left: -$spacing-medium; }\n .nl4-ns { margin-left: -$spacing-large; }\n .nl5-ns { margin-left: -$spacing-extra-large; }\n .nl6-ns { margin-left: -$spacing-extra-extra-large; }\n .nl7-ns { margin-left: -$spacing-extra-extra-extra-large; }\n\n .nr1-ns { margin-right: -$spacing-extra-small; }\n .nr2-ns { margin-right: -$spacing-small; }\n .nr3-ns { margin-right: -$spacing-medium; }\n .nr4-ns { margin-right: -$spacing-large; }\n .nr5-ns { margin-right: -$spacing-extra-large; }\n .nr6-ns { margin-right: -$spacing-extra-extra-large; }\n .nr7-ns { margin-right: -$spacing-extra-extra-extra-large; }\n\n .nb1-ns { margin-bottom: -$spacing-extra-small; }\n .nb2-ns { margin-bottom: -$spacing-small; }\n .nb3-ns { margin-bottom: -$spacing-medium; }\n .nb4-ns { margin-bottom: -$spacing-large; }\n .nb5-ns { margin-bottom: -$spacing-extra-large; }\n .nb6-ns { margin-bottom: -$spacing-extra-extra-large; }\n .nb7-ns { margin-bottom: -$spacing-extra-extra-extra-large; }\n\n .nt1-ns { margin-top: -$spacing-extra-small; }\n .nt2-ns { margin-top: -$spacing-small; }\n .nt3-ns { margin-top: -$spacing-medium; }\n .nt4-ns { margin-top: -$spacing-large; }\n .nt5-ns { margin-top: -$spacing-extra-large; }\n .nt6-ns { margin-top: -$spacing-extra-extra-large; }\n .nt7-ns { margin-top: -$spacing-extra-extra-extra-large; }\n\n}\n\n@media #{$breakpoint-medium} {\n .na1-m { margin: -$spacing-extra-small; }\n .na2-m { margin: -$spacing-small; }\n .na3-m { margin: -$spacing-medium; }\n .na4-m { margin: -$spacing-large; }\n .na5-m { margin: -$spacing-extra-large; }\n .na6-m { margin: -$spacing-extra-extra-large; }\n .na7-m { margin: -$spacing-extra-extra-extra-large; }\n\n .nl1-m { margin-left: -$spacing-extra-small; }\n .nl2-m { margin-left: -$spacing-small; }\n .nl3-m { margin-left: -$spacing-medium; }\n .nl4-m { margin-left: -$spacing-large; }\n .nl5-m { margin-left: -$spacing-extra-large; }\n .nl6-m { margin-left: -$spacing-extra-extra-large; }\n .nl7-m { margin-left: -$spacing-extra-extra-extra-large; }\n\n .nr1-m { margin-right: -$spacing-extra-small; }\n .nr2-m { margin-right: -$spacing-small; }\n .nr3-m { margin-right: -$spacing-medium; }\n .nr4-m { margin-right: -$spacing-large; }\n .nr5-m { margin-right: -$spacing-extra-large; }\n .nr6-m { margin-right: -$spacing-extra-extra-large; }\n .nr7-m { margin-right: -$spacing-extra-extra-extra-large; }\n\n .nb1-m { margin-bottom: -$spacing-extra-small; }\n .nb2-m { margin-bottom: -$spacing-small; }\n .nb3-m { margin-bottom: -$spacing-medium; }\n .nb4-m { margin-bottom: -$spacing-large; }\n .nb5-m { margin-bottom: -$spacing-extra-large; }\n .nb6-m { margin-bottom: -$spacing-extra-extra-large; }\n .nb7-m { margin-bottom: -$spacing-extra-extra-extra-large; }\n\n .nt1-m { margin-top: -$spacing-extra-small; }\n .nt2-m { margin-top: -$spacing-small; }\n .nt3-m { margin-top: -$spacing-medium; }\n .nt4-m { margin-top: -$spacing-large; }\n .nt5-m { margin-top: -$spacing-extra-large; }\n .nt6-m { margin-top: -$spacing-extra-extra-large; }\n .nt7-m { margin-top: -$spacing-extra-extra-extra-large; }\n\n}\n\n@media #{$breakpoint-large} {\n .na1-l { margin: -$spacing-extra-small; }\n .na2-l { margin: -$spacing-small; }\n .na3-l { margin: -$spacing-medium; }\n .na4-l { margin: -$spacing-large; }\n .na5-l { margin: -$spacing-extra-large; }\n .na6-l { margin: -$spacing-extra-extra-large; }\n .na7-l { margin: -$spacing-extra-extra-extra-large; }\n\n .nl1-l { margin-left: -$spacing-extra-small; }\n .nl2-l { margin-left: -$spacing-small; }\n .nl3-l { margin-left: -$spacing-medium; }\n .nl4-l { margin-left: -$spacing-large; }\n .nl5-l { margin-left: -$spacing-extra-large; }\n .nl6-l { margin-left: -$spacing-extra-extra-large; }\n .nl7-l { margin-left: -$spacing-extra-extra-extra-large; }\n\n .nr1-l { margin-right: -$spacing-extra-small; }\n .nr2-l { margin-right: -$spacing-small; }\n .nr3-l { margin-right: -$spacing-medium; }\n .nr4-l { margin-right: -$spacing-large; }\n .nr5-l { margin-right: -$spacing-extra-large; }\n .nr6-l { margin-right: -$spacing-extra-extra-large; }\n .nr7-l { margin-right: -$spacing-extra-extra-extra-large; }\n\n .nb1-l { margin-bottom: -$spacing-extra-small; }\n .nb2-l { margin-bottom: -$spacing-small; }\n .nb3-l { margin-bottom: -$spacing-medium; }\n .nb4-l { margin-bottom: -$spacing-large; }\n .nb5-l { margin-bottom: -$spacing-extra-large; }\n .nb6-l { margin-bottom: -$spacing-extra-extra-large; }\n .nb7-l { margin-bottom: -$spacing-extra-extra-extra-large; }\n\n .nt1-l { margin-top: -$spacing-extra-small; }\n .nt2-l { margin-top: -$spacing-small; }\n .nt3-l { margin-top: -$spacing-medium; }\n .nt4-l { margin-top: -$spacing-large; }\n .nt5-l { margin-top: -$spacing-extra-large; }\n .nt6-l { margin-top: -$spacing-extra-extra-large; }\n .nt7-l { margin-top: -$spacing-extra-extra-extra-large; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n TABLES\n Docs: http://tachyons.io/docs/elements/tables/\n\n*/\n\n.collapse {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\n.striped--light-silver:nth-child(odd) {\n background-color: $light-silver;\n}\n\n.striped--moon-gray:nth-child(odd) {\n background-color: $moon-gray;\n}\n\n.striped--light-gray:nth-child(odd) {\n background-color: $light-gray;\n}\n\n.striped--near-white:nth-child(odd) {\n background-color: $near-white;\n}\n\n.stripe-light:nth-child(odd) {\n background-color: $white-10;\n}\n\n.stripe-dark:nth-child(odd) {\n background-color: $black-10;\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n TEXT DECORATION\n Docs: http://tachyons.io/docs/typography/text-decoration/\n\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.strike { text-decoration: line-through; }\n.underline { text-decoration: underline; }\n.no-underline { text-decoration: none; }\n\n\n@media #{$breakpoint-not-small} {\n .strike-ns { text-decoration: line-through; }\n .underline-ns { text-decoration: underline; }\n .no-underline-ns { text-decoration: none; }\n}\n\n@media #{$breakpoint-medium} {\n .strike-m { text-decoration: line-through; }\n .underline-m { text-decoration: underline; }\n .no-underline-m { text-decoration: none; }\n}\n\n@media #{$breakpoint-large} {\n .strike-l { text-decoration: line-through; }\n .underline-l { text-decoration: underline; }\n .no-underline-l { text-decoration: none; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n TEXT ALIGN\n Docs: http://tachyons.io/docs/typography/text-align/\n\n Base\n t = text-align\n\n Modifiers\n l = left\n r = right\n c = center\n j = justify\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.tl { text-align: left; }\n.tr { text-align: right; }\n.tc { text-align: center; }\n.tj { text-align: justify; }\n\n@media #{$breakpoint-not-small} {\n .tl-ns { text-align: left; }\n .tr-ns { text-align: right; }\n .tc-ns { text-align: center; }\n .tj-ns { text-align: justify; }\n}\n\n@media #{$breakpoint-medium} {\n .tl-m { text-align: left; }\n .tr-m { text-align: right; }\n .tc-m { text-align: center; }\n .tj-m { text-align: justify; }\n}\n\n@media #{$breakpoint-large} {\n .tl-l { text-align: left; }\n .tr-l { text-align: right; }\n .tc-l { text-align: center; }\n .tj-l { text-align: justify; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n TEXT TRANSFORM\n Docs: http://tachyons.io/docs/typography/text-transform/\n\n Base:\n tt = text-transform\n\n Modifiers\n c = capitalize\n l = lowercase\n u = uppercase\n n = none\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.ttc { text-transform: capitalize; }\n.ttl { text-transform: lowercase; }\n.ttu { text-transform: uppercase; }\n.ttn { text-transform: none; }\n\n@media #{$breakpoint-not-small} {\n .ttc-ns { text-transform: capitalize; }\n .ttl-ns { text-transform: lowercase; }\n .ttu-ns { text-transform: uppercase; }\n .ttn-ns { text-transform: none; }\n}\n\n@media #{$breakpoint-medium} {\n .ttc-m { text-transform: capitalize; }\n .ttl-m { text-transform: lowercase; }\n .ttu-m { text-transform: uppercase; }\n .ttn-m { text-transform: none; }\n}\n\n@media #{$breakpoint-large} {\n .ttc-l { text-transform: capitalize; }\n .ttl-l { text-transform: lowercase; }\n .ttu-l { text-transform: uppercase; }\n .ttn-l { text-transform: none; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n TYPE SCALE\n Docs: http://tachyons.io/docs/typography/scale/\n\n Base:\n f = font-size\n\n Modifiers\n 1 = 1st step in size scale\n 2 = 2nd step in size scale\n 3 = 3rd step in size scale\n 4 = 4th step in size scale\n 5 = 5th step in size scale\n 6 = 6th step in size scale\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n*/\n\n/*\n * For Hero/Marketing Titles\n *\n * These generally are too large for mobile\n * so be careful using them on smaller screens.\n * */\n\n.f-6,\n.f-headline {\n font-size: $font-size-headline;\n}\n.f-5,\n.f-subheadline {\n font-size: $font-size-subheadline;\n}\n\n\n/* Type Scale */\n\n\n.f1 { font-size: $font-size-1; }\n.f2 { font-size: $font-size-2; }\n.f3 { font-size: $font-size-3; }\n.f4 { font-size: $font-size-4; }\n.f5 { font-size: $font-size-5; }\n.f6 { font-size: $font-size-6; }\n.f7 { font-size: $font-size-7; }\n\n@media #{$breakpoint-not-small}{\n .f-6-ns,\n .f-headline-ns { font-size: $font-size-headline; }\n .f-5-ns,\n .f-subheadline-ns { font-size: $font-size-subheadline; }\n .f1-ns { font-size: $font-size-1; }\n .f2-ns { font-size: $font-size-2; }\n .f3-ns { font-size: $font-size-3; }\n .f4-ns { font-size: $font-size-4; }\n .f5-ns { font-size: $font-size-5; }\n .f6-ns { font-size: $font-size-6; }\n .f7-ns { font-size: $font-size-7; }\n}\n\n@media #{$breakpoint-medium} {\n .f-6-m,\n .f-headline-m { font-size: $font-size-headline; }\n .f-5-m,\n .f-subheadline-m { font-size: $font-size-subheadline; }\n .f1-m { font-size: $font-size-1; }\n .f2-m { font-size: $font-size-2; }\n .f3-m { font-size: $font-size-3; }\n .f4-m { font-size: $font-size-4; }\n .f5-m { font-size: $font-size-5; }\n .f6-m { font-size: $font-size-6; }\n .f7-m { font-size: $font-size-7; }\n}\n\n@media #{$breakpoint-large} {\n .f-6-l,\n .f-headline-l {\n font-size: $font-size-headline;\n }\n .f-5-l,\n .f-subheadline-l {\n font-size: $font-size-subheadline;\n }\n .f1-l { font-size: $font-size-1; }\n .f2-l { font-size: $font-size-2; }\n .f3-l { font-size: $font-size-3; }\n .f4-l { font-size: $font-size-4; }\n .f5-l { font-size: $font-size-5; }\n .f6-l { font-size: $font-size-6; }\n .f7-l { font-size: $font-size-7; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n TYPOGRAPHY\n http://tachyons.io/docs/typography/measure/\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n\n\n/* Measure is limited to ~66 characters */\n.measure {\n max-width: $measure;\n}\n\n/* Measure is limited to ~80 characters */\n.measure-wide {\n max-width: $measure-wide;\n}\n\n/* Measure is limited to ~45 characters */\n.measure-narrow {\n max-width: $measure-narrow;\n}\n\n/* Book paragraph style - paragraphs are indented with no vertical spacing. */\n.indent {\n text-indent: 1em;\n margin-top: 0;\n margin-bottom: 0;\n}\n\n.small-caps {\n font-variant: small-caps;\n}\n\n/* Combine this class with a width to truncate text (or just leave as is to truncate at width of containing element. */\n\n.truncate {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n@media #{$breakpoint-not-small} {\n .measure-ns {\n max-width: $measure;\n }\n .measure-wide-ns {\n max-width: $measure-wide;\n }\n .measure-narrow-ns {\n max-width: $measure-narrow;\n }\n .indent-ns {\n text-indent: 1em;\n margin-top: 0;\n margin-bottom: 0;\n }\n .small-caps-ns {\n font-variant: small-caps;\n }\n .truncate-ns {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n}\n\n@media #{$breakpoint-medium} {\n .measure-m {\n max-width: $measure;\n }\n .measure-wide-m {\n max-width: $measure-wide;\n }\n .measure-narrow-m {\n max-width: $measure-narrow;\n }\n .indent-m {\n text-indent: 1em;\n margin-top: 0;\n margin-bottom: 0;\n }\n .small-caps-m {\n font-variant: small-caps;\n }\n .truncate-m {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n}\n\n@media #{$breakpoint-large} {\n .measure-l {\n max-width: $measure;\n }\n .measure-wide-l {\n max-width: $measure-wide;\n }\n .measure-narrow-l {\n max-width: $measure-narrow;\n }\n .indent-l {\n text-indent: 1em;\n margin-top: 0;\n margin-bottom: 0;\n }\n .small-caps-l {\n font-variant: small-caps;\n }\n .truncate-l {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n UTILITIES\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n/* Equivalent to .overflow-y-scroll */\n.overflow-container {\n overflow-y: scroll;\n}\n\n.center {\n margin-right: auto;\n margin-left: auto;\n}\n\n.mr-auto { margin-right: auto; }\n.ml-auto { margin-left: auto; }\n\n@media #{$breakpoint-not-small}{\n .center-ns {\n margin-right: auto;\n margin-left: auto;\n }\n .mr-auto-ns { margin-right: auto; }\n .ml-auto-ns { margin-left: auto; }\n}\n\n@media #{$breakpoint-medium}{\n .center-m {\n margin-right: auto;\n margin-left: auto;\n }\n .mr-auto-m { margin-right: auto; }\n .ml-auto-m { margin-left: auto; }\n}\n\n@media #{$breakpoint-large}{\n .center-l {\n margin-right: auto;\n margin-left: auto;\n }\n .mr-auto-l { margin-right: auto; }\n .ml-auto-l { margin-left: auto; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n VISIBILITY\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n\n/*\n Text that is hidden but accessible\n Ref: http://snook.ca/archives/html_and_css/hiding-content-for-accessibility\n*/\n\n.clip {\n position: fixed !important;\n _position: absolute !important;\n clip: rect(1px 1px 1px 1px); /* IE6, IE7 */\n clip: rect(1px, 1px, 1px, 1px);\n}\n\n@media #{$breakpoint-not-small} {\n .clip-ns {\n position: fixed !important;\n _position: absolute !important;\n clip: rect(1px 1px 1px 1px); /* IE6, IE7 */\n clip: rect(1px, 1px, 1px, 1px);\n }\n}\n\n@media #{$breakpoint-medium} {\n .clip-m {\n position: fixed !important;\n _position: absolute !important;\n clip: rect(1px 1px 1px 1px); /* IE6, IE7 */\n clip: rect(1px, 1px, 1px, 1px);\n }\n}\n\n@media #{$breakpoint-large} {\n .clip-l {\n position: fixed !important;\n _position: absolute !important;\n clip: rect(1px 1px 1px 1px); /* IE6, IE7 */\n clip: rect(1px, 1px, 1px, 1px);\n }\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n WHITE SPACE\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n\n.ws-normal { white-space: normal; }\n.nowrap { white-space: nowrap; }\n.pre { white-space: pre; }\n\n@media #{$breakpoint-not-small} {\n .ws-normal-ns { white-space: normal; }\n .nowrap-ns { white-space: nowrap; }\n .pre-ns { white-space: pre; }\n}\n\n@media #{$breakpoint-medium} {\n .ws-normal-m { white-space: normal; }\n .nowrap-m { white-space: nowrap; }\n .pre-m { white-space: pre; }\n}\n\n@media #{$breakpoint-large} {\n .ws-normal-l { white-space: normal; }\n .nowrap-l { white-space: nowrap; }\n .pre-l { white-space: pre; }\n}\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n VERTICAL ALIGN\n\n Media Query Extensions:\n -ns = not-small\n -m = medium\n -l = large\n\n*/\n\n.v-base { vertical-align: baseline; }\n.v-mid { vertical-align: middle; }\n.v-top { vertical-align: top; }\n.v-btm { vertical-align: bottom; }\n\n@media #{$breakpoint-not-small} {\n .v-base-ns { vertical-align: baseline; }\n .v-mid-ns { vertical-align: middle; }\n .v-top-ns { vertical-align: top; }\n .v-btm-ns { vertical-align: bottom; }\n}\n\n@media #{$breakpoint-medium} {\n .v-base-m { vertical-align: baseline; }\n .v-mid-m { vertical-align: middle; }\n .v-top-m { vertical-align: top; }\n .v-btm-m { vertical-align: bottom; }\n}\n\n@media #{$breakpoint-large} {\n .v-base-l { vertical-align: baseline; }\n .v-mid-l { vertical-align: middle; }\n .v-top-l { vertical-align: top; }\n .v-btm-l { vertical-align: bottom; }\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n HOVER EFFECTS\n Docs: http://tachyons.io/docs/themes/hovers/\n\n - Dim\n - Glow\n - Hide Child\n - Underline text\n - Grow\n - Pointer\n - Shadow\n\n*/\n\n/*\n\n Dim element on hover by adding the dim class.\n\n*/\n.dim {\n opacity: 1;\n transition: opacity .15s ease-in;\n}\n.dim:hover,\n.dim:focus {\n opacity: .5;\n transition: opacity .15s ease-in;\n}\n.dim:active {\n opacity: .8; transition: opacity .15s ease-out;\n}\n\n/*\n\n Animate opacity to 100% on hover by adding the glow class.\n\n*/\n.glow {\n transition: opacity .15s ease-in;\n}\n.glow:hover,\n.glow:focus {\n opacity: 1;\n transition: opacity .15s ease-in;\n}\n\n/*\n\n Hide child & reveal on hover:\n\n Put the hide-child class on a parent element and any nested element with the\n child class will be hidden and displayed on hover or focus.\n\n
\n
Hidden until hover or focus
\n
Hidden until hover or focus
\n
Hidden until hover or focus
\n
Hidden until hover or focus
\n
\n*/\n\n.hide-child .child {\n opacity: 0;\n transition: opacity .15s ease-in;\n}\n.hide-child:hover .child,\n.hide-child:focus .child,\n.hide-child:active .child {\n opacity: 1;\n transition: opacity .15s ease-in;\n}\n\n.underline-hover:hover,\n.underline-hover:focus {\n text-decoration: underline;\n}\n\n/* Can combine this with overflow-hidden to make background images grow on hover\n * even if you are using background-size: cover */\n\n.grow {\n -moz-osx-font-smoothing: grayscale;\n backface-visibility: hidden;\n transform: translateZ(0);\n transition: transform 0.25s ease-out;\n}\n\n.grow:hover,\n.grow:focus {\n transform: scale(1.05);\n}\n\n.grow:active {\n transform: scale(.90);\n}\n\n.grow-large {\n -moz-osx-font-smoothing: grayscale;\n backface-visibility: hidden;\n transform: translateZ(0);\n transition: transform .25s ease-in-out;\n}\n\n.grow-large:hover,\n.grow-large:focus {\n transform: scale(1.2);\n}\n\n.grow-large:active {\n transform: scale(.95);\n}\n\n/* Add pointer on hover */\n\n.pointer:hover {\n cursor: pointer;\n}\n\n/*\n Add shadow on hover.\n\n Performant box-shadow animation pattern from\n http://tobiasahlin.com/blog/how-to-animate-box-shadow/\n*/\n\n.shadow-hover {\n cursor: pointer;\n position: relative;\n transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1);\n}\n\n.shadow-hover::after {\n content: '';\n box-shadow: 0px 0px 16px 2px rgba( 0, 0, 0, .2 );\n border-radius: inherit;\n opacity: 0;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: -1;\n transition: opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1);\n}\n\n.shadow-hover:hover::after,\n.shadow-hover:focus::after {\n opacity: 1;\n}\n\n/* Combine with classes in skins and skins-pseudo for\n * many different transition possibilities. */\n\n.bg-animate,\n.bg-animate:hover,\n.bg-animate:focus {\n transition: background-color .15s ease-in-out;\n}\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n Z-INDEX\n\n Base\n z = z-index\n\n Modifiers\n -0 = literal value 0\n -1 = literal value 1\n -2 = literal value 2\n -3 = literal value 3\n -4 = literal value 4\n -5 = literal value 5\n -999 = literal value 999\n -9999 = literal value 9999\n\n -max = largest accepted z-index value as integer\n\n -inherit = string value inherit\n -initial = string value initial\n -unset = string value unset\n\n MDN: https://developer.mozilla.org/en/docs/Web/CSS/z-index\n Spec: http://www.w3.org/TR/CSS2/zindex.html\n Articles:\n https://philipwalton.com/articles/what-no-one-told-you-about-z-index/\n\n Tips on extending:\n There might be a time worth using negative z-index values.\n Or if you are using tachyons with another project, you might need to\n adjust these values to suit your needs.\n\n*/\n\n.z-0 { z-index: 0; }\n.z-1 { z-index: 1; }\n.z-2 { z-index: 2; }\n.z-3 { z-index: 3; }\n.z-4 { z-index: 4; }\n.z-5 { z-index: 5; }\n\n.z-999 { z-index: 999; }\n.z-9999 { z-index: 9999; }\n\n.z-max {\n z-index: 2147483647;\n}\n\n.z-inherit { z-index: inherit; }\n.z-initial { z-index: initial; }\n.z-unset { z-index: unset; }\n\n","\n// Converted Variables\n\n\n// Custom Media Query Variables\n\n\n/*\n\n NESTED\n Tachyons module for styling nested elements\n that are generated by a cms.\n\n*/\n\n.nested-copy-line-height p,\n.nested-copy-line-height ul,\n.nested-copy-line-height ol {\n line-height: $line-height-copy;\n}\n\n.nested-headline-line-height h1,\n.nested-headline-line-height h2,\n.nested-headline-line-height h3,\n.nested-headline-line-height h4,\n.nested-headline-line-height h5,\n.nested-headline-line-height h6 {\n line-height: $line-height-title;\n}\n\n.nested-list-reset ul,\n.nested-list-reset ol {\n padding-left: 0;\n margin-left: 0;\n list-style-type: none;\n}\n\n.nested-copy-indent p+p {\n text-indent: $letter-spacing-1;\n margin-top: $spacing-none;\n margin-bottom: $spacing-none;\n}\n\n.nested-copy-seperator p+p {\n margin-top: $spacing-copy-separator;\n}\n\n.nested-img img {\n width: 100%;\n max-width: 100%;\n display: block;\n}\n\n.nested-links a {\n color: $blue;\n transition: color .15s ease-in;\n}\n\n.nested-links a:hover,\n.nested-links a:focus {\n color: $light-blue;\n transition: color .15s ease-in;\n}\n",".wrapper\n{\n width: 100%;\n max-width: 1460px;\n margin: 0 auto;\n padding: 0 20px;\n box-sizing: border-box;\n}\n\n.opblock-tag-section\n{\n display: flex;\n flex-direction: column;\n}\n\n.opblock-tag\n{\n display: flex;\n align-items: center;\n\n padding: 10px 20px 10px 10px;\n\n cursor: pointer;\n transition: all .2s;\n\n border-bottom: 1px solid rgba($opblock-tag-border-bottom-color, .3);\n\n &:hover\n {\n background: rgba($opblock-tag-background-color-hover,.02);\n }\n}\n\n@mixin method($color)\n{\n border-color: $color;\n background: rgba($color, .1);\n\n .opblock-summary-method\n {\n background: $color;\n }\n\n .opblock-summary\n {\n border-color: $color;\n }\n\n .tab-header .tab-item.active h4 span:after\n {\n background: $color;\n }\n}\n\n\n\n\n.opblock-tag\n{\n font-size: 24px;\n\n margin: 0 0 5px 0;\n\n @include text_headline();\n\n &.no-desc\n {\n span\n {\n flex: 1;\n }\n }\n\n svg\n {\n transition: all .4s;\n }\n\n small\n {\n font-size: 14px;\n font-weight: normal;\n\n flex: 1;\n\n padding: 0 10px;\n\n @include text_body();\n }\n}\n\n.parameter__type\n{\n font-size: 12px;\n\n padding: 5px 0;\n\n @include text_code();\n}\n\n.parameter-controls {\n margin-top: 0.75em;\n}\n\n.examples {\n &__title {\n display: block;\n font-size: 1.1em;\n font-weight: bold;\n margin-bottom: 0.75em;\n }\n\n &__section {\n margin-top: 1.5em;\n }\n &__section-header {\n font-weight: bold;\n font-size: .9rem;\n margin-bottom: .5rem;\n // color: #555;\n }\n}\n\n.examples-select {\n margin-bottom: .75em;\n\n &__section-label {\n font-weight: bold;\n font-size: .9rem;\n margin-right: .5rem;\n }\n}\n\n.example {\n &__section {\n margin-top: 1.5em;\n }\n &__section-header {\n font-weight: bold;\n font-size: .9rem;\n margin-bottom: .5rem;\n // color: #555;\n }\n}\n\n.view-line-link\n{\n position: relative;\n top: 3px;\n\n width: 20px;\n margin: 0 5px;\n\n cursor: pointer;\n transition: all .5s;\n}\n\n\n\n.opblock\n{\n margin: 0 0 15px 0;\n\n border: 1px solid $opblock-border-color;\n border-radius: 4px;\n box-shadow: 0 0 3px rgba($opblock-box-shadow-color,.19);\n\n .tab-header\n {\n display: flex;\n\n flex: 1;\n\n .tab-item\n {\n padding: 0 40px;\n\n cursor: pointer;\n\n &:first-of-type\n {\n padding: 0 40px 0 0;\n }\n &.active\n {\n h4\n {\n span\n {\n position: relative;\n\n\n &:after\n {\n position: absolute;\n bottom: -15px;\n left: 50%;\n\n width: 120%;\n height: 4px;\n\n content: '';\n transform: translateX(-50%);\n\n background: $opblock-tab-header-tab-item-active-h4-span-after-background-color;\n }\n }\n }\n }\n }\n }\n\n\n &.is-open\n {\n .opblock-summary\n {\n border-bottom: 1px solid $opblock-isopen-summary-border-bottom-color;\n }\n }\n\n .opblock-section-header\n {\n display: flex;\n align-items: center;\n\n padding: 8px 20px;\n\n min-height: 50px;\n\n background: rgba($opblock-isopen-section-header-background-color,.8);\n box-shadow: 0 1px 2px rgba($opblock-isopen-section-header-box-shadow-color,.1);\n\n >label\n {\n font-size: 12px;\n font-weight: bold;\n\n display: flex;\n align-items: center;\n\n margin: 0;\n margin-left: auto;\n\n @include text_headline();\n\n >span\n {\n padding: 0 10px 0 0;\n }\n }\n\n h4\n {\n font-size: 14px;\n\n flex: 1;\n\n margin: 0;\n\n @include text_headline();\n }\n }\n\n .opblock-summary-method\n {\n font-size: 14px;\n font-weight: bold;\n\n min-width: 80px;\n padding: 6px 15px;\n\n text-align: center;\n\n border-radius: 3px;\n background: $opblock-summary-method-background-color;\n text-shadow: 0 1px 0 rgba($opblock-summary-method-text-shadow-color,.1);\n\n @include text_headline($opblock-summary-method-font-color);\n }\n\n .opblock-summary-path,\n .opblock-summary-operation-id,\n .opblock-summary-path__deprecated\n {\n font-size: 16px;\n @media (max-width: 768px) {\n font-size: 12px;\n }\n\n\n display: flex;\n align-items: center;\n\n word-break: break-word;\n\n padding: 0 10px;\n\n @include text_code();\n\n }\n\n .opblock-summary-path__deprecated\n {\n text-decoration: line-through;\n }\n\n .opblock-summary-operation-id\n {\n font-size: 14px;\n }\n\n .opblock-summary-description\n {\n font-size: 13px;\n\n flex: 1 1 auto;\n\n word-break: break-word;\n\n @include text_body();\n }\n\n .opblock-summary\n {\n display: flex;\n align-items: center;\n\n padding: 5px;\n\n cursor: pointer;\n\n .view-line-link\n {\n position: relative;\n top: 2px;\n\n width: 0;\n margin: 0;\n\n cursor: pointer;\n transition: all .5s;\n }\n\n &:hover\n {\n .view-line-link\n {\n width: 18px;\n margin: 0 5px;\n }\n }\n }\n\n\n\n &.opblock-post\n {\n @include method($_color-post);\n }\n\n &.opblock-put\n {\n @include method($_color-put);\n }\n\n &.opblock-delete\n {\n @include method($_color-delete);\n }\n\n &.opblock-get\n {\n @include method($_color-get);\n }\n\n &.opblock-patch\n {\n @include method($_color-patch);\n }\n\n &.opblock-head\n {\n @include method($_color-head);\n }\n\n &.opblock-options\n {\n @include method($_color-options);\n }\n\n &.opblock-deprecated\n {\n opacity: .6;\n\n @include method($_color-disabled);\n }\n\n .opblock-schemes\n {\n padding: 8px 20px;\n\n .schemes-title\n {\n padding: 0 10px 0 0;\n }\n }\n}\n\n.filter\n{\n .operation-filter-input\n {\n width: 100%;\n margin: 20px 0;\n padding: 10px 10px;\n\n border: 2px solid $operational-filter-input-border-color;\n }\n}\n\n.filter, .download-url-wrapper\n{\n .failed\n {\n color: red;\n }\n\n .loading\n {\n color: #aaa;\n }\n}\n\n.model-example {\n margin-top: 1em;\n}\n\n.tab\n{\n display: flex;\n\n padding: 0;\n\n list-style: none;\n\n li\n {\n font-size: 12px;\n\n min-width: 60px;\n padding: 0;\n\n cursor: pointer;\n\n @include text_headline();\n\n &:first-of-type\n {\n position: relative;\n\n padding-left: 0;\n padding-right: 12px;\n\n &:after\n {\n position: absolute;\n top: 0;\n right: 6px;\n\n width: 1px;\n height: 100%;\n\n content: '';\n\n background: rgba($tab-list-item-first-background-color,.2);\n }\n }\n\n &.active\n {\n font-weight: bold;\n }\n }\n}\n\n.opblock-description-wrapper,\n.opblock-external-docs-wrapper,\n.opblock-title_normal\n{\n font-size: 12px;\n\n margin: 0 0 5px 0;\n padding: 15px 20px;\n\n @include text_body();\n\n h4\n {\n font-size: 12px;\n\n margin: 0 0 5px 0;\n\n @include text_body();\n }\n\n p\n {\n font-size: 14px;\n\n margin: 0;\n\n @include text_body();\n }\n}\n\n.opblock-external-docs-wrapper {\n h4 {\n padding-left: 0px;\n }\n}\n\n.execute-wrapper\n{\n padding: 20px;\n\n text-align: right;\n\n .btn\n {\n width: 100%;\n padding: 8px 40px;\n }\n}\n\n.body-param-options\n{\n display: flex;\n flex-direction: column;\n\n .body-param-edit\n {\n padding: 10px 0;\n }\n\n label\n {\n padding: 8px 0;\n select\n {\n margin: 3px 0 0 0;\n }\n }\n}\n\n.responses-inner\n{\n padding: 20px;\n\n h5,\n h4\n {\n font-size: 12px;\n\n margin: 10px 0 5px 0;\n\n @include text_body();\n }\n\n .curl\n {\n white-space: normal;\n }\n}\n\n.response-col_status\n{\n font-size: 14px;\n\n @include text_body();\n\n .response-undocumented\n {\n font-size: 11px;\n\n @include text_code($response-col-status-undocumented-font-color);\n }\n}\n\n.response-col_links\n{\n padding-left: 2em;\n max-width: 40em;\n font-size: 14px;\n\n @include text_body();\n\n .response-undocumented\n {\n font-size: 11px;\n\n @include text_code($response-col-links-font-color);\n }\n\n .operation-link\n {\n margin-bottom: 1.5em;\n\n .description\n {\n margin-bottom: 0.5em;\n }\n }\n}\n\n.opblock-body\n{\n .opblock-loading-animation\n {\n display: block;\n margin: 3em;\n margin-left: auto;\n margin-right: auto;\n }\n}\n\n.opblock-body pre.microlight\n{\n font-size: 12px;\n\n margin: 0;\n padding: 10px;\n\n white-space: pre-wrap;\n word-wrap: break-word;\n word-break: break-all;\n word-break: break-word;\n hyphens: auto;\n\n border-radius: 4px;\n background: $opblock-body-background-color;\n\n overflow-wrap: break-word;\n @include text_code($opblock-body-font-color);\n\n // disabled to have syntax highliting with react-syntax-highlight\n // span\n // {\n // color: $opblock-body-font-color !important;\n // }\n\n .headerline\n {\n display: block;\n }\n}\n\n.highlight-code {\n position: relative;\n\n > .microlight {\n overflow-y: auto;\n max-height: 400px;\n min-height: 6em;\n }\n}\n.curl-command {\n position: relative;\n}\n\n.download-contents {\n position: absolute;\n bottom: 10px;\n right: 10px;\n cursor: pointer;\n background: #7d8293;\n text-align: center;\n padding: 5px;\n border-radius: 4px;\n font-family: sans-serif;\n font-weight: 600;\n color: white;\n font-size: 14px;\n height: 30px;\n width: 75px;\n}\n\n.scheme-container\n{\n margin: 0 0 20px 0;\n padding: 30px 0;\n\n background: $scheme-container-background-color;\n box-shadow: 0 1px 2px 0 rgba($scheme-container-box-shadow-color,.15);\n\n .schemes\n {\n display: flex;\n align-items: flex-end;\n\n > label\n {\n font-size: 12px;\n font-weight: bold;\n\n display: flex;\n flex-direction: column;\n\n margin: -20px 15px 0 0;\n\n @include text_headline();\n\n select\n {\n min-width: 130px;\n\n text-transform: uppercase;\n }\n }\n }\n}\n\n.loading-container\n{\n padding: 40px 0 60px;\n margin-top: 1em;\n min-height: 1px;\n display: flex;\n justify-content: center;\n align-items: center;\n flex-direction: column;\n\n .loading\n {\n position: relative;\n\n\n &:after\n {\n font-size: 10px;\n font-weight: bold;\n\n position: absolute;\n top: 50%;\n left: 50%;\n\n content: 'loading';\n transform: translate(-50%,-50%);\n text-transform: uppercase;\n\n @include text_headline();\n }\n\n &:before\n {\n position: absolute;\n top: 50%;\n left: 50%;\n\n display: block;\n\n width: 60px;\n height: 60px;\n margin: -30px -30px;\n\n content: '';\n animation: rotation 1s infinite linear, opacity .5s;\n\n opacity: 1;\n border: 2px solid rgba($loading-container-before-border-color, .1);\n border-top-color: rgba($loading-container-before-border-top-color, .6);\n border-radius: 100%;\n\n backface-visibility: hidden;\n\n @keyframes rotation\n {\n to\n {\n transform: rotate(360deg);\n }\n }\n }\n }\n}\n\n.response-controls {\n padding-top: 1em;\n display: flex;\n}\n\n.response-control-media-type {\n margin-right: 1em;\n\n &--accept-controller {\n select {\n border-color: $response-content-type-controls-accept-header-select-border-color;\n }\n }\n\n &__accept-message {\n color: $response-content-type-controls-accept-header-small-font-color;\n font-size: .7em;\n }\n\n &__title {\n display: block;\n margin-bottom: 0.2em;\n font-size: .7em;\n }\n}\n\n.response-control-examples {\n &__title {\n display: block;\n margin-bottom: 0.2em;\n font-size: .7em;\n }\n}\n\n@keyframes blinker\n{\n 50%\n {\n opacity: 0;\n }\n}\n\n.hidden\n{\n display: none;\n}\n\n.no-margin\n{\n height: auto;\n border: none;\n margin: 0;\n padding: 0;\n}\n\n.float-right\n{\n float: right;\n}\n\nimg.full-width\n{\n width: 100%;\n}\n\n.svg-assets\n{\n position: absolute;\n width: 0;\n height: 0;\n}\n\nsection\n{\n h3\n {\n @include text_headline();\n }\n}\n\na.nostyle {\n text-decoration: inherit;\n color: inherit;\n cursor: pointer;\n display: inline;\n\n &:visited {\n text-decoration: inherit;\n color: inherit;\n cursor: pointer;\n }\n}\n\n.fallback\n{\n padding: 1em;\n color: #aaa;\n}\n\n.version-pragma {\n height: 100%;\n padding: 5em 0px;\n\n &__message {\n display: flex;\n justify-content: center;\n height: 100%;\n font-size: 1.2em;\n text-align: center;\n line-height: 1.5em;\n\n padding: 0px .6em;\n\n > div {\n max-width: 55ch;\n flex: 1;\n }\n\n code {\n background-color: #dedede;\n padding: 4px 4px 2px;\n white-space: pre;\n }\n }\n}\n\n.opblock-link\n{\n font-weight: normal;\n\n &.shown\n {\n font-weight: bold;\n }\n}\n\nspan\n{\n &.token-string\n {\n color: #555;\n }\n\n &.token-not-formatted\n {\n color: #555; \n font-weight: bold;\n }\n}\n"," // Base Colours\n$black: #000;\n$white: #fff;\n$gray-50: lighten($black, 92%); //ebebeb\n$gray-200: lighten($black, 62.75%); // #a0a0a0\n$gray-300: lighten($black, 56.5%); // #909090\n$gray-400: lighten($black, 50%); // #808080\n$gray-500: lighten($black, 43.75%); // #707070\n$gray-600: lighten($black, 37.5%); // #606060\n$gray-650: lighten($black, 33.3%); // #555555\n$gray-700: lighten($black, 31.25%); // #505050\n$gray-800: lighten($black, 25%); // #404040\n$gray-900: lighten($black, 18.75%); // #303030\n\n$cod-gray: #1b1b1b;\n$bright-gray: #3b4151;\n$mako-gray: #41444e;\n$waterloo-gray: #7d8492;\n$alto-gray: #d9d9d9;\n$mercury-gray: #e4e4e4;\n$concrete-gray: #e8e8e8;\n$alabaster: #f7f7f7;\n$apple-green: #62a03f;\n$green-haze: #009d77;\n$japanese-laurel: #008000;\n$persian-green: #00a0a7;\n$geyser-blue: #d8dde7;\n$dodger-blue: #1391ff;\n$endeavour-blue: #005dae;\n$scampi-purple: #55a;\n$electric-violet: #7300e5;\n$persian-red: #cf3030;\n$mango-tango: #e97500;\n\n// Theme\n\n$color-primary: #89bf04 !default;\n$color-secondary: #9012fe !default;\n$color-info: #4990e2 !default;\n$color-warning: #ff6060 !default;\n$color-danger: #f00 !default;\n\n$color-primary-hover: lighten($color-primary, .5%) !default;\n\n$_color-post: #49cc90 !default;\n$_color-get: #61affe !default;\n$_color-put: #fca130 !default;\n$_color-delete: #f93e3e !default;\n$_color-head: #9012fe !default;\n$_color-patch: #50e3c2 !default;\n$_color-disabled: #ebebeb !default;\n$_color-options: #0d5aa7 !default;\n\n// Authorize\n\n$auth-container-border-color: $gray-50 !default;\n$auth-select-all-none-link-font-color: $color-info !default;\n// Buttons\n\n$btn-background-color: transparent !default;\n$btn-border-color: $gray-400 !default;\n$btn-font-color: inherit !default;\n$btn-box-shadow-color: $black !default;\n\n$btn-authorize-background-color: transparent !default;\n$btn-authorize-border-color: $_color-post !default;\n$btn-authorize-font-color: $_color-post !default;\n$btn-authorize-svg-fill-color: $_color-post !default;\n\n$btn-cancel-background-color: transparent !default;\n$btn-cancel-border-color: $color-warning !default;\n$btn-cancel-font-color: $color-warning !default;\n\n$btn-execute-background-color: transparent !default;\n$btn-execute-border-color: $color-info !default;\n$btn-execute-font-color: $white !default;\n$btn-execute-background-color-alt: $color-info !default;\n\n$expand-methods-svg-fill-color: $gray-500 !default;\n$expand-methods-svg-fill-color-hover: $gray-800 !default;\n\n// Errors\n\n$errors-wrapper-background-color: $_color-delete !default;\n$errors-wrapper-border-color: $_color-delete !default;\n\n$errors-wrapper-errors-small-font-color: $gray-600 !default;\n\n// Form\n\n$form-select-background-color: $alabaster !default;\n$form-select-border-color: $mako-gray !default;\n$form-select-box-shadow-color: $black !default;\n\n$form-input-border-color: $alto-gray !default;\n$form-input-background-color: $white !default;\n\n$form-textarea-background-color: $white !default;\n$form-textarea-focus-border-color: $_color-get !default;\n\n$form-textarea-curl-background-color: $mako-gray !default;\n$form-textarea-curl-font-color: $white !default;\n\n$form-checkbox-label-font-color: $gray-900 !default;\n$form-checkbox-background-color: $concrete-gray !default;\n$form-checkbox-box-shadow-color: $concrete-gray !default;\n\n// Information\n\n$info-code-background-color: $black !default;\n$info-code-font-color: $_color-head !default;\n\n$info-link-font-color: $color-info !default;\n$info-link-font-color-hover: $info-link-font-color !default;\n\n$info-title-small-background-color: $waterloo-gray !default;\n\n$info-title-small-pre-font-color: $white !default;\n\n// Layout\n\n$opblock-border-color: $black !default;\n$opblock-box-shadow-color: $black !default;\n\n$opblock-tag-border-bottom-color: $bright-gray !default;\n$opblock-tag-background-color-hover: $black !default;\n\n$opblock-tab-header-tab-item-active-h4-span-after-background-color: $gray-400 !default;\n\n$opblock-isopen-summary-border-bottom-color: $black !default;\n\n$opblock-isopen-section-header-background-color: $white !default;\n$opblock-isopen-section-header-box-shadow-color: $black !default;\n\n$opblock-summary-method-background-color: $black !default;\n$opblock-summary-method-font-color: $white !default;\n$opblock-summary-method-text-shadow-color: $black !default;\n\n$operational-filter-input-border-color: $geyser-blue !default;\n\n$tab-list-item-first-background-color: $black !default;\n\n$response-col-status-undocumented-font-color: $gray-300 !default;\n\n$response-col-links-font-color: $gray-300 !default;\n\n$opblock-body-background-color: $mako-gray !default;\n$opblock-body-font-color: $white !default;\n\n$scheme-container-background-color: $white !default;\n$scheme-container-box-shadow-color: $black !default;\n\n$server-container-background-color: $white !default;\n$server-container-box-shadow-color: $black !default;\n\n$server-container-computed-url-code-font-color: $gray-400 !default;\n\n$loading-container-before-border-color: $gray-650 !default;\n$loading-container-before-border-top-color: $black !default;\n\n$response-content-type-controls-accept-header-select-border-color: $japanese-laurel !default;\n$response-content-type-controls-accept-header-small-font-color: $japanese-laurel !default;\n\n// Modal\n\n$dialog-ux-backdrop-background-color: $black !default;\n\n\n$dialog-ux-modal-background-color: $white !default;\n$dialog-ux-modal-border-color: $gray-50 !default;\n$dialog-ux-modal-box-shadow-color: $black !default;\n\n$dialog-ux-modal-content-font-color: $mako-gray !default;\n\n$dialog-ux-modal-header-border-bottom-color: $gray-50 !default;\n\n// Models\n\n$model-deprecated-font-color: $gray-200 !default;\n\n$model-hint-font-color: $gray-50 !default;\n$model-hint-background-color: $black !default;\n\n$section-models-border-color: $bright-gray !default;\n\n$section-models-isopen-h4-border-bottom-color: $section-models-border-color !default;\n\n$section-models-h4-font-color: $gray-600 !default;\n$section-models-h4-background-color-hover: $black !default;\n\n$section-models-h5-font-color: $gray-500 !default;\n\n$section-models-model-container-background-color: $black !default;\n\n$section-models-model-box-background-color: $black !default;\n\n$section-models-model-title-font-color: $gray-700 !default;\n\n$prop-type-font-color: $scampi-purple !default;\n\n$prop-format-font-color: $gray-600 !default;\n\n// Tables\n\n$table-thead-td-border-bottom-color: $bright-gray !default;\n\n$table-parameter-name-required-font-color: $color-danger !default;\n\n$table-parameter-in-font-color: $gray-400 !default;\n\n$table-parameter-deprecated-font-color: $color-danger !default;\n\n// Topbar\n\n$topbar-background-color: $cod-gray !default;\n\n$topbar-link-font-color: $white !default;\n\n$topbar-download-url-wrapper-element-border-color: $apple-green !default;\n\n$topbar-download-url-button-background-color: $apple-green !default;\n$topbar-download-url-button-font-color: $white !default;\n\n// Type\n\n$text-body-default-font-color: $bright-gray !default;\n$text-code-default-font-color: $bright-gray !default;\n$text-headline-default-font-color: $bright-gray !default;\n",".btn\n{\n font-size: 14px;\n font-weight: bold;\n\n padding: 5px 23px;\n\n transition: all .3s;\n\n border: 2px solid $btn-border-color;\n border-radius: 4px;\n background: transparent;\n box-shadow: 0 1px 2px rgba($btn-box-shadow-color,.1);\n\n @include text_headline();\n\n &.btn-sm\n {\n font-size: 12px;\n padding: 4px 23px;\n }\n\n &[disabled]\n {\n cursor: not-allowed;\n\n opacity: .3;\n }\n\n &:hover\n {\n box-shadow: 0 0 5px rgba($btn-box-shadow-color,.3);\n }\n\n &.cancel\n {\n border-color: $btn-cancel-border-color;\n background-color: $btn-cancel-background-color;\n @include text_headline($btn-cancel-font-color);\n }\n\n &.authorize\n {\n line-height: 1;\n\n display: inline;\n\n color: $btn-authorize-font-color;\n border-color: $btn-authorize-border-color;\n background-color: $btn-authorize-background-color;\n\n span\n {\n float: left;\n\n padding: 4px 20px 0 0;\n }\n\n svg\n {\n fill: $btn-authorize-svg-fill-color;\n }\n }\n\n &.execute\n {\n background-color: $btn-execute-background-color-alt;\n color: $btn-execute-font-color;\n border-color: $btn-execute-border-color;\n }\n}\n\n.btn-group\n{\n display: flex;\n\n padding: 30px;\n\n .btn\n {\n flex: 1;\n\n &:first-child\n {\n border-radius: 4px 0 0 4px;\n }\n\n &:last-child\n {\n border-radius: 0 4px 4px 0;\n }\n }\n}\n\n.authorization__btn\n{\n padding: 0 10px;\n\n border: none;\n background: none;\n\n &.locked\n {\n opacity: 1;\n }\n\n &.unlocked\n {\n opacity: .4;\n }\n}\n\n.expand-methods,\n.expand-operation\n{\n border: none;\n background: none;\n\n svg\n {\n width: 20px;\n height: 20px;\n }\n}\n\n.expand-methods\n{\n padding: 0 10px;\n\n &:hover\n {\n svg\n {\n fill: $expand-methods-svg-fill-color-hover;\n }\n }\n\n svg\n {\n transition: all .3s;\n\n fill: $expand-methods-svg-fill-color;\n }\n}\n\n\nbutton\n{\n cursor: pointer;\n outline: none;\n\n &.invalid\n {\n @include invalidFormElement();\n }\n}\n\n.copy-to-clipboard\n{\n position: absolute;\n bottom: 10px;\n right: 100px;\n width: 30px;\n height: 30px;\n background: #7d8293;\n border-radius: 4px;\n border: none;\n\n button\n {\n padding-left: 25px;\n border: none;\n height: 25px;\n background: url(\"data:image/svg+xml, \") center center no-repeat;\n }\n}\n\n// overrides for smaller copy button for curl command\n.curl-command .copy-to-clipboard\n{\n bottom: 5px;\n right: 10px;\n width: 20px;\n height: 20px;\n button\n {\n padding-left: 18px;\n height: 18px\n }\n}\n","// - - - - - - - - - - - - - - - - - - -\n// - - _mixins.scss module\n// styles for the _mixins.scss module\n@function calculateRem($size)\n{\n $remSize: $size / 16px;\n @return $remSize * 1rem;\n}\n\n@mixin font-size($size)\n{\n font-size: $size;\n font-size: calculateRem($size);\n}\n\n%clearfix\n{\n *zoom: 1;\n &:before,\n &:after\n {\n display: table;\n\n content: ' ';\n }\n &:after\n {\n clear: both;\n }\n}\n\n@mixin size($width, $height: $width)\n{\n width: $width;\n height: $height;\n}\n\n$ease: (\n in-quad: cubic-bezier(.550, .085, .680, .530),\n in-cubic: cubic-bezier(.550, .055, .675, .190),\n in-quart: cubic-bezier(.895, .030, .685, .220),\n in-quint: cubic-bezier(.755, .050, .855, .060),\n in-sine: cubic-bezier(.470, .000, .745, .715),\n in-expo: cubic-bezier(.950, .050, .795, .035),\n in-circ: cubic-bezier(.600, .040, .980, .335),\n in-back: cubic-bezier(.600, -.280, .735, .045),\n out-quad: cubic-bezier(.250, .460, .450, .940),\n out-cubic: cubic-bezier(.215, .610, .355, 1.000),\n out-quart: cubic-bezier(.165, .840, .440, 1.000),\n out-quint: cubic-bezier(.230, 1.000, .320, 1.000),\n out-sine: cubic-bezier(.390, .575, .565, 1.000),\n out-expo: cubic-bezier(.190, 1.000, .220, 1.000),\n out-circ: cubic-bezier(.075, .820, .165, 1.000),\n out-back: cubic-bezier(.175, .885, .320, 1.275),\n in-out-quad: cubic-bezier(.455, .030, .515, .955),\n in-out-cubic: cubic-bezier(.645, .045, .355, 1.000),\n in-out-quart: cubic-bezier(.770, .000, .175, 1.000),\n in-out-quint: cubic-bezier(.860, .000, .070, 1.000),\n in-out-sine: cubic-bezier(.445, .050, .550, .950),\n in-out-expo: cubic-bezier(1.000, .000, .000, 1.000),\n in-out-circ: cubic-bezier(.785, .135, .150, .860),\n in-out-back: cubic-bezier(.680, -.550, .265, 1.550)\n);\n\n@function ease($key)\n{\n @if map-has-key($ease, $key)\n {\n @return map-get($ease, $key);\n }\n\n @warn 'Unkown \\'#{$key}\\' in $ease.';\n @return null;\n}\n\n\n@mixin ease($key)\n{\n transition-timing-function: ease($key);\n}\n\n@mixin text-truncate\n{\n overflow: hidden;\n\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n@mixin aspect-ratio($width, $height)\n{\n position: relative;\n &:before\n {\n display: block;\n\n width: 100%;\n padding-top: ($height / $width) * 100%;\n\n content: '';\n }\n > iframe\n {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n }\n}\n\n$browser-context: 16;\n\n@function em($pixels, $context: $browser-context)\n{\n @if (unitless($pixels))\n {\n $pixels: $pixels * 1px;\n }\n\n @if (unitless($context))\n {\n $context: $context * 1px;\n }\n\n @return $pixels / $context * 1em;\n}\n\n@mixin maxHeight($height)\n{\n @media (max-height: $height)\n {\n @content;\n }\n}\n\n\n@mixin breakpoint($class)\n{\n @if $class == tablet\n {\n @media (min-width: 768px) and (max-width: 1024px)\n {\n @content;\n }\n }\n\n @else if $class == mobile\n {\n @media (min-width: 320px) and (max-width : 736px)\n {\n @content;\n }\n }\n\n @else if $class == desktop\n {\n @media (min-width: 1400px)\n {\n @content;\n }\n }\n\n @else\n {\n @warn 'Breakpoint mixin supports: tablet, mobile, desktop';\n }\n}\n\n@mixin invalidFormElement() {\n animation: shake .4s 1;\n border-color: $_color-delete;\n background: lighten($_color-delete, 35%);\n}\n","select\n{\n font-size: 14px;\n font-weight: bold;\n\n padding: 5px 40px 5px 10px;\n\n border: 2px solid $form-select-border-color;\n border-radius: 4px;\n background: $form-select-background-color url('data:image/svg+xml, ') right 10px center no-repeat;\n background-size: 20px;\n box-shadow: 0 1px 2px 0 rgba($form-select-box-shadow-color, .25);\n\n @include text_headline();\n appearance: none;\n\n &[multiple]\n {\n margin: 5px 0;\n padding: 5px;\n\n background: $form-select-background-color;\n }\n\n &.invalid {\n @include invalidFormElement();\n }\n}\n\n.opblock-body select\n{\n min-width: 230px;\n @media (max-width: 768px)\n {\n min-width: 180px;\n }\n}\n\nlabel\n{\n font-size: 12px;\n font-weight: bold;\n\n margin: 0 0 5px 0;\n\n @include text_headline();\n}\n\ninput[type=text],\ninput[type=password],\ninput[type=search],\ninput[type=email],\ninput[type=file]\n{\n @media (max-width: 768px) {\n max-width: 175px;\n }\n}\n\n\ninput[type=text],\ninput[type=password],\ninput[type=search],\ninput[type=email],\ninput[type=file],\ntextarea\n{\n min-width: 100px;\n margin: 5px 0;\n padding: 8px 10px;\n\n border: 1px solid $form-input-border-color;\n border-radius: 4px;\n background: $form-input-background-color;\n\n\n &.invalid\n {\n @include invalidFormElement();\n }\n\n}\n\ninput,\ntextarea,\nselect {\n &[disabled] {\n // opacity: 0.85;\n background-color: #fafafa;\n color: #888;\n cursor: not-allowed;\n }\n}\n\nselect[disabled] {\n border-color: #888;\n}\n\ntextarea[disabled] {\n background-color: #41444e;\n color: #fff;\n}\n\n@keyframes shake\n{\n 10%,\n 90%\n {\n transform: translate3d(-1px, 0, 0);\n }\n\n 20%,\n 80%\n {\n transform: translate3d(2px, 0, 0);\n }\n\n 30%,\n 50%,\n 70%\n {\n transform: translate3d(-4px, 0, 0);\n }\n\n 40%,\n 60%\n {\n transform: translate3d(4px, 0, 0);\n }\n}\n\ntextarea\n{\n font-size: 12px;\n\n width: 100%;\n min-height: 280px;\n padding: 10px;\n\n border: none;\n border-radius: 4px;\n outline: none;\n background: rgba($form-textarea-background-color,.8);\n\n @include text_code();\n\n &:focus\n {\n border: 2px solid $form-textarea-focus-border-color;\n }\n\n &.curl\n {\n font-size: 12px;\n\n min-height: 100px;\n margin: 0;\n padding: 10px;\n\n resize: none;\n\n border-radius: 4px;\n background: $form-textarea-curl-background-color;\n\n @include text_code($form-textarea-curl-font-color);\n }\n}\n\n\n.checkbox\n{\n padding: 5px 0 10px;\n\n transition: opacity .5s;\n\n color: $form-checkbox-label-font-color;\n\n label\n {\n display: flex;\n }\n\n p\n {\n font-weight: normal !important;\n font-style: italic;\n\n margin: 0 !important;\n\n @include text_code();\n }\n\n input[type=checkbox]\n {\n display: none;\n\n & + label > .item\n {\n position: relative;\n top: 3px;\n\n display: inline-block;\n\n width: 16px;\n height: 16px;\n margin: 0 8px 0 0;\n padding: 5px;\n\n cursor: pointer;\n\n border-radius: 1px;\n background: $form-checkbox-background-color;\n box-shadow: 0 0 0 2px $form-checkbox-box-shadow-color;\n\n flex: none;\n\n &:active\n {\n transform: scale(.9);\n }\n }\n\n &:checked + label > .item\n {\n background: $form-checkbox-background-color url('data:image/svg+xml, ') center center no-repeat;\n }\n }\n}\n",".dialog-ux\n{\n position: fixed;\n z-index: 9999;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n\n .backdrop-ux\n {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n\n background: rgba($dialog-ux-backdrop-background-color,.8);\n }\n\n .modal-ux\n {\n position: absolute;\n z-index: 9999;\n top: 50%;\n left: 50%;\n\n width: 100%;\n min-width: 300px;\n max-width: 650px;\n\n transform: translate(-50%,-50%);\n\n border: 1px solid $dialog-ux-modal-border-color;\n border-radius: 4px;\n background: $dialog-ux-modal-background-color;\n box-shadow: 0 10px 30px 0 rgba($dialog-ux-modal-box-shadow-color,.20);\n }\n\n .modal-ux-content\n {\n overflow-y: auto;\n\n max-height: 540px;\n padding: 20px;\n\n p\n {\n font-size: 12px;\n\n margin: 0 0 5px 0;\n\n color: $dialog-ux-modal-content-font-color;\n\n @include text_body();\n }\n\n h4\n {\n font-size: 18px;\n font-weight: 600;\n\n margin: 15px 0 0 0;\n\n @include text_headline();\n }\n }\n\n .modal-ux-header\n {\n display: flex;\n\n padding: 12px 0;\n\n border-bottom: 1px solid $dialog-ux-modal-header-border-bottom-color;\n\n align-items: center;\n\n .close-modal\n {\n padding: 0 10px;\n\n border: none;\n background: none;\n\n appearance: none;\n }\n\n\n h3\n {\n font-size: 20px;\n font-weight: 600;\n\n margin: 0;\n padding: 0 20px;\n\n flex: 1;\n @include text_headline();\n }\n }\n}\n",".model\n{\n font-size: 12px;\n font-weight: 300;\n\n @include text_code();\n\n .deprecated\n {\n span,\n td\n {\n color: $model-deprecated-font-color !important;\n }\n\n > td:first-of-type {\n text-decoration: line-through;\n }\n }\n &-toggle\n {\n font-size: 10px;\n\n position: relative;\n top: 6px;\n\n display: inline-block;\n\n margin: auto .3em;\n\n cursor: pointer;\n transition: transform .15s ease-in;\n transform: rotate(90deg);\n transform-origin: 50% 50%;\n\n &.collapsed\n {\n transform: rotate(0deg);\n }\n\n &:after\n {\n display: block;\n\n width: 20px;\n height: 20px;\n\n content: '';\n\n background: url('data:image/svg+xml, ') center no-repeat;\n background-size: 100%;\n }\n }\n\n &-jump-to-path\n {\n position: relative;\n\n cursor: pointer;\n\n .view-line-link\n {\n position: absolute;\n top: -.4em;\n\n cursor: pointer;\n }\n }\n\n &-title\n {\n position: relative;\n\n &:hover .model-hint\n {\n visibility: visible;\n }\n }\n\n &-hint\n {\n position: absolute;\n top: -1.8em;\n\n visibility: hidden;\n\n padding: .1em .5em;\n\n white-space: nowrap;\n\n color: $model-hint-font-color;\n border-radius: 4px;\n background: rgba($model-hint-background-color,.7);\n }\n\n p\n {\n margin: 0 0 1em 0;\n }\n\n .property\n {\n color: #999;\n font-style: italic;\n\n &.primitive\n {\n color: #6b6b6b;\n }\n }\n}\n\ntable.model\n{\n tr\n {\n &.description\n {\n color: #666;\n font-weight: normal;\n \n td:first-child\n {\n font-weight: bold;\n }\n }\n\n &.property-row\n {\n &.required td:first-child\n {\n font-weight: bold;\n }\n\n td\n {\n vertical-align: top;\n\n &:first-child\n {\n padding-right: 0.2em;\n }\n }\n\n .star\n {\n color: red;\n }\n }\n\n &.extension\n {\n color: #777;\n\n td:last-child\n {\n vertical-align: top;\n }\n }\n }\n}\n\nsection.models\n{\n margin: 30px 0;\n\n border: 1px solid rgba($section-models-border-color, .3);\n border-radius: 4px;\n\n .pointer\n {\n cursor: pointer;\n }\n\n &.is-open\n {\n padding: 0 0 20px;\n h4\n {\n margin: 0 0 5px 0;\n\n border-bottom: 1px solid rgba($section-models-isopen-h4-border-bottom-color, .3);\n }\n }\n h4\n {\n font-size: 16px;\n\n display: flex;\n align-items: center;\n\n margin: 0;\n padding: 10px 20px 10px 10px;\n\n cursor: pointer;\n transition: all .2s;\n\n @include text_headline($section-models-h4-font-color);\n\n svg\n {\n transition: all .4s;\n }\n\n span\n {\n flex: 1;\n }\n\n &:hover\n {\n background: rgba($section-models-h4-background-color-hover,.02);\n }\n }\n\n h5\n {\n font-size: 16px;\n\n margin: 0 0 10px 0;\n\n @include text_headline($section-models-h5-font-color);\n }\n\n .model-jump-to-path\n {\n position: relative;\n top: 5px;\n }\n\n .model-container\n {\n margin: 0 20px 15px;\n position: relative;\n\n transition: all .5s;\n\n border-radius: 4px;\n background: rgba($section-models-model-container-background-color,.05);\n\n &:hover\n {\n background: rgba($section-models-model-container-background-color,.07);\n }\n\n &:first-of-type\n {\n margin: 20px;\n }\n\n &:last-of-type\n {\n margin: 0 20px;\n }\n\n .models-jump-to-path {\n position: absolute;\n top: 8px;\n right: 5px;\n opacity: 0.65;\n }\n }\n\n .model-box\n {\n background: none;\n }\n}\n\n\n.model-box\n{\n padding: 10px;\n display: inline-block;\n\n border-radius: 4px;\n background: rgba($section-models-model-box-background-color,.1);\n\n .model-jump-to-path\n {\n position: relative;\n top: 4px;\n }\n\n &.deprecated\n {\n opacity: .5;\n }\n}\n\n\n.model-title\n{\n font-size: 16px;\n\n @include text_headline($section-models-model-title-font-color);\n\n img\n {\n margin-left: 1em;\n position: relative;\n bottom: 0px;\n }\n}\n\n.model-deprecated-warning\n{\n font-size: 16px;\n font-weight: 600;\n\n margin-right: 1em;\n\n @include text_headline($_color-delete);\n}\n\n\nspan\n{\n > span.model\n {\n .brace-close\n {\n padding: 0 0 0 10px;\n }\n }\n}\n\n.prop-name\n{\n display: inline-block;\n\n margin-right: 1em;\n}\n\n.prop-type\n{\n color: $prop-type-font-color;\n}\n\n.prop-enum\n{\n display: block;\n}\n.prop-format\n{\n color: $prop-format-font-color;\n}\n",".servers\n{\n > label\n {\n font-size: 12px;\n\n margin: -20px 15px 0 0;\n\n @include text_headline();\n\n select\n {\n min-width: 130px;\n max-width: 100%;\n }\n }\n\n h4.message {\n padding-bottom: 2em;\n }\n\n table {\n tr {\n width: 30em;\n }\n td {\n display: inline-block;\n max-width: 15em;\n vertical-align: middle;\n padding-top: 10px;\n padding-bottom: 10px;\n\n &:first-of-type {\n padding-right: 2em;\n }\n\n input {\n width: 100%;\n height: 100%;\n }\n }\n }\n\n .computed-url {\n margin: 2em 0;\n\n code {\n display: inline-block;\n padding: 4px;\n font-size: 16px;\n margin: 0 1em;\n }\n }\n}\n\n.servers-title {\n font-size: 12px;\n font-weight: bold;\n}\n\n.operation-servers {\n h4.message {\n margin-bottom: 2em;\n }\n}\n","table\n{\n width: 100%;\n padding: 0 10px;\n\n border-collapse: collapse;\n\n &.model\n {\n tbody\n {\n tr\n {\n td\n {\n padding: 0;\n\n vertical-align: top;\n\n &:first-of-type\n {\n width: 174px;\n padding: 0 0 0 2em;\n }\n }\n }\n }\n }\n\n &.headers\n {\n td\n {\n font-size: 12px;\n font-weight: 300;\n\n vertical-align: middle;\n\n @include text_code();\n }\n\n .header-example \n {\n color: #999; \n font-style: italic;\n }\n }\n\n tbody\n {\n tr\n {\n td\n {\n padding: 10px 0 0 0;\n\n vertical-align: top;\n\n &:first-of-type\n {\n max-width: 20%;\n min-width: 6em;\n padding: 10px 0;\n }\n }\n }\n }\n\n thead\n {\n tr\n {\n th,\n td\n {\n font-size: 12px;\n font-weight: bold;\n\n padding: 12px 0;\n\n text-align: left;\n\n border-bottom: 1px solid rgba($table-thead-td-border-bottom-color, .2);\n\n @include text_body();\n }\n }\n }\n}\n\n.parameters-col_description\n{\n width: 99%; // forces other columns to shrink to their content widths\n margin-bottom: 2em;\n input[type=text]\n {\n width: 100%;\n max-width: 340px;\n }\n\n select {\n border-width: 1px;\n }\n}\n\n.parameter__name\n{\n font-size: 16px;\n font-weight: normal;\n\n // hack to give breathing room to the name column\n // TODO: refactor all of this to flexbox\n margin-right: .75em;\n\n @include text_headline();\n\n &.required\n {\n font-weight: bold;\n\n span\n {\n color: red;\n }\n\n &:after\n {\n font-size: 10px;\n\n position: relative;\n top: -6px;\n\n padding: 5px;\n\n content: 'required';\n\n color: rgba($table-parameter-name-required-font-color, .6);\n }\n }\n}\n\n.parameter__in,\n.parameter__extension\n{\n font-size: 12px;\n font-style: italic;\n\n @include text_code($table-parameter-in-font-color);\n}\n\n.parameter__deprecated\n{\n font-size: 12px;\n font-style: italic;\n\n @include text_code($table-parameter-deprecated-font-color);\n}\n\n.parameter__empty_value_toggle {\n display: block;\n font-size: 13px;\n padding-top: 5px;\n padding-bottom: 12px;\n\n input {\n margin-right: 7px;\n }\n\n &.disabled {\n opacity: 0.7;\n }\n}\n\n\n.table-container\n{\n padding: 20px;\n}\n\n\n.response-col_description {\n width: 99%; // forces other columns to shrink to their content widths\n}\n\n.response-col_links {\n min-width: 6em;\n}\n",".topbar\n{\n padding: 10px 0;\n\n background-color: $topbar-background-color;\n .topbar-wrapper\n {\n display: flex;\n align-items: center;\n }\n a\n {\n font-size: 1.5em;\n font-weight: bold;\n\n display: flex;\n align-items: center;\n flex: 1;\n\n max-width: 300px;\n\n text-decoration: none;\n\n @include text_headline($topbar-link-font-color);\n\n span\n {\n margin: 0;\n padding: 0 10px;\n }\n }\n\n .download-url-wrapper\n {\n display: flex;\n flex: 3;\n justify-content: flex-end;\n\n input[type=text]\n {\n width: 100%;\n margin: 0;\n\n border: 2px solid $topbar-download-url-wrapper-element-border-color;\n border-radius: 4px 0 0 4px;\n outline: none;\n }\n\n .select-label\n {\n display: flex;\n align-items: center;\n\n width: 100%;\n max-width: 600px;\n margin: 0;\n color: #f0f0f0;\n span\n {\n font-size: 16px;\n\n flex: 1;\n\n padding: 0 10px 0 0;\n\n text-align: right;\n }\n\n select\n {\n flex: 2;\n\n width: 100%;\n\n border: 2px solid $topbar-download-url-wrapper-element-border-color;\n outline: none;\n box-shadow: none;\n }\n }\n\n\n .download-url-button\n {\n font-size: 16px;\n font-weight: bold;\n\n padding: 4px 30px;\n\n border: none;\n border-radius: 0 4px 4px 0;\n background: $topbar-download-url-button-background-color;\n\n @include text_headline($topbar-download-url-button-font-color);\n }\n }\n}\n",".info\n{\n margin: 50px 0;\n\n &.failed-config\n { \n max-width: 880px;\n margin-left: auto;\n margin-right: auto;\n text-align: center\n }\n\n hgroup.main\n {\n margin: 0 0 20px 0;\n a\n {\n font-size: 12px;\n }\n }\n pre \n {\n font-size: 14px;\n }\n p, li, table\n {\n font-size: 14px;\n\n @include text_body();\n }\n\n h1, h2, h3, h4, h5\n {\n @include text_body();\n }\n\n a\n {\n font-size: 14px;\n\n transition: all .4s;\n\n @include text_body($info-link-font-color);\n\n &:hover\n {\n color: darken($info-link-font-color-hover, 15%);\n }\n }\n > div\n {\n margin: 0 0 5px 0;\n }\n\n .base-url\n {\n font-size: 12px;\n font-weight: 300 !important;\n\n margin: 0;\n\n @include text_code();\n }\n\n .title\n {\n font-size: 36px;\n\n margin: 0;\n\n @include text_body();\n\n small\n {\n font-size: 10px;\n\n position: relative;\n top: -5px;\n\n display: inline-block;\n\n margin: 0 0 0 5px;\n padding: 2px 4px;\n\n vertical-align: super;\n\n border-radius: 57px;\n background: $info-title-small-background-color;\n \n &.version-stamp\n {\n background-color: #89bf04;\n }\n\n pre\n {\n margin: 0;\n padding: 0;\n\n @include text_headline($info-title-small-pre-font-color);\n }\n }\n }\n}\n",".auth-btn-wrapper\n{\n display: flex;\n\n padding: 10px 0;\n\n justify-content: center;\n\n .btn-done {\n margin-right: 1em;\n }\n}\n\n.auth-wrapper\n{\n display: flex;\n\n flex: 1;\n justify-content: flex-end;\n\n .authorize\n {\n padding-right: 20px;\n margin-right: 10px;\n }\n}\n\n.auth-container\n{\n margin: 0 0 10px 0;\n padding: 10px 20px;\n\n border-bottom: 1px solid $auth-container-border-color;\n\n &:last-of-type\n {\n margin: 0;\n padding: 10px 20px;\n\n border: 0;\n }\n\n h4\n {\n margin: 5px 0 15px 0 !important;\n }\n\n .wrapper\n {\n margin: 0;\n padding: 0;\n }\n\n input[type=text],\n input[type=password]\n {\n min-width: 230px;\n }\n\n .errors\n {\n font-size: 12px;\n\n padding: 10px;\n\n border-radius: 4px;\n\n background-color: #ffeeee;\n\n color: red;\n \n margin: 1em;\n\n @include text_code();\n\n b\n {\n text-transform: capitalize;\n margin-right: 1em;\n }\n }\n}\n\n.scopes\n{\n h2\n {\n font-size: 14px;\n\n @include text_headline();\n\n a\n {\n font-size: 12px;\n color: $auth-select-all-none-link-font-color;\n cursor: pointer;\n padding-left: 10px;\n text-decoration: underline;\n }\n }\n}\n\n.scope-def\n{\n padding: 0 0 20px 0;\n}\n",".errors-wrapper\n{\n margin: 20px;\n padding: 10px 20px;\n\n animation: scaleUp .5s;\n\n border: 2px solid $_color-delete;\n border-radius: 4px;\n background: rgba($_color-delete, .1);\n\n .error-wrapper\n {\n margin: 0 0 10px 0;\n }\n\n .errors\n {\n h4\n {\n font-size: 14px;\n\n margin: 0;\n\n @include text_code();\n }\n\n small\n {\n color: $errors-wrapper-errors-small-font-color;\n }\n\n .message\n { \n white-space: pre-line;\n \n &.thrown\n {\n max-width: 100%;\n }\n }\n\n .error-line\n {\n text-decoration: underline;\n cursor: pointer;\n }\n }\n\n hgroup\n {\n display: flex;\n\n align-items: center;\n\n h4\n {\n font-size: 20px;\n\n margin: 0;\n\n flex: 1;\n @include text_headline();\n }\n }\n}\n\n\n@keyframes scaleUp\n{\n 0%\n {\n transform: scale(.8);\n\n opacity: 0;\n }\n 100%\n {\n transform: scale(1);\n\n opacity: 1;\n }\n}\n",".Resizer.vertical.disabled {\n display: none;\n}",".markdown, .renderedMarkdown {\n p, pre {\n margin: 1em auto;\n }\n pre {\n color: black;\n font-weight: normal;\n white-space: pre-wrap;\n background: none;\n padding: 0px;\n }\n\n code {\n font-size: 14px;\n padding: 5px 7px;\n\n border-radius: 4px;\n background: rgba($info-code-background-color,.05);\n\n @include text_code($info-code-font-color);\n }\n\n pre > code {\n display: block;\n }\n}"],"sourceRoot":""} \ No newline at end of file diff --git a/public/assets/swagger/swagger-ui.js b/public/assets/swagger/swagger-ui.js new file mode 100755 index 0000000..0a90e1f --- /dev/null +++ b/public/assets/swagger/swagger-ui.js @@ -0,0 +1,9 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUICore=t():e.SwaggerUICore=t()}(window,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=450)}([function(e,t){e.exports=require("react")},function(e,t,n){e.exports=n(658)},function(e,t){e.exports=require("immutable")},function(e,t,n){var r=n(51);e.exports=function(e,t,n){return t in e?r(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}},function(e,t,n){var r=n(667),o=n(375);function a(t){return e.exports=a=o?r:function(e){return e.__proto__||r(e)},a(t)}e.exports=a},function(e,t,n){var r=n(51);function o(e,t){for(var n=0;n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var u,s=!0,f=!1;return{s:function(){n=o()(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){f=!0,u=e},f:function(){try{s||null==n.return||n.return()}finally{if(f)throw u}}}}function $(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1)try{return decodeURIComponent(t[1])}catch(e){console.error(e)}return null}function le(e){return t=e.replace(/\.[^./]*$/,""),C()(w()(t));var t}var fe=function(e,t){if(e>t)return"Value must be less than Maximum"},pe=function(e,t){if(et)return"Value must be less than MaxLength"},xe=function(e,t){if(e.length2&&void 0!==arguments[2]?arguments[2]:{},r=n.isOAS3,o=void 0!==r&&r,a=n.bypassRequiredCheck,i=void 0!==a&&a,u=[],c=e.get("required"),s=Object(V.a)(e,{isOAS3:o}),l=s.schema,f=s.parameterContentMediaType;if(!l)return u;var p=l.get("required"),h=l.get("maximum"),d=l.get("minimum"),v=l.get("type"),m=l.get("format"),y=l.get("maxLength"),b=l.get("minLength"),x=l.get("pattern");if(v&&(c||p||t)){var j="string"===v&&t,_="array"===v&&g()(t)&&t.length,w="array"===v&&S.a.List.isList(t)&&t.count(),O="array"===v&&"string"==typeof t&&t,C="file"===v&&t instanceof B.a.File,A="boolean"===v&&(t||!1===t),k="number"===v&&(t||0===t),P="integer"===v&&(t||0===t),I="object"===v&&"object"===E()(t)&&null!==t,T="object"===v&&"string"==typeof t&&t,R=[j,_,w,O,C,A,k,P,I,T],N=R.some((function(e){return!!e}));if((c||p)&&!N&&!i)return u.push("Required field is not provided"),u;if("object"===v&&"string"==typeof t&&(null===f||"application/json"===f))try{JSON.parse(t)}catch(e){return u.push("Parameter string value must be valid JSON"),u}if(x){var M=Se(t,x);M&&u.push(M)}if(y||0===y){var D=Ee(t,y);D&&u.push(D)}if(b){var q=xe(t,b);q&&u.push(q)}if(h||0===h){var L=fe(t,h);L&&u.push(L)}if(d||0===d){var U=pe(t,d);U&&u.push(U)}if("string"===v){var z;if(!(z="date-time"===m?ge(t):"uuid"===m?be(t):ye(t)))return u;u.push(z)}else if("boolean"===v){var F=me(t);if(!F)return u;u.push(F)}else if("number"===v){var J=he(t);if(!J)return u;u.push(J)}else if("integer"===v){var W=de(t);if(!W)return u;u.push(W)}else if("array"===v){var H;if(!w||!t.count())return u;H=l.getIn(["items","type"]),t.forEach((function(e,t){var n;"number"===H?n=he(e):"integer"===H?n=de(e):"string"===H&&(n=ye(e)),n&&u.push({index:t,error:n})}))}else if("file"===v){var $=ve(t);if(!$)return u;u.push($)}}return u},_e=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(/xml/.test(t)){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e':null;var r=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=r[1]}return Object(D.memoizedCreateXMLExample)(e,n)}var o=Object(D.memoizedSampleFromSchema)(e,n);return"object"===E()(o)?p()(o,null,2):o},we=function(){var e={},t=B.a.location.search;if(!t)return{};if(""!=t){var n=t.substr(1).split("&");for(var r in n)n.hasOwnProperty(r)&&(r=n[r].split("="),e[decodeURIComponent(r[0])]=r[1]&&decodeURIComponent(r[1])||"")}return e},Oe=function(t){return(t instanceof e?t:new e(t.toString(),"utf-8")).toString("base64")},Ce={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function(e,t){return e.localeCompare(t)}}},Ae=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},ke=function(e,t,n){return!!I()(n,(function(n){return R()(e[n],t[n])}))};function Pe(e){return"string"!=typeof e||""===e?"":Object(j.sanitizeUrl)(e)}function Ie(e){return!(!e||e.indexOf("localhost")>=0||e.indexOf("127.0.0.1")>=0||"none"===e)}function Te(e){if(!S.a.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=e.find((function(e,t){return t.startsWith("2")&&m()(e.get("content")||{}).length>0})),n=e.get("default")||S.a.OrderedMap(),r=(n.get("content")||S.a.OrderedMap()).keySeq().toJS().length?n:null;return t||r}var Re=function(e){return"string"==typeof e||e instanceof String?e.trim().replace(/\s/g,"%20"):""},Ne=function(e){return U()(Re(e).replace(/%20/g,"_"))},Me=function(e){return e.filter((function(e,t){return/^x-/.test(t)}))},De=function(e){return e.filter((function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)}))};function qe(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==E()(e)||g()(e)||null===e||!t)return e;var r=d()({},e);return m()(r).forEach((function(e){e===t&&n(r[e],e)?delete r[e]:r[e]=qe(r[e],t,n)})),r}function Be(e){if("string"==typeof e)return e;if(e&&e.toJS&&(e=e.toJS()),"object"===E()(e)&&null!==e)try{return p()(e,null,2)}catch(t){return String(e)}return null==e?"":e.toString()}function Le(e){return"number"==typeof e?e.toString():e}function Ue(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.returnAll,r=void 0!==n&&n,o=t.allowHashes,a=void 0===o||o;if(!S.a.Map.isMap(e))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");var i=e.get("name"),u=e.get("in"),c=[];return e&&e.hashCode&&u&&i&&a&&c.push("".concat(u,".").concat(i,".hash-").concat(e.hashCode())),u&&i&&c.push("".concat(u,".").concat(i)),c.push(i),r?c:c[0]||""}function Ve(e,t){return Ue(e,{returnAll:!0}).map((function(e){return t[e]})).filter((function(e){return void 0!==e}))[0]}function ze(){return Je(F()(32).toString("base64"))}function Fe(e){return Je(W()("sha256").update(e).digest("base64"))}function Je(e){return e.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}var We=function(e){return!e||!(!Y(e)||!e.isEmpty())}}).call(this,n(535).Buffer)},function(e,t,n){var r=n(174),o=n(663);e.exports=function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=r(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&o(e,t)}},function(e,t){e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}},function(e,t,n){var r=n(17),o=n(9);e.exports=function(e,t){return!t||"object"!==r(t)&&"function"!=typeof t?o(e):t}},function(e,t){e.exports=require("prop-types")},function(e,t,n){e.exports=n(515)},function(e,t,n){e.exports=n(529)},function(e,t,n){var r=n(341),o=n(543),a=n(151),i=n(344);e.exports=function(e,t){return r(e)||o(e,t)||a(e,t)||i()}},function(e,t,n){var r=n(636),o=n(366),a=n(151),i=n(637);e.exports=function(e){return r(e)||o(e)||a(e)||i()}},function(e,t){e.exports=require("reselect")},function(e,t,n){var r=n(107),o=n(76);function a(t){return e.exports=a="function"==typeof o&&"symbol"==typeof r?function(e){return typeof e}:function(e){return e&&"function"==typeof o&&e.constructor===o&&e!==o.prototype?"symbol":typeof e},a(t)}e.exports=a},function(e,t,n){e.exports=n(533)},function(e,t){e.exports=function(){var e={location:{},history:{},open:function(){},close:function(){},File:function(){}};if("undefined"==typeof window)return e;try{e=window;for(var t=0,n=["File","Blob","FormData"];t4)}function c(e){var t=e.get("swagger");return"string"==typeof t&&t.startsWith("2.0")}function s(e){return function(t,n){return function(r){return n&&n.specSelectors&&n.specSelectors.specJson?u(n.specSelectors.specJson())?i.a.createElement(e,o()({},r,n,{Ori:t})):i.a.createElement(t,r):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}}},function(e,t,n){var r=n(29),o=n(22),a=n(62),i=n(72),u=n(60),c=function(e,t,n){var s,l,f,p=e&c.F,h=e&c.G,d=e&c.S,v=e&c.P,m=e&c.B,y=e&c.W,g=h?o:o[t]||(o[t]={}),b=g.prototype,E=h?r:d?r[t]:(r[t]||{}).prototype;for(s in h&&(n=t),n)(l=!p&&E&&void 0!==E[s])&&u(g,s)||(f=l?E[s]:n[s],g[s]=h&&"function"!=typeof E[s]?n[s]:m&&l?a(f,r):y&&E[s]==f?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(f):v&&"function"==typeof f?a(Function.call,f):f,v&&((g.virtual||(g.virtual={}))[s]=f,e&c.R&&b&&!b[s]&&i(b,s,f)))};c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_SPEC",(function(){return G})),n.d(t,"UPDATE_URL",(function(){return K})),n.d(t,"UPDATE_JSON",(function(){return Z})),n.d(t,"UPDATE_PARAM",(function(){return X})),n.d(t,"UPDATE_EMPTY_PARAM_INCLUSION",(function(){return Q})),n.d(t,"VALIDATE_PARAMS",(function(){return ee})),n.d(t,"SET_RESPONSE",(function(){return te})),n.d(t,"SET_REQUEST",(function(){return ne})),n.d(t,"SET_MUTATED_REQUEST",(function(){return re})),n.d(t,"LOG_REQUEST",(function(){return oe})),n.d(t,"CLEAR_RESPONSE",(function(){return ae})),n.d(t,"CLEAR_REQUEST",(function(){return ie})),n.d(t,"CLEAR_VALIDATE_PARAMS",(function(){return ue})),n.d(t,"UPDATE_OPERATION_META_VALUE",(function(){return ce})),n.d(t,"UPDATE_RESOLVED",(function(){return se})),n.d(t,"UPDATE_RESOLVED_SUBTREE",(function(){return le})),n.d(t,"SET_SCHEME",(function(){return fe})),n.d(t,"updateSpec",(function(){return pe})),n.d(t,"updateResolved",(function(){return he})),n.d(t,"updateUrl",(function(){return de})),n.d(t,"updateJsonSpec",(function(){return ve})),n.d(t,"parseToJson",(function(){return me})),n.d(t,"resolveSpec",(function(){return ge})),n.d(t,"requestResolvedSubtree",(function(){return xe})),n.d(t,"changeParam",(function(){return Se})),n.d(t,"changeParamByIdentity",(function(){return je})),n.d(t,"updateResolvedSubtree",(function(){return _e})),n.d(t,"invalidateResolvedSubtreeCache",(function(){return we})),n.d(t,"validateParams",(function(){return Oe})),n.d(t,"updateEmptyParamInclusion",(function(){return Ce})),n.d(t,"clearValidateParams",(function(){return Ae})),n.d(t,"changeConsumesValue",(function(){return ke})),n.d(t,"changeProducesValue",(function(){return Pe})),n.d(t,"setResponse",(function(){return Ie})),n.d(t,"setRequest",(function(){return Te})),n.d(t,"setMutatedRequest",(function(){return Re})),n.d(t,"logRequest",(function(){return Ne})),n.d(t,"executeRequest",(function(){return Me})),n.d(t,"execute",(function(){return De})),n.d(t,"clearResponse",(function(){return qe})),n.d(t,"clearRequest",(function(){return Be})),n.d(t,"setScheme",(function(){return Le}));var r=n(88),o=n.n(r),a=n(57),i=n.n(a),u=n(52),c=n.n(u),s=n(53),l=n.n(s),f=n(3),p=n.n(f),h=n(33),d=n.n(h),v=n(309),m=n.n(v),y=n(18),g=n.n(y),b=n(12),E=n.n(b),x=n(48),S=n.n(x),j=n(28),_=n.n(j),w=n(65),O=n.n(w),C=n(51),A=n.n(C),k=n(13),P=n.n(k),I=n(17),T=n.n(I),R=n(77),N=n.n(R),M=n(2),D=n(89),q=n.n(D),B=n(109),L=n.n(B),U=n(417),V=n.n(U),z=n(418),F=n.n(z),J=n(310),W=n.n(J),H=n(7);function $(e,t){var n=E()(e);if(l.a){var r=l()(e);t&&(r=r.filter((function(t){return c()(e,t).enumerable}))),n.push.apply(n,r)}return n}function Y(e){for(var t=1;t0){var o=n.map((function(e){return console.error(e),e.line=e.fullPath?y(g,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e}));a.newThrownErrBatch(o)}return r.updateResolved(t)}))}},be=[],Ee=F()(O()(_.a.mark((function e(){var t,n,r,o,a,i,u,c,s,l,f,p,h,d,v,m,y;return _.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(t=be.system){e.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),e.abrupt("return");case 4:if(n=t.errActions,r=t.errSelectors,o=t.fn,a=o.resolveSubtree,i=o.AST,u=void 0===i?{}:i,c=t.specSelectors,s=t.specActions,a){e.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),e.abrupt("return");case 8:return l=u.getLineNumberForPath?u.getLineNumberForPath:function(){},f=c.specStr(),p=t.getConfigs(),h=p.modelPropertyMacro,d=p.parameterMacro,v=p.requestInterceptor,m=p.responseInterceptor,e.prev=11,e.next=14,be.reduce(function(){var e=O()(_.a.mark((function e(t,o){var i,u,s,p,y,g,b;return _.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t;case 2:return i=e.sent,u=i.resultMap,s=i.specWithCurrentSubtrees,e.next=7,a(s,o,{baseDoc:c.url(),modelPropertyMacro:h,parameterMacro:d,requestInterceptor:v,responseInterceptor:m});case 7:return p=e.sent,y=p.errors,g=p.spec,r.allErrors().size&&n.clearBy((function(e){return"thrown"!==e.get("type")||"resolver"!==e.get("source")||!e.get("fullPath").every((function(e,t){return e===o[t]||void 0===o[t]}))})),P()(y)&&y.length>0&&(b=y.map((function(e){return e.line=e.fullPath?l(f,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e})),n.newThrownErrBatch(b)),W()(u,o,g),W()(s,o,g),e.abrupt("return",{resultMap:u,specWithCurrentSubtrees:s});case 15:case"end":return e.stop()}}),e)})));return function(t,n){return e.apply(this,arguments)}}(),S.a.resolve({resultMap:(c.specResolvedSubtree([])||Object(M.Map)()).toJS(),specWithCurrentSubtrees:c.specJson().toJS()}));case 14:y=e.sent,delete be.system,be=[],e.next=22;break;case 19:e.prev=19,e.t0=e.catch(11),console.error(e.t0);case 22:s.updateResolvedSubtree([],y.resultMap);case 23:case"end":return e.stop()}}),e,null,[[11,19]])}))),35),xe=function(e){return function(t){be.map((function(e){return e.join("@@")})).indexOf(e.join("@@"))>-1||(be.push(e),be.system=t,Ee())}};function Se(e,t,n,r,o){return{type:X,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:o}}}function je(e,t,n,r){return{type:X,payload:{path:e,param:t,value:n,isXml:r}}}var _e=function(e,t){return{type:le,payload:{path:e,value:t}}},we=function(){return{type:le,payload:{path:[],value:Object(M.Map)()}}},Oe=function(e,t){return{type:ee,payload:{pathMethod:e,isOAS3:t}}},Ce=function(e,t,n,r){return{type:Q,payload:{pathMethod:e,paramName:t,paramIn:n,includeEmptyValue:r}}};function Ae(e){return{type:ue,payload:{pathMethod:e}}}function ke(e,t){return{type:ce,payload:{path:e,value:t,key:"consumes_value"}}}function Pe(e,t){return{type:ce,payload:{path:e,value:t,key:"produces_value"}}}var Ie=function(e,t,n){return{payload:{path:e,method:t,res:n},type:te}},Te=function(e,t,n){return{payload:{path:e,method:t,req:n},type:ne}},Re=function(e,t,n){return{payload:{path:e,method:t,req:n},type:re}},Ne=function(e){return{payload:e,type:oe}},Me=function(e){return function(t){var n=t.fn,r=t.specActions,o=t.specSelectors,a=t.getConfigs,i=t.oas3Selectors,u=e.pathName,c=e.method,s=e.operation,l=a(),f=l.requestInterceptor,p=l.responseInterceptor,h=s.toJS();if(s&&s.get("parameters")&&s.get("parameters").filter((function(e){return e&&!0===e.get("allowEmptyValue")})).forEach((function(t){if(o.parameterInclusionSettingFor([u,c],t.get("name"),t.get("in"))){e.parameters=e.parameters||{};var n=Object(H.C)(t,e.parameters);(!n||n&&0===n.size)&&(e.parameters[t.get("name")]="")}})),e.contextUrl=q()(o.url()).toString(),h&&h.operationId?e.operationId=h.operationId:h&&u&&c&&(e.operationId=n.opId(h,u,c)),o.isOAS3()){var d="".concat(u,":").concat(c);e.server=i.selectedServer(d)||i.selectedServer();var v=i.serverVariables({server:e.server,namespace:d}).toJS(),y=i.serverVariables({server:e.server}).toJS();e.serverVariables=E()(v).length?v:y,e.requestContentType=i.requestContentType(u,c),e.responseContentType=i.responseContentType(u,c)||"*/*";var b=i.requestBodyValue(u,c),x=i.requestBodyInclusionSetting(u,c);Object(H.t)(b)?e.requestBody=JSON.parse(b):b&&b.toJS?e.requestBody=b.map((function(e){return M.Map.isMap(e)?e.get("value"):e})).filter((function(e,t){return!Object(H.q)(e)||x.get(t)})).toJS():e.requestBody=b}var S=g()({},e);S=n.buildRequest(S),r.setRequest(e.pathName,e.method,S);e.requestInterceptor=function(t){var n=f.apply(this,[t]),o=g()({},n);return r.setMutatedRequest(e.pathName,e.method,o),n},e.responseInterceptor=p;var j=m()();return n.execute(e).then((function(t){t.duration=m()()-j,r.setResponse(e.pathName,e.method,t)})).catch((function(t){console.error(t),r.setResponse(e.pathName,e.method,{error:!0,err:L()(t)})}))}},De=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,n=e.method,r=d()(e,["path","method"]);return function(e){var o=e.fn.fetch,a=e.specSelectors,i=e.specActions,u=a.specJsonWithResolvedSubtrees().toJS(),c=a.operationScheme(t,n),s=a.contentTypeValues([t,n]).toJS(),l=s.requestContentType,f=s.responseContentType,p=/xml/i.test(l),h=a.parameterValues([t,n],p).toJS();return i.executeRequest(Y(Y({},r),{},{fetch:o,spec:u,pathName:t,method:n,parameters:h,requestContentType:l,scheme:c,responseContentType:f}))}};function qe(e,t){return{type:ae,payload:{path:e,method:t}}}function Be(e,t){return{type:ie,payload:{path:e,method:t}}}function Le(e,t,n){return{type:fe,payload:{scheme:e,path:t,method:n}}}},function(e,t,n){e.exports=n(655)},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(186)("wks"),o=n(188),a=n(36).Symbol,i="function"==typeof a;(e.exports=function(e){return r[e]||(r[e]=i&&a[e]||(i?a:o)("Symbol."+e))}).store=r},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){var r=n(203)("wks"),o=n(147),a=n(29).Symbol,i="function"==typeof a;(e.exports=function(e){return r[e]||(r[e]=i&&a[e]||(i?a:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(53),o=n(646);e.exports=function(e,t){if(null==e)return{};var n,a,i=o(e,t);if(r){var u=r(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}},function(e,t,n){var r=n(36),o=n(69),a=n(81),i=n(91),u=n(141),c=function(e,t,n){var s,l,f,p,h=e&c.F,d=e&c.G,v=e&c.S,m=e&c.P,y=e&c.B,g=d?r:v?r[t]||(r[t]={}):(r[t]||{}).prototype,b=d?o:o[t]||(o[t]={}),E=b.prototype||(b.prototype={});for(s in d&&(n=t),n)f=((l=!h&&g&&void 0!==g[s])?g:n)[s],p=y&&l?u(f,r):m&&"function"==typeof f?u(Function.call,f):f,g&&i(g,s,f,e&c.U),b[s]!=f&&a(b,s,p),m&&E[s]!=f&&(E[s]=f)};r.core=o,c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,t,n){var r=n(31);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(34),o=n(93),a=n(70),i=/"/g,u=function(e,t,n,r){var o=String(a(e)),u="<"+t;return""!==n&&(u+=" "+n+'="'+String(r).replace(i,""")+'"'),u+">"+o+""};e.exports=function(e,t){var n={};n[e]=t(u),r(r.P+r.F*o((function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3})),"String",n)}},function(e,t,n){e.exports=!n(73)((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},function(e,t){var n=Array.isArray;e.exports=n},function(e,t,n){"use strict";n.d(t,"b",(function(){return h})),n.d(t,"e",(function(){return d})),n.d(t,"c",(function(){return m})),n.d(t,"a",(function(){return y})),n.d(t,"d",(function(){return g}));var r=n(59),o=n.n(r),a=n(17),i=n.n(a),u=n(45),c=n.n(u),s=n(314),l=n.n(s),f=function(e){return String.prototype.toLowerCase.call(e)},p=function(e){return e.replace(/[^\w]/gi,"_")};function h(e){var t=e.openapi;return!!t&&l()(t,"3")}function d(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=r.v2OperationIdCompatibilityMode;if(!e||"object"!==i()(e))return null;var a=(e.operationId||"").replace(/\s/g,"");return a.length?p(e.operationId):v(t,n,{v2OperationIdCompatibilityMode:o})}function v(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.v2OperationIdCompatibilityMode;if(r){var o="".concat(t.toLowerCase(),"_").concat(e).replace(/[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g,"_");return(o=o||"".concat(e.substring(1),"_").concat(t)).replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return"".concat(f(t)).concat(p(e))}function m(e,t){return"".concat(f(t),"-").concat(e)}function y(e,t){return e&&e.paths?function(e,t){return function(e,t,n){if(!e||"object"!==i()(e)||!e.paths||"object"!==i()(e.paths))return null;var r=e.paths;for(var o in r)for(var a in r[o])if("PARAMETERS"!==a.toUpperCase()){var u=r[o][a];if(u&&"object"===i()(u)){var c={spec:e,pathName:o,method:a.toUpperCase(),operation:u},s=t(c);if(n&&s)return c}}return}(e,t,!0)||null}(e,(function(e){var n=e.pathName,r=e.method,o=e.operation;if(!o||"object"!==i()(o))return!1;var a=o.operationId;return[d(o,n,r),m(n,r),a].some((function(e){return e&&e===t}))})):null}function g(e){var t=e.spec,n=t.paths,r={};if(!n||t.$$normalized)return e;for(var a in n){var i=n[a];if(c()(i)){var u=i.parameters,s=function(e){var n=i[e];if(!c()(n))return"continue";var s=d(n,a,e);if(s){r[s]?r[s].push(n):r[s]=[n];var l=r[s];if(l.length>1)l.forEach((function(e,t){e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId="".concat(s).concat(t+1)}));else if(void 0!==n.operationId){var f=l[0];f.__originalOperationId=f.__originalOperationId||n.operationId,f.operationId=s}}if("parameters"!==e){var p=[],h={};for(var v in t)"produces"!==v&&"consumes"!==v&&"security"!==v||(h[v]=t[v],p.push(h));if(u&&(h.parameters=u,p.push(h)),p.length){var m,y=o()(p);try{for(y.s();!(m=y.n()).done;){var g=m.value;for(var b in g)if(n[b]){if("parameters"===b){var E,x=o()(g[b]);try{var S=function(){var e=E.value;n[b].some((function(t){return t.name&&t.name===e.name||t.$ref&&t.$ref===e.$ref||t.$$ref&&t.$$ref===e.$$ref||t===e}))||n[b].push(e)};for(x.s();!(E=x.n()).done;)S()}catch(e){x.e(e)}finally{x.f()}}}else n[b]=g[b]}}catch(e){y.e(e)}finally{y.f()}}}};for(var l in i)s(l)}}return t.$$normalized=!0,e}},function(e,t,n){"use strict";n.r(t),n.d(t,"NEW_THROWN_ERR",(function(){return a})),n.d(t,"NEW_THROWN_ERR_BATCH",(function(){return i})),n.d(t,"NEW_SPEC_ERR",(function(){return u})),n.d(t,"NEW_SPEC_ERR_BATCH",(function(){return c})),n.d(t,"NEW_AUTH_ERR",(function(){return s})),n.d(t,"CLEAR",(function(){return l})),n.d(t,"CLEAR_BY",(function(){return f})),n.d(t,"newThrownErr",(function(){return p})),n.d(t,"newThrownErrBatch",(function(){return h})),n.d(t,"newSpecErr",(function(){return d})),n.d(t,"newSpecErrBatch",(function(){return v})),n.d(t,"newAuthErr",(function(){return m})),n.d(t,"clear",(function(){return y})),n.d(t,"clearBy",(function(){return g}));var r=n(109),o=n.n(r),a="err_new_thrown_err",i="err_new_thrown_err_batch",u="err_new_spec_err",c="err_new_spec_err_batch",s="err_new_auth_err",l="err_clear",f="err_clear_by";function p(e){return{type:a,payload:o()(e)}}function h(e){return{type:i,payload:e}}function d(e){return{type:u,payload:e}}function v(e){return{type:c,payload:e}}function m(e){return{type:s,payload:e}}function y(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:l,payload:e}}function g(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:f,payload:e}}},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_SELECTED_SERVER",(function(){return r})),n.d(t,"UPDATE_REQUEST_BODY_VALUE",(function(){return o})),n.d(t,"UPDATE_REQUEST_BODY_INCLUSION",(function(){return a})),n.d(t,"UPDATE_ACTIVE_EXAMPLES_MEMBER",(function(){return i})),n.d(t,"UPDATE_REQUEST_CONTENT_TYPE",(function(){return u})),n.d(t,"UPDATE_RESPONSE_CONTENT_TYPE",(function(){return c})),n.d(t,"UPDATE_SERVER_VARIABLE_VALUE",(function(){return s})),n.d(t,"SET_REQUEST_BODY_VALIDATE_ERROR",(function(){return l})),n.d(t,"CLEAR_REQUEST_BODY_VALIDATE_ERROR",(function(){return f})),n.d(t,"setSelectedServer",(function(){return p})),n.d(t,"setRequestBodyValue",(function(){return h})),n.d(t,"setRequestBodyInclusion",(function(){return d})),n.d(t,"setActiveExamplesMember",(function(){return v})),n.d(t,"setRequestContentType",(function(){return m})),n.d(t,"setResponseContentType",(function(){return y})),n.d(t,"setServerVariableValue",(function(){return g})),n.d(t,"setRequestBodyValidateError",(function(){return b})),n.d(t,"clearRequestBodyValidateError",(function(){return E})),n.d(t,"initRequestBodyValidateError",(function(){return x}));var r="oas3_set_servers",o="oas3_set_request_body_value",a="oas3_set_request_body_inclusion",i="oas3_set_active_examples_member",u="oas3_set_request_content_type",c="oas3_set_response_content_type",s="oas3_set_server_variable_value",l="oas3_set_request_body_validate_error",f="oas3_clear_request_body_validate_error";function p(e,t){return{type:r,payload:{selectedServerUrl:e,namespace:t}}}function h(e){var t=e.value,n=e.pathMethod;return{type:o,payload:{value:t,pathMethod:n}}}function d(e){var t=e.value,n=e.pathMethod,r=e.name;return{type:a,payload:{value:t,pathMethod:n,name:r}}}function v(e){var t=e.name,n=e.pathMethod,r=e.contextType,o=e.contextName;return{type:i,payload:{name:t,pathMethod:n,contextType:r,contextName:o}}}function m(e){var t=e.value,n=e.pathMethod;return{type:u,payload:{value:t,pathMethod:n}}}function y(e){var t=e.value,n=e.path,r=e.method;return{type:c,payload:{value:t,path:n,method:r}}}function g(e){var t=e.server,n=e.namespace,r=e.key,o=e.val;return{type:s,payload:{server:t,namespace:n,key:r,val:o}}}var b=function(e){var t=e.path,n=e.method,r=e.validationErrors;return{type:l,payload:{path:t,method:n,validationErrors:r}}},E=function(e){var t=e.path,n=e.method;return{type:f,payload:{path:t,method:n}}},x=function(e){var t=e.pathMethod;return{type:f,payload:{path:t[0],method:t[1]}}}},function(e,t,n){var r=n(92);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){var n=Array.isArray;e.exports=n},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t){e.exports=require("classnames")},function(e,t,n){var r=n(228);e.exports=function(e,t,n){var o=null==e?void 0:r(e,t);return void 0===o?n:o}},function(e,t,n){e.exports=n(649)},function(e,t,n){var r=n(35),o=n(331),a=n(207),i=Object.defineProperty;t.f=n(38)?Object.defineProperty:function(e,t,n){if(r(e),t=a(t,!0),r(n),o)try{return i(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){var r=n(345),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();e.exports=a},function(e,t,n){e.exports=n(531)},function(e,t,n){e.exports=n(643)},function(e,t,n){e.exports=n(645)},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t,n){var r=n(379),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();e.exports=a},function(e,t,n){e.exports=n(541)},function(e,t,n){e.exports=n(640)},function(e,t){e.exports=require("deep-extend")},function(e,t,n){var r=n(108),o=n(13),a=n(107),i=n(76),u=n(151);e.exports=function(e,t){var n;if(void 0===i||null==e[a]){if(o(e)||(n=u(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var c=0,s=function(){};return{s:s,n:function(){return c>=e.length?{done:!0}:{done:!1,value:e[c++]}},e:function(e){throw e},f:s}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,f=!0,p=!1;return{s:function(){n=r(e)},n:function(){var e=n.next();return f=e.done,e},e:function(e){p=!0,l=e},f:function(){try{f||null==n.return||n.return()}finally{if(p)throw l}}}}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(200),o=n(199);e.exports=function(e){return r(o(e))}},function(e,t,n){var r=n(83);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t,n){"use strict";n.r(t),n.d(t,"lastError",(function(){return d})),n.d(t,"url",(function(){return v})),n.d(t,"specStr",(function(){return m})),n.d(t,"specSource",(function(){return y})),n.d(t,"specJson",(function(){return g})),n.d(t,"specResolved",(function(){return b})),n.d(t,"specResolvedSubtree",(function(){return E})),n.d(t,"specJsonWithResolvedSubtrees",(function(){return S})),n.d(t,"spec",(function(){return j})),n.d(t,"isOAS3",(function(){return _})),n.d(t,"info",(function(){return w})),n.d(t,"externalDocs",(function(){return O})),n.d(t,"version",(function(){return C})),n.d(t,"semver",(function(){return A})),n.d(t,"paths",(function(){return k})),n.d(t,"operations",(function(){return P})),n.d(t,"consumes",(function(){return I})),n.d(t,"produces",(function(){return T})),n.d(t,"security",(function(){return R})),n.d(t,"securityDefinitions",(function(){return N})),n.d(t,"findDefinition",(function(){return M})),n.d(t,"definitions",(function(){return D})),n.d(t,"basePath",(function(){return q})),n.d(t,"host",(function(){return B})),n.d(t,"schemes",(function(){return L})),n.d(t,"operationsWithRootInherited",(function(){return U})),n.d(t,"tags",(function(){return V})),n.d(t,"tagDetails",(function(){return z})),n.d(t,"operationsWithTags",(function(){return F})),n.d(t,"taggedOperations",(function(){return J})),n.d(t,"responses",(function(){return W})),n.d(t,"requests",(function(){return H})),n.d(t,"mutatedRequests",(function(){return $})),n.d(t,"responseFor",(function(){return Y})),n.d(t,"requestFor",(function(){return G})),n.d(t,"mutatedRequestFor",(function(){return K})),n.d(t,"allowTryItOutFor",(function(){return Z})),n.d(t,"parameterWithMetaByIdentity",(function(){return X})),n.d(t,"parameterInclusionSettingFor",(function(){return Q})),n.d(t,"parameterWithMeta",(function(){return ee})),n.d(t,"operationWithMeta",(function(){return te})),n.d(t,"getParameter",(function(){return ne})),n.d(t,"hasHost",(function(){return re})),n.d(t,"parameterValues",(function(){return oe})),n.d(t,"parametersIncludeIn",(function(){return ae})),n.d(t,"parametersIncludeType",(function(){return ie})),n.d(t,"contentTypeValues",(function(){return ue})),n.d(t,"currentProducesFor",(function(){return ce})),n.d(t,"producesOptionsFor",(function(){return se})),n.d(t,"consumesOptionsFor",(function(){return le})),n.d(t,"operationScheme",(function(){return fe})),n.d(t,"canExecuteScheme",(function(){return pe})),n.d(t,"validateBeforeExecute",(function(){return he})),n.d(t,"getOAS3RequiredRequestBodyContentType",(function(){return de}));var r=n(13),o=n.n(r),a=n(14),i=n.n(a),u=n(15),c=n.n(u),s=n(16),l=n(7),f=n(2),p=["get","put","post","delete","options","head","patch","trace"],h=function(e){return e||Object(f.Map)()},d=Object(s.createSelector)(h,(function(e){return e.get("lastError")})),v=Object(s.createSelector)(h,(function(e){return e.get("url")})),m=Object(s.createSelector)(h,(function(e){return e.get("spec")||""})),y=Object(s.createSelector)(h,(function(e){return e.get("specSource")||"not-editor"})),g=Object(s.createSelector)(h,(function(e){return e.get("json",Object(f.Map)())})),b=Object(s.createSelector)(h,(function(e){return e.get("resolved",Object(f.Map)())})),E=function(e,t){return e.getIn(["resolvedSubtrees"].concat(c()(t)),void 0)},x=function e(t,n){return f.Map.isMap(t)&&f.Map.isMap(n)?n.get("$$ref")?n:Object(f.OrderedMap)().mergeWith(e,t,n):n},S=Object(s.createSelector)(h,(function(e){return Object(f.OrderedMap)().mergeWith(x,e.get("json"),e.get("resolvedSubtrees"))})),j=function(e){return g(e)},_=Object(s.createSelector)(j,(function(){return!1})),w=Object(s.createSelector)(j,(function(e){return ve(e&&e.get("info"))})),O=Object(s.createSelector)(j,(function(e){return ve(e&&e.get("externalDocs"))})),C=Object(s.createSelector)(w,(function(e){return e&&e.get("version")})),A=Object(s.createSelector)(C,(function(e){return/v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(e).slice(1)})),k=Object(s.createSelector)(S,(function(e){return e.get("paths")})),P=Object(s.createSelector)(k,(function(e){if(!e||e.size<1)return Object(f.List)();var t=Object(f.List)();return e&&e.forEach?(e.forEach((function(e,n){if(!e||!e.forEach)return{};e.forEach((function(e,r){p.indexOf(r)<0||(t=t.push(Object(f.fromJS)({path:n,method:r,operation:e,id:"".concat(r,"-").concat(n)})))}))})),t):Object(f.List)()})),I=Object(s.createSelector)(j,(function(e){return Object(f.Set)(e.get("consumes"))})),T=Object(s.createSelector)(j,(function(e){return Object(f.Set)(e.get("produces"))})),R=Object(s.createSelector)(j,(function(e){return e.get("security",Object(f.List)())})),N=Object(s.createSelector)(j,(function(e){return e.get("securityDefinitions")})),M=function(e,t){var n=e.getIn(["resolvedSubtrees","definitions",t],null),r=e.getIn(["json","definitions",t],null);return n||r||null},D=Object(s.createSelector)(j,(function(e){var t=e.get("definitions");return f.Map.isMap(t)?t:Object(f.Map)()})),q=Object(s.createSelector)(j,(function(e){return e.get("basePath")})),B=Object(s.createSelector)(j,(function(e){return e.get("host")})),L=Object(s.createSelector)(j,(function(e){return e.get("schemes",Object(f.Map)())})),U=Object(s.createSelector)(P,I,T,(function(e,t,n){return e.map((function(e){return e.update("operation",(function(e){if(e){if(!f.Map.isMap(e))return;return e.withMutations((function(e){return e.get("consumes")||e.update("consumes",(function(e){return Object(f.Set)(e).merge(t)})),e.get("produces")||e.update("produces",(function(e){return Object(f.Set)(e).merge(n)})),e}))}return Object(f.Map)()}))}))})),V=Object(s.createSelector)(j,(function(e){var t=e.get("tags",Object(f.List)());return f.List.isList(t)?t.filter((function(e){return f.Map.isMap(e)})):Object(f.List)()})),z=function(e,t){return(V(e)||Object(f.List)()).filter(f.Map.isMap).find((function(e){return e.get("name")===t}),Object(f.Map)())},F=Object(s.createSelector)(U,V,(function(e,t){return e.reduce((function(e,t){var n=Object(f.Set)(t.getIn(["operation","tags"]));return n.count()<1?e.update("default",Object(f.List)(),(function(e){return e.push(t)})):n.reduce((function(e,n){return e.update(n,Object(f.List)(),(function(e){return e.push(t)}))}),e)}),t.reduce((function(e,t){return e.set(t.get("name"),Object(f.List)())}),Object(f.OrderedMap)()))})),J=function(e){return function(t){var n=(0,t.getConfigs)(),r=n.tagsSorter,o=n.operationsSorter;return F(e).sortBy((function(e,t){return t}),(function(e,t){var n="function"==typeof r?r:l.I.tagsSorter[r];return n?n(e,t):null})).map((function(t,n){var r="function"==typeof o?o:l.I.operationsSorter[o],a=r?t.sort(r):t;return Object(f.Map)({tagDetails:z(e,n),operations:a})}))}},W=Object(s.createSelector)(h,(function(e){return e.get("responses",Object(f.Map)())})),H=Object(s.createSelector)(h,(function(e){return e.get("requests",Object(f.Map)())})),$=Object(s.createSelector)(h,(function(e){return e.get("mutatedRequests",Object(f.Map)())})),Y=function(e,t,n){return W(e).getIn([t,n],null)},G=function(e,t,n){return H(e).getIn([t,n],null)},K=function(e,t,n){return $(e).getIn([t,n],null)},Z=function(){return!0},X=function(e,t,n){var r=S(e).getIn(["paths"].concat(c()(t),["parameters"]),Object(f.OrderedMap)()),o=e.getIn(["meta","paths"].concat(c()(t),["parameters"]),Object(f.OrderedMap)());return r.map((function(e){var t=o.get("".concat(n.get("in"),".").concat(n.get("name"))),r=o.get("".concat(n.get("in"),".").concat(n.get("name"),".hash-").concat(n.hashCode()));return Object(f.OrderedMap)().merge(e,t,r)})).find((function(e){return e.get("in")===n.get("in")&&e.get("name")===n.get("name")}),Object(f.OrderedMap)())},Q=function(e,t,n,r){var o="".concat(r,".").concat(n);return e.getIn(["meta","paths"].concat(c()(t),["parameter_inclusions",o]),!1)},ee=function(e,t,n,r){var o=S(e).getIn(["paths"].concat(c()(t),["parameters"]),Object(f.OrderedMap)()).find((function(e){return e.get("in")===r&&e.get("name")===n}),Object(f.OrderedMap)());return X(e,t,o)},te=function(e,t,n){var r=S(e).getIn(["paths",t,n],Object(f.OrderedMap)()),o=e.getIn(["meta","paths",t,n],Object(f.OrderedMap)()),a=r.get("parameters",Object(f.List)()).map((function(r){return X(e,[t,n],r)}));return Object(f.OrderedMap)().merge(r,o).set("parameters",a)};function ne(e,t,n,r){return t=t||[],e.getIn(["meta","paths"].concat(c()(t),["parameters"]),Object(f.fromJS)([])).find((function(e){return f.Map.isMap(e)&&e.get("name")===n&&e.get("in")===r}))||Object(f.Map)()}var re=Object(s.createSelector)(j,(function(e){var t=e.get("host");return"string"==typeof t&&t.length>0&&"/"!==t[0]}));function oe(e,t,n){return t=t||[],te.apply(void 0,[e].concat(c()(t))).get("parameters",Object(f.List)()).reduce((function(e,t){var r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(Object(l.B)(t,{allowHashes:!1}),r)}),Object(f.fromJS)({}))}function ae(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(f.List.isList(e))return e.some((function(e){return f.Map.isMap(e)&&e.get("in")===t}))}function ie(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(f.List.isList(e))return e.some((function(e){return f.Map.isMap(e)&&e.get("type")===t}))}function ue(e,t){t=t||[];var n=S(e).getIn(["paths"].concat(c()(t)),Object(f.fromJS)({})),r=e.getIn(["meta","paths"].concat(c()(t)),Object(f.fromJS)({})),o=ce(e,t),a=n.get("parameters")||new f.List,i=r.get("consumes_value")?r.get("consumes_value"):ie(a,"file")?"multipart/form-data":ie(a,"formData")?"application/x-www-form-urlencoded":void 0;return Object(f.fromJS)({requestContentType:i,responseContentType:o})}function ce(e,t){t=t||[];var n=S(e).getIn(["paths"].concat(c()(t)),null);if(null!==n){var r=e.getIn(["meta","paths"].concat(c()(t),["produces_value"]),null),o=n.getIn(["produces",0],null);return r||o||"application/json"}}function se(e,t){t=t||[];var n=S(e),r=n.getIn(["paths"].concat(c()(t)),null);if(null!==r){var o=t,a=i()(o,1)[0],u=r.get("produces",null),s=n.getIn(["paths",a,"produces"],null),l=n.getIn(["produces"],null);return u||s||l}}function le(e,t){t=t||[];var n=S(e),r=n.getIn(["paths"].concat(c()(t)),null);if(null!==r){var o=t,a=i()(o,1)[0],u=r.get("consumes",null),s=n.getIn(["paths",a,"consumes"],null),l=n.getIn(["consumes"],null);return u||s||l}}var fe=function(e,t,n){var r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),a=o()(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||a||""},pe=function(e,t,n){return["http","https"].indexOf(fe(e,t,n))>-1},he=function(e,t){t=t||[];var n=e.getIn(["meta","paths"].concat(c()(t),["parameters"]),Object(f.fromJS)([])),r=!0;return n.forEach((function(e){var t=e.get("errors");t&&t.count()&&(r=!1)})),r},de=function(e,t){var n={requestBody:!1,requestContentType:{}},r=e.getIn(["resolvedSubtrees","paths"].concat(c()(t),["requestBody"]),Object(f.fromJS)([]));return r.size<1||(r.getIn(["required"])&&(n.requestBody=r.getIn(["required"])),r.getIn(["content"]).entrySeq().forEach((function(e){var t=e[0];if(e[1].getIn(["schema","required"])){var r=e[1].getIn(["schema","required"]).toJS();n.requestContentType[t]=r}}))),n};function ve(e){return f.Map.isMap(e)?e:new f.Map}},function(e,t,n){var r=n(48);function o(e,t,n,o,a,i,u){try{var c=e[i](u),s=c.value}catch(e){return void n(e)}c.done?t(s):r.resolve(s).then(o,a)}e.exports=function(e){return function(){var t=this,n=arguments;return new r((function(r,a){var i=e.apply(t,n);function u(e){o(i,r,a,u,c,"next",e)}function c(e){o(i,r,a,u,c,"throw",e)}u(void 0)}))}}},function(e,t,n){var r=n(103),o=n(45);e.exports=function(e){if(!o(e))return!1;var t=r(e);return"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t}},function(e,t,n){var r=n(234);e.exports=function(e,t,n){var o=null==e?void 0:r(e,t);return void 0===o?n:o}},function(e,t,n){"use strict";n.r(t),n.d(t,"SHOW_AUTH_POPUP",(function(){return d})),n.d(t,"AUTHORIZE",(function(){return v})),n.d(t,"LOGOUT",(function(){return m})),n.d(t,"PRE_AUTHORIZE_OAUTH2",(function(){return y})),n.d(t,"AUTHORIZE_OAUTH2",(function(){return g})),n.d(t,"VALIDATE",(function(){return b})),n.d(t,"CONFIGURE_AUTH",(function(){return E})),n.d(t,"showDefinitions",(function(){return x})),n.d(t,"authorize",(function(){return S})),n.d(t,"logout",(function(){return j})),n.d(t,"preAuthorizeImplicit",(function(){return _})),n.d(t,"authorizeOauth2",(function(){return w})),n.d(t,"authorizePassword",(function(){return O})),n.d(t,"authorizeApplication",(function(){return C})),n.d(t,"authorizeAccessCodeWithFormParams",(function(){return A})),n.d(t,"authorizeAccessCodeWithBasicAuthentication",(function(){return k})),n.d(t,"authorizeRequest",(function(){return P})),n.d(t,"configureAuth",(function(){return I}));var r=n(17),o=n.n(r),a=n(18),i=n.n(a),u=n(24),c=n.n(u),s=n(89),l=n.n(s),f=n(19),p=n.n(f),h=n(7),d="show_popup",v="authorize",m="logout",y="pre_authorize_oauth2",g="authorize_oauth2",b="validate",E="configure_auth";function x(e){return{type:d,payload:e}}function S(e){return{type:v,payload:e}}function j(e){return{type:m,payload:e}}var _=function(e){return function(t){var n=t.authActions,r=t.errActions,o=e.auth,a=e.token,i=e.isValid,u=o.schema,s=o.name,l=u.get("flow");delete p.a.swaggerUIRedirectOauth2,"accessCode"===l||i||r.newAuthErr({authId:s,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),a.error?r.newAuthErr({authId:s,source:"auth",level:"error",message:c()(a)}):n.authorizeOauth2({auth:o,token:a})}};function w(e){return{type:g,payload:e}}var O=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.name,a=e.username,u=e.password,c=e.passwordType,s=e.clientId,l=e.clientSecret,f={grant_type:"password",scope:e.scopes.join(" "),username:a,password:u},p={};switch(c){case"request-body":!function(e,t,n){t&&i()(e,{client_id:t});n&&i()(e,{client_secret:n})}(f,s,l);break;case"basic":p.Authorization="Basic "+Object(h.a)(s+":"+l);break;default:console.warn("Warning: invalid passwordType ".concat(c," was passed, not including client id and secret"))}return n.authorizeRequest({body:Object(h.b)(f),url:r.get("tokenUrl"),name:o,headers:p,query:{},auth:e})}};var C=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.scopes,a=e.name,i=e.clientId,u=e.clientSecret,c={Authorization:"Basic "+Object(h.a)(i+":"+u)},s={grant_type:"client_credentials",scope:o.join(" ")};return n.authorizeRequest({body:Object(h.b)(s),name:a,url:r.get("tokenUrl"),auth:e,headers:c})}},A=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,a=t.name,i=t.clientId,u=t.clientSecret,c=t.codeVerifier,s={grant_type:"authorization_code",code:t.code,client_id:i,client_secret:u,redirect_uri:n,code_verifier:c};return r.authorizeRequest({body:Object(h.b)(s),name:a,url:o.get("tokenUrl"),auth:t})}},k=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,a=t.name,i=t.clientId,u=t.clientSecret,c={Authorization:"Basic "+Object(h.a)(i+":"+u)},s={grant_type:"authorization_code",code:t.code,client_id:i,redirect_uri:n};return r.authorizeRequest({body:Object(h.b)(s),name:a,url:o.get("tokenUrl"),auth:t,headers:c})}},P=function(e){return function(t){var n,r=t.fn,a=t.getConfigs,u=t.authActions,s=t.errActions,f=t.oas3Selectors,p=t.specSelectors,h=t.authSelectors,d=e.body,v=e.query,m=void 0===v?{}:v,y=e.headers,g=void 0===y?{}:y,b=e.name,E=e.url,x=e.auth,S=(h.getConfigs()||{}).additionalQueryStringParams;if(p.isOAS3()){var j=f.selectedServer();n=l()(E,f.serverEffectiveValue({server:j}),!0)}else n=l()(E,p.url(),!0);"object"===o()(S)&&(n.query=i()({},n.query,S));var _=n.toString(),w=i()({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"},g);r.fetch({url:_,method:"post",headers:w,query:m,body:d,requestInterceptor:a().requestInterceptor,responseInterceptor:a().responseInterceptor}).then((function(e){var t=JSON.parse(e.data),n=t&&(t.error||""),r=t&&(t.parseError||"");e.ok?n||r?s.newAuthErr({authId:b,level:"error",source:"auth",message:c()(t)}):u.authorizeOauth2({auth:x,token:t}):s.newAuthErr({authId:b,level:"error",source:"auth",message:e.statusText})})).catch((function(e){var t=new Error(e).message;if(e.response&&e.response.data){var n=e.response.data;try{var r="string"==typeof n?JSON.parse(n):n;r.error&&(t+=", error: ".concat(r.error)),r.error_description&&(t+=", description: ".concat(r.error_description))}catch(e){}}s.newAuthErr({authId:b,level:"error",source:"auth",message:t})}))}};function I(e){return{type:E,payload:e}}},function(e,t){var n=e.exports={version:"2.6.11"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(116),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t,n){var r=n(49),o=n(120);e.exports=n(38)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t,n){var r=n(547);e.exports=function(e){return null==e?"":r(e)}},function(e,t,n){e.exports=n(524)},function(e,t){e.exports=require("js-yaml")},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_LAYOUT",(function(){return o})),n.d(t,"UPDATE_FILTER",(function(){return a})),n.d(t,"UPDATE_MODE",(function(){return i})),n.d(t,"SHOW",(function(){return u})),n.d(t,"updateLayout",(function(){return c})),n.d(t,"updateFilter",(function(){return s})),n.d(t,"show",(function(){return l})),n.d(t,"changeMode",(function(){return f}));var r=n(7),o="layout_update_layout",a="layout_update_filter",i="layout_update_mode",u="layout_show";function c(e){return{type:o,payload:e}}function s(e){return{type:a,payload:e}}function l(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e=Object(r.w)(e),{type:u,payload:{thing:e,shown:t}}}function f(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=Object(r.w)(e),{type:i,payload:{thing:e,mode:t}}}},function(e,t){e.exports=require("url")},function(e,t){e.exports=require("react-syntax-highlighter")},function(e,t,n){var r=n(139),o=n(318);e.exports=n(115)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){var r=n(199);e.exports=function(e){return Object(r(e))}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(98),o=n(548),a=n(549),i=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":i&&i in Object(e)?o(e):a(e)}},function(e,t,n){var r=n(566),o=n(569);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(721),o=n(724);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t){e.exports=function(e,t){return e===t||e!=e&&t!=t}},function(e,t,n){e.exports=n(638)},function(e,t){e.exports=require("url-parse")},function(e,t,n){"use strict";n.r(t),n.d(t,"default",(function(){return i}));var r=n(416),o=n.n(r),a=[n(254),n(255)];function i(e,t){var n={jsSpec:t.specSelectors.specJson().toJS()};return o()(a,(function(e,t){try{return t.transform(e,n).filter((function(e){return!!e}))}catch(t){return console.error("Transformer error:",t),e}}),e).filter((function(e){return!!e})).map((function(e){return!e.get("line")&&e.get("path"),e}))}},function(e,t,n){var r=n(36),o=n(81),a=n(140),i=n(188)("src"),u=n(456),c=(""+u).split("toString");n(69).inspectSource=function(e){return u.call(e)},(e.exports=function(e,t,n,u){var s="function"==typeof n;s&&(a(n,"name")||o(n,"name",t)),e[t]!==n&&(s&&(a(n,i)||o(n,i,e[t]?""+e[t]:c.join(String(t)))),e===r?e[t]=n:u?e[t]?e[t]=n:o(e,t,n):(delete e[t],o(e,t,n)))})(Function.prototype,"toString",(function(){return"function"==typeof this&&this[i]||u.call(this)}))},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t,n){var r=n(330),o=n(204);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t,n){"use strict";var r=n(520)(!0);n(208)(String,"String",(function(e){this._t=String(e),this._i=0}),(function(){var e,t=this._t,n=this._i;return n>=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})}))},function(e,t){e.exports={}},function(e,t,n){n(522);for(var r=n(29),o=n(72),a=n(96),i=n(32)("toStringTag"),u="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),c=0;cb;b++)if((m=t?g(i(d=e[b])[0],d[1]):g(e[b]))===s||m===l)return m}else for(v=y.call(e);!(d=v.next()).done;)if((m=o(v,g,d.value,t))===s||m===l)return m}).BREAK=s,t.RETURN=l},function(e,t,n){var r=n(104),o=n(713),a=n(714),i=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":i&&i in Object(e)?o(e):a(e)}},function(e,t,n){var r=n(55).Symbol;e.exports=r},function(e,t,n){var r=n(395),o=n(779),a=n(130);e.exports=function(e){return a(e)?r(e):o(e)}},function(e,t){e.exports=function(e){return e&&e.__esModule?e:{default:e}}},function(e,t,n){e.exports=n(519)},function(e,t,n){e.exports=n(539)},function(e,t){e.exports=require("serialize-error")},function(e,t,n){"use strict";n.r(t),n.d(t,"sampleFromSchema",(function(){return d})),n.d(t,"inferSchema",(function(){return v})),n.d(t,"sampleXmlFromSchema",(function(){return m})),n.d(t,"createXMLExample",(function(){return y})),n.d(t,"memoizedCreateXMLExample",(function(){return g})),n.d(t,"memoizedSampleFromSchema",(function(){return b}));var r=n(13),o=n.n(r),a=n(7),i=n(412),u=n.n(i),c=n(308),s=n.n(c),l=n(173),f=n.n(l),p={string:function(){return"string"},string_email:function(){return"user@example.com"},"string_date-time":function(){return(new Date).toISOString()},string_date:function(){return(new Date).toISOString().substring(0,10)},string_uuid:function(){return"3fa85f64-5717-4562-b3fc-2c963f66afa6"},string_hostname:function(){return"example.com"},string_ipv4:function(){return"198.51.100.42"},string_ipv6:function(){return"2001:0db8:5b96:0000:0000:426f:8e17:642a"},number:function(){return 0},number_float:function(){return 0},integer:function(){return 0},boolean:function(e){return"boolean"!=typeof e.default||e.default}},h=function(e){var t=e=Object(a.A)(e),n=t.type,r=t.format,o=p["".concat(n,"_").concat(r)]||p[n];return Object(a.s)(o)?o(e):"Unknown Type: "+e.type},d=function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=Object(a.A)(t),i=r.type,u=r.example,c=r.properties,s=r.additionalProperties,l=r.items,f=n.includeReadOnly,p=n.includeWriteOnly;if(void 0!==u)return Object(a.e)(u,"$$ref",(function(e){return"string"==typeof e&&e.indexOf("#")>-1}));if(!i)if(c)i="object";else{if(!l)return;i="array"}if("object"===i){var d=Object(a.A)(c),v={};for(var m in d)d[m]&&d[m].deprecated||d[m]&&d[m].readOnly&&!f||d[m]&&d[m].writeOnly&&!p||(v[m]=e(d[m],n));if(!0===s)v.additionalProp1={};else if(s)for(var y=Object(a.A)(s),g=e(y,n),b=1;b<4;b++)v["additionalProp"+b]=g;return v}return"array"===i?o()(l.anyOf)?l.anyOf.map((function(t){return e(t,n)})):o()(l.oneOf)?l.oneOf.map((function(t){return e(t,n)})):[e(l,n)]:t.enum?t.default?t.default:Object(a.w)(t.enum)[0]:"file"!==i?h(t):void 0},v=function(e){return e.schema&&(e=e.schema),e.properties&&(e.type="object"),e},m=function e(t){var n,r,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},u=f()({},Object(a.A)(t)),c=u.type,s=u.properties,l=u.additionalProperties,p=u.items,d=u.example,v=i.includeReadOnly,m=i.includeWriteOnly,y=u.default,g={},b={},E=t.xml,x=E.name,S=E.prefix,j=E.namespace,_=u.enum;if(!c)if(s||l)c="object";else{if(!p)return;c="array"}if(n=(S?S+":":"")+(x=x||"notagname"),j){var w=S?"xmlns:"+S:"xmlns";b[w]=j}if("array"===c&&p){if(p.xml=p.xml||E||{},p.xml.name=p.xml.name||E.name,E.wrapped)return g[n]=[],o()(d)?d.forEach((function(t){p.example=t,g[n].push(e(p,i))})):o()(y)?y.forEach((function(t){p.default=t,g[n].push(e(p,i))})):g[n]=[e(p,i)],b&&g[n].push({_attr:b}),g;var O=[];return o()(d)?(d.forEach((function(t){p.example=t,O.push(e(p,i))})),O):o()(y)?(y.forEach((function(t){p.default=t,O.push(e(p,i))})),O):e(p,i)}if("object"===c){var C=Object(a.A)(s);for(var A in g[n]=[],d=d||{},C)if(C.hasOwnProperty(A)&&(!C[A].readOnly||v)&&(!C[A].writeOnly||m))if(C[A].xml=C[A].xml||{},C[A].xml.attribute){var k=o()(C[A].enum)&&C[A].enum[0],P=C[A].example,I=C[A].default;b[C[A].xml.name||A]=void 0!==P&&P||void 0!==d[A]&&d[A]||void 0!==I&&I||k||h(C[A])}else{C[A].xml.name=C[A].xml.name||A,void 0===C[A].example&&void 0!==d[A]&&(C[A].example=d[A]);var T=e(C[A]);o()(T)?g[n]=g[n].concat(T):g[n].push(T)}return!0===l?g[n].push({additionalProp:"Anything can be here"}):l&&g[n].push({additionalProp:h(l)}),b&&g[n].push({_attr:b}),g}return r=void 0!==d?d:void 0!==y?y:o()(_)?_[0]:h(t),g[n]=b?[{_attr:b},r]:r,g};function y(e,t){var n=m(e,t);if(n)return u()(n,{declaration:!0,indent:"\t"})}var g=s()(y),b=s()(d)},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_CONFIGS",(function(){return a})),n.d(t,"TOGGLE_CONFIGS",(function(){return i})),n.d(t,"update",(function(){return u})),n.d(t,"toggle",(function(){return c})),n.d(t,"loaded",(function(){return s}));var r=n(3),o=n.n(r),a="configs_update",i="configs_toggle";function u(e,t){return{type:a,payload:o()({},e,t)}}function c(e){return{type:i,payload:e}}var s=function(){return function(){}}},function(e,t,n){"use strict";n.d(t,"a",(function(){return i}));var r=n(2),o=n.n(r),a=o.a.Set.of("type","format","items","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf");function i(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.isOAS3;if(!o.a.Map.isMap(e))return{schema:o.a.Map(),parameterContentMediaType:null};if(!n)return"body"===e.get("in")?{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}:{schema:e.filter((function(e,t){return a.includes(t)})),parameterContentMediaType:null};if(e.get("content")){var r=e.get("content",o.a.Map({})).keySeq(),i=r.first();return{schema:e.getIn(["content",i,"schema"],o.a.Map()),parameterContentMediaType:i}}return{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}}},function(e,t){e.exports=require("fast-json-patch")},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t,n){e.exports=!n(93)((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t){e.exports={}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=!0},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,n){var r=n(35),o=n(332),a=n(204),i=n(202)("IE_PROTO"),u=function(){},c=function(){var e,t=n(206)("iframe"),r=a.length;for(t.style.display="none",n(333).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("' + . '' + . '' + . $kintScript + . PHP_EOL; + + if (str_contains((string) $response->getBody(), '')) { + $response->setBody( + preg_replace( + '//', + '' . $script, + $response->getBody(), + 1, + ), + ); + + return; + } + + $response->appendBody($script); + } + } + + /** + * Inject debug toolbar into the response. + * + * @codeCoverageIgnore + * + * @return void + */ + public function respond() + { + if (ENVIRONMENT === 'testing') { + return; + } + + $request = service('request'); + + // If the request contains '?debugbar then we're + // simply returning the loading script + if ($request->getGet('debugbar') !== null) { + header('Content-Type: application/javascript'); + + ob_start(); + include $this->config->viewsPath . 'toolbarloader.js'; + $output = ob_get_clean(); + $output = str_replace('{url}', rtrim(site_url(), '/'), $output); + echo $output; + + exit; + } + + // Otherwise, if it includes ?debugbar_time, then + // we should return the entire debugbar. + if ($request->getGet('debugbar_time')) { + helper('security'); + + // Negotiate the content-type to format the output + $format = $request->negotiate('media', ['text/html', 'application/json', 'application/xml']); + $format = explode('/', $format)[1]; + + $filename = sanitize_filename('debugbar_' . $request->getGet('debugbar_time')); + $filename = WRITEPATH . 'debugbar/' . $filename . '.json'; + + if (is_file($filename)) { + // Show the toolbar if it exists + echo $this->format(file_get_contents($filename), $format); + + exit; + } + + // Filename not found + http_response_code(404); + + exit; // Exit here is needed to avoid loading the index page + } + } + + /** + * Format output + */ + protected function format(string $data, string $format = 'html'): string + { + $data = json_decode($data, true); + + if ($this->config->maxHistory !== 0 && preg_match('/\d+\.\d{6}/s', (string) service('request')->getGet('debugbar_time'), $debugbarTime)) { + $history = new History(); + $history->setFiles( + $debugbarTime[0], + $this->config->maxHistory, + ); + + $data['collectors'][] = $history->getAsArray(); + } + + $output = ''; + + switch ($format) { + case 'html': + $data['styles'] = []; + extract($data); + $parser = service('parser', $this->config->viewsPath, null, false); + ob_start(); + include $this->config->viewsPath . 'toolbar.tpl.php'; + $output = ob_get_clean(); + break; + + case 'json': + $formatter = new JSONFormatter(); + $output = $formatter->format($data); + break; + + case 'xml': + $formatter = new XMLFormatter(); + $output = $formatter->format($data); + break; + } + + return $output; + } +} diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php new file mode 100755 index 0000000..81cc631 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/BaseCollector.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +/** + * Base Toolbar collector + */ +class BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = false; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = false; + + /** + * Whether this collector needs to display + * a label or not. + * + * @var bool + */ + protected $hasLabel = false; + + /** + * Whether this collector has data that + * should be shown in the Vars tab. + * + * @var bool + */ + protected $hasVarData = false; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = ''; + + /** + * Gets the Collector's title. + */ + public function getTitle(bool $safe = false): string + { + if ($safe) { + return str_replace(' ', '-', strtolower($this->title)); + } + + return $this->title; + } + + /** + * Returns any information that should be shown next to the title. + */ + public function getTitleDetails(): string + { + return ''; + } + + /** + * Does this collector need it's own tab? + */ + public function hasTabContent(): bool + { + return (bool) $this->hasTabContent; + } + + /** + * Does this collector have a label? + */ + public function hasLabel(): bool + { + return (bool) $this->hasLabel; + } + + /** + * Does this collector have information for the timeline? + */ + public function hasTimelineData(): bool + { + return (bool) $this->hasTimeline; + } + + /** + * Grabs the data for the timeline, properly formatted, + * or returns an empty array. + */ + public function timelineData(): array + { + if (! $this->hasTimeline) { + return []; + } + + return $this->formatTimelineData(); + } + + /** + * Does this Collector have data that should be shown in the + * 'Vars' tab? + */ + public function hasVarData(): bool + { + return (bool) $this->hasVarData; + } + + /** + * Gets a collection of data that should be shown in the 'Vars' tab. + * The format is an array of sections, each with their own array + * of key/value pairs: + * + * $data = [ + * 'section 1' => [ + * 'foo' => 'bar, + * 'bar' => 'baz' + * ], + * 'section 2' => [ + * 'foo' => 'bar, + * 'bar' => 'baz' + * ], + * ]; + * + * @return array|null + */ + public function getVarData() + { + return null; + } + + /** + * Child classes should implement this to return the timeline data + * formatted for correct usage. + * + * Timeline data should be formatted into arrays that look like: + * + * [ + * 'name' => 'Database::Query', + * 'component' => 'Database', + * 'start' => 10 // milliseconds + * 'duration' => 15 // milliseconds + * ] + */ + protected function formatTimelineData(): array + { + return []; + } + + /** + * Returns the data of this collector to be formatted in the toolbar + * + * @return array|string + */ + public function display() + { + return []; + } + + /** + * This makes nicer looking paths for the error output. + * + * @deprecated Use the dedicated `clean_path()` function. + */ + public function cleanPath(string $file): string + { + return clean_path($file); + } + + /** + * Gets the "badge" value for the button. + * + * @return int|null + */ + public function getBadgeValue() + { + return null; + } + + /** + * Does this collector have any data collected? + * + * If not, then the toolbar button won't get shown. + */ + public function isEmpty(): bool + { + return false; + } + + /** + * Returns the HTML to display the icon. Should either + * be SVG, or a base-64 encoded. + * + * Recommended dimensions are 24px x 24px + */ + public function icon(): string + { + return ''; + } + + /** + * Return settings as an array. + */ + public function getAsArray(): array + { + return [ + 'title' => $this->getTitle(), + 'titleSafe' => $this->getTitle(true), + 'titleDetails' => $this->getTitleDetails(), + 'display' => $this->display(), + 'badgeValue' => $this->getBadgeValue(), + 'isEmpty' => $this->isEmpty(), + 'hasTabContent' => $this->hasTabContent(), + 'hasLabel' => $this->hasLabel(), + 'icon' => $this->icon(), + 'hasTimelineData' => $this->hasTimelineData(), + 'timelineData' => $this->timelineData(), + ]; + } +} diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php new file mode 100755 index 0000000..80673f9 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +use CodeIgniter\CodeIgniter; +use Config\App; + +/** + * Debug toolbar configuration + */ +class Config +{ + /** + * Return toolbar config values as an array. + */ + public static function display(): array + { + $config = config(App::class); + + return [ + 'ciVersion' => CodeIgniter::CI_VERSION, + 'phpVersion' => PHP_VERSION, + 'phpSAPI' => PHP_SAPI, + 'environment' => ENVIRONMENT, + 'baseURL' => $config->baseURL, + 'timezone' => app_timezone(), + 'locale' => service('request')->getLocale(), + 'cspEnabled' => $config->CSPEnabled, + ]; + } +} diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php new file mode 100755 index 0000000..df87642 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +use CodeIgniter\Database\Query; +use CodeIgniter\I18n\Time; +use Config\Toolbar; + +/** + * Collector for the Database tab of the Debug Toolbar. + * + * @see \CodeIgniter\Debug\Toolbar\Collectors\DatabaseTest + */ +class Database extends BaseCollector +{ + /** + * Whether this collector has timeline data. + * + * @var bool + */ + protected $hasTimeline = true; + + /** + * Whether this collector should display its own tab. + * + * @var bool + */ + protected $hasTabContent = true; + + /** + * Whether this collector has data for the Vars tab. + * + * @var bool + */ + protected $hasVarData = false; + + /** + * The name used to reference this collector in the toolbar. + * + * @var string + */ + protected $title = 'Database'; + + /** + * Array of database connections. + * + * @var array + */ + protected $connections; + + /** + * The query instances that have been collected + * through the DBQuery Event. + * + * @var array + */ + protected static $queries = []; + + /** + * Constructor + */ + public function __construct() + { + $this->getConnections(); + } + + /** + * The static method used during Events to collect + * data. + * + * @internal + * + * @return void + */ + public static function collect(Query $query) + { + $config = config(Toolbar::class); + + // Provide default in case it's not set + $max = $config->maxQueries ?: 100; + + if (count(static::$queries) < $max) { + $queryString = $query->getQuery(); + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + if (! is_cli()) { + // when called in the browser, the first two trace arrays + // are from the DB event trigger, which are unneeded + $backtrace = array_slice($backtrace, 2); + } + + static::$queries[] = [ + 'query' => $query, + 'string' => $queryString, + 'duplicate' => in_array($queryString, array_column(static::$queries, 'string', null), true), + 'trace' => $backtrace, + ]; + } + } + + /** + * Returns timeline data formatted for the toolbar. + * + * @return array The formatted data or an empty array. + */ + protected function formatTimelineData(): array + { + $data = []; + + foreach ($this->connections as $alias => $connection) { + // Connection Time + $data[] = [ + 'name' => 'Connecting to Database: "' . $alias . '"', + 'component' => 'Database', + 'start' => $connection->getConnectStart(), + 'duration' => $connection->getConnectDuration(), + ]; + } + + foreach (static::$queries as $query) { + $data[] = [ + 'name' => 'Query', + 'component' => 'Database', + 'start' => $query['query']->getStartTime(true), + 'duration' => $query['query']->getDuration(), + 'query' => $query['query']->debugToolbarDisplay(), + ]; + } + + return $data; + } + + /** + * Returns the data of this collector to be formatted in the toolbar + */ + public function display(): array + { + $data = []; + $data['queries'] = array_map(static function (array $query): array { + $isDuplicate = $query['duplicate'] === true; + + $firstNonSystemLine = ''; + + foreach ($query['trace'] as $index => &$line) { + // simplify file and line + if (isset($line['file'])) { + $line['file'] = clean_path($line['file']) . ':' . $line['line']; + unset($line['line']); + } else { + $line['file'] = '[internal function]'; + } + + // find the first trace line that does not originate from `system/` + if ($firstNonSystemLine === '' && ! str_contains($line['file'], 'SYSTEMPATH')) { + $firstNonSystemLine = $line['file']; + } + + // simplify function call + if (isset($line['class'])) { + $line['function'] = $line['class'] . $line['type'] . $line['function']; + unset($line['class'], $line['type']); + } + + if (strrpos($line['function'], '{closure}') === false) { + $line['function'] .= '()'; + } + + $line['function'] = str_repeat(chr(0xC2) . chr(0xA0), 8) . $line['function']; + + // add index numbering padded with nonbreaking space + $indexPadded = str_pad(sprintf('%d', $index + 1), 3, ' ', STR_PAD_LEFT); + $indexPadded = preg_replace('/\s/', chr(0xC2) . chr(0xA0), $indexPadded); + + $line['index'] = $indexPadded . str_repeat(chr(0xC2) . chr(0xA0), 4); + } + + return [ + 'hover' => $isDuplicate ? 'This query was called more than once.' : '', + 'class' => $isDuplicate ? 'duplicate' : '', + 'duration' => ((float) $query['query']->getDuration(5) * 1000) . ' ms', + 'sql' => $query['query']->debugToolbarDisplay(), + 'trace' => $query['trace'], + 'trace-file' => $firstNonSystemLine, + 'qid' => md5($query['query'] . Time::now()->format('0.u00 U')), + ]; + }, static::$queries); + + return $data; + } + + /** + * Gets the "badge" value for the button. + */ + public function getBadgeValue(): int + { + return count(static::$queries); + } + + /** + * Information to be displayed next to the title. + * + * @return string The number of queries (in parentheses) or an empty string. + */ + public function getTitleDetails(): string + { + $this->getConnections(); + + $queryCount = count(static::$queries); + $uniqueCount = count(array_filter(static::$queries, static fn ($query): bool => $query['duplicate'] === false)); + $connectionCount = count($this->connections); + + return sprintf( + '(%d total Quer%s, %d %s unique across %d Connection%s)', + $queryCount, + $queryCount > 1 ? 'ies' : 'y', + $uniqueCount, + $uniqueCount > 1 ? 'of them' : '', + $connectionCount, + $connectionCount > 1 ? 's' : '', + ); + } + + /** + * Does this collector have any data collected? + */ + public function isEmpty(): bool + { + return static::$queries === []; + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADMSURBVEhLY6A3YExLSwsA4nIycQDIDIhRWEBqamo/UNF/SjDQjF6ocZgAKPkRiFeEhoYyQ4WIBiA9QAuWAPEHqBAmgLqgHcolGQD1V4DMgHIxwbCxYD+QBqcKINseKo6eWrBioPrtQBq/BcgY5ht0cUIYbBg2AJKkRxCNWkDQgtFUNJwtABr+F6igE8olGQD114HMgHIxAVDyAhA/AlpSA8RYUwoeXAPVex5qHCbIyMgwBCkAuQJIY00huDBUz/mUlBQDqHGjgBjAwAAACexpph6oHSQAAAAASUVORK5CYII='; + } + + /** + * Gets the connections from the database config + */ + private function getConnections(): void + { + $this->connections = \Config\Database::getConnections(); + } +} diff --git a/system/Debug/Toolbar/Collectors/Events.php b/system/Debug/Toolbar/Collectors/Events.php new file mode 100755 index 0000000..173c71a --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Events.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +/** + * Events collector + */ +class Events extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = true; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = true; + + /** + * Whether this collector has data that + * should be shown in the Vars tab. + * + * @var bool + */ + protected $hasVarData = false; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'Events'; + + /** + * Child classes should implement this to return the timeline data + * formatted for correct usage. + */ + protected function formatTimelineData(): array + { + $data = []; + + $rows = \CodeIgniter\Events\Events::getPerformanceLogs(); + + foreach ($rows as $info) { + $data[] = [ + 'name' => 'Event: ' . $info['event'], + 'component' => 'Events', + 'start' => $info['start'], + 'duration' => $info['end'] - $info['start'], + ]; + } + + return $data; + } + + /** + * Returns the data of this collector to be formatted in the toolbar + */ + public function display(): array + { + $data = [ + 'events' => [], + ]; + + foreach (\CodeIgniter\Events\Events::getPerformanceLogs() as $row) { + $key = $row['event']; + + if (! array_key_exists($key, $data['events'])) { + $data['events'][$key] = [ + 'event' => $key, + 'duration' => ($row['end'] - $row['start']) * 1000, + 'count' => 1, + ]; + + continue; + } + + $data['events'][$key]['duration'] += ($row['end'] - $row['start']) * 1000; + $data['events'][$key]['count']++; + } + + foreach ($data['events'] as &$row) { + $row['duration'] = number_format($row['duration'], 2); + } + + return $data; + } + + /** + * Gets the "badge" value for the button. + */ + public function getBadgeValue(): int + { + return count(\CodeIgniter\Events\Events::getPerformanceLogs()); + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEASURBVEhL7ZXNDcIwDIVTsRBH1uDQDdquUA6IM1xgCA6MwJUN2hk6AQzAz0vl0ETUxC5VT3zSU5w81/mRMGZysixbFEVR0jSKNt8geQU9aRpFmp/keX6AbjZ5oB74vsaN5lSzA4tLSjpBFxsjeSuRy4d2mDdQTWU7YLbXTNN05mKyovj5KL6B7q3hoy3KwdZxBlT+Ipz+jPHrBqOIynZgcZonoukb/0ckiTHqNvDXtXEAaygRbaB9FvUTjRUHsIYS0QaSp+Dw6wT4hiTmYHOcYZsdLQ2CbXa4ftuuYR4x9vYZgdb4vsFYUdmABMYeukK9/SUme3KMFQ77+Yfzh8eYF8+orDuDWU5LAAAAAElFTkSuQmCC'; + } +} diff --git a/system/Debug/Toolbar/Collectors/Files.php b/system/Debug/Toolbar/Collectors/Files.php new file mode 100755 index 0000000..a573388 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Files.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +/** + * Files collector + */ +class Files extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = false; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = true; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'Files'; + + /** + * Returns any information that should be shown next to the title. + */ + public function getTitleDetails(): string + { + return '( ' . count(get_included_files()) . ' )'; + } + + /** + * Returns the data of this collector to be formatted in the toolbar + */ + public function display(): array + { + $rawFiles = get_included_files(); + $coreFiles = []; + $userFiles = []; + + foreach ($rawFiles as $file) { + $path = clean_path($file); + + if (str_contains($path, 'SYSTEMPATH')) { + $coreFiles[] = [ + 'path' => $path, + 'name' => basename($file), + ]; + } else { + $userFiles[] = [ + 'path' => $path, + 'name' => basename($file), + ]; + } + } + + sort($userFiles); + sort($coreFiles); + + return [ + 'coreFiles' => $coreFiles, + 'userFiles' => $userFiles, + ]; + } + + /** + * Displays the number of included files as a badge in the tab button. + */ + public function getBadgeValue(): int + { + return count(get_included_files()); + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVEhL7ZQ9S8NQGIVTBQUncfMfCO4uLgoKbuKQOWg+OkXERRE1IAXrIHbVDrqIDuLiJgj+gro7S3dnpfq88b1FMTE3VZx64HBzzvvZWxKnj15QCcPwCD5HUfSWR+JtzgmtsUcQBEva5IIm9SwSu+95CAWbUuy67qBa32ByZEDpIaZYZSZMjjQuPcQUq8yEyYEb8FSerYeQVGbAFzJkX1PyQWLhgCz0BxTCekC1Wp0hsa6yokzhed4oje6Iz6rlJEkyIKfUEFtITVtQdAibn5rMyaYsMS+a5wTv8qeXMhcU16QZbKgl3hbs+L4/pnpdc87MElZgq10p5DxGdq8I7xrvUWUKvG3NbSK7ubngYzdJwSsF7TiOh9VOgfcEz1UayNe3JUPM1RWC5GXYgTfc75B4NBmXJnAtTfpABX0iPvEd9ezALwkplCFXcr9styiNOKc1RRZpaPM9tcqBwlWzGY1qPL9wjqRBgF5BH6j8HWh2S7MHlX8PrmbK+k/8PzjOOzx1D3i1pKTTAAAAAElFTkSuQmCC'; + } +} diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php new file mode 100755 index 0000000..4e5276b --- /dev/null +++ b/system/Debug/Toolbar/Collectors/History.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +use DateTime; + +/** + * History collector + * + * @see \CodeIgniter\Debug\Toolbar\Collectors\HistoryTest + */ +class History extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = false; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = true; + + /** + * Whether this collector needs to display + * a label or not. + * + * @var bool + */ + protected $hasLabel = true; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'History'; + + /** + * @var array History files + */ + protected $files = []; + + /** + * Specify time limit & file count for debug history. + * + * @param string $current Current history time + * @param int $limit Max history files + * + * @return void + */ + public function setFiles(string $current, int $limit = 20) + { + $filenames = glob(WRITEPATH . 'debugbar/debugbar_*.json'); + + $files = []; + $counter = 0; + + foreach (array_reverse($filenames) as $filename) { + $counter++; + + // Oldest files will be deleted + if ($limit >= 0 && $counter > $limit) { + unlink($filename); + + continue; + } + + // Get the contents of this specific history request + $contents = file_get_contents($filename); + + $contents = @json_decode($contents); + if (json_last_error() === JSON_ERROR_NONE) { + preg_match('/debugbar_(.*)\.json$/s', $filename, $time); + $time = sprintf('%.6f', $time[1] ?? 0); + + // Debugbar files shown in History Collector + $files[] = [ + 'time' => $time, + 'datetime' => DateTime::createFromFormat('U.u', $time)->format('Y-m-d H:i:s.u'), + 'active' => $time === $current, + 'status' => $contents->vars->response->statusCode, + 'method' => $contents->method, + 'url' => $contents->url, + 'isAJAX' => $contents->isAJAX ? 'Yes' : 'No', + 'contentType' => $contents->vars->response->contentType, + ]; + } + } + + $this->files = $files; + } + + /** + * Returns the data of this collector to be formatted in the toolbar + */ + public function display(): array + { + return ['files' => $this->files]; + } + + /** + * Displays the number of included files as a badge in the tab button. + */ + public function getBadgeValue(): int + { + return count($this->files); + } + + /** + * Return true if there are no history files. + */ + public function isEmpty(): bool + { + return $this->files === []; + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJySURBVEhL3ZU7aJNhGIVTpV6i4qCIgkIHxcXLErS4FBwUFNwiCKGhuTYJGaIgnRoo4qRu6iCiiIuIXXTTIkIpuqoFwaGgonUQlC5KafU5ycmNP0lTdPLA4fu+8573/a4/f6hXpFKpwUwmc9fDfweKbk+n07fgEv33TLSbtt/hvwNFT1PsG/zdTE0Gp+GFfD6/2fbVIxqNrqPIRbjg4t/hY8aztcngfDabHXbKyiiXy2vcrcPH8oDCry2FKDrA+Ar6L01E/ypyXzXaARjDGGcoeNxSDZXE0dHRA5VRE5LJ5CFy5jzJuOX2wHRHRnjbklZ6isQ3tIctBaAd4vlK3jLtkOVWqABBXd47jGHLmjTmSScttQV5J+SjfcUweFQEbsjAas5aqoCLXutJl7vtQsAzpRowYqkBinyCC8Vicb2lOih8zoldd0F8RD7qTFiqAnGrAy8stUAvi/hbqDM+YzkAFrLPdR5ZqoLXsd+Bh5YCIH7JniVdquUWxOPxDfboHhrI5XJ7HHhiqQXox+APe/Qk64+gGYVCYZs8cMpSFQj9JOoFzVqqo7k4HIvFYpscCoAjOmLffUsNUGRaQUwDlmofUa34ecsdgXdcXo4wbakBgiUFafXJV8A4DJ/2UrxUKm3E95H8RbjLcgOJRGILhnmCP+FBy5XvwN2uIPcy1AJvWgqC4xm2aU4Xb3lF4I+Tpyf8hRe5w3J7YLymSeA8Z3nSclv4WLRyFdfOjzrUFX0klJUEtZtntCNc+F69cz/FiDzEPtjzmcUMOr83kDQEX6pAJxJfpL3OX22n01YN7SZCoQnaSdoZ+Jz+PZihH3wt/xlCoT9M6nEtmRSPCQAAAABJRU5ErkJggg=='; + } +} diff --git a/system/Debug/Toolbar/Collectors/Logs.php b/system/Debug/Toolbar/Collectors/Logs.php new file mode 100755 index 0000000..f36dfb1 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Logs.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +/** + * Loags collector + */ +class Logs extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = false; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = true; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'Logs'; + + /** + * Our collected data. + * + * @var array + */ + protected $data; + + /** + * Returns the data of this collector to be formatted in the toolbar + */ + public function display(): array + { + return [ + 'logs' => $this->collectLogs(), + ]; + } + + /** + * Does this collector actually have any data to display? + */ + public function isEmpty(): bool + { + $this->collectLogs(); + + return empty($this->data); + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACYSURBVEhLYxgFJIHU1FSjtLS0i0D8AYj7gEKMEBkqAaAFF4D4ERCvAFrwH4gDoFIMKSkpFkB+OTEYqgUTACXfA/GqjIwMQyD9H2hRHlQKJFcBEiMGQ7VgAqCBvUgK32dmZspCpagGGNPT0/1BLqeF4bQHQJePpiIwhmrBBEADR1MRfgB0+WgqAmOoFkwANHA0FY0CUgEDAwCQ0PUpNB3kqwAAAABJRU5ErkJggg=='; + } + + /** + * Ensures the data has been collected. + * + * @return array + */ + protected function collectLogs() + { + if (! empty($this->data)) { + return $this->data; + } + + return $this->data = service('logger', true)->logCache ?? []; + } +} diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php new file mode 100755 index 0000000..47c808e --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +use CodeIgniter\Router\DefinedRouteCollector; +use ReflectionException; +use ReflectionFunction; +use ReflectionMethod; + +/** + * Routes collector + */ +class Routes extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = false; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = true; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'Routes'; + + /** + * Returns the data of this collector to be formatted in the toolbar + * + * @return array{ + * matchedRoute: array + * }>, + * routes: list + * } + * + * @throws ReflectionException + */ + public function display(): array + { + $rawRoutes = service('routes', true); + $router = service('router', null, null, true); + + // Get our parameters + // Closure routes + if (is_callable($router->controllerName())) { + $method = new ReflectionFunction($router->controllerName()); + } else { + try { + $method = new ReflectionMethod($router->controllerName(), $router->methodName()); + } catch (ReflectionException) { + try { + // If we're here, the method doesn't exist + // and is likely calculated in _remap. + $method = new ReflectionMethod($router->controllerName(), '_remap'); + } catch (ReflectionException) { + // If we're here, page cache is returned. The router is not executed. + return [ + 'matchedRoute' => [], + 'routes' => [], + ]; + } + } + } + + $rawParams = $method->getParameters(); + + $params = []; + + foreach ($rawParams as $key => $param) { + $params[] = [ + 'name' => '$' . $param->getName() . ' = ', + 'value' => $router->params()[$key] ?? + ' | default: ' + . var_export( + $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, + true, + ), + ]; + } + + $matchedRoute = [ + [ + 'directory' => $router->directory(), + 'controller' => $router->controllerName(), + 'method' => $router->methodName(), + 'paramCount' => count($router->params()), + 'truePCount' => count($params), + 'params' => $params, + ], + ]; + + // Defined Routes + $routes = []; + + $definedRouteCollector = new DefinedRouteCollector($rawRoutes); + + foreach ($definedRouteCollector->collect() as $route) { + // filter for strings, as callbacks aren't displayable + if ($route['handler'] !== '(Closure)') { + $routes[] = [ + 'method' => strtoupper($route['method']), + 'route' => $route['route'], + 'handler' => $route['handler'], + ]; + } + } + + return [ + 'matchedRoute' => $matchedRoute, + 'routes' => $routes, + ]; + } + + /** + * Returns a count of all the routes in the system. + */ + public function getBadgeValue(): int + { + $rawRoutes = service('routes', true); + + return count($rawRoutes->getRoutes()); + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg=='; + } +} diff --git a/system/Debug/Toolbar/Collectors/Timers.php b/system/Debug/Toolbar/Collectors/Timers.php new file mode 100755 index 0000000..d450cc5 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Timers.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +/** + * Timers collector + */ +class Timers extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = true; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = false; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'Timers'; + + /** + * Child classes should implement this to return the timeline data + * formatted for correct usage. + */ + protected function formatTimelineData(): array + { + $data = []; + + $benchmark = service('timer', true); + $rows = $benchmark->getTimers(6); + + foreach ($rows as $name => $info) { + if ($name === 'total_execution') { + continue; + } + + $data[] = [ + 'name' => ucwords(str_replace('_', ' ', $name)), + 'component' => 'Timer', + 'start' => $info['start'], + 'duration' => $info['end'] - $info['start'], + ]; + } + + return $data; + } +} diff --git a/system/Debug/Toolbar/Collectors/Views.php b/system/Debug/Toolbar/Collectors/Views.php new file mode 100755 index 0000000..a63e172 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Views.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Debug\Toolbar\Collectors; + +use CodeIgniter\View\RendererInterface; + +/** + * Views collector + */ +class Views extends BaseCollector +{ + /** + * Whether this collector has data that can + * be displayed in the Timeline. + * + * @var bool + */ + protected $hasTimeline = true; + + /** + * Whether this collector needs to display + * content in a tab or not. + * + * @var bool + */ + protected $hasTabContent = false; + + /** + * Whether this collector needs to display + * a label or not. + * + * @var bool + */ + protected $hasLabel = true; + + /** + * Whether this collector has data that + * should be shown in the Vars tab. + * + * @var bool + */ + protected $hasVarData = true; + + /** + * The 'title' of this Collector. + * Used to name things in the toolbar HTML. + * + * @var string + */ + protected $title = 'Views'; + + /** + * Instance of the shared Renderer service + * + * @var RendererInterface|null + */ + protected $viewer; + + /** + * Views counter + * + * @var array + */ + protected $views = []; + + private function initViewer(): void + { + $this->viewer ??= service('renderer'); + } + + /** + * Child classes should implement this to return the timeline data + * formatted for correct usage. + */ + protected function formatTimelineData(): array + { + $this->initViewer(); + + $data = []; + + $rows = $this->viewer->getPerformanceData(); + + foreach ($rows as $info) { + $data[] = [ + 'name' => 'View: ' . $info['view'], + 'component' => 'Views', + 'start' => $info['start'], + 'duration' => $info['end'] - $info['start'], + ]; + } + + return $data; + } + + /** + * Gets a collection of data that should be shown in the 'Vars' tab. + * The format is an array of sections, each with their own array + * of key/value pairs: + * + * $data = [ + * 'section 1' => [ + * 'foo' => 'bar, + * 'bar' => 'baz' + * ], + * 'section 2' => [ + * 'foo' => 'bar, + * 'bar' => 'baz' + * ], + * ]; + */ + public function getVarData(): array + { + $this->initViewer(); + + return [ + 'View Data' => $this->viewer->getData(), + ]; + } + + /** + * Returns a count of all views. + */ + public function getBadgeValue(): int + { + $this->initViewer(); + + return count($this->viewer->getPerformanceData()); + } + + /** + * Display the icon. + * + * Icon from https://icons8.com - 1em package + */ + public function icon(): string + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII='; + } +} diff --git a/system/Debug/Toolbar/Views/_config.tpl b/system/Debug/Toolbar/Views/_config.tpl new file mode 100755 index 0000000..e3235ec --- /dev/null +++ b/system/Debug/Toolbar/Views/_config.tpl @@ -0,0 +1,48 @@ +

+ Read the CodeIgniter docs... +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeIgniter Version:{ ciVersion }
PHP Version:{ phpVersion }
PHP SAPI:{ phpSAPI }
Environment:{ environment }
Base URL: + { if $baseURL == '' } +
+ The $baseURL should always be set manually to prevent possible URL personification from external parties. +
+ { else } + { baseURL } + { endif } +
Timezone:{ timezone }
Locale:{ locale }
Content Security Policy Enabled:{ if $cspEnabled } Yes { else } No { endif }
diff --git a/system/Debug/Toolbar/Views/_database.tpl b/system/Debug/Toolbar/Views/_database.tpl new file mode 100755 index 0000000..054dd36 --- /dev/null +++ b/system/Debug/Toolbar/Views/_database.tpl @@ -0,0 +1,26 @@ + + + + + + + + + {queries} + + + + + + + + + + {/queries} + +
TimeQuery String
{duration}{! sql !}{trace-file}
+ {trace} + {index}{file}
+ {function}

+ {/trace} +
diff --git a/system/Debug/Toolbar/Views/_events.tpl b/system/Debug/Toolbar/Views/_events.tpl new file mode 100755 index 0000000..88d732f --- /dev/null +++ b/system/Debug/Toolbar/Views/_events.tpl @@ -0,0 +1,18 @@ + + + + + + + + + + {events} + + + + + + {/events} + +
TimeEvent NameTimes Called
{ duration } ms{event}{count}
diff --git a/system/Debug/Toolbar/Views/_files.tpl b/system/Debug/Toolbar/Views/_files.tpl new file mode 100755 index 0000000..9c992ab --- /dev/null +++ b/system/Debug/Toolbar/Views/_files.tpl @@ -0,0 +1,16 @@ + + + {userFiles} + + + + + {/userFiles} + {coreFiles} + + + + + {/coreFiles} + +
{name}{path}
{name}{path}
diff --git a/system/Debug/Toolbar/Views/_history.tpl b/system/Debug/Toolbar/Views/_history.tpl new file mode 100755 index 0000000..7f22f56 --- /dev/null +++ b/system/Debug/Toolbar/Views/_history.tpl @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + {files} + + + + + + + + + + {/files} + +
ActionDatetimeStatusMethodURLContent-TypeIs AJAX?
+ + {datetime}{status}{method}{url}{contentType}{isAJAX}
diff --git a/system/Debug/Toolbar/Views/_logs.tpl b/system/Debug/Toolbar/Views/_logs.tpl new file mode 100755 index 0000000..7c80d84 --- /dev/null +++ b/system/Debug/Toolbar/Views/_logs.tpl @@ -0,0 +1,20 @@ +{ if $logs == [] } +

Nothing was logged. If you were expecting logged items, ensure that LoggerConfig file has the correct threshold set.

+{ else } + + + + + + + + + {logs} + + + + + {/logs} + +
SeverityMessage
{level}{msg}
+{ endif } diff --git a/system/Debug/Toolbar/Views/_routes.tpl b/system/Debug/Toolbar/Views/_routes.tpl new file mode 100755 index 0000000..e277046 --- /dev/null +++ b/system/Debug/Toolbar/Views/_routes.tpl @@ -0,0 +1,52 @@ +

Matched Route

+ + + + {matchedRoute} + + + + + + + + + + + + + + + + + {params} + + + + + {/params} + {/matchedRoute} + +
Directory:{directory}
Controller:{controller}
Method:{method}
Params:{paramCount} / {truePCount}
{name}{value}
+ + +

Defined Routes

+ + + + + + + + + + + {routes} + + + + + + {/routes} + +
MethodRouteHandler
{method}{route}{handler}
diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css new file mode 100755 index 0000000..b9f13c5 --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -0,0 +1,867 @@ +/** + * This file is part of the CodeIgniter 4 framework. + * + * (c) CodeIgniter Foundation + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +#debug-icon { + bottom: 0; + position: fixed; + right: 0; + z-index: 10000; + height: 36px; + width: 36px; + margin: 0; + padding: 0; + clear: both; + text-align: center; + cursor: pointer; +} +#debug-icon a svg { + margin: 8px; + max-width: 20px; + max-height: 20px; +} +#debug-icon.fixed-top { + bottom: auto; + top: 0; +} +#debug-icon .debug-bar-ndisplay { + display: none; +} + +.debug-bar-vars { + cursor: pointer; +} + +#debug-bar { + bottom: 0; + left: 0; + position: fixed; + right: 0; + z-index: 10000; + height: 36px; + line-height: 36px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + font-weight: 400; +} +#debug-bar h1 { + display: flex; + font-weight: normal; + margin: 0 0 0 auto; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; +} +#debug-bar h1 svg { + width: 16px; + margin-right: 5px; +} +#debug-bar h2 { + font-weight: bold; + font-size: 16px; + margin: 0; + padding: 5px 0 10px 0; +} +#debug-bar h2 span { + font-size: 13px; +} +#debug-bar h3 { + font-size: 12px; + font-weight: 200; + margin: 0 0 0 10px; + padding: 0; + text-transform: uppercase; +} +#debug-bar p { + font-size: 12px; + margin: 0 0 0 15px; + padding: 0; +} +#debug-bar a { + text-decoration: none; +} +#debug-bar a:hover { + text-decoration: underline; +} +#debug-bar button { + border: 1px solid; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + cursor: pointer; + line-height: 15px; +} +#debug-bar button:hover { + text-decoration: underline; +} +#debug-bar table { + border-collapse: collapse; + font-size: 14px; + line-height: normal; + margin: 5px 10px 15px 10px; + width: calc(100% - 10px); +} +#debug-bar table strong { + font-weight: 500; +} +#debug-bar table th { + display: table-cell; + font-weight: 600; + padding-bottom: 0.7em; + text-align: left; +} +#debug-bar table tr { + border: none; +} +#debug-bar table td { + border: none; + display: table-cell; + margin: 0; + text-align: left; +} +#debug-bar table td:first-child { + max-width: 20%; +} +#debug-bar table td:first-child.narrow { + width: 7em; +} +#debug-bar td[data-debugbar-route] form { + display: none; +} +#debug-bar td[data-debugbar-route]:hover form { + display: block; +} +#debug-bar td[data-debugbar-route]:hover > div { + display: none; +} +#debug-bar td[data-debugbar-route] input[type=text] { + padding: 2px; +} +#debug-bar .toolbar { + display: flex; + overflow: hidden; + overflow-y: auto; + padding: 0 12px 0 12px; + white-space: nowrap; + z-index: 10000; +} +#debug-bar .toolbar .rotate { + animation: toolbar-rotate 9s linear infinite; +} +@keyframes toolbar-rotate { + to { + transform: rotate(360deg); + } +} +#debug-bar.fixed-top { + bottom: auto; + top: 0; +} +#debug-bar.fixed-top .tab { + bottom: auto; + top: 36px; +} +#debug-bar #toolbar-position, +#debug-bar #toolbar-theme { + padding: 0 6px; + display: inline-flex; + vertical-align: top; + cursor: pointer; +} +#debug-bar #toolbar-position:hover, +#debug-bar #toolbar-theme:hover { + text-decoration: none; +} +#debug-bar #debug-bar-link { + display: flex; + padding: 6px; + cursor: pointer; +} +#debug-bar .ci-label { + display: inline-flex; + font-size: 14px; +} +#debug-bar .ci-label:hover { + cursor: pointer; +} +#debug-bar .ci-label a { + color: inherit; + display: flex; + letter-spacing: normal; + padding: 0 10px; + text-decoration: none; + align-items: center; +} +#debug-bar .ci-label img { + margin: 6px 3px 6px 0; + width: 16px !important; +} +#debug-bar .ci-label .badge { + border-radius: 12px; + -moz-border-radius: 12px; + -webkit-border-radius: 12px; + display: inline-block; + font-size: 75%; + font-weight: bold; + line-height: 12px; + margin-left: 5px; + padding: 2px 5px; + text-align: center; + vertical-align: baseline; + white-space: nowrap; +} +#debug-bar .tab { + height: fit-content; + text-align: left; + bottom: 35px; + display: none; + left: 0; + max-height: 62%; + overflow: hidden; + overflow-y: auto; + padding: 1em 2em; + position: fixed; + right: 0; + z-index: 9999; +} +#debug-bar .timeline { + position: static; + display: table; + margin-left: 0; + width: 100%; +} +#debug-bar .timeline th { + border-left: 1px solid; + font-size: 12px; + font-weight: 200; + padding: 5px 5px 10px 5px; + position: relative; + text-align: left; +} +#debug-bar .timeline th:first-child { + border-left: 0; +} +#debug-bar .timeline td { + border-left: 1px solid; + padding: 5px; + position: relative; +} +#debug-bar .timeline td:first-child { + border-left: 0; + max-width: none; +} +#debug-bar .timeline td.child-container { + padding: 0px; +} +#debug-bar .timeline td.child-container .timeline { + margin: 0px; +} +#debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) { + padding-left: calc(5px + 10px * var(--level)); +} +#debug-bar .timeline .timer { + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + display: inline-block; + padding: 5px; + position: absolute; + top: 30%; +} +#debug-bar .timeline .timeline-parent { + cursor: pointer; +} +#debug-bar .timeline .timeline-parent td:first-child nav { + background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent; + background-position: 0 25%; + display: inline-block; + height: 15px; + width: 15px; + margin-right: 3px; + vertical-align: middle; +} +#debug-bar .timeline .timeline-parent-open { + background-color: #DFDFDF; +} +#debug-bar .timeline .timeline-parent-open td:first-child nav { + background-position: 0 75%; +} +#debug-bar .timeline .child-row:hover { + background: transparent; +} +#debug-bar .route-params, +#debug-bar .route-params-item { + vertical-align: top; +} +#debug-bar .route-params td:first-child, +#debug-bar .route-params-item td:first-child { + font-style: italic; + padding-left: 1em; + text-align: right; +} +#debug-bar > .debug-bar-dblock { + display: block; +} + +.debug-view.show-view { + border: 1px solid; + margin: 4px; +} + +.debug-view-path { + font-family: monospace; + font-size: 12px; + letter-spacing: normal; + min-height: 16px; + padding: 2px; + text-align: left; +} + +.show-view .debug-view-path { + display: block !important; +} + +@media screen and (max-width: 1024px) { + #debug-bar .ci-label img { + margin: unset; + } + .hide-sm { + display: none !important; + } +} +#debug-icon { + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#debug-icon a:active, +#debug-icon a:link, +#debug-icon a:visited { + color: #DD8615; +} + +#debug-bar { + background-color: #FFFFFF; + color: #434343; +} +#debug-bar h1, +#debug-bar h2, +#debug-bar h3, +#debug-bar p, +#debug-bar a, +#debug-bar button, +#debug-bar table, +#debug-bar thead, +#debug-bar tr, +#debug-bar td, +#debug-bar button, +#debug-bar .toolbar { + background-color: transparent; + color: #434343; +} +#debug-bar button { + background-color: #FFFFFF; +} +#debug-bar table strong { + color: #DD8615; +} +#debug-bar table tbody tr:hover { + background-color: #DFDFDF; +} +#debug-bar table tbody tr.current { + background-color: #FDC894; +} +#debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; +} +#debug-bar .toolbar { + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#debug-bar .toolbar img { + filter: brightness(0) invert(0.4); +} +#debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #DFDFDF; + -moz-box-shadow: 0 1px 4px #DFDFDF; + -webkit-box-shadow: 0 1px 4px #DFDFDF; +} +#debug-bar .muted { + color: #434343; +} +#debug-bar .muted td { + color: #DFDFDF; +} +#debug-bar .muted:hover td { + color: #434343; +} +#debug-bar #toolbar-position, +#debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); +} +#debug-bar .ci-label.active { + background-color: #DFDFDF; +} +#debug-bar .ci-label:hover { + background-color: #DFDFDF; +} +#debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; +} +#debug-bar .tab { + background-color: #FFFFFF; + box-shadow: 0 -1px 4px #DFDFDF; + -moz-box-shadow: 0 -1px 4px #DFDFDF; + -webkit-box-shadow: 0 -1px 4px #DFDFDF; +} +#debug-bar .timeline th, +#debug-bar .timeline td { + border-color: #DFDFDF; +} +#debug-bar .timeline .timer { + background-color: #DD8615; +} + +.debug-view.show-view { + border-color: #DD8615; +} + +.debug-view-path { + background-color: #FDC894; + color: #434343; +} + +@media (prefers-color-scheme: dark) { + #debug-icon { + background-color: #252525; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; + } + #debug-icon a:active, + #debug-icon a:link, + #debug-icon a:visited { + color: #DD8615; + } + #debug-bar { + background-color: #252525; + color: #DFDFDF; + } + #debug-bar h1, + #debug-bar h2, + #debug-bar h3, + #debug-bar p, + #debug-bar a, + #debug-bar button, + #debug-bar table, + #debug-bar thead, + #debug-bar tr, + #debug-bar td, + #debug-bar button, + #debug-bar .toolbar { + background-color: transparent; + color: #DFDFDF; + } + #debug-bar button { + background-color: #252525; + } + #debug-bar table strong { + color: #DD8615; + } + #debug-bar table tbody tr:hover { + background-color: #434343; + } + #debug-bar table tbody tr.current { + background-color: #FDC894; + } + #debug-bar table tbody tr.current td { + color: #252525; + } + #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; + } + #debug-bar .toolbar { + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar .toolbar img { + filter: brightness(0) invert(1); + } + #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; + } + #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; + } + #debug-bar .muted { + color: #DFDFDF; + } + #debug-bar .muted td { + color: #434343; + } + #debug-bar .muted:hover td { + color: #DFDFDF; + } + #debug-bar #toolbar-position, + #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); + } + #debug-bar .ci-label.active { + background-color: #252525; + } + #debug-bar .ci-label:hover { + background-color: #252525; + } + #debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; + } + #debug-bar .tab { + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; + } + #debug-bar .timeline th, + #debug-bar .timeline td { + border-color: #434343; + } + #debug-bar .timeline .timer { + background-color: #DD8615; + } + .debug-view.show-view { + border-color: #DD8615; + } + .debug-view-path { + background-color: #FDC894; + color: #434343; + } +} +#toolbarContainer.dark #debug-icon { + background-color: #252525; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.dark #debug-icon a:active, +#toolbarContainer.dark #debug-icon a:link, +#toolbarContainer.dark #debug-icon a:visited { + color: #DD8615; +} +#toolbarContainer.dark #debug-bar { + background-color: #252525; + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar h1, +#toolbarContainer.dark #debug-bar h2, +#toolbarContainer.dark #debug-bar h3, +#toolbarContainer.dark #debug-bar p, +#toolbarContainer.dark #debug-bar a, +#toolbarContainer.dark #debug-bar button, +#toolbarContainer.dark #debug-bar table, +#toolbarContainer.dark #debug-bar thead, +#toolbarContainer.dark #debug-bar tr, +#toolbarContainer.dark #debug-bar td, +#toolbarContainer.dark #debug-bar button, +#toolbarContainer.dark #debug-bar .toolbar { + background-color: transparent; + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar button { + background-color: #252525; +} +#toolbarContainer.dark #debug-bar table strong { + color: #DD8615; +} +#toolbarContainer.dark #debug-bar table tbody tr:hover { + background-color: #434343; +} +#toolbarContainer.dark #debug-bar table tbody tr.current { + background-color: #FDC894; +} +#toolbarContainer.dark #debug-bar table tbody tr.current td { + color: #252525; +} +#toolbarContainer.dark #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.dark #debug-bar .toolbar { + background-color: #434343; + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; +} +#toolbarContainer.dark #debug-bar .toolbar img { + filter: brightness(0) invert(1); +} +#toolbarContainer.dark #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #434343; + -moz-box-shadow: 0 0 4px #434343; + -webkit-box-shadow: 0 0 4px #434343; +} +#toolbarContainer.dark #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #434343; + -moz-box-shadow: 0 1px 4px #434343; + -webkit-box-shadow: 0 1px 4px #434343; +} +#toolbarContainer.dark #debug-bar .muted { + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar .muted td { + color: #434343; +} +#toolbarContainer.dark #debug-bar .muted:hover td { + color: #DFDFDF; +} +#toolbarContainer.dark #debug-bar #toolbar-position, +#toolbarContainer.dark #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); +} +#toolbarContainer.dark #debug-bar .ci-label.active { + background-color: #252525; +} +#toolbarContainer.dark #debug-bar .ci-label:hover { + background-color: #252525; +} +#toolbarContainer.dark #debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.dark #debug-bar .tab { + background-color: #252525; + box-shadow: 0 -1px 4px #434343; + -moz-box-shadow: 0 -1px 4px #434343; + -webkit-box-shadow: 0 -1px 4px #434343; +} +#toolbarContainer.dark #debug-bar .timeline th, +#toolbarContainer.dark #debug-bar .timeline td { + border-color: #434343; +} +#toolbarContainer.dark #debug-bar .timeline .timer { + background-color: #DD8615; +} +#toolbarContainer.dark .debug-view.show-view { + border-color: #DD8615; +} +#toolbarContainer.dark .debug-view-path { + background-color: #FDC894; + color: #434343; +} +#toolbarContainer.dark td[data-debugbar-route] input[type=text] { + background: #000; + color: #fff; +} + +#toolbarContainer.light #debug-icon { + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.light #debug-icon a:active, +#toolbarContainer.light #debug-icon a:link, +#toolbarContainer.light #debug-icon a:visited { + color: #DD8615; +} +#toolbarContainer.light #debug-bar { + background-color: #FFFFFF; + color: #434343; +} +#toolbarContainer.light #debug-bar h1, +#toolbarContainer.light #debug-bar h2, +#toolbarContainer.light #debug-bar h3, +#toolbarContainer.light #debug-bar p, +#toolbarContainer.light #debug-bar a, +#toolbarContainer.light #debug-bar button, +#toolbarContainer.light #debug-bar table, +#toolbarContainer.light #debug-bar thead, +#toolbarContainer.light #debug-bar tr, +#toolbarContainer.light #debug-bar td, +#toolbarContainer.light #debug-bar button, +#toolbarContainer.light #debug-bar .toolbar { + background-color: transparent; + color: #434343; +} +#toolbarContainer.light #debug-bar button { + background-color: #FFFFFF; +} +#toolbarContainer.light #debug-bar table strong { + color: #DD8615; +} +#toolbarContainer.light #debug-bar table tbody tr:hover { + background-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar table tbody tr.current { + background-color: #FDC894; +} +#toolbarContainer.light #debug-bar table tbody tr.current:hover td { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.light #debug-bar .toolbar { + background-color: #FFFFFF; + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar .toolbar img { + filter: brightness(0) invert(0.4); +} +#toolbarContainer.light #debug-bar.fixed-top .toolbar { + box-shadow: 0 0 4px #DFDFDF; + -moz-box-shadow: 0 0 4px #DFDFDF; + -webkit-box-shadow: 0 0 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar.fixed-top .tab { + box-shadow: 0 1px 4px #DFDFDF; + -moz-box-shadow: 0 1px 4px #DFDFDF; + -webkit-box-shadow: 0 1px 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar .muted { + color: #434343; +} +#toolbarContainer.light #debug-bar .muted td { + color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .muted:hover td { + color: #434343; +} +#toolbarContainer.light #debug-bar #toolbar-position, +#toolbarContainer.light #debug-bar #toolbar-theme { + filter: brightness(0) invert(0.6); +} +#toolbarContainer.light #debug-bar .ci-label.active { + background-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .ci-label:hover { + background-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .ci-label .badge { + background-color: #DD4814; + color: #FFFFFF; +} +#toolbarContainer.light #debug-bar .tab { + background-color: #FFFFFF; + box-shadow: 0 -1px 4px #DFDFDF; + -moz-box-shadow: 0 -1px 4px #DFDFDF; + -webkit-box-shadow: 0 -1px 4px #DFDFDF; +} +#toolbarContainer.light #debug-bar .timeline th, +#toolbarContainer.light #debug-bar .timeline td { + border-color: #DFDFDF; +} +#toolbarContainer.light #debug-bar .timeline .timer { + background-color: #DD8615; +} +#toolbarContainer.light .debug-view.show-view { + border-color: #DD8615; +} +#toolbarContainer.light .debug-view-path { + background-color: #FDC894; + color: #434343; +} + +.debug-bar-width30 { + width: 30%; +} + +.debug-bar-width10 { + width: 10%; +} + +.debug-bar-width70p { + width: 70px; +} + +.debug-bar-width190p { + width: 190px; +} + +.debug-bar-width20e { + width: 20em; +} + +.debug-bar-width6r { + width: 6rem; +} + +.debug-bar-ndisplay { + display: none; +} + +.debug-bar-alignRight { + text-align: right; +} + +.debug-bar-alignLeft { + text-align: left; +} + +.debug-bar-noverflow { + overflow: hidden; +} + +.debug-bar-dtableRow { + display: table-row; +} + +.debug-bar-dinlineBlock { + display: inline-block; +} + +.debug-bar-pointer { + cursor: pointer; +} + +.debug-bar-mleft4 { + margin-left: 4px; +} + +.debug-bar-level-0 { + --level: 0; +} + +.debug-bar-level-1 { + --level: 1; +} + +.debug-bar-level-2 { + --level: 2; +} + +.debug-bar-level-3 { + --level: 3; +} + +.debug-bar-level-4 { + --level: 4; +} + +.debug-bar-level-5 { + --level: 5; +} + +.debug-bar-level-6 { + --level: 6; +} diff --git a/system/Debug/Toolbar/Views/toolbar.js b/system/Debug/Toolbar/Views/toolbar.js new file mode 100755 index 0000000..54fe3d6 --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.js @@ -0,0 +1,825 @@ +/* + * Functionality for the CodeIgniter Debug Toolbar. + */ + +var ciDebugBar = { + toolbarContainer: null, + toolbar: null, + icon: null, + + init: function () { + this.toolbarContainer = document.getElementById("toolbarContainer"); + this.toolbar = document.getElementById("debug-bar"); + this.icon = document.getElementById("debug-icon"); + + ciDebugBar.createListeners(); + ciDebugBar.setToolbarState(); + ciDebugBar.setToolbarPosition(); + ciDebugBar.setToolbarTheme(); + ciDebugBar.toggleViewsHints(); + ciDebugBar.routerLink(); + ciDebugBar.setHotReloadState(); + + document + .getElementById("debug-bar-link") + .addEventListener("click", ciDebugBar.toggleToolbar, true); + document + .getElementById("debug-icon-link") + .addEventListener("click", ciDebugBar.toggleToolbar, true); + + // Allows to highlight the row of the current history request + var btn = this.toolbar.querySelector( + 'button[data-time="' + localStorage.getItem("debugbar-time") + '"]' + ); + ciDebugBar.addClass(btn.parentNode.parentNode, "current"); + + historyLoad = this.toolbar.getElementsByClassName("ci-history-load"); + + for (var i = 0; i < historyLoad.length; i++) { + historyLoad[i].addEventListener( + "click", + function () { + loadDoc(this.getAttribute("data-time")); + }, + true + ); + } + + // Display the active Tab on page load + var tab = ciDebugBar.readCookie("debug-bar-tab"); + if (document.getElementById(tab)) { + var el = document.getElementById(tab); + ciDebugBar.switchClass(el, "debug-bar-ndisplay", "debug-bar-dblock"); + ciDebugBar.addClass(el, "active"); + tab = document.querySelector("[data-tab=" + tab + "]"); + if (tab) { + ciDebugBar.addClass(tab.parentNode, "active"); + } + } + }, + + createListeners: function () { + var buttons = [].slice.call( + this.toolbar.querySelectorAll(".ci-label a") + ); + + for (var i = 0; i < buttons.length; i++) { + buttons[i].addEventListener("click", ciDebugBar.showTab, true); + } + + // Hook up generic toggle via data attributes `data-toggle="foo"` + var links = this.toolbar.querySelectorAll("[data-toggle]"); + for (var i = 0; i < links.length; i++) { + let toggleData = links[i].getAttribute("data-toggle"); + if (toggleData === "datatable") { + + let datatable = links[i].getAttribute("data-table"); + links[i].addEventListener("click", function() { + ciDebugBar.toggleDataTable(datatable) + }, true); + + } else if (toggleData === "childrows") { + + let child = links[i].getAttribute("data-child"); + links[i].addEventListener("click", function() { + ciDebugBar.toggleChildRows(child) + }, true); + + } else { + links[i].addEventListener("click", ciDebugBar.toggleRows, true); + } + } + }, + + showTab: function () { + // Get the target tab, if any + var tab = document.getElementById(this.getAttribute("data-tab")); + + // If the label have not a tab stops here + if (! tab) { + return; + } + + // Remove debug-bar-tab cookie + ciDebugBar.createCookie("debug-bar-tab", "", -1); + + // Check our current state. + var state = tab.classList.contains("debug-bar-dblock"); + + // Hide all tabs + var tabs = document.querySelectorAll("#debug-bar .tab"); + + for (var i = 0; i < tabs.length; i++) { + ciDebugBar.switchClass(tabs[i], "debug-bar-dblock", "debug-bar-ndisplay"); + } + + // Mark all labels as inactive + var labels = document.querySelectorAll("#debug-bar .ci-label"); + + for (var i = 0; i < labels.length; i++) { + ciDebugBar.removeClass(labels[i], "active"); + } + + // Show/hide the selected tab + if (! state) { + ciDebugBar.switchClass(tab, "debug-bar-ndisplay", "debug-bar-dblock"); + ciDebugBar.addClass(this.parentNode, "active"); + // Create debug-bar-tab cookie to persistent state + ciDebugBar.createCookie( + "debug-bar-tab", + this.getAttribute("data-tab"), + 365 + ); + } + }, + + addClass: function (el, className) { + if (el.classList) { + el.classList.add(className); + } else { + el.className += " " + className; + } + }, + + removeClass: function (el, className) { + if (el.classList) { + el.classList.remove(className); + } else { + el.className = el.className.replace( + new RegExp( + "(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", + "gi" + ), + " " + ); + } + }, + + switchClass : function(el, classFrom, classTo) { + ciDebugBar.removeClass(el, classFrom); + ciDebugBar.addClass(el, classTo); + }, + + /** + * Toggle display of another object based on + * the data-toggle value of this object + * + * @param event + */ + toggleRows: function (event) { + if (event.target) { + let row = event.target.closest("tr"); + let target = document.getElementById( + row.getAttribute("data-toggle") + ); + + if (target.classList.contains("debug-bar-ndisplay")) { + ciDebugBar.switchClass(target, "debug-bar-ndisplay", "debug-bar-dtableRow"); + } else { + ciDebugBar.switchClass(target, "debug-bar-dtableRow", "debug-bar-ndisplay"); + } + } + }, + + /** + * Toggle display of a data table + * + * @param obj + */ + toggleDataTable: function (obj) { + if (typeof obj == "string") { + obj = document.getElementById(obj + "_table"); + } + + if (obj) { + if (obj.classList.contains("debug-bar-ndisplay")) { + ciDebugBar.switchClass(obj, "debug-bar-ndisplay", "debug-bar-dblock"); + } else { + ciDebugBar.switchClass(obj, "debug-bar-dblock", "debug-bar-ndisplay"); + } + } + }, + + /** + * Toggle display of timeline child elements + * + * @param obj + */ + toggleChildRows: function (obj) { + if (typeof obj == "string") { + par = document.getElementById(obj + "_parent"); + obj = document.getElementById(obj + "_children"); + } + + if (par && obj) { + + if (obj.classList.contains("debug-bar-ndisplay")) { + ciDebugBar.removeClass(obj, "debug-bar-ndisplay"); + } else { + ciDebugBar.addClass(obj, "debug-bar-ndisplay"); + } + + par.classList.toggle("timeline-parent-open"); + } + }, + + //-------------------------------------------------------------------- + + /** + * Toggle tool bar from full to icon and icon to full + */ + toggleToolbar: function () { + var open = ! ciDebugBar.toolbar.classList.contains("debug-bar-ndisplay"); + + if (open) { + ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); + ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); + } else { + ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); + ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); + } + + // Remember it for other page loads on this site + ciDebugBar.createCookie("debug-bar-state", "", -1); + ciDebugBar.createCookie( + "debug-bar-state", + open == true ? "minimized" : "open", + 365 + ); + }, + + /** + * Sets the initial state of the toolbar (open or minimized) when + * the page is first loaded to allow it to remember the state between refreshes. + */ + setToolbarState: function () { + var open = ciDebugBar.readCookie("debug-bar-state"); + + if (open != "open") { + ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); + ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); + } else { + ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay"); + ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock"); + } + }, + + toggleViewsHints: function () { + // Avoid toggle hints on history requests that are not the initial + if ( + localStorage.getItem("debugbar-time") != + localStorage.getItem("debugbar-time-new") + ) { + var a = document.querySelector('a[data-tab="ci-views"]'); + a.href = "#"; + return; + } + + var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ] + var sortedComments = []; + var comments = []; + + var getComments = function () { + var nodes = []; + var result = []; + var xpathResults = document.evaluate( + "//comment()[starts-with(., ' DEBUG-VIEW')]", + document, + null, + XPathResult.ANY_TYPE, + null + ); + var nextNode = xpathResults.iterateNext(); + while (nextNode) { + nodes.push(nextNode); + nextNode = xpathResults.iterateNext(); + } + + // sort comment by opening and closing tags + for (var i = 0; i < nodes.length; ++i) { + // get file path + name to use as key + var path = nodes[i].nodeValue.substring( + 18, + nodes[i].nodeValue.length - 1 + ); + + if (nodes[i].nodeValue[12] === "S") { + // simple check for start comment + // create new entry + result[path] = [nodes[i], null]; + } else if (result[path]) { + // add to existing entry + result[path][1] = nodes[i]; + } + } + + return result; + }; + + // find node that has TargetNode as parentNode + var getParentNode = function (node, targetNode) { + if (node.parentNode === null) { + return null; + } + + if (node.parentNode !== targetNode) { + return getParentNode(node.parentNode, targetNode); + } + + return node; + }; + + // define invalid & outer ( also invalid ) elements + const INVALID_ELEMENTS = ["NOSCRIPT", "SCRIPT", "STYLE"]; + const OUTER_ELEMENTS = ["HTML", "BODY", "HEAD"]; + + var getValidElementInner = function (node, reverse) { + // handle invalid tags + if (OUTER_ELEMENTS.indexOf(node.nodeName) !== -1) { + for (var i = 0; i < document.body.children.length; ++i) { + var index = reverse + ? document.body.children.length - (i + 1) + : i; + var element = document.body.children[index]; + + // skip invalid tags + if (INVALID_ELEMENTS.indexOf(element.nodeName) !== -1) { + continue; + } + + return [element, reverse]; + } + + return null; + } + + // get to next valid element + while ( + node !== null && + INVALID_ELEMENTS.indexOf(node.nodeName) !== -1 + ) { + node = reverse + ? node.previousElementSibling + : node.nextElementSibling; + } + + // return non array if we couldnt find something + if (node === null) { + return null; + } + + return [node, reverse]; + }; + + // get next valid element ( to be safe to add divs ) + // @return [ element, skip element ] or null if we couldnt find a valid place + var getValidElement = function (nodeElement) { + if (nodeElement) { + if (nodeElement.nextElementSibling !== null) { + return ( + getValidElementInner( + nodeElement.nextElementSibling, + false + ) || + getValidElementInner( + nodeElement.previousElementSibling, + true + ) + ); + } + if (nodeElement.previousElementSibling !== null) { + return getValidElementInner( + nodeElement.previousElementSibling, + true + ); + } + } + + // something went wrong! -> element is not in DOM + return null; + }; + + function showHints() { + // Had AJAX? Reset view blocks + sortedComments = getComments(); + + for (var key in sortedComments) { + var startElement = getValidElement(sortedComments[key][0]); + var endElement = getValidElement(sortedComments[key][1]); + + // skip if we couldnt get a valid element + if (startElement === null || endElement === null) { + continue; + } + + // find element which has same parent as startelement + var jointParent = getParentNode( + endElement[0], + startElement[0].parentNode + ); + if (jointParent === null) { + // find element which has same parent as endelement + jointParent = getParentNode( + startElement[0], + endElement[0].parentNode + ); + if (jointParent === null) { + // both tries failed + continue; + } else { + startElement[0] = jointParent; + } + } else { + endElement[0] = jointParent; + } + + var debugDiv = document.createElement("div"); // holder + var debugPath = document.createElement("div"); // path + var childArray = startElement[0].parentNode.childNodes; // target child array + var parent = startElement[0].parentNode; + var start, end; + + // setup container + debugDiv.classList.add("debug-view"); + debugDiv.classList.add("show-view"); + debugPath.classList.add("debug-view-path"); + debugPath.innerText = key; + debugDiv.appendChild(debugPath); + + // calc distance between them + // start + for (var i = 0; i < childArray.length; ++i) { + // check for comment ( start & end ) -> if its before valid start element + if ( + childArray[i] === sortedComments[key][1] || + childArray[i] === sortedComments[key][0] || + childArray[i] === startElement[0] + ) { + start = i; + if (childArray[i] === sortedComments[key][0]) { + start++; // increase to skip the start comment + } + break; + } + } + // adjust if we want to skip the start element + if (startElement[1]) { + start++; + } + + // end + for (var i = start; i < childArray.length; ++i) { + if (childArray[i] === endElement[0]) { + end = i; + // dont break to check for end comment after end valid element + } else if (childArray[i] === sortedComments[key][1]) { + // if we found the end comment, we can break + end = i; + break; + } + } + + // move elements + var number = end - start; + if (endElement[1]) { + number++; + } + for (var i = 0; i < number; ++i) { + if (INVALID_ELEMENTS.indexOf(childArray[start]) !== -1) { + // skip invalid childs that can cause problems if moved + start++; + continue; + } + debugDiv.appendChild(childArray[start]); + } + + // add container to DOM + nodeList.push(parent.insertBefore(debugDiv, childArray[start])); + } + + ciDebugBar.createCookie("debug-view", "show", 365); + ciDebugBar.addClass(btn, "active"); + } + + function hideHints() { + for (var i = 0; i < nodeList.length; ++i) { + var index; + + // find index + for ( + var j = 0; + j < nodeList[i].parentNode.childNodes.length; + ++j + ) { + if (nodeList[i].parentNode.childNodes[j] === nodeList[i]) { + index = j; + break; + } + } + + // move child back + while (nodeList[i].childNodes.length !== 1) { + nodeList[i].parentNode.insertBefore( + nodeList[i].childNodes[1], + nodeList[i].parentNode.childNodes[index].nextSibling + ); + index++; + } + + nodeList[i].parentNode.removeChild(nodeList[i]); + } + nodeList.length = 0; + + ciDebugBar.createCookie("debug-view", "", -1); + ciDebugBar.removeClass(btn, "active"); + } + + var btn = document.querySelector("[data-tab=ci-views]"); + + // If the Views Collector is inactive stops here + if (! btn) { + return; + } + + btn.parentNode.onclick = function () { + if (ciDebugBar.readCookie("debug-view")) { + hideHints(); + } else { + showHints(); + } + }; + + // Determine Hints state on page load + if (ciDebugBar.readCookie("debug-view")) { + showHints(); + } + }, + + setToolbarPosition: function () { + var btnPosition = this.toolbar.querySelector("#toolbar-position"); + + if (ciDebugBar.readCookie("debug-bar-position") === "top") { + ciDebugBar.addClass(ciDebugBar.icon, "fixed-top"); + ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top"); + } + + btnPosition.addEventListener( + "click", + function () { + var position = ciDebugBar.readCookie("debug-bar-position"); + + ciDebugBar.createCookie("debug-bar-position", "", -1); + + if (! position || position === "bottom") { + ciDebugBar.createCookie("debug-bar-position", "top", 365); + ciDebugBar.addClass(ciDebugBar.icon, "fixed-top"); + ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top"); + } else { + ciDebugBar.createCookie( + "debug-bar-position", + "bottom", + 365 + ); + ciDebugBar.removeClass(ciDebugBar.icon, "fixed-top"); + ciDebugBar.removeClass(ciDebugBar.toolbar, "fixed-top"); + } + }, + true + ); + }, + + setToolbarTheme: function () { + var btnTheme = this.toolbar.querySelector("#toolbar-theme"); + var isDarkMode = window.matchMedia( + "(prefers-color-scheme: dark)" + ).matches; + var isLightMode = window.matchMedia( + "(prefers-color-scheme: light)" + ).matches; + + // If a cookie is set with a value, we force the color scheme + if (ciDebugBar.readCookie("debug-bar-theme") === "dark") { + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "light"); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, "dark"); + } else if (ciDebugBar.readCookie("debug-bar-theme") === "light") { + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark"); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light"); + } + + btnTheme.addEventListener( + "click", + function () { + var theme = ciDebugBar.readCookie("debug-bar-theme"); + + if ( + ! theme && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + // If there is no cookie, and "prefers-color-scheme" is set to "dark" + // It means that the user wants to switch to light mode + ciDebugBar.createCookie("debug-bar-theme", "light", 365); + ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark"); + ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light"); + } else { + if (theme === "dark") { + ciDebugBar.createCookie( + "debug-bar-theme", + "light", + 365 + ); + ciDebugBar.removeClass( + ciDebugBar.toolbarContainer, + "dark" + ); + ciDebugBar.addClass( + ciDebugBar.toolbarContainer, + "light" + ); + } else { + // In any other cases: if there is no cookie, or the cookie is set to + // "light", or the "prefers-color-scheme" is "light"... + ciDebugBar.createCookie("debug-bar-theme", "dark", 365); + ciDebugBar.removeClass( + ciDebugBar.toolbarContainer, + "light" + ); + ciDebugBar.addClass( + ciDebugBar.toolbarContainer, + "dark" + ); + } + } + }, + true + ); + }, + + setHotReloadState: function () { + var btn = document.getElementById("debug-hot-reload").parentNode; + var btnImg = btn.getElementsByTagName("img")[0]; + var eventSource; + + // If the Hot Reload Collector is inactive stops here + if (! btn) { + return; + } + + btn.onclick = function () { + if (ciDebugBar.readCookie("debug-hot-reload")) { + ciDebugBar.createCookie("debug-hot-reload", "", -1); + ciDebugBar.removeClass(btn, "active"); + ciDebugBar.removeClass(btnImg, "rotate"); + + // Close the EventSource connection if it exists + if (typeof eventSource !== "undefined") { + eventSource.close(); + eventSource = void 0; // Undefine the variable + } + } else { + ciDebugBar.createCookie("debug-hot-reload", "show", 365); + ciDebugBar.addClass(btn, "active"); + ciDebugBar.addClass(btnImg, "rotate"); + + eventSource = ciDebugBar.hotReloadConnect(); + } + }; + + // Determine Hot Reload state on page load + if (ciDebugBar.readCookie("debug-hot-reload")) { + ciDebugBar.addClass(btn, "active"); + ciDebugBar.addClass(btnImg, "rotate"); + eventSource = ciDebugBar.hotReloadConnect(); + } + }, + + hotReloadConnect: function () { + const eventSource = new EventSource(ciSiteURL + "/__hot-reload"); + + eventSource.addEventListener("reload", function (e) { + console.log("reload", e); + window.location.reload(); + }); + + eventSource.onerror = (err) => { + console.error("EventSource failed:", err); + }; + + return eventSource; + }, + + /** + * Helper to create a cookie. + * + * @param name + * @param value + * @param days + */ + createCookie: function (name, value, days) { + if (days) { + var date = new Date(); + + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + + var expires = "; expires=" + date.toGMTString(); + } else { + var expires = ""; + } + + document.cookie = + name + "=" + value + expires + "; path=/; samesite=Lax"; + }, + + readCookie: function (name) { + var nameEQ = name + "="; + var ca = document.cookie.split(";"); + + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) == 0) { + return c.substring(nameEQ.length, c.length); + } + } + return null; + }, + + trimSlash: function (text) { + return text.replace(/^\/|\/$/g, ""); + }, + + routerLink: function () { + var row, _location; + var rowGet = this.toolbar.querySelectorAll( + 'td[data-debugbar-route="GET"]' + ); + var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/; + + for (var i = 0; i < rowGet.length; i++) { + row = rowGet[i]; + if (!/\/\(.+?\)/.test(rowGet[i].innerText)) { + ciDebugBar.addClass(row, "debug-bar-pointer"); + row.setAttribute( + "title", + location.origin + "/" + ciDebugBar.trimSlash(row.innerText) + ); + row.addEventListener("click", function (ev) { + _location = + location.origin + + "/" + + ciDebugBar.trimSlash(ev.target.innerText); + var redirectWindow = window.open(_location, "_blank"); + redirectWindow.location; + }); + } else { + row.innerHTML = + "
" + + row.innerText + + "
" + + '' + + row.innerText.replace( + patt, + '' + ) + + '' + + ""; + } + } + + rowGet = this.toolbar.querySelectorAll( + 'td[data-debugbar-route="GET"] form' + ); + for (var i = 0; i < rowGet.length; i++) { + row = rowGet[i]; + + row.addEventListener("submit", function (event) { + event.preventDefault(); + var inputArray = [], + t = 0; + var input = event.target.querySelectorAll("input[type=text]"); + var tpl = event.target.getAttribute("data-debugbar-route-tpl"); + + for (var n = 0; n < input.length; n++) { + if (input[n].value.length > 0) { + inputArray.push(input[n].value); + } + } + + if (inputArray.length > 0) { + _location = + location.origin + + "/" + + tpl.replace(/\?/g, function () { + return inputArray[t++]; + }); + + var redirectWindow = window.open(_location, "_blank"); + redirectWindow.location; + } + }); + } + }, +}; diff --git a/system/Debug/Toolbar/Views/toolbar.tpl.php b/system/Debug/Toolbar/Views/toolbar.tpl.php new file mode 100755 index 0000000..8179c1a --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.tpl.php @@ -0,0 +1,277 @@ + + + + + +
+
+ + 🔅 + + + + + + + + + ms   MB + + + + + + + + + + + + + + + + + + + + + + + Vars + + + +

+ + + + + + +

+ + + + + +
+ + +
+ + + + + + + + + + + + + renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?> + +
NAMECOMPONENTDURATION ms
+
+ + + + + +
+

+ + setData($c['display'])->render("_{$c['titleSafe']}.tpl") ?> +
+ + + + + +
+ + + + $items) : ?> + + +

+
+ + + + + + $value) : ?> + + + + + + +
+ + +

No data to display.

+ + + + + + +

Session User Data

+
+ + + + + + $value) : ?> + + + + + + +
+ +

No data to display.

+ + +

Session doesn't seem to be active.

+ + +

Request ( )

+ + + +

$_GET

+
+ + + + $value) : ?> + + + + + + +
+ + + + +

$_POST

+
+ + + + $value) : ?> + + + + + + +
+ + + + +

Headers

+
+ + + + $value) : ?> + + + + + + +
+ + + + +

Cookies

+
+ + + + $value) : ?> + + + + + + + + + +

Response + ( ) +

+ + + +

Headers

+
+ + + + $value) : ?> + + + + + + +
+ +
+ + +
+

System Configuration

+ + setData($config)->render('_config.tpl') ?> +
+
+ diff --git a/system/Debug/Toolbar/Views/toolbarloader.js b/system/Debug/Toolbar/Views/toolbarloader.js new file mode 100755 index 0000000..a8bacb4 --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbarloader.js @@ -0,0 +1,90 @@ +document.addEventListener('DOMContentLoaded', loadDoc, false); + +function loadDoc(time) { + if (isNaN(time)) { + time = document.getElementById("debugbar_loader").getAttribute("data-time"); + localStorage.setItem('debugbar-time', time); + } + + localStorage.setItem('debugbar-time-new', time); + + let url = '{url}'; + let xhttp = new XMLHttpRequest(); + + xhttp.onreadystatechange = function() { + if (this.readyState === 4 && this.status === 200) { + let toolbar = document.getElementById("toolbarContainer"); + + if (! toolbar) { + toolbar = document.createElement('div'); + toolbar.setAttribute('id', 'toolbarContainer'); + document.body.appendChild(toolbar); + } + + let responseText = this.responseText; + let dynamicStyle = document.getElementById('debugbar_dynamic_style'); + let dynamicScript = document.getElementById('debugbar_dynamic_script'); + + // get the first style block, copy contents to dynamic_style, then remove here + let start = responseText.indexOf('>', responseText.indexOf('', start); + dynamicStyle.innerHTML = responseText.substr(start, end - start); + responseText = responseText.substr(end + 8); + + // get the first script after the first style, copy contents to dynamic_script, then remove here + start = responseText.indexOf('>', responseText.indexOf('', start); + dynamicScript.innerHTML = responseText.substr(start, end - start); + responseText = responseText.substr(end + 9); + + // check for last style block, append contents to dynamic_style, then remove here + start = responseText.indexOf('>', responseText.indexOf('', start); + dynamicStyle.innerHTML += responseText.substr(start, end - start); + responseText = responseText.substr(0, start - 8); + + toolbar.innerHTML = responseText; + + if (typeof ciDebugBar === 'object') { + ciDebugBar.init(); + } + } else if (this.readyState === 4 && this.status === 404) { + console.log('CodeIgniter DebugBar: File "WRITEPATH/debugbar/debugbar_' + time + '" not found.'); + } + }; + + xhttp.open("GET", url + "?debugbar_time=" + time, true); + xhttp.send(); +} + +window.oldXHR = window.ActiveXObject + ? new ActiveXObject('Microsoft.XMLHTTP') + : window.XMLHttpRequest; + +function newXHR() { + const realXHR = new window.oldXHR(); + + realXHR.addEventListener("readystatechange", function() { + // Only success responses and URLs that do not contains "debugbar_time" are tracked + if (realXHR.readyState === 4 && realXHR.status.toString()[0] === '2' && realXHR.responseURL.indexOf('debugbar_time') === -1) { + if (realXHR.getAllResponseHeaders().indexOf("Debugbar-Time") >= 0) { + let debugbarTime = realXHR.getResponseHeader('Debugbar-Time'); + + if (debugbarTime) { + let h2 = document.querySelector('#ci-history > h2'); + + if (h2) { + h2.innerHTML = 'History You have new debug data. '; + document.querySelector('a[data-tab="ci-history"] > span > .badge').className += ' active'; + document.getElementById('ci-history-update').addEventListener('click', function () { + loadDoc(debugbarTime); + }, false) + } + } + } + } + }, false); + return realXHR; +} + +window.XMLHttpRequest = newXHR; diff --git a/system/Email/Email.php b/system/Email/Email.php new file mode 100755 index 0000000..17b24c9 --- /dev/null +++ b/system/Email/Email.php @@ -0,0 +1,2276 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Email; + +use CodeIgniter\Events\Events; +use CodeIgniter\I18n\Time; +use Config\Mimes; +use ErrorException; + +/** + * CodeIgniter Email Class + * + * Permits email to be sent using Mail, Sendmail, or SMTP. + * + * @see \CodeIgniter\Email\EmailTest + */ +class Email +{ + /** + * Properties from the last successful send. + * + * @var array|null + */ + public $archive; + + /** + * Properties to be added to the next archive. + * + * @var array + */ + protected $tmpArchive = []; + + /** + * @var string + */ + public $fromEmail; + + /** + * @var string + */ + public $fromName; + + /** + * Used as the User-Agent and X-Mailer headers' value. + * + * @var string + */ + public $userAgent = 'CodeIgniter'; + + /** + * Path to the Sendmail binary. + * + * @var string + */ + public $mailPath = '/usr/sbin/sendmail'; + + /** + * Which method to use for sending e-mails. + * + * @var string 'mail', 'sendmail' or 'smtp' + */ + public $protocol = 'mail'; + + /** + * STMP Server Hostname + * + * @var string + */ + public $SMTPHost = ''; + + /** + * SMTP Username + * + * @var string + */ + public $SMTPUser = ''; + + /** + * SMTP Password + * + * @var string + */ + public $SMTPPass = ''; + + /** + * SMTP Server port + * + * @var int + */ + public $SMTPPort = 25; + + /** + * SMTP connection timeout in seconds + * + * @var int + */ + public $SMTPTimeout = 5; + + /** + * SMTP persistent connection + * + * @var bool + */ + public $SMTPKeepAlive = false; + + /** + * SMTP Encryption + * + * @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command + * to the server. 'ssl' means implicit SSL. Connection on port + * 465 should set this to ''. + */ + public $SMTPCrypto = ''; + + /** + * Whether to apply word-wrapping to the message body. + * + * @var bool + */ + public $wordWrap = true; + + /** + * Number of characters to wrap at. + * + * @see Email::$wordWrap + * + * @var int + */ + public $wrapChars = 76; + + /** + * Message format. + * + * @var string 'text' or 'html' + */ + public $mailType = 'text'; + + /** + * Character set (default: utf-8) + * + * @var string + */ + public $charset = 'UTF-8'; + + /** + * Alternative message (for HTML messages only) + * + * @var string + */ + public $altMessage = ''; + + /** + * Whether to validate e-mail addresses. + * + * @var bool + */ + public $validate = true; + + /** + * X-Priority header value. + * + * @var int 1-5 + */ + public $priority = 3; + + /** + * Newline character sequence. + * Use "\r\n" to comply with RFC 822. + * + * @see http://www.ietf.org/rfc/rfc822.txt + * + * @var string "\r\n" or "\n" + */ + public $newline = "\r\n"; + + /** + * CRLF character sequence + * + * RFC 2045 specifies that for 'quoted-printable' encoding, + * "\r\n" must be used. However, it appears that some servers + * (even on the receiving end) don't handle it properly and + * switching to "\n", while improper, is the only solution + * that seems to work for all environments. + * + * @see http://www.ietf.org/rfc/rfc822.txt + * + * @var string + */ + public $CRLF = "\r\n"; + + /** + * Whether to use Delivery Status Notification. + * + * @var bool + */ + public $DSN = false; + + /** + * Whether to send multipart alternatives. + * Yahoo! doesn't seem to like these. + * + * @var bool + */ + public $sendMultipart = true; + + /** + * Whether to send messages to BCC recipients in batches. + * + * @var bool + */ + public $BCCBatchMode = false; + + /** + * BCC Batch max number size. + * + * @see Email::$BCCBatchMode + * + * @var int|string + */ + public $BCCBatchSize = 200; + + /** + * Subject header + * + * @var string + */ + protected $subject = ''; + + /** + * Message body + * + * @var string + */ + protected $body = ''; + + /** + * Final message body to be sent. + * + * @var string + */ + protected $finalBody = ''; + + /** + * Final headers to send + * + * @var string + */ + protected $headerStr = ''; + + /** + * SMTP Connection socket placeholder + * + * @var resource|null + */ + protected $SMTPConnect; + + /** + * Mail encoding + * + * @var string '8bit' or '7bit' + */ + protected $encoding = '8bit'; + + /** + * Whether to perform SMTP authentication + * + * @var bool + */ + protected $SMTPAuth = false; + + /** + * Whether to send a Reply-To header + * + * @var bool + */ + protected $replyToFlag = false; + + /** + * Debug messages + * + * @see Email::printDebugger() + * + * @var array + */ + protected $debugMessage = []; + + /** + * Raw debug messages + * + * @var list + */ + private array $debugMessageRaw = []; + + /** + * Recipients + * + * @var array|string + */ + protected $recipients = []; + + /** + * CC Recipients + * + * @var array + */ + protected $CCArray = []; + + /** + * BCC Recipients + * + * @var array + */ + protected $BCCArray = []; + + /** + * Message headers + * + * @var array + */ + protected $headers = []; + + /** + * Attachment data + * + * @var array + */ + protected $attachments = []; + + /** + * Valid $protocol values + * + * @see Email::$protocol + * + * @var array + */ + protected $protocols = [ + 'mail', + 'sendmail', + 'smtp', + ]; + + /** + * Character sets valid for 7-bit encoding, + * excluding language suffix. + * + * @var list + */ + protected $baseCharsets = [ + 'us-ascii', + 'iso-2022-', + ]; + + /** + * Bit depths + * + * Valid mail encodings + * + * @see Email::$encoding + * + * @var array + */ + protected $bitDepths = [ + '7bit', + '8bit', + ]; + + /** + * $priority translations + * + * Actual values to send with the X-Priority header + * + * @var array + */ + protected $priorities = [ + 1 => '1 (Highest)', + 2 => '2 (High)', + 3 => '3 (Normal)', + 4 => '4 (Low)', + 5 => '5 (Lowest)', + ]; + + /** + * mbstring.func_overload flag + * + * @var bool + */ + protected static $func_overload; + + /** + * @param array|\Config\Email|null $config + */ + public function __construct($config = null) + { + $this->initialize($config); + if (! isset(static::$func_overload)) { + static::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); + } + } + + /** + * Initialize preferences + * + * @param array|\Config\Email|null $config + * + * @return Email + */ + public function initialize($config) + { + $this->clear(); + + if ($config instanceof \Config\Email) { + $config = get_object_vars($config); + } + + foreach (array_keys(get_class_vars(static::class)) as $key) { + if (property_exists($this, $key) && isset($config[$key])) { + $method = 'set' . ucfirst($key); + + if (method_exists($this, $method)) { + $this->{$method}($config[$key]); + } else { + $this->{$key} = $config[$key]; + } + } + } + + $this->charset = strtoupper($this->charset); + $this->SMTPAuth = isset($this->SMTPUser[0], $this->SMTPPass[0]); + + return $this; + } + + /** + * @param bool $clearAttachments + * + * @return Email + */ + public function clear($clearAttachments = false) + { + $this->subject = ''; + $this->body = ''; + $this->finalBody = ''; + $this->headerStr = ''; + $this->replyToFlag = false; + $this->recipients = []; + $this->CCArray = []; + $this->BCCArray = []; + $this->headers = []; + $this->debugMessage = []; + $this->debugMessageRaw = []; + + $this->setHeader('Date', $this->setDate()); + + if ($clearAttachments !== false) { + $this->attachments = []; + } + + return $this; + } + + /** + * @param string $from + * @param string $name + * @param string|null $returnPath Return-Path + * + * @return Email + */ + public function setFrom($from, $name = '', $returnPath = null) + { + if (preg_match('/\<(.*)\>/', $from, $match)) { + $from = $match[1]; + } + + if ($this->validate) { + $this->validateEmail($this->stringToArray($from)); + + if ($returnPath !== null) { + $this->validateEmail($this->stringToArray($returnPath)); + } + } + + $this->tmpArchive['fromEmail'] = $from; + $this->tmpArchive['fromName'] = $name; + + if ($name !== '') { + // only use Q encoding if there are characters that would require it + if (preg_match('/[\200-\377]/', $name) !== 1) { + $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"'; + } else { + $name = $this->prepQEncoding($name); + } + } + + $this->setHeader('From', $name . ' <' . $from . '>'); + if (! isset($returnPath)) { + $returnPath = $from; + } + $this->setHeader('Return-Path', '<' . $returnPath . '>'); + $this->tmpArchive['returnPath'] = $returnPath; + + return $this; + } + + /** + * @param string $replyto + * @param string $name + * + * @return Email + */ + public function setReplyTo($replyto, $name = '') + { + if (preg_match('/\<(.*)\>/', $replyto, $match)) { + $replyto = $match[1]; + } + + if ($this->validate) { + $this->validateEmail($this->stringToArray($replyto)); + } + + if ($name !== '') { + $this->tmpArchive['replyName'] = $name; + + // only use Q encoding if there are characters that would require it + if (preg_match('/[\200-\377]/', $name) !== 1) { + $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"'; + } else { + $name = $this->prepQEncoding($name); + } + } + + $this->setHeader('Reply-To', $name . ' <' . $replyto . '>'); + $this->replyToFlag = true; + $this->tmpArchive['replyTo'] = $replyto; + + return $this; + } + + /** + * @param array|string $to + * + * @return Email + */ + public function setTo($to) + { + $to = $this->stringToArray($to); + $to = $this->cleanEmail($to); + + if ($this->validate) { + $this->validateEmail($to); + } + + if ($this->getProtocol() !== 'mail') { + $this->setHeader('To', implode(', ', $to)); + } + + $this->recipients = $to; + + return $this; + } + + /** + * @param string $cc + * + * @return Email + */ + public function setCC($cc) + { + $cc = $this->cleanEmail($this->stringToArray($cc)); + + if ($this->validate) { + $this->validateEmail($cc); + } + + $this->setHeader('Cc', implode(', ', $cc)); + + if ($this->getProtocol() === 'smtp') { + $this->CCArray = $cc; + } + + $this->tmpArchive['CCArray'] = $cc; + + return $this; + } + + /** + * @param string $bcc + * @param string $limit + * + * @return Email + */ + public function setBCC($bcc, $limit = '') + { + if ($limit !== '' && is_numeric($limit)) { + $this->BCCBatchMode = true; + $this->BCCBatchSize = $limit; + } + + $bcc = $this->cleanEmail($this->stringToArray($bcc)); + + if ($this->validate) { + $this->validateEmail($bcc); + } + + if ($this->getProtocol() === 'smtp' || ($this->BCCBatchMode && count($bcc) > $this->BCCBatchSize)) { + $this->BCCArray = $bcc; + } else { + $this->setHeader('Bcc', implode(', ', $bcc)); + $this->tmpArchive['BCCArray'] = $bcc; + } + + return $this; + } + + /** + * @param string $subject + * + * @return Email + */ + public function setSubject($subject) + { + $this->tmpArchive['subject'] = $subject; + + $subject = $this->prepQEncoding($subject); + $this->setHeader('Subject', $subject); + + return $this; + } + + /** + * @param string $body + * + * @return Email + */ + public function setMessage($body) + { + $this->body = rtrim(str_replace("\r", '', $body)); + + return $this; + } + + /** + * @param string $file Can be local path, URL or buffered content + * @param string $disposition 'attachment' + * @param string|null $newname + * @param string $mime + * + * @return bool|Email + */ + public function attach($file, $disposition = '', $newname = null, $mime = '') + { + if ($mime === '') { + if (! str_contains($file, '://') && ! is_file($file)) { + $this->setErrorMessage(lang('Email.attachmentMissing', [$file])); + + return false; + } + + if (! $fp = @fopen($file, 'rb')) { + $this->setErrorMessage(lang('Email.attachmentUnreadable', [$file])); + + return false; + } + + $fileContent = stream_get_contents($fp); + + $mime = $this->mimeTypes(pathinfo($file, PATHINFO_EXTENSION)); + + fclose($fp); + } else { + $fileContent = &$file; // buffered file + } + + // declare names on their own, to make phpcbf happy + $namesAttached = [$file, $newname]; + + $this->attachments[] = [ + 'name' => $namesAttached, + 'disposition' => empty($disposition) ? 'attachment' : $disposition, + // Can also be 'inline' Not sure if it matters + 'type' => $mime, + 'content' => chunk_split(base64_encode($fileContent)), + 'multipart' => 'mixed', + ]; + + return $this; + } + + /** + * Set and return attachment Content-ID + * Useful for attached inline pictures + * + * @param string $filename + * + * @return bool|string + */ + public function setAttachmentCID($filename) + { + foreach ($this->attachments as $i => $attachment) { + // For file path. + if ($attachment['name'][0] === $filename) { + $this->attachments[$i]['multipart'] = 'related'; + + $this->attachments[$i]['cid'] = uniqid(basename($attachment['name'][0]) . '@', true); + + return $this->attachments[$i]['cid']; + } + + // For buffer string. + if ($attachment['name'][1] === $filename) { + $this->attachments[$i]['multipart'] = 'related'; + + $this->attachments[$i]['cid'] = uniqid(basename($attachment['name'][1]) . '@', true); + + return $this->attachments[$i]['cid']; + } + } + + return false; + } + + /** + * @param string $header + * @param string $value + * + * @return Email + */ + public function setHeader($header, $value) + { + $this->headers[$header] = str_replace(["\n", "\r"], '', $value); + + return $this; + } + + /** + * @param array|string $email + * + * @return array + */ + protected function stringToArray($email) + { + if (! is_array($email)) { + return (str_contains($email, ',')) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email); + } + + return $email; + } + + /** + * @param string $str + * + * @return Email + */ + public function setAltMessage($str) + { + $this->altMessage = (string) $str; + + return $this; + } + + /** + * @param string $type + * + * @return Email + */ + public function setMailType($type = 'text') + { + $this->mailType = ($type === 'html') ? 'html' : 'text'; + + return $this; + } + + /** + * @param bool $wordWrap + * + * @return Email + */ + public function setWordWrap($wordWrap = true) + { + $this->wordWrap = (bool) $wordWrap; + + return $this; + } + + /** + * @param string $protocol + * + * @return Email + */ + public function setProtocol($protocol = 'mail') + { + $this->protocol = in_array($protocol, $this->protocols, true) ? strtolower($protocol) : 'mail'; + + return $this; + } + + /** + * @param int $n + * + * @return Email + */ + public function setPriority($n = 3) + { + $this->priority = preg_match('/^[1-5]$/', (string) $n) ? (int) $n : 3; + + return $this; + } + + /** + * @param string $newline + * + * @return Email + */ + public function setNewline($newline = "\n") + { + $this->newline = in_array($newline, ["\n", "\r\n", "\r"], true) ? $newline : "\n"; + + return $this; + } + + /** + * @param string $CRLF + * + * @return Email + */ + public function setCRLF($CRLF = "\n") + { + $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF; + + return $this; + } + + /** + * @return string + */ + protected function getMessageID() + { + $from = str_replace(['>', '<'], '', $this->headers['Return-Path']); + + return '<' . uniqid('', true) . strstr($from, '@') . '>'; + } + + /** + * @return string + */ + protected function getProtocol() + { + $this->protocol = strtolower($this->protocol); + + if (! in_array($this->protocol, $this->protocols, true)) { + $this->protocol = 'mail'; + } + + return $this->protocol; + } + + /** + * @return string + */ + protected function getEncoding() + { + if (! in_array($this->encoding, $this->bitDepths, true)) { + $this->encoding = '8bit'; + } + + foreach ($this->baseCharsets as $charset) { + if (str_starts_with($this->charset, $charset)) { + $this->encoding = '7bit'; + + break; + } + } + + return $this->encoding; + } + + /** + * @return string + */ + protected function getContentType() + { + if ($this->mailType === 'html') { + return empty($this->attachments) ? 'html' : 'html-attach'; + } + + if ($this->mailType === 'text' && ! empty($this->attachments)) { + return 'plain-attach'; + } + + return 'plain'; + } + + /** + * Set RFC 822 Date + * + * @return string + */ + protected function setDate() + { + $timezone = date('Z'); + $operator = ($timezone[0] === '-') ? '-' : '+'; + $timezone = abs((int) $timezone); + $timezone = floor($timezone / 3600) * 100 + ($timezone % 3600) / 60; + + return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone); + } + + /** + * @return string + */ + protected function getMimeMessage() + { + return 'This is a multi-part message in MIME format.' . $this->newline . 'Your email application may not support this format.'; + } + + /** + * @param array|string $email + * + * @return bool + */ + public function validateEmail($email) + { + if (! is_array($email)) { + $this->setErrorMessage(lang('Email.mustBeArray')); + + return false; + } + + foreach ($email as $val) { + if (! $this->isValidEmail($val)) { + $this->setErrorMessage(lang('Email.invalidAddress', [$val])); + + return false; + } + } + + return true; + } + + /** + * @param string $email + * + * @return bool + */ + public function isValidEmail($email) + { + if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && $atpos = strpos($email, '@')) { + $email = static::substr($email, 0, ++$atpos) + . idn_to_ascii(static::substr($email, $atpos), 0, INTL_IDNA_VARIANT_UTS46); + } + + return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); + } + + /** + * @param array|string $email + * + * @return array|string + */ + public function cleanEmail($email) + { + if (! is_array($email)) { + return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email; + } + + $cleanEmail = []; + + foreach ($email as $addy) { + $cleanEmail[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy; + } + + return $cleanEmail; + } + + /** + * Build alternative plain text message + * + * Provides the raw message for use in plain-text headers of + * HTML-formatted emails. + * + * If the user hasn't specified his own alternative message + * it creates one by stripping the HTML + * + * @return string + */ + protected function getAltMessage() + { + if (! empty($this->altMessage)) { + return ($this->wordWrap) ? $this->wordWrap($this->altMessage, 76) : $this->altMessage; + } + + $body = preg_match('/\(.*)\<\/body\>/si', $this->body, $match) ? $match[1] : $this->body; + $body = str_replace("\t", '', preg_replace('# [Entity] --- (2) --> [Database] + * [App Code] <-- (4) --- [Entity] <-- (3) --- [Database] + */ +interface CastInterface +{ + /** + * Takes a raw value from Entity, returns its value for PHP. + * + * @param array|bool|float|int|object|string|null $value Data + * @param array $params Additional param + * + * @return array|bool|float|int|object|string|null + */ + public static function get($value, array $params = []); + + /** + * Takes a PHP value, returns its raw value for Entity. + * + * @param array|bool|float|int|object|string|null $value Data + * @param array $params Additional param + * + * @return array|bool|float|int|object|string|null + */ + public static function set($value, array $params = []); +} diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php new file mode 100755 index 0000000..88b7b29 --- /dev/null +++ b/system/Entity/Cast/DatetimeCast.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +use CodeIgniter\I18n\Time; +use DateTime; +use Exception; + +/** + * Class DatetimeCast + */ +class DatetimeCast extends BaseCast +{ + /** + * {@inheritDoc} + * + * @return Time + * + * @throws Exception + */ + public static function get($value, array $params = []) + { + if ($value instanceof Time) { + return $value; + } + + if ($value instanceof DateTime) { + return Time::createFromInstance($value); + } + + if (is_numeric($value)) { + return Time::createFromTimestamp((int) $value, date_default_timezone_get()); + } + + if (is_string($value)) { + return Time::parse($value); + } + + return $value; + } +} diff --git a/system/Entity/Cast/FloatCast.php b/system/Entity/Cast/FloatCast.php new file mode 100755 index 0000000..642e849 --- /dev/null +++ b/system/Entity/Cast/FloatCast.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +/** + * Class FloatCast + */ +class FloatCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []): float + { + return (float) $value; + } +} diff --git a/system/Entity/Cast/IntBoolCast.php b/system/Entity/Cast/IntBoolCast.php new file mode 100755 index 0000000..fb1c470 --- /dev/null +++ b/system/Entity/Cast/IntBoolCast.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +/** + * Int Bool Cast + * + * DB column: int (0/1) <--> Class property: bool + */ +final class IntBoolCast extends BaseCast +{ + /** + * @param int $value + */ + public static function get($value, array $params = []): bool + { + return (bool) $value; + } + + /** + * @param bool|int|string $value + */ + public static function set($value, array $params = []): int + { + return (int) $value; + } +} diff --git a/system/Entity/Cast/IntegerCast.php b/system/Entity/Cast/IntegerCast.php new file mode 100755 index 0000000..c0ecec7 --- /dev/null +++ b/system/Entity/Cast/IntegerCast.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +/** + * Class IntegerCast + */ +class IntegerCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []): int + { + return (int) $value; + } +} diff --git a/system/Entity/Cast/JsonCast.php b/system/Entity/Cast/JsonCast.php new file mode 100755 index 0000000..ef38926 --- /dev/null +++ b/system/Entity/Cast/JsonCast.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +use CodeIgniter\Entity\Exceptions\CastException; +use JsonException; +use stdClass; + +/** + * Class JsonCast + */ +class JsonCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []) + { + $associative = in_array('array', $params, true); + + $tmp = $value !== null ? ($associative ? [] : new stdClass()) : null; + + if (function_exists('json_decode') + && ( + (is_string($value) + && strlen($value) > 1 + && in_array($value[0], ['[', '{', '"'], true)) + || is_numeric($value) + ) + ) { + try { + $tmp = json_decode($value, $associative, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw CastException::forInvalidJsonFormat($e->getCode()); + } + } + + return $tmp; + } + + /** + * {@inheritDoc} + */ + public static function set($value, array $params = []): string + { + if (function_exists('json_encode')) { + try { + $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw CastException::forInvalidJsonFormat($e->getCode()); + } + } + + return $value; + } +} diff --git a/system/Entity/Cast/ObjectCast.php b/system/Entity/Cast/ObjectCast.php new file mode 100755 index 0000000..1bee213 --- /dev/null +++ b/system/Entity/Cast/ObjectCast.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +/** + * Class ObjectCast + */ +class ObjectCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []): object + { + return (object) $value; + } +} diff --git a/system/Entity/Cast/StringCast.php b/system/Entity/Cast/StringCast.php new file mode 100755 index 0000000..e4ed04b --- /dev/null +++ b/system/Entity/Cast/StringCast.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +/** + * Class StringCast + */ +class StringCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []): string + { + return (string) $value; + } +} diff --git a/system/Entity/Cast/TimestampCast.php b/system/Entity/Cast/TimestampCast.php new file mode 100755 index 0000000..a445bad --- /dev/null +++ b/system/Entity/Cast/TimestampCast.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +use CodeIgniter\Entity\Exceptions\CastException; + +/** + * Class TimestampCast + */ +class TimestampCast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []) + { + $value = strtotime($value); + + if ($value === false) { + throw CastException::forInvalidTimestamp(); + } + + return $value; + } +} diff --git a/system/Entity/Cast/URICast.php b/system/Entity/Cast/URICast.php new file mode 100755 index 0000000..d0d510b --- /dev/null +++ b/system/Entity/Cast/URICast.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Cast; + +use CodeIgniter\HTTP\URI; + +/** + * Class URICast + */ +class URICast extends BaseCast +{ + /** + * {@inheritDoc} + */ + public static function get($value, array $params = []): URI + { + return $value instanceof URI ? $value : new URI($value); + } +} diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php new file mode 100755 index 0000000..0e54c72 --- /dev/null +++ b/system/Entity/Entity.php @@ -0,0 +1,572 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity; + +use CodeIgniter\DataCaster\DataCaster; +use CodeIgniter\Entity\Cast\ArrayCast; +use CodeIgniter\Entity\Cast\BooleanCast; +use CodeIgniter\Entity\Cast\CSVCast; +use CodeIgniter\Entity\Cast\DatetimeCast; +use CodeIgniter\Entity\Cast\FloatCast; +use CodeIgniter\Entity\Cast\IntBoolCast; +use CodeIgniter\Entity\Cast\IntegerCast; +use CodeIgniter\Entity\Cast\JsonCast; +use CodeIgniter\Entity\Cast\ObjectCast; +use CodeIgniter\Entity\Cast\StringCast; +use CodeIgniter\Entity\Cast\TimestampCast; +use CodeIgniter\Entity\Cast\URICast; +use CodeIgniter\Entity\Exceptions\CastException; +use CodeIgniter\I18n\Time; +use DateTime; +use Exception; +use JsonSerializable; +use ReturnTypeWillChange; + +/** + * Entity encapsulation, for use with CodeIgniter\Model + * + * @see \CodeIgniter\Entity\EntityTest + */ +class Entity implements JsonSerializable +{ + /** + * Maps names used in sets and gets against unique + * names within the class, allowing independence from + * database column names. + * + * Example: + * $datamap = [ + * 'class_property_name' => 'db_column_name' + * ]; + * + * @var array + */ + protected $datamap = []; + + /** + * The date fields. + * + * @var list + */ + protected $dates = [ + 'created_at', + 'updated_at', + 'deleted_at', + ]; + + /** + * Array of field names and the type of value to cast them as when + * they are accessed. + * + * @var array + */ + protected $casts = []; + + /** + * Custom convert handlers + * + * @var array + */ + protected $castHandlers = []; + + /** + * Default convert handlers + * + * @var array + */ + private array $defaultCastHandlers = [ + 'array' => ArrayCast::class, + 'bool' => BooleanCast::class, + 'boolean' => BooleanCast::class, + 'csv' => CSVCast::class, + 'datetime' => DatetimeCast::class, + 'double' => FloatCast::class, + 'float' => FloatCast::class, + 'int' => IntegerCast::class, + 'integer' => IntegerCast::class, + 'int-bool' => IntBoolCast::class, + 'json' => JsonCast::class, + 'object' => ObjectCast::class, + 'string' => StringCast::class, + 'timestamp' => TimestampCast::class, + 'uri' => URICast::class, + ]; + + /** + * Holds the current values of all class vars. + * + * @var array + */ + protected $attributes = []; + + /** + * Holds original copies of all class vars so we can determine + * what's actually been changed and not accidentally write + * nulls where we shouldn't. + * + * @var array + */ + protected $original = []; + + /** + * The data caster. + */ + protected DataCaster $dataCaster; + + /** + * Holds info whenever properties have to be casted + */ + private bool $_cast = true; + + /** + * Allows filling in Entity parameters during construction. + */ + public function __construct(?array $data = null) + { + $this->dataCaster = new DataCaster( + array_merge($this->defaultCastHandlers, $this->castHandlers), + null, + null, + false, + ); + + $this->syncOriginal(); + + $this->fill($data); + } + + /** + * Takes an array of key/value pairs and sets them as class + * properties, using any `setCamelCasedProperty()` methods + * that may or may not exist. + * + * @param array $data + * + * @return $this + */ + public function fill(?array $data = null) + { + if (! is_array($data)) { + return $this; + } + + foreach ($data as $key => $value) { + $this->__set($key, $value); + } + + return $this; + } + + /** + * General method that will return all public and protected values + * of this entity as an array. All values are accessed through the + * __get() magic method so will have any casts, etc applied to them. + * + * @param bool $onlyChanged If true, only return values that have changed since object creation + * @param bool $cast If true, properties will be cast. + * @param bool $recursive If true, inner entities will be cast as array as well. + */ + public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false): array + { + $this->_cast = $cast; + + $keys = array_filter(array_keys($this->attributes), static fn ($key): bool => ! str_starts_with($key, '_')); + + if (is_array($this->datamap)) { + $keys = array_unique( + [...array_diff($keys, $this->datamap), ...array_keys($this->datamap)], + ); + } + + $return = []; + + // Loop over the properties, to allow magic methods to do their thing. + foreach ($keys as $key) { + if ($onlyChanged && ! $this->hasChanged($key)) { + continue; + } + + $return[$key] = $this->__get($key); + + if ($recursive) { + if ($return[$key] instanceof self) { + $return[$key] = $return[$key]->toArray($onlyChanged, $cast, $recursive); + } elseif (is_callable([$return[$key], 'toArray'])) { + $return[$key] = $return[$key]->toArray(); + } + } + } + + $this->_cast = true; + + return $return; + } + + /** + * Returns the raw values of the current attributes. + * + * @param bool $onlyChanged If true, only return values that have changed since object creation + * @param bool $recursive If true, inner entities will be cast as array as well. + */ + public function toRawArray(bool $onlyChanged = false, bool $recursive = false): array + { + $return = []; + + if (! $onlyChanged) { + if ($recursive) { + return array_map(static function ($value) use ($onlyChanged, $recursive) { + if ($value instanceof self) { + $value = $value->toRawArray($onlyChanged, $recursive); + } elseif (is_callable([$value, 'toRawArray'])) { + $value = $value->toRawArray(); + } + + return $value; + }, $this->attributes); + } + + return $this->attributes; + } + + foreach ($this->attributes as $key => $value) { + if (! $this->hasChanged($key)) { + continue; + } + + if ($recursive) { + if ($value instanceof self) { + $value = $value->toRawArray($onlyChanged, $recursive); + } elseif (is_callable([$value, 'toRawArray'])) { + $value = $value->toRawArray(); + } + } + + $return[$key] = $value; + } + + return $return; + } + + /** + * Ensures our "original" values match the current values. + * + * @return $this + */ + public function syncOriginal() + { + $this->original = $this->attributes; + + return $this; + } + + /** + * Checks a property to see if it has changed since the entity + * was created. Or, without a parameter, checks if any + * properties have changed. + * + * @param string|null $key class property + */ + public function hasChanged(?string $key = null): bool + { + // If no parameter was given then check all attributes + if ($key === null) { + return $this->original !== $this->attributes; + } + + $dbColumn = $this->mapProperty($key); + + // Key doesn't exist in either + if (! array_key_exists($dbColumn, $this->original) && ! array_key_exists($dbColumn, $this->attributes)) { + return false; + } + + // It's a new element + if (! array_key_exists($dbColumn, $this->original) && array_key_exists($dbColumn, $this->attributes)) { + return true; + } + + return $this->original[$dbColumn] !== $this->attributes[$dbColumn]; + } + + /** + * Set raw data array without any mutations + * + * @return $this + */ + public function injectRawData(array $data) + { + $this->attributes = $data; + + $this->syncOriginal(); + + return $this; + } + + /** + * Set raw data array without any mutations + * + * @return $this + * + * @deprecated Use injectRawData() instead. + */ + public function setAttributes(array $data) + { + return $this->injectRawData($data); + } + + /** + * Checks the datamap to see if this property name is being mapped, + * and returns the db column name, if any, or the original property name. + * + * @return string db column name + */ + protected function mapProperty(string $key) + { + if ($this->datamap === []) { + return $key; + } + + if (! empty($this->datamap[$key])) { + return $this->datamap[$key]; + } + + return $key; + } + + /** + * Converts the given string|timestamp|DateTime|Time instance + * into the "CodeIgniter\I18n\Time" object. + * + * @param DateTime|float|int|string|Time $value + * + * @return Time + * + * @throws Exception + */ + protected function mutateDate($value) + { + return DatetimeCast::get($value); + } + + /** + * Provides the ability to cast an item as a specific data type. + * Add ? at the beginning of the type (i.e. ?string) to get `null` + * instead of casting $value when $value is null. + * + * @param bool|float|int|string|null $value Attribute value + * @param string $attribute Attribute name + * @param string $method Allowed to "get" and "set" + * + * @return array|bool|float|int|object|string|null + * + * @throws CastException + */ + protected function castAs($value, string $attribute, string $method = 'get') + { + return $this->dataCaster + // @TODO if $casts is readonly, we don't need the setTypes() method. + ->setTypes($this->casts) + ->castAs($value, $attribute, $method); + } + + /** + * Support for json_encode() + * + * @return array + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * Change the value of the private $_cast property + * + * @return bool|Entity + */ + public function cast(?bool $cast = null) + { + if ($cast === null) { + return $this->_cast; + } + + $this->_cast = $cast; + + return $this; + } + + /** + * Magic method to all protected/private class properties to be + * easily set, either through a direct access or a + * `setCamelCasedProperty()` method. + * + * Examples: + * $this->my_property = $p; + * $this->setMyProperty() = $p; + * + * @param array|bool|float|int|object|string|null $value + * + * @return void + * + * @throws Exception + */ + public function __set(string $key, $value = null) + { + $dbColumn = $this->mapProperty($key); + + // Check if the field should be mutated into a date + if (in_array($dbColumn, $this->dates, true)) { + $value = $this->mutateDate($value); + } + + $value = $this->castAs($value, $dbColumn, 'set'); + + // if a setter method exists for this key, use that method to + // insert this value. should be outside $isNullable check, + // so maybe wants to do sth with null value automatically + $method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn))); + + // If a "`_set` + $key" method exists, it is a setter. + if (method_exists($this, '_' . $method)) { + $this->{'_' . $method}($value); + + return; + } + + // If a "`set` + $key" method exists, it is also a setter. + if (method_exists($this, $method) && $method !== 'setAttributes') { + $this->{$method}($value); + + return; + } + + // Otherwise, just the value. This allows for creation of new + // class properties that are undefined, though they cannot be + // saved. Useful for grabbing values through joins, assigning + // relationships, etc. + $this->attributes[$dbColumn] = $value; + } + + /** + * Magic method to allow retrieval of protected and private class properties + * either by their name, or through a `getCamelCasedProperty()` method. + * + * Examples: + * $p = $this->my_property + * $p = $this->getMyProperty() + * + * @return array|bool|float|int|object|string|null + * + * @throws Exception + * + * @params string $key class property + */ + public function __get(string $key) + { + $dbColumn = $this->mapProperty($key); + + $result = null; + + // Convert to CamelCase for the method + $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn))); + + // if a getter method exists for this key, + // use that method to insert this value. + if (method_exists($this, '_' . $method)) { + // If a "`_get` + $key" method exists, it is a getter. + $result = $this->{'_' . $method}(); + } elseif (method_exists($this, $method)) { + // If a "`get` + $key" method exists, it is also a getter. + $result = $this->{$method}(); + } + + // Otherwise return the protected property + // if it exists. + elseif (array_key_exists($dbColumn, $this->attributes)) { + $result = $this->attributes[$dbColumn]; + } + + // Do we need to mutate this into a date? + if (in_array($dbColumn, $this->dates, true)) { + $result = $this->mutateDate($result); + } + // Or cast it as something? + elseif ($this->_cast) { + $result = $this->castAs($result, $dbColumn); + } + + return $result; + } + + /** + * Returns true if a property exists names $key, or a getter method + * exists named like for __get(). + */ + public function __isset(string $key): bool + { + if ($this->isMappedDbColumn($key)) { + return false; + } + + $dbColumn = $this->mapProperty($key); + + $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $dbColumn))); + + if (method_exists($this, $method)) { + return true; + } + + return isset($this->attributes[$dbColumn]); + } + + /** + * Unsets an attribute property. + */ + public function __unset(string $key): void + { + if ($this->isMappedDbColumn($key)) { + return; + } + + $dbColumn = $this->mapProperty($key); + + unset($this->attributes[$dbColumn]); + } + + /** + * Whether this key is mapped db column name? + */ + protected function isMappedDbColumn(string $key): bool + { + $dbColumn = $this->mapProperty($key); + + // The $key is a property name which has mapped db column name + if ($key !== $dbColumn) { + return false; + } + + return $this->hasMappedProperty($key); + } + + /** + * Whether this key has mapped property? + */ + protected function hasMappedProperty(string $key): bool + { + $property = array_search($key, $this->datamap, true); + + return $property !== false; + } +} diff --git a/system/Entity/Exceptions/CastException.php b/system/Entity/Exceptions/CastException.php new file mode 100755 index 0000000..90d3885 --- /dev/null +++ b/system/Entity/Exceptions/CastException.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Entity\Exceptions; + +use CodeIgniter\Exceptions\FrameworkException; +use CodeIgniter\Exceptions\HasExitCodeInterface; + +/** + * CastException is thrown for invalid cast initialization and management. + */ +class CastException extends FrameworkException implements HasExitCodeInterface +{ + public function getExitCode(): int + { + return EXIT_CONFIG; + } + + /** + * Thrown when the cast class does not extends BaseCast. + * + * @return static + */ + public static function forInvalidInterface(string $class) + { + return new static(lang('Cast.baseCastMissing', [$class])); + } + + /** + * Thrown when the Json format is invalid. + * + * @return static + */ + public static function forInvalidJsonFormat(int $error) + { + return match ($error) { + JSON_ERROR_DEPTH => new static(lang('Cast.jsonErrorDepth')), + JSON_ERROR_STATE_MISMATCH => new static(lang('Cast.jsonErrorStateMismatch')), + JSON_ERROR_CTRL_CHAR => new static(lang('Cast.jsonErrorCtrlChar')), + JSON_ERROR_SYNTAX => new static(lang('Cast.jsonErrorSyntax')), + JSON_ERROR_UTF8 => new static(lang('Cast.jsonErrorUtf8')), + default => new static(lang('Cast.jsonErrorUnknown')), + }; + } + + /** + * Thrown when the cast method is not `get` or `set`. + * + * @return static + */ + public static function forInvalidMethod(string $method) + { + return new static(lang('Cast.invalidCastMethod', [$method])); + } + + /** + * Thrown when the casting timestamp is not correct timestamp. + * + * @return static + */ + public static function forInvalidTimestamp() + { + return new static(lang('Cast.invalidTimestamp')); + } +} diff --git a/system/Events/Events.php b/system/Events/Events.php new file mode 100755 index 0000000..97f4e81 --- /dev/null +++ b/system/Events/Events.php @@ -0,0 +1,285 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Events; + +use Config\Modules; + +/** + * Events + * + * @see \CodeIgniter\Events\EventsTest + */ +class Events +{ + public const PRIORITY_LOW = 200; + public const PRIORITY_NORMAL = 100; + public const PRIORITY_HIGH = 10; + + /** + * The list of listeners. + * + * @var array + */ + protected static $listeners = []; + + /** + * Flag to let us know if we've read from the Config file(s) + * and have all of the defined events. + * + * @var bool + */ + protected static $initialized = false; + + /** + * If true, events will not actually be fired. + * Useful during testing. + * + * @var bool + */ + protected static $simulate = false; + + /** + * Stores information about the events + * for display in the debug toolbar. + * + * @var list> + */ + protected static $performanceLog = []; + + /** + * A list of found files. + * + * @var list + */ + protected static $files = []; + + /** + * Ensures that we have a events file ready. + * + * @return void + */ + public static function initialize() + { + // Don't overwrite anything.... + if (static::$initialized) { + return; + } + + $config = new Modules(); + $events = APPPATH . 'Config' . DIRECTORY_SEPARATOR . 'Events.php'; + $files = []; + + if ($config->shouldDiscover('events')) { + $files = service('locator')->search('Config/Events.php'); + } + + $files = array_filter(array_map(static function (string $file): false|string { + if (is_file($file)) { + return realpath($file) ?: $file; + } + + return false; // @codeCoverageIgnore + }, $files)); + + static::$files = array_unique(array_merge($files, [$events])); + + foreach (static::$files as $file) { + include $file; + } + + static::$initialized = true; + } + + /** + * Registers an action to happen on an event. The action can be any sort + * of callable: + * + * Events::on('create', 'myFunction'); // procedural function + * Events::on('create', ['myClass', 'myMethod']); // Class::method + * Events::on('create', [$myInstance, 'myMethod']); // Method on an existing instance + * Events::on('create', function() {}); // Closure + * + * @param string $eventName + * @param callable $callback + * @param int $priority + * + * @return void + */ + public static function on($eventName, $callback, $priority = self::PRIORITY_NORMAL) + { + if (! isset(static::$listeners[$eventName])) { + static::$listeners[$eventName] = [ + true, // If there's only 1 item, it's sorted. + [$priority], + [$callback], + ]; + } else { + static::$listeners[$eventName][0] = false; // Not sorted + static::$listeners[$eventName][1][] = $priority; + static::$listeners[$eventName][2][] = $callback; + } + } + + /** + * Runs through all subscribed methods running them one at a time, + * until either: + * a) All subscribers have finished or + * b) a method returns false, at which point execution of subscribers stops. + * + * @param string $eventName + * @param mixed $arguments + */ + public static function trigger($eventName, ...$arguments): bool + { + // Read in our Config/Events file so that we have them all! + if (! static::$initialized) { + static::initialize(); + } + + $listeners = static::listeners($eventName); + + foreach ($listeners as $listener) { + $start = microtime(true); + + $result = static::$simulate === false ? $listener(...$arguments) : true; + + if (CI_DEBUG) { + static::$performanceLog[] = [ + 'start' => $start, + 'end' => microtime(true), + 'event' => strtolower($eventName), + ]; + } + + if ($result === false) { + return false; + } + } + + return true; + } + + /** + * Returns an array of listeners for a single event. They are + * sorted by priority. + * + * @param string $eventName + */ + public static function listeners($eventName): array + { + if (! isset(static::$listeners[$eventName])) { + return []; + } + + // The list is not sorted + if (! static::$listeners[$eventName][0]) { + // Sort it! + array_multisort(static::$listeners[$eventName][1], SORT_NUMERIC, static::$listeners[$eventName][2]); + + // Mark it as sorted already! + static::$listeners[$eventName][0] = true; + } + + return static::$listeners[$eventName][2]; + } + + /** + * Removes a single listener from an event. + * + * If the listener couldn't be found, returns FALSE, else TRUE if + * it was removed. + * + * @param string $eventName + */ + public static function removeListener($eventName, callable $listener): bool + { + if (! isset(static::$listeners[$eventName])) { + return false; + } + + foreach (static::$listeners[$eventName][2] as $index => $check) { + if ($check === $listener) { + unset( + static::$listeners[$eventName][1][$index], + static::$listeners[$eventName][2][$index], + ); + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the event_name is specified, only listeners for that event will be + * removed, otherwise all listeners for all events are removed. + * + * @param string|null $eventName + * + * @return void + */ + public static function removeAllListeners($eventName = null) + { + if ($eventName !== null) { + unset(static::$listeners[$eventName]); + } else { + static::$listeners = []; + } + } + + /** + * Sets the path to the file that routes are read from. + * + * @return void + */ + public static function setFiles(array $files) + { + static::$files = $files; + } + + /** + * Returns the files that were found/loaded during this request. + * + * @return list + */ + public static function getFiles() + { + return static::$files; + } + + /** + * Turns simulation on or off. When on, events will not be triggered, + * simply logged. Useful during testing when you don't actually want + * the tests to run. + * + * @return void + */ + public static function simulate(bool $choice = true) + { + static::$simulate = $choice; + } + + /** + * Getter for the performance log records. + * + * @return list> + */ + public static function getPerformanceLogs() + { + return static::$performanceLog; + } +} diff --git a/system/Exceptions/BadFunctionCallException.php b/system/Exceptions/BadFunctionCallException.php new file mode 100755 index 0000000..84d9798 --- /dev/null +++ b/system/Exceptions/BadFunctionCallException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if a function is called in the wrong way, or the function + * does not exist. + */ +class BadFunctionCallException extends \BadFunctionCallException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/BadMethodCallException.php b/system/Exceptions/BadMethodCallException.php new file mode 100755 index 0000000..977e8ae --- /dev/null +++ b/system/Exceptions/BadMethodCallException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if a method is called in the wrong way, or the method + * does not exist. + */ +class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/ConfigException.php b/system/Exceptions/ConfigException.php new file mode 100755 index 0000000..b308181 --- /dev/null +++ b/system/Exceptions/ConfigException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if the value of the Config class is invalid or the type is + * incorrect. + */ +class ConfigException extends RuntimeException implements HasExitCodeInterface +{ + use DebugTraceableTrait; + + public function getExitCode(): int + { + return EXIT_CONFIG; + } + + /** + * @return static + */ + public static function forDisabledMigrations() + { + return new static(lang('Migrations.disabled')); + } +} diff --git a/system/Exceptions/CriticalError.php b/system/Exceptions/CriticalError.php new file mode 100755 index 0000000..d3f6803 --- /dev/null +++ b/system/Exceptions/CriticalError.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Error: Critical conditions, like component unavailable, etc. + */ +class CriticalError extends RuntimeException +{ +} diff --git a/system/Exceptions/DebugTraceableTrait.php b/system/Exceptions/DebugTraceableTrait.php new file mode 100755 index 0000000..e8f204a --- /dev/null +++ b/system/Exceptions/DebugTraceableTrait.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +use Throwable; + +/** + * This trait provides framework exceptions the ability to pinpoint + * accurately where the exception was raised rather than instantiated. + * + * This is used primarily for factory-instantiated exceptions. + */ +trait DebugTraceableTrait +{ + /** + * Tweaks the exception's constructor to assign the file/line to where + * it is actually raised rather than were it is instantiated. + */ + final public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + + $trace = $this->getTrace()[0]; + + if (isset($trace['class']) && $trace['class'] === static::class) { + [ + 'line' => $this->line, + 'file' => $this->file, + ] = $trace; + } + } +} diff --git a/system/Exceptions/DownloadException.php b/system/Exceptions/DownloadException.php new file mode 100755 index 0000000..c3bde8e --- /dev/null +++ b/system/Exceptions/DownloadException.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Class DownloadException + */ +class DownloadException extends RuntimeException +{ + use DebugTraceableTrait; + + /** + * @return static + */ + public static function forCannotSetFilePath(string $path) + { + return new static(lang('HTTP.cannotSetFilepath', [$path])); + } + + /** + * @return static + */ + public static function forCannotSetBinary() + { + return new static(lang('HTTP.cannotSetBinary')); + } + + /** + * @return static + */ + public static function forNotFoundDownloadSource() + { + return new static(lang('HTTP.notFoundDownloadSource')); + } + + /** + * @deprecated Since v4.5.6 + * + * @return static + */ + public static function forCannotSetCache() + { + return new static(lang('HTTP.cannotSetCache')); + } + + /** + * @return static + */ + public static function forCannotSetStatusCode(int $code, string $reason) + { + return new static(lang('HTTP.cannotSetStatusCode', [$code, $reason])); + } +} diff --git a/system/Exceptions/ExceptionInterface.php b/system/Exceptions/ExceptionInterface.php new file mode 100755 index 0000000..27b9c2f --- /dev/null +++ b/system/Exceptions/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Provides a domain-level interface for broad capture + * of all framework-related exceptions. + * + * catch (\CodeIgniter\Exceptions\ExceptionInterface) { ... } + */ +interface ExceptionInterface +{ +} diff --git a/system/Exceptions/FrameworkException.php b/system/Exceptions/FrameworkException.php new file mode 100755 index 0000000..4650c6c --- /dev/null +++ b/system/Exceptions/FrameworkException.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Class FrameworkException + * + * A collection of exceptions thrown by the framework + * that can only be determined at run time. + */ +class FrameworkException extends RuntimeException +{ + use DebugTraceableTrait; + + /** + * @return static + */ + public static function forEnabledZlibOutputCompression() + { + return new static(lang('Core.enabledZlibOutputCompression')); + } + + /** + * @return static + */ + public static function forInvalidFile(string $path) + { + return new static(lang('Core.invalidFile', [$path])); + } + + /** + * @return static + */ + public static function forInvalidDirectory(string $path) + { + return new static(lang('Core.invalidDirectory', [$path])); + } + + /** + * @return static + */ + public static function forCopyError(string $path) + { + return new static(lang('Core.copyError', [$path])); + } + + /** + * @return static + * + * @deprecated 4.5.0 No longer used. + */ + public static function forMissingExtension(string $extension) + { + if (str_contains($extension, 'intl')) { + // @codeCoverageIgnoreStart + $message = sprintf( + 'The framework needs the following extension(s) installed and loaded: %s.', + $extension, + ); + // @codeCoverageIgnoreEnd + } else { + $message = lang('Core.missingExtension', [$extension]); + } + + return new static($message); + } + + /** + * @return static + */ + public static function forNoHandlers(string $class) + { + return new static(lang('Core.noHandlers', [$class])); + } + + /** + * @return static + */ + public static function forFabricatorCreateFailed(string $table, string $reason) + { + return new static(lang('Fabricator.createFailed', [$table, $reason])); + } +} diff --git a/system/Exceptions/HTTPExceptionInterface.php b/system/Exceptions/HTTPExceptionInterface.php new file mode 100755 index 0000000..b26f121 --- /dev/null +++ b/system/Exceptions/HTTPExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Interface for Exceptions that has exception code as HTTP status code. + */ +interface HTTPExceptionInterface extends ExceptionInterface +{ +} diff --git a/system/Exceptions/HasExitCodeInterface.php b/system/Exceptions/HasExitCodeInterface.php new file mode 100755 index 0000000..48d3265 --- /dev/null +++ b/system/Exceptions/HasExitCodeInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Interface for Exceptions that has exception code as exit code. + */ +interface HasExitCodeInterface extends ExceptionInterface +{ + /** + * Returns exit status code. + */ + public function getExitCode(): int; +} diff --git a/system/Exceptions/InvalidArgumentException.php b/system/Exceptions/InvalidArgumentException.php new file mode 100755 index 0000000..4790249 --- /dev/null +++ b/system/Exceptions/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if an argument is not of the expected type. + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/LogicException.php b/system/Exceptions/LogicException.php new file mode 100755 index 0000000..2fe4679 --- /dev/null +++ b/system/Exceptions/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception that represents error in the program logic. + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/ModelException.php b/system/Exceptions/ModelException.php new file mode 100755 index 0000000..a92e388 --- /dev/null +++ b/system/Exceptions/ModelException.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Model Exceptions. + */ +class ModelException extends FrameworkException +{ + /** + * @return static + */ + public static function forNoPrimaryKey(string $modelName) + { + return new static(lang('Database.noPrimaryKey', [$modelName])); + } + + /** + * @return static + */ + public static function forNoDateFormat(string $modelName) + { + return new static(lang('Database.noDateFormat', [$modelName])); + } + + /** + * @return static + */ + public static function forMethodNotAvailable(string $modelName, string $methodName) + { + return new static(lang('Database.methodNotAvailable', [$modelName, $methodName])); + } +} diff --git a/system/Exceptions/PageNotFoundException.php b/system/Exceptions/PageNotFoundException.php new file mode 100755 index 0000000..f84763e --- /dev/null +++ b/system/Exceptions/PageNotFoundException.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +class PageNotFoundException extends RuntimeException implements HTTPExceptionInterface +{ + use DebugTraceableTrait; + + /** + * HTTP status code + * + * @var int + */ + protected $code = 404; + + /** + * @return static + */ + public static function forPageNotFound(?string $message = null) + { + return new static($message ?? self::lang('HTTP.pageNotFound')); + } + + /** + * @return static + */ + public static function forEmptyController() + { + return new static(self::lang('HTTP.emptyController')); + } + + /** + * @return static + */ + public static function forControllerNotFound(string $controller, string $method) + { + return new static(self::lang('HTTP.controllerNotFound', [$controller, $method])); + } + + /** + * @return static + */ + public static function forMethodNotFound(string $method) + { + return new static(self::lang('HTTP.methodNotFound', [$method])); + } + + /** + * @return static + */ + public static function forLocaleNotSupported(string $locale) + { + return new static(self::lang('HTTP.localeNotSupported', [$locale])); + } + + /** + * Get translated system message + * + * Use a non-shared Language instance in the Services. + * If a shared instance is created, the Language will + * have the current locale, so even if users call + * `$this->request->setLocale()` in the controller afterwards, + * the Language locale will not be changed. + */ + private static function lang(string $line, array $args = []): string + { + $lang = service('language', null, false); + + return $lang->getLine($line, $args); + } +} diff --git a/system/Exceptions/RuntimeException.php b/system/Exceptions/RuntimeException.php new file mode 100755 index 0000000..c35d55b --- /dev/null +++ b/system/Exceptions/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown if an error which can only be found on runtime occurs. + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php new file mode 100755 index 0000000..4487ae1 --- /dev/null +++ b/system/Exceptions/TestException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception thrown when there is an error with the test code. + */ +class TestException extends LogicException +{ + use DebugTraceableTrait; + + /** + * @return static + */ + public static function forInvalidMockClass(string $name) + { + return new static(lang('Test.invalidMockClass', [$name])); + } +} diff --git a/system/Files/Exceptions/ExceptionInterface.php b/system/Files/Exceptions/ExceptionInterface.php new file mode 100755 index 0000000..346ff16 --- /dev/null +++ b/system/Files/Exceptions/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files\Exceptions; + +/** + * Provides a domain-level interface for broad capture + * of all Files-related exceptions. + * + * catch (\CodeIgniter\Files\Exceptions\ExceptionInterface) { ... } + */ +interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface +{ +} diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php new file mode 100755 index 0000000..3202978 --- /dev/null +++ b/system/Files/Exceptions/FileException.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files\Exceptions; + +use CodeIgniter\Exceptions\DebugTraceableTrait; +use CodeIgniter\Exceptions\RuntimeException; + +class FileException extends RuntimeException implements ExceptionInterface +{ + use DebugTraceableTrait; + + /** + * @return static + */ + public static function forUnableToMove(?string $from = null, ?string $to = null, ?string $error = null) + { + return new static(lang('Files.cannotMove', [$from, $to, $error])); + } + + /** + * Throws when an item is expected to be a directory but is not or is missing. + * + * @param string $caller The method causing the exception + * + * @return static + */ + public static function forExpectedDirectory(string $caller) + { + return new static(lang('Files.expectedDirectory', [$caller])); + } + + /** + * Throws when an item is expected to be a file but is not or is missing. + * + * @param string $caller The method causing the exception + * + * @return static + */ + public static function forExpectedFile(string $caller) + { + return new static(lang('Files.expectedFile', [$caller])); + } +} diff --git a/system/Files/Exceptions/FileNotFoundException.php b/system/Files/Exceptions/FileNotFoundException.php new file mode 100755 index 0000000..4c043f9 --- /dev/null +++ b/system/Files/Exceptions/FileNotFoundException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files\Exceptions; + +use CodeIgniter\Exceptions\DebugTraceableTrait; +use CodeIgniter\Exceptions\RuntimeException; + +class FileNotFoundException extends RuntimeException implements ExceptionInterface +{ + use DebugTraceableTrait; + + /** + * @return static + */ + public static function forFileNotFound(string $path) + { + return new static(lang('Files.fileNotFound', [$path])); + } +} diff --git a/system/Files/File.php b/system/Files/File.php new file mode 100755 index 0000000..23eb324 --- /dev/null +++ b/system/Files/File.php @@ -0,0 +1,227 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files; + +use CodeIgniter\Files\Exceptions\FileException; +use CodeIgniter\Files\Exceptions\FileNotFoundException; +use CodeIgniter\I18n\Time; +use Config\Mimes; +use ReturnTypeWillChange; +use SplFileInfo; + +/** + * Wrapper for PHP's built-in SplFileInfo, with goodies. + * + * @see \CodeIgniter\Files\FileTest + */ +class File extends SplFileInfo +{ + /** + * The files size in bytes + * + * @var int + */ + protected $size; + + /** + * @var string|null + */ + protected $originalMimeType; + + /** + * Run our SplFileInfo constructor with an optional verification + * that the path is really a file. + * + * @throws FileNotFoundException + */ + public function __construct(string $path, bool $checkFile = false) + { + if ($checkFile && ! is_file($path)) { + throw FileNotFoundException::forFileNotFound($path); + } + + parent::__construct($path); + } + + /** + * Retrieve the file size. + * + * Implementations SHOULD return the value stored in the "size" key of + * the file in the $_FILES array if available, as PHP calculates this based + * on the actual size transmitted. A RuntimeException will be thrown if the file + * does not exist or an error occurs. + * + * @return false|int The file size in bytes, or false on failure + */ + #[ReturnTypeWillChange] + public function getSize() + { + return $this->size ?? ($this->size = parent::getSize()); + } + + /** + * Retrieve the file size by unit, calculated in IEC standards with 1024 as base value. + * + * @phpstan-param positive-int $precision + */ + public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string + { + return $this->getSizeByUnitInternal(1024, $unit, $precision); + } + + /** + * Retrieve the file size by unit, calculated in metric standards with 1000 as base value. + * + * @phpstan-param positive-int $precision + */ + public function getSizeByMetricUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string + { + return $this->getSizeByUnitInternal(1000, $unit, $precision); + } + + /** + * Retrieve the file size by unit. + * + * @deprecated 4.6.0 Use getSizeByBinaryUnit() or getSizeByMetricUnit() instead + * + * @return false|int|string + */ + public function getSizeByUnit(string $unit = 'b') + { + return match (strtolower($unit)) { + 'kb' => $this->getSizeByBinaryUnit(FileSizeUnit::KB), + 'mb' => $this->getSizeByBinaryUnit(FileSizeUnit::MB), + default => $this->getSize(), + }; + } + + /** + * Attempts to determine the file extension based on the trusted + * getType() method. If the mime type is unknown, will return null. + */ + public function guessExtension(): ?string + { + // naively get the path extension using pathinfo + $pathinfo = pathinfo($this->getRealPath() ?: $this->__toString()) + ['extension' => '']; + + $proposedExtension = $pathinfo['extension']; + + return Mimes::guessExtensionFromType($this->getMimeType(), $proposedExtension); + } + + /** + * Retrieve the media type of the file. SHOULD not use information from + * the $_FILES array, but should use other methods to more accurately + * determine the type of file, like finfo, or mime_content_type(). + * + * @return string The media type we determined it to be. + */ + public function getMimeType(): string + { + if (! function_exists('finfo_open')) { + return $this->originalMimeType ?? 'application/octet-stream'; // @codeCoverageIgnore + } + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimeType = finfo_file($finfo, $this->getRealPath() ?: $this->__toString()); + finfo_close($finfo); + + return $mimeType; + } + + /** + * Generates a random names based on a simple hash and the time, with + * the correct file extension attached. + */ + public function getRandomName(): string + { + $extension = $this->getExtension(); + $extension = empty($extension) ? '' : '.' . $extension; + + return Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . $extension; + } + + /** + * Moves a file to a new location. + * + * @return File + */ + public function move(string $targetPath, ?string $name = null, bool $overwrite = false) + { + $targetPath = rtrim($targetPath, '/') . '/'; + $name ??= $this->getBasename(); + $destination = $overwrite ? $targetPath . $name : $this->getDestination($targetPath . $name); + + $oldName = $this->getRealPath() ?: $this->__toString(); + + if (! @rename($oldName, $destination)) { + $error = error_get_last(); + + throw FileException::forUnableToMove($this->getBasename(), $targetPath, strip_tags($error['message'])); + } + + @chmod($destination, 0777 & ~umask()); + + return new self($destination); + } + + /** + * Returns the destination path for the move operation where overwriting is not expected. + * + * First, it checks whether the delimiter is present in the filename, if it is, then it checks whether the + * last element is an integer as there may be cases that the delimiter may be present in the filename. + * For the all other cases, it appends an integer starting from zero before the file's extension. + */ + public function getDestination(string $destination, string $delimiter = '_', int $i = 0): string + { + if ($delimiter === '') { + $delimiter = '_'; + } + + while (is_file($destination)) { + $info = pathinfo($destination); + $extension = isset($info['extension']) ? '.' . $info['extension'] : ''; + + if (str_contains($info['filename'], $delimiter)) { + $parts = explode($delimiter, $info['filename']); + + if (is_numeric(end($parts))) { + $i = end($parts); + array_pop($parts); + $parts[] = ++$i; + $destination = $info['dirname'] . DIRECTORY_SEPARATOR . implode($delimiter, $parts) . $extension; + } else { + $destination = $info['dirname'] . DIRECTORY_SEPARATOR . $info['filename'] . $delimiter . ++$i . $extension; + } + } else { + $destination = $info['dirname'] . DIRECTORY_SEPARATOR . $info['filename'] . $delimiter . ++$i . $extension; + } + } + + return $destination; + } + + private function getSizeByUnitInternal(int $fileSizeBase, FileSizeUnit $unit, int $precision): int|string + { + $exponent = $unit->value; + $divider = $fileSizeBase ** $exponent; + $size = $this->getSize() / $divider; + + if ($unit !== FileSizeUnit::B) { + $size = number_format($size, $precision); + } + + return $size; + } +} diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php new file mode 100755 index 0000000..b293b4d --- /dev/null +++ b/system/Files/FileCollection.php @@ -0,0 +1,408 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files; + +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\Files\Exceptions\FileException; +use CodeIgniter\Files\Exceptions\FileNotFoundException; +use Countable; +use Generator; +use IteratorAggregate; + +/** + * File Collection Class + * + * Representation for a group of files, with utilities for locating, + * filtering, and ordering them. + * + * @template-implements IteratorAggregate + * @see \CodeIgniter\Files\FileCollectionTest + */ +class FileCollection implements Countable, IteratorAggregate +{ + /** + * The current list of file paths. + * + * @var list + */ + protected $files = []; + + // -------------------------------------------------------------------- + // Support Methods + // -------------------------------------------------------------------- + + /** + * Resolves a full path and verifies it is an actual directory. + * + * @throws FileException + */ + final protected static function resolveDirectory(string $directory): string + { + if (! is_dir($directory = set_realpath($directory))) { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + + throw FileException::forExpectedDirectory($caller['function']); + } + + return $directory; + } + + /** + * Resolves a full path and verifies it is an actual file. + * + * @throws FileException + */ + final protected static function resolveFile(string $file): string + { + if (! is_file($file = set_realpath($file))) { + $caller = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]; + + throw FileException::forExpectedFile($caller['function']); + } + + return $file; + } + + /** + * Removes files that are not part of the given directory (recursive). + * + * @param list $files + * + * @return list + */ + final protected static function filterFiles(array $files, string $directory): array + { + $directory = self::resolveDirectory($directory); + + return array_filter($files, static fn (string $value): bool => str_starts_with($value, $directory)); + } + + /** + * Returns any files whose `basename` matches the given pattern. + * + * @param list $files + * @param string $pattern Regex or pseudo-regex string + * + * @return list + */ + final protected static function matchFiles(array $files, string $pattern): array + { + // Convert pseudo-regex into their true form + if (@preg_match($pattern, '') === false) { + $pattern = str_replace( + ['#', '.', '*', '?'], + ['\#', '\.', '.*', '.'], + $pattern, + ); + $pattern = "#\\A{$pattern}\\z#"; + } + + return array_filter($files, static fn ($value): bool => (bool) preg_match($pattern, basename($value))); + } + + // -------------------------------------------------------------------- + // Class Core + // -------------------------------------------------------------------- + + /** + * Loads the Filesystem helper and adds any initial files. + * + * @param list $files + */ + public function __construct(array $files = []) + { + helper(['filesystem']); + + $this->add($files)->define(); + } + + /** + * Applies any initial inputs after the constructor. + * This method is a stub to be implemented by child classes. + */ + protected function define(): void + { + } + + /** + * Optimizes and returns the current file list. + * + * @return list + */ + public function get(): array + { + $this->files = array_unique($this->files); + sort($this->files, SORT_STRING); + + return $this->files; + } + + /** + * Sets the file list directly, files are still subject to verification. + * This works as a "reset" method with []. + * + * @param list $files The new file list to use + * + * @return $this + */ + public function set(array $files) + { + $this->files = []; + + return $this->addFiles($files); + } + + /** + * Adds an array/single file or directory to the list. + * + * @param list|string $paths + * + * @return $this + */ + public function add($paths, bool $recursive = true) + { + $paths = (array) $paths; + + foreach ($paths as $path) { + if (! is_string($path)) { + throw new InvalidArgumentException('FileCollection paths must be strings.'); + } + + try { + // Test for a directory + self::resolveDirectory($path); + } catch (FileException) { + $this->addFile($path); + + continue; + } + + $this->addDirectory($path, $recursive); + } + + return $this; + } + + // -------------------------------------------------------------------- + // File Handling + // -------------------------------------------------------------------- + + /** + * Verifies and adds files to the list. + * + * @param list $files + * + * @return $this + */ + public function addFiles(array $files) + { + foreach ($files as $file) { + $this->addFile($file); + } + + return $this; + } + + /** + * Verifies and adds a single file to the file list. + * + * @return $this + */ + public function addFile(string $file) + { + $this->files[] = self::resolveFile($file); + + return $this; + } + + /** + * Removes files from the list. + * + * @param list $files + * + * @return $this + */ + public function removeFiles(array $files) + { + $this->files = array_diff($this->files, $files); + + return $this; + } + + /** + * Removes a single file from the list. + * + * @return $this + */ + public function removeFile(string $file) + { + return $this->removeFiles([$file]); + } + + // -------------------------------------------------------------------- + // Directory Handling + // -------------------------------------------------------------------- + + /** + * Verifies and adds files from each + * directory to the list. + * + * @param list $directories + * + * @return $this + */ + public function addDirectories(array $directories, bool $recursive = false) + { + foreach ($directories as $directory) { + $this->addDirectory($directory, $recursive); + } + + return $this; + } + + /** + * Verifies and adds all files from a directory. + * + * @return $this + */ + public function addDirectory(string $directory, bool $recursive = false) + { + $directory = self::resolveDirectory($directory); + + // Map the directory to depth 2 to so directories become arrays + foreach (directory_map($directory, 2, true) as $key => $path) { + if (is_string($path)) { + $this->addFile($directory . $path); + } elseif ($recursive && is_array($path)) { + $this->addDirectory($directory . $key, true); + } + } + + return $this; + } + + // -------------------------------------------------------------------- + // Filtering + // -------------------------------------------------------------------- + + /** + * Removes any files from the list that match the supplied pattern + * (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope The directory to limit the scope + * + * @return $this + */ + public function removePattern(string $pattern, ?string $scope = null) + { + if ($pattern === '') { + return $this; + } + + // Start with all files or those in scope + $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope); + + // Remove any files that match the pattern + return $this->removeFiles(self::matchFiles($files, $pattern)); + } + + /** + * Keeps only the files from the list that match + * (within the optional scope). + * + * @param string $pattern Regex or pseudo-regex string + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function retainPattern(string $pattern, ?string $scope = null) + { + if ($pattern === '') { + return $this; + } + + // Start with all files or those in scope + $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope); + + // Matches the pattern within the scoped files and remove their inverse. + return $this->removeFiles(array_diff($files, self::matchFiles($files, $pattern))); + } + + /** + * Keeps only the files from the list that match multiple patterns + * (within the optional scope). + * + * @param list $patterns Array of regex or pseudo-regex strings + * @param string|null $scope A directory to limit the scope + * + * @return $this + */ + public function retainMultiplePatterns(array $patterns, ?string $scope = null) + { + if ($patterns === []) { + return $this; + } + + if (count($patterns) === 1 && $patterns[0] === '') { + return $this; + } + + // Start with all files or those in scope + $files = $scope === null ? $this->files : self::filterFiles($this->files, $scope); + + // Add files to retain to array + $filesToRetain = []; + + foreach ($patterns as $pattern) { + if ($pattern === '') { + continue; + } + + // Matches the pattern within the scoped files + $filesToRetain = array_merge($filesToRetain, self::matchFiles($files, $pattern)); + } + + // Remove the inverse of files to retain + return $this->removeFiles(array_diff($files, $filesToRetain)); + } + + // -------------------------------------------------------------------- + // Interface Methods + // -------------------------------------------------------------------- + + /** + * Returns the current number of files in the collection. + * Fulfills Countable. + */ + public function count(): int + { + return count($this->files); + } + + /** + * Yields as an Iterator for the current files. + * Fulfills IteratorAggregate. + * + * @return Generator + * + * @throws FileNotFoundException + */ + public function getIterator(): Generator + { + foreach ($this->get() as $file) { + yield new File($file, true); + } + } +} diff --git a/system/Files/FileSizeUnit.php b/system/Files/FileSizeUnit.php new file mode 100755 index 0000000..84c2ba4 --- /dev/null +++ b/system/Files/FileSizeUnit.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Files; + +use CodeIgniter\Exceptions\InvalidArgumentException; + +enum FileSizeUnit: int +{ + case B = 0; + case KB = 1; + case MB = 2; + case GB = 3; + case TB = 4; + + /** + * Allows the creation of a FileSizeUnit from Strings like "kb" or "mb" + * + * @throws InvalidArgumentException + */ + public static function fromString(string $unit): self + { + return match (strtolower($unit)) { + 'b' => self::B, + 'kb' => self::KB, + 'mb' => self::MB, + 'gb' => self::GB, + 'tb' => self::TB, + default => throw new InvalidArgumentException("Invalid unit: {$unit}"), + }; + } +} diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php new file mode 100755 index 0000000..ea9a993 --- /dev/null +++ b/system/Filters/CSRF.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\Security\Exceptions\SecurityException; +use CodeIgniter\Security\Security; + +/** + * CSRF filter. + * + * This filter is not intended to be used from the command line. + * + * @codeCoverageIgnore + * @see \CodeIgniter\Filters\CSRFTest + */ +class CSRF implements FilterInterface +{ + /** + * CSRF verification. + * + * @param list|null $arguments + * + * @return RedirectResponse|null + * + * @throws SecurityException + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return null; + } + + /** @var Security $security */ + $security = service('security'); + + try { + $security->verify($request); + } catch (SecurityException $e) { + if ($security->shouldRedirect() && ! $request->isAJAX()) { + return redirect()->back()->with('error', $e->getMessage()); + } + + throw $e; + } + + return null; + } + + /** + * We don't have anything to do here. + * + * @param list|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + return null; + } +} diff --git a/system/Filters/Cors.php b/system/Filters/Cors.php new file mode 100755 index 0000000..9a9bbb2 --- /dev/null +++ b/system/Filters/Cors.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\Cors as CorsService; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * @see \CodeIgniter\Filters\CorsTest + */ +class Cors implements FilterInterface +{ + private ?CorsService $cors = null; + + /** + * @testTag $config is used for testing purposes only. + * + * @param array{ + * allowedOrigins?: list, + * allowedOriginsPatterns?: list, + * supportsCredentials?: bool, + * allowedHeaders?: list, + * exposedHeaders?: list, + * allowedMethods?: list, + * maxAge?: int, + * } $config + */ + public function __construct(array $config = []) + { + if ($config !== []) { + $this->cors = new CorsService($config); + } + } + + /** + * @param list|null $arguments + * + * @return ResponseInterface|null + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return null; + } + + $this->createCorsService($arguments); + + if (! $this->cors->isPreflightRequest($request)) { + return null; + } + + /** @var ResponseInterface $response */ + $response = service('response'); + + $response = $this->cors->handlePreflightRequest($request, $response); + + // Always adds `Vary: Access-Control-Request-Method` header for cacheability. + // If there is an intermediate cache server such as a CDN, if a plain + // OPTIONS request is sent, it may be cached. But valid preflight requests + // have this header, so it will be cached separately. + $response->appendHeader('Vary', 'Access-Control-Request-Method'); + + return $response; + } + + /** + * @param list|null $arguments + */ + private function createCorsService(?array $arguments): void + { + $this->cors ??= ($arguments === null) ? CorsService::factory() + : CorsService::factory($arguments[0]); + } + + /** + * @param list|null $arguments + * + * @return ResponseInterface|null + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return null; + } + + $this->createCorsService($arguments); + + // Always adds `Vary: Access-Control-Request-Method` header for cacheability. + // If there is an intermediate cache server such as a CDN, if a plain + // OPTIONS request is sent, it may be cached. But valid preflight requests + // have this header, so it will be cached separately. + if ($request->is('OPTIONS')) { + $response->appendHeader('Vary', 'Access-Control-Request-Method'); + } + + return $this->cors->addResponseHeaders($request, $response); + } +} diff --git a/system/Filters/DebugToolbar.php b/system/Filters/DebugToolbar.php new file mode 100755 index 0000000..7199b85 --- /dev/null +++ b/system/Filters/DebugToolbar.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Debug toolbar filter + * + * @see \CodeIgniter\Filters\DebugToolbarTest + */ +class DebugToolbar implements FilterInterface +{ + /** + * We don't need to do anything here. + * + * @param list|null $arguments + */ + public function before(RequestInterface $request, $arguments = null) + { + return null; + } + + /** + * If the debug flag is set (CI_DEBUG) then collect performance + * and debug information and display it in a toolbar. + * + * @param list|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + service('toolbar')->prepare($request, $response); + + return null; + } +} diff --git a/system/Filters/Exceptions/FilterException.php b/system/Filters/Exceptions/FilterException.php new file mode 100755 index 0000000..3a4d914 --- /dev/null +++ b/system/Filters/Exceptions/FilterException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters\Exceptions; + +use CodeIgniter\Exceptions\ConfigException; + +/** + * FilterException + */ +class FilterException extends ConfigException +{ + /** + * Thrown when the provided alias is not within + * the list of configured filter aliases. + * + * @return static + */ + public static function forNoAlias(string $alias) + { + return new static(lang('Filters.noFilter', [$alias])); + } + + /** + * Thrown when the filter class does not implement FilterInterface. + * + * @return static + */ + public static function forIncorrectInterface(string $class) + { + return new static(lang('Filters.incorrectInterface', [$class])); + } +} diff --git a/system/Filters/FilterInterface.php b/system/Filters/FilterInterface.php new file mode 100755 index 0000000..6df33e9 --- /dev/null +++ b/system/Filters/FilterInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Filter interface + */ +interface FilterInterface +{ + /** + * Do whatever processing this filter needs to do. + * By default it should not return anything during + * normal execution. However, when an abnormal state + * is found, it should return an instance of + * CodeIgniter\HTTP\Response. If it does, script + * execution will end and that Response will be + * sent back to the client, allowing for error pages, + * redirects, etc. + * + * @param list|null $arguments + * + * @return RequestInterface|ResponseInterface|string|null + */ + public function before(RequestInterface $request, $arguments = null); + + /** + * Allows After filters to inspect and modify the response + * object as needed. This method does not allow any way + * to stop execution of other after filters, short of + * throwing an Exception or Error. + * + * @param list|null $arguments + * + * @return ResponseInterface|null + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null); +} diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php new file mode 100755 index 0000000..412a9f4 --- /dev/null +++ b/system/Filters/Filters.php @@ -0,0 +1,930 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\Config\Filters as BaseFiltersConfig; +use CodeIgniter\Filters\Exceptions\FilterException; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\Feature; +use Config\Filters as FiltersConfig; +use Config\Modules; + +/** + * Filters + * + * @see \CodeIgniter\Filters\FiltersTest + */ +class Filters +{ + /** + * The Config\Filters instance + * + * @var FiltersConfig + */ + protected $config; + + /** + * The active IncomingRequest or CLIRequest + * + * @var RequestInterface + */ + protected $request; + + /** + * The active Response instance + * + * @var ResponseInterface + */ + protected $response; + + /** + * The Config\Modules instance + * + * @var Modules + */ + protected $modules; + + /** + * Whether we've done initial processing on the filter lists. + * + * @var bool + */ + protected $initialized = false; + + /** + * The filter list to execute for the current request (URI path). + * + * This property is for display. Use $filtersClass to execute filters. + * This does not include "Required Filters". + * + * [ + * 'before' => [ + * 'alias', + * 'alias:arg1', + * 'alias:arg1,arg2', + * ], + * 'after' => [ + * 'alias', + * 'alias:arg1', + * 'alias:arg1,arg2', + * ], + * ] + * + * @var array{ + * before: list, + * after: list + * } + */ + protected $filters = [ + 'before' => [], + 'after' => [], + ]; + + /** + * The collection of filter classnames and their arguments to execute for + * the current request (URI path). + * + * This does not include "Required Filters". + * + * [ + * 'before' => [ + * [classname, arguments], + * ], + * 'after' => [ + * [classname, arguments], + * ], + * ] + * + * @var array{ + * before: list}>, + * after: list}> + * } + */ + protected $filtersClass = [ + 'before' => [], + 'after' => [], + ]; + + /** + * List of filter class instances. + * + * @var array [classname => instance] + */ + protected array $filterClassInstances = []; + + /** + * Any arguments to be passed to filters. + * + * @var array|null> [name => params] + * + * @deprecated 4.6.0 No longer used. + */ + protected $arguments = []; + + /** + * Any arguments to be passed to filtersClass. + * + * @var array|null> [classname => arguments] + * + * @deprecated 4.6.0 No longer used. + */ + protected $argumentsClass = []; + + /** + * Constructor. + * + * @param FiltersConfig $config + */ + public function __construct($config, RequestInterface $request, ResponseInterface $response, ?Modules $modules = null) + { + $this->config = $config; + $this->request = &$request; + $this->setResponse($response); + + $this->modules = $modules instanceof Modules ? $modules : new Modules(); + + if ($this->modules->shouldDiscover('filters')) { + $this->discoverFilters(); + } + } + + /** + * If discoverFilters is enabled in Config then system will try to + * auto-discover custom filters files in namespaces and allow access to + * the config object via the variable $filters as with the routes file. + * + * Sample: + * $filters->aliases['custom-auth'] = \Acme\Blob\Filters\BlobAuth::class; + * + * @deprecated 4.4.2 Use Registrar instead. + */ + private function discoverFilters(): void + { + $locator = service('locator'); + + // for access by custom filters + $filters = $this->config; + + $files = $locator->search('Config/Filters.php'); + + foreach ($files as $file) { + // The $file may not be a class file. + $className = $locator->getClassname($file); + + // Don't include our main Filter config again... + if ($className === FiltersConfig::class || $className === BaseFiltersConfig::class) { + continue; + } + + include $file; + } + } + + /** + * Set the response explicitly. + * + * @return void + */ + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + } + + /** + * Runs through all the filters (except "Required Filters") for the specified + * URI and position. + * + * @param string $uri URI path relative to baseURL + * @phpstan-param 'before'|'after' $position + * + * @return RequestInterface|ResponseInterface|string|null + * + * @throws FilterException + */ + public function run(string $uri, string $position = 'before') + { + $this->initialize(strtolower($uri)); + + if ($position === 'before') { + return $this->runBefore($this->filtersClass[$position]); + } + + // After + return $this->runAfter($this->filtersClass[$position]); + } + + /** + * @param list}> $filterClassList [[classname, arguments], ...] + * + * @return RequestInterface|ResponseInterface|string + */ + private function runBefore(array $filterClassList) + { + foreach ($filterClassList as $filterClassInfo) { + $className = $filterClassInfo[0]; + $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; + + $instance = $this->createFilter($className); + + $result = $instance->before($this->request, $arguments); + + if ($result instanceof RequestInterface) { + $this->request = $result; + + continue; + } + + // If the response object was sent back, + // then send it and quit. + if ($result instanceof ResponseInterface) { + // short circuit - bypass any other filters + return $result; + } + + // Ignore an empty result + if (empty($result)) { + continue; + } + + return $result; + } + + return $this->request; + } + + /** + * @param list}> $filterClassList [[classname, arguments], ...] + */ + private function runAfter(array $filterClassList): ResponseInterface + { + foreach ($filterClassList as $filterClassInfo) { + $className = $filterClassInfo[0]; + $arguments = ($filterClassInfo[1] === []) ? null : $filterClassInfo[1]; + + $instance = $this->createFilter($className); + + $result = $instance->after($this->request, $this->response, $arguments); + + if ($result instanceof ResponseInterface) { + $this->response = $result; + + continue; + } + } + + return $this->response; + } + + /** + * @param class-string $className + */ + private function createFilter(string $className): FilterInterface + { + if (isset($this->filterClassInstances[$className])) { + return $this->filterClassInstances[$className]; + } + + $instance = new $className(); + + if (! $instance instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($instance::class); + } + + $this->filterClassInstances[$className] = $instance; + + return $instance; + } + + /** + * Returns the "Required Filters" class list. + * + * @phpstan-param 'before'|'after' $position + * + * @return list}> [[classname, arguments], ...] + */ + public function getRequiredClasses(string $position): array + { + [$filters, $aliases] = $this->getRequiredFilters($position); + + if ($filters === []) { + return []; + } + + $filterClassList = []; + + foreach ($filters as $alias) { + if (is_array($aliases[$alias])) { + foreach ($this->config->aliases[$alias] as $class) { + $filterClassList[] = [$class, []]; + } + } else { + $filterClassList[] = [$aliases[$alias], []]; + } + } + + return $filterClassList; + } + + /** + * Runs "Required Filters" for the specified position. + * + * @phpstan-param 'before'|'after' $position + * + * @return RequestInterface|ResponseInterface|string|null + * + * @throws FilterException + * + * @internal + */ + public function runRequired(string $position = 'before') + { + $filterClassList = $this->getRequiredClasses($position); + + if ($filterClassList === []) { + return $position === 'before' ? $this->request : $this->response; + } + + if ($position === 'before') { + return $this->runBefore($filterClassList); + } + + // After + return $this->runAfter($filterClassList); + } + + /** + * Returns "Required Filters" for the specified position. + * + * @phpstan-param 'before'|'after' $position + * + * @internal + */ + public function getRequiredFilters(string $position = 'before'): array + { + // For backward compatibility. For users who do not update Config\Filters. + if (! isset($this->config->required[$position])) { + $baseConfig = config(BaseFiltersConfig::class); // @phpstan-ignore-line + $filters = $baseConfig->required[$position]; + $aliases = $baseConfig->aliases; + } else { + $filters = $this->config->required[$position]; + $aliases = $this->config->aliases; + } + + if ($filters === []) { + return [[], $aliases]; + } + + if ($position === 'after') { + if (in_array('toolbar', $this->filters['after'], true)) { + // It was already run in globals filters. So remove it. + $filters = $this->setToolbarToLast($filters, true); + } else { + // Set the toolbar filter to the last position to be executed + $filters = $this->setToolbarToLast($filters); + } + } + + foreach ($filters as $alias) { + if (! array_key_exists($alias, $aliases)) { + throw FilterException::forNoAlias($alias); + } + } + + return [$filters, $aliases]; + } + + /** + * Set the toolbar filter to the last position to be executed. + * + * @param list $filters `after` filter array + * @param bool $remove if true, remove `toolbar` filter + */ + private function setToolbarToLast(array $filters, bool $remove = false): array + { + $afters = []; + $found = false; + + foreach ($filters as $alias) { + if ($alias === 'toolbar') { + $found = true; + + continue; + } + + $afters[] = $alias; + } + + if ($found && ! $remove) { + $afters[] = 'toolbar'; + } + + return $afters; + } + + /** + * Runs through our list of filters provided by the configuration + * object to get them ready for use, including getting uri masks + * to proper regex, removing those we can from the possibilities + * based on HTTP method, etc. + * + * The resulting $this->filters is an array of only filters + * that should be applied to this request. + * + * We go ahead and process the entire tree because we'll need to + * run through both a before and after and don't want to double + * process the rows. + * + * @param string|null $uri URI path relative to baseURL (all lowercase) + * + * @TODO We don't need to accept null as $uri. + * + * @return Filters + * + * @testTag Only for test code. The run() calls this, so you don't need to + * call this in your app. + */ + public function initialize(?string $uri = null) + { + if ($this->initialized === true) { + return $this; + } + + // Decode URL-encoded string + $uri = urldecode($uri ?? ''); + + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if ($oldFilterOrder) { + $this->processGlobals($uri); + $this->processMethods(); + $this->processFilters($uri); + } else { + $this->processFilters($uri); + $this->processMethods(); + $this->processGlobals($uri); + } + + // Set the toolbar filter to the last position to be executed + $this->filters['after'] = $this->setToolbarToLast($this->filters['after']); + + // Since some filters like rate limiters rely on being executed once a request, + // we filter em here. + $this->filters['before'] = array_unique($this->filters['before']); + $this->filters['after'] = array_unique($this->filters['after']); + + $this->processAliasesToClass('before'); + $this->processAliasesToClass('after'); + + $this->initialized = true; + + return $this; + } + + /** + * Restores instance to its pre-initialized state. + * Most useful for testing so the service can be + * re-initialized to a different path. + */ + public function reset(): self + { + $this->initialized = false; + + $this->arguments = $this->argumentsClass = []; + + $this->filters = $this->filtersClass = [ + 'before' => [], + 'after' => [], + ]; + + return $this; + } + + /** + * Returns the processed filters array. + * This does not include "Required Filters". + * + * @return array{ + * before: list, + * after: list + * } + */ + public function getFilters(): array + { + return $this->filters; + } + + /** + * Returns the filtersClass array. + * This does not include "Required Filters". + * + * @return array{ + * before: list}>, + * after: list}> + * } + */ + public function getFiltersClass(): array + { + return $this->filtersClass; + } + + /** + * Adds a new alias to the config file. + * MUST be called prior to initialize(); + * Intended for use within routes files. + * + * @phpstan-param 'before'|'after' $position + * + * @return $this + */ + public function addFilter(string $class, ?string $alias = null, string $position = 'before', string $section = 'globals') + { + $alias ??= md5($class); + + if (! isset($this->config->{$section})) { + $this->config->{$section} = []; + } + + if (! isset($this->config->{$section}[$position])) { + $this->config->{$section}[$position] = []; + } + + $this->config->aliases[$alias] = $class; + + $this->config->{$section}[$position][] = $alias; + + return $this; + } + + /** + * Ensures that a specific filter is on and enabled for the current request. + * + * Filters can have "arguments". This is done by placing a colon immediately + * after the filter name, followed by a comma-separated list of arguments that + * are passed to the filter when executed. + * + * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' + * or filter classname. + * @phpstan-param 'before'|'after' $position + */ + private function enableFilter(string $filter, string $position = 'before'): void + { + // Normalize the arguments. + [$alias, $arguments] = $this->getCleanName($filter); + $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); + + if (class_exists($alias)) { + $this->config->aliases[$alias] = $alias; + } elseif (! array_key_exists($alias, $this->config->aliases)) { + throw FilterException::forNoAlias($alias); + } + + if (! isset($this->filters[$position][$filter])) { + $this->filters[$position][] = $filter; + } + + // Since some filters like rate limiters rely on being executed once a request, + // we filter em here. + $this->filters[$position] = array_unique($this->filters[$position]); + } + + /** + * Get clean name and arguments + * + * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' + * + * @return array{0: string, 1: list} [name, arguments] + */ + private function getCleanName(string $filter): array + { + $arguments = []; + + if (! str_contains($filter, ':')) { + return [$filter, $arguments]; + } + + [$alias, $arguments] = explode(':', $filter); + + $arguments = explode(',', $arguments); + array_walk($arguments, static function (&$item): void { + $item = trim($item); + }); + + return [$alias, $arguments]; + } + + /** + * Ensures that specific filters are on and enabled for the current request. + * + * Filters can have "arguments". This is done by placing a colon immediately + * after the filter name, followed by a comma-separated list of arguments that + * are passed to the filter when executed. + * + * @param list $filters filter_name or filter_name:arguments like 'role:admin,manager' + * + * @return Filters + */ + public function enableFilters(array $filters, string $when = 'before') + { + foreach ($filters as $filter) { + $this->enableFilter($filter, $when); + } + + return $this; + } + + /** + * Returns the arguments for a specified key, or all. + * + * @return array|string + * + * @deprecated 4.6.0 Already does not work. + */ + public function getArguments(?string $key = null) + { + return ((string) $key === '') ? $this->arguments : $this->arguments[$key]; + } + + // -------------------------------------------------------------------- + // Processors + // -------------------------------------------------------------------- + + /** + * Add any applicable (not excluded) global filter settings to the mix. + * + * @param string|null $uri URI path relative to baseURL (all lowercase) + * + * @return void + */ + protected function processGlobals(?string $uri = null) + { + if (! isset($this->config->globals) || ! is_array($this->config->globals)) { + return; + } + + $uri = strtolower(trim($uri ?? '', '/ ')); + + // Add any global filters, unless they are excluded for this URI + $sets = ['before', 'after']; + + $filters = []; + + foreach ($sets as $set) { + if (isset($this->config->globals[$set])) { + // look at each alias in the group + foreach ($this->config->globals[$set] as $alias => $rules) { + $keep = true; + if (is_array($rules)) { + // see if it should be excluded + if (isset($rules['except'])) { + // grab the exclusion rules + $check = $rules['except']; + if ($this->checkExcept($uri, $check)) { + $keep = false; + } + } + } else { + $alias = $rules; // simple name of filter to apply + } + + if ($keep) { + $filters[$set][] = $alias; + } + } + } + } + + if (isset($filters['before'])) { + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if ($oldFilterOrder) { + $this->filters['before'] = array_merge($this->filters['before'], $filters['before']); + } else { + $this->filters['before'] = array_merge($filters['before'], $this->filters['before']); + } + } + + if (isset($filters['after'])) { + $this->filters['after'] = array_merge($this->filters['after'], $filters['after']); + } + } + + /** + * Add any method-specific filters to the mix. + * + * @return void + */ + protected function processMethods() + { + if (! isset($this->config->methods) || ! is_array($this->config->methods)) { + return; + } + + $method = $this->request->getMethod(); + + $found = false; + + if (array_key_exists($method, $this->config->methods)) { + $found = true; + } + // Checks lowercase HTTP method for backward compatibility. + // @deprecated 4.5.0 + // @TODO remove this in the future. + elseif (array_key_exists(strtolower($method), $this->config->methods)) { + @trigger_error( + 'Setting lowercase HTTP method key "' . strtolower($method) . '" is deprecated.' + . ' Use uppercase HTTP method like "' . strtoupper($method) . '".', + E_USER_DEPRECATED, + ); + + $found = true; + $method = strtolower($method); + } + + if ($found) { + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + if ($oldFilterOrder) { + $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]); + } else { + $this->filters['before'] = array_merge($this->config->methods[$method], $this->filters['before']); + } + } + } + + /** + * Add any applicable configured filters to the mix. + * + * @param string|null $uri URI path relative to baseURL (all lowercase) + * + * @return void + */ + protected function processFilters(?string $uri = null) + { + if (! isset($this->config->filters) || $this->config->filters === []) { + return; + } + + $uri = strtolower(trim($uri, '/ ')); + + // Add any filters that apply to this URI + $filters = []; + + foreach ($this->config->filters as $filter => $settings) { + // Normalize the arguments. + [$alias, $arguments] = $this->getCleanName($filter); + $filter = ($arguments === []) ? $alias : $alias . ':' . implode(',', $arguments); + + // Look for inclusion rules + if (isset($settings['before'])) { + $path = $settings['before']; + + if ($this->pathApplies($uri, $path)) { + $filters['before'][] = $filter; + } + } + + if (isset($settings['after'])) { + $path = $settings['after']; + + if ($this->pathApplies($uri, $path)) { + $filters['after'][] = $filter; + } + } + } + + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + + if (isset($filters['before'])) { + if ($oldFilterOrder) { + $this->filters['before'] = array_merge($this->filters['before'], $filters['before']); + } else { + $this->filters['before'] = array_merge($filters['before'], $this->filters['before']); + } + } + + if (isset($filters['after'])) { + if (! $oldFilterOrder) { + $filters['after'] = array_reverse($filters['after']); + } + + $this->filters['after'] = array_merge($this->filters['after'], $filters['after']); + } + } + + /** + * Maps filter aliases to the equivalent filter classes + * + * @phpstan-param 'before'|'after' $position + * + * @return void + * + * @throws FilterException + */ + protected function processAliasesToClass(string $position) + { + $filterClassList = []; + + foreach ($this->filters[$position] as $filter) { + // Get arguments and clean alias + [$alias, $arguments] = $this->getCleanName($filter); + + if (! array_key_exists($alias, $this->config->aliases)) { + throw FilterException::forNoAlias($alias); + } + + if (is_array($this->config->aliases[$alias])) { + foreach ($this->config->aliases[$alias] as $class) { + $filterClassList[] = [$class, $arguments]; + } + } else { + $filterClassList[] = [$this->config->aliases[$alias], $arguments]; + } + } + + if ($position === 'before') { + $this->filtersClass[$position] = array_merge($filterClassList, $this->filtersClass[$position]); + } else { + $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClassList); + } + } + + /** + * Check paths for match for URI + * + * @param string $uri URI to test against + * @param array|string $paths The path patterns to test + * + * @return bool True if any of the paths apply to the URI + */ + private function pathApplies(string $uri, $paths) + { + // empty path matches all + if ($paths === '' || $paths === []) { + return true; + } + + // make sure the paths are iterable + if (is_string($paths)) { + $paths = [$paths]; + } + + return $this->checkPseudoRegex($uri, $paths); + } + + /** + * Check except paths + * + * @param string $uri URI path relative to baseURL (all lowercase) + * @param array|string $paths The except path patterns + * + * @return bool True if the URI matches except paths. + */ + private function checkExcept(string $uri, $paths): bool + { + // empty array does not match anything + if ($paths === []) { + return false; + } + + // make sure the paths are iterable + if (is_string($paths)) { + $paths = [$paths]; + } + + return $this->checkPseudoRegex($uri, $paths); + } + + /** + * Check the URI path as pseudo-regex + * + * @param string $uri URI path relative to baseURL (all lowercase, URL-decoded) + * @param array $paths The except path patterns + */ + private function checkPseudoRegex(string $uri, array $paths): bool + { + // treat each path as pseudo-regex + foreach ($paths as $path) { + // need to escape path separators + $path = str_replace('/', '\/', trim($path, '/ ')); + // need to make pseudo wildcard real + $path = strtolower(str_replace('*', '.*', $path)); + + // Does this rule apply here? + if (preg_match('#\A' . $path . '\z#u', $uri, $match) === 1) { + return true; + } + } + + return false; + } +} diff --git a/system/Filters/ForceHTTPS.php b/system/Filters/ForceHTTPS.php new file mode 100755 index 0000000..aa9cb37 --- /dev/null +++ b/system/Filters/ForceHTTPS.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\Exceptions\RedirectException; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use Config\App; + +/** + * Force HTTPS filter + */ +class ForceHTTPS implements FilterInterface +{ + /** + * Force Secure Site Access? If the config value 'forceGlobalSecureRequests' + * is true, will enforce that all requests to this site are made through + * HTTPS. Will redirect the user to the current page with HTTPS, as well + * as set the HTTP Strict Transport Security (HSTS) header for those browsers + * that support it. + * + * @param array|null $arguments + * + * @return ResponseInterface|null + */ + public function before(RequestInterface $request, $arguments = null) + { + $config = config(App::class); + + if ($config->forceGlobalSecureRequests !== true) { + return null; + } + + $response = service('response'); + + try { + force_https(YEAR, $request, $response); + } catch (RedirectException $e) { + return $e->getResponse(); + } + + return null; + } + + /** + * We don't have anything to do here. + * + * @param array|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + return null; + } +} diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php new file mode 100755 index 0000000..4b24691 --- /dev/null +++ b/system/Filters/Honeypot.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\Honeypot\Exceptions\HoneypotException; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Honeypot filter + * + * @see \CodeIgniter\Filters\HoneypotTest + */ +class Honeypot implements FilterInterface +{ + /** + * Checks if Honeypot field is empty, if not then the + * requester is a bot + * + * @param list|null $arguments + * + * @throws HoneypotException + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return null; + } + + if (service('honeypot')->hasContent($request)) { + throw HoneypotException::isBot(); + } + + return null; + } + + /** + * Attach a honeypot to the current response. + * + * @param list|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + service('honeypot')->attachHoneypot($response); + + return null; + } +} diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php new file mode 100755 index 0000000..1cd7751 --- /dev/null +++ b/system/Filters/InvalidChars.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\Security\Exceptions\SecurityException; + +/** + * InvalidChars filter. + * + * Check if user input data ($_GET, $_POST, $_COOKIE, php://input) do not contain + * invalid characters: + * - invalid UTF-8 characters + * - control characters except line break and tab code + * + * @see \CodeIgniter\Filters\InvalidCharsTest + */ +class InvalidChars implements FilterInterface +{ + /** + * Data source + * + * @var string + */ + protected $source; + + /** + * Regular expressions for valid control codes + * + * @var string + */ + protected $controlCodeRegex = '/\A[\r\n\t[:^cntrl:]]*\z/u'; + + /** + * Check invalid characters. + * + * @param list|null $arguments + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return null; + } + + $data = [ + 'get' => $request->getGet(), + 'post' => $request->getPost(), + 'cookie' => $request->getCookie(), + 'rawInput' => $request->getRawInput(), + ]; + + foreach ($data as $source => $values) { + $this->source = $source; + $this->checkEncoding($values); + $this->checkControl($values); + } + + return null; + } + + /** + * We don't have anything to do here. + * + * @param list|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + return null; + } + + /** + * Check the character encoding is valid UTF-8. + * + * @param array|string $value + * + * @return array|string + */ + protected function checkEncoding($value) + { + if (is_array($value)) { + array_map($this->checkEncoding(...), $value); + + return $value; + } + + if (mb_check_encoding($value, 'UTF-8')) { + return $value; + } + + throw SecurityException::forInvalidUTF8Chars($this->source, $value); + } + + /** + * Check for the presence of control characters except line breaks and tabs. + * + * @param array|string $value + * + * @return array|string + */ + protected function checkControl($value) + { + if (is_array($value)) { + array_map($this->checkControl(...), $value); + + return $value; + } + + if (preg_match($this->controlCodeRegex, $value) === 1) { + return $value; + } + + throw SecurityException::forInvalidControlChars($this->source, $value); + } +} diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php new file mode 100755 index 0000000..b0013c8 --- /dev/null +++ b/system/Filters/PageCache.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\Cache\ResponseCache; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\DownloadResponse; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Page Cache filter + */ +class PageCache implements FilterInterface +{ + private readonly ResponseCache $pageCache; + + public function __construct() + { + $this->pageCache = service('responsecache'); + } + + /** + * Checks page cache and return if found. + * + * @param array|null $arguments + * + * @return ResponseInterface|null + */ + public function before(RequestInterface $request, $arguments = null) + { + assert($request instanceof CLIRequest || $request instanceof IncomingRequest); + + $response = service('response'); + + $cachedResponse = $this->pageCache->get($request, $response); + + if ($cachedResponse instanceof ResponseInterface) { + return $cachedResponse; + } + + return null; + } + + /** + * Cache the page. + * + * @param array|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + assert($request instanceof CLIRequest || $request instanceof IncomingRequest); + + if ( + ! $response instanceof DownloadResponse + && ! $response instanceof RedirectResponse + ) { + // Cache it without the performance metrics replaced + // so that we can have live speed updates along the way. + // Must be run after filters to preserve the Response headers. + $this->pageCache->make($request, $response); + + return $response; + } + + return null; + } +} diff --git a/system/Filters/PerformanceMetrics.php b/system/Filters/PerformanceMetrics.php new file mode 100755 index 0000000..97582ad --- /dev/null +++ b/system/Filters/PerformanceMetrics.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Performance Metrics filter + */ +class PerformanceMetrics implements FilterInterface +{ + /** + * We don't need to do anything here. + * + * @param array|null $arguments + */ + public function before(RequestInterface $request, $arguments = null) + { + return null; + } + + /** + * Replaces the performance metrics. + * + * @param array|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + $body = $response->getBody(); + + if ($body !== null) { + $benchmark = service('timer'); + + $output = str_replace( + [ + '{elapsed_time}', + '{memory_usage}', + ], + [ + (string) $benchmark->getElapsedTime('total_execution'), + number_format(memory_get_peak_usage() / 1024 / 1024, 3), + ], + $body, + ); + + $response->setBody($output); + + return $response; + } + + return null; + } +} diff --git a/system/Filters/SecureHeaders.php b/system/Filters/SecureHeaders.php new file mode 100755 index 0000000..42e5842 --- /dev/null +++ b/system/Filters/SecureHeaders.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Filters; + +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; + +/** + * Add Common Security Headers + * + * @see \CodeIgniter\Filters\SecureHeadersTest + */ +class SecureHeaders implements FilterInterface +{ + /** + * @var array + */ + protected $headers = [ + // https://owasp.org/www-project-secure-headers/#x-frame-options + 'X-Frame-Options' => 'SAMEORIGIN', + + // https://owasp.org/www-project-secure-headers/#x-content-type-options + 'X-Content-Type-Options' => 'nosniff', + + // https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/jj542450(v=vs.85)#the-noopen-directive + 'X-Download-Options' => 'noopen', + + // https://owasp.org/www-project-secure-headers/#x-permitted-cross-domain-policies + 'X-Permitted-Cross-Domain-Policies' => 'none', + + // https://owasp.org/www-project-secure-headers/#referrer-policy + 'Referrer-Policy' => 'same-origin', + + // https://owasp.org/www-project-secure-headers/#x-xss-protection + // If you do not need to support legacy browsers, it is recommended that you use + // Content-Security-Policy without allowing unsafe-inline scripts instead. + // 'X-XSS-Protection' => '1; mode=block', + ]; + + /** + * We don't have anything to do here. + * + * @param list|null $arguments + */ + public function before(RequestInterface $request, $arguments = null) + { + return null; + } + + /** + * Add security headers. + * + * @param list|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + foreach ($this->headers as $header => $value) { + $response->setHeader($header, $value); + } + + return $response; + } +} diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php new file mode 100755 index 0000000..46daad5 --- /dev/null +++ b/system/Format/Exceptions/FormatException.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Format\Exceptions; + +use CodeIgniter\Exceptions\DebugTraceableTrait; +use CodeIgniter\Exceptions\RuntimeException; + +/** + * FormatException + */ +class FormatException extends RuntimeException +{ + use DebugTraceableTrait; + + /** + * Thrown when the instantiated class does not exist. + * + * @return static + */ + public static function forInvalidFormatter(string $class) + { + return new static(lang('Format.invalidFormatter', [$class])); + } + + /** + * Thrown in JSONFormatter when the json_encode produces + * an error code other than JSON_ERROR_NONE and JSON_ERROR_RECURSION. + * + * @param string $error The error message + * + * @return static + */ + public static function forInvalidJSON(?string $error = null) + { + return new static(lang('Format.invalidJSON', [$error])); + } + + /** + * Thrown when the supplied MIME type has no + * defined Formatter class. + * + * @return static + */ + public static function forInvalidMime(string $mime) + { + return new static(lang('Format.invalidMime', [$mime])); + } + + /** + * Thrown on XMLFormatter when the `simplexml` extension + * is not installed. + * + * @return static + * + * @codeCoverageIgnore + */ + public static function forMissingExtension() + { + return new static(lang('Format.missingExtension')); + } +} diff --git a/system/Format/Format.php b/system/Format/Format.php new file mode 100755 index 0000000..4a9cb60 --- /dev/null +++ b/system/Format/Format.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Format; + +use CodeIgniter\Format\Exceptions\FormatException; +use Config\Format as FormatConfig; + +/** + * The Format class is a convenient place to create Formatters. + * + * @see \CodeIgniter\Format\FormatTest + */ +class Format +{ + /** + * Configuration instance + * + * @var FormatConfig + */ + protected $config; + + /** + * Constructor. + */ + public function __construct(FormatConfig $config) + { + $this->config = $config; + } + + /** + * Returns the current configuration instance. + * + * @return FormatConfig + */ + public function getConfig() + { + return $this->config; + } + + /** + * A Factory method to return the appropriate formatter for the given mime type. + * + * @throws FormatException + */ + public function getFormatter(string $mime): FormatterInterface + { + if (! array_key_exists($mime, $this->config->formatters)) { + throw FormatException::forInvalidMime($mime); + } + + $className = $this->config->formatters[$mime]; + + if (! class_exists($className)) { + throw FormatException::forInvalidFormatter($className); + } + + $class = new $className(); + + if (! $class instanceof FormatterInterface) { + throw FormatException::forInvalidFormatter($className); + } + + return $class; + } +} diff --git a/system/Format/FormatterInterface.php b/system/Format/FormatterInterface.php new file mode 100755 index 0000000..0f00556 --- /dev/null +++ b/system/Format/FormatterInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Format; + +/** + * Formatter interface + */ +interface FormatterInterface +{ + /** + * Takes the given data and formats it. + * + * @param array|object|string $data + * + * @return false|string + */ + public function format($data); +} diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php new file mode 100755 index 0000000..7d2ad52 --- /dev/null +++ b/system/Format/JSONFormatter.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Format; + +use CodeIgniter\Format\Exceptions\FormatException; +use Config\Format; + +/** + * JSON data formatter + * + * @see \CodeIgniter\Format\JSONFormatterTest + */ +class JSONFormatter implements FormatterInterface +{ + /** + * Takes the given data and formats it. + * + * @param array|bool|float|int|object|string|null $data + * + * @return false|string (JSON string | false) + */ + public function format($data) + { + $config = new Format(); + + $options = $config->formatterOptions['application/json'] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + $options |= JSON_PARTIAL_OUTPUT_ON_ERROR; + + $options = ENVIRONMENT === 'production' ? $options : $options | JSON_PRETTY_PRINT; + + $result = json_encode($data, $options, 512); + + if (! in_array(json_last_error(), [JSON_ERROR_NONE, JSON_ERROR_RECURSION], true)) { + throw FormatException::forInvalidJSON(json_last_error_msg()); + } + + return $result; + } +} diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php new file mode 100755 index 0000000..c85eae5 --- /dev/null +++ b/system/Format/XMLFormatter.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Format; + +use CodeIgniter\Format\Exceptions\FormatException; +use Config\Format; +use SimpleXMLElement; + +/** + * XML data formatter + * + * @see \CodeIgniter\Format\XMLFormatterTest + */ +class XMLFormatter implements FormatterInterface +{ + /** + * Takes the given data and formats it. + * + * @param array|bool|float|int|object|string|null $data + * + * @return false|string (XML string | false) + */ + public function format($data) + { + $config = new Format(); + + // SimpleXML is installed but default + // but best to check, and then provide a fallback. + if (! extension_loaded('simplexml')) { + throw FormatException::forMissingExtension(); // @codeCoverageIgnore + } + + $options = $config->formatterOptions['application/xml'] ?? 0; + $output = new SimpleXMLElement('', $options); + + $this->arrayToXML((array) $data, $output); + + return $output->asXML(); + } + + /** + * A recursive method to convert an array into a valid XML string. + * + * Written by CodexWorld. Received permission by email on Nov 24, 2016 to use this code. + * + * @see http://www.codexworld.com/convert-array-to-xml-in-php/ + * + * @param SimpleXMLElement $output + * + * @return void + */ + protected function arrayToXML(array $data, &$output) + { + foreach ($data as $key => $value) { + $key = $this->normalizeXMLTag($key); + + if (is_array($value)) { + $subnode = $output->addChild("{$key}"); + $this->arrayToXML($value, $subnode); + } else { + $output->addChild("{$key}", htmlspecialchars("{$value}")); + } + } + } + + /** + * Normalizes tags into the allowed by W3C. + * Regex adopted from this StackOverflow answer. + * + * @param int|string $key + * + * @return string + * + * @see https://stackoverflow.com/questions/60001029/invalid-characters-in-xml-tag-name + */ + protected function normalizeXMLTag($key) + { + $startChar = 'A-Z_a-z' . + '\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}' . + '\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}' . + '\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}' . + '\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}'; + $validName = $startChar . '\\.\\d\\x{B7}\\x{300}-\\x{36F}\\x{203F}-\\x{2040}'; + + $key = (string) $key; + + $key = trim($key); + $key = preg_replace("/[^{$validName}-]+/u", '', $key); + $key = preg_replace("/^[^{$startChar}]+/u", 'item$0', $key); + + return preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word + } +} diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php new file mode 100755 index 0000000..6ef9717 --- /dev/null +++ b/system/HTTP/CLIRequest.php @@ -0,0 +1,325 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Exceptions\RuntimeException; +use Config\App; +use Locale; + +/** + * Represents a request from the command-line. Provides additional + * tools to interact with that request since CLI requests are not + * static like HTTP requests might be. + * + * Portions of this code were initially from the FuelPHP Framework, + * version 1.7.x, and used here under the MIT license they were + * originally made available under. + * + * http://fuelphp.com + * + * @see \CodeIgniter\HTTP\CLIRequestTest + */ +class CLIRequest extends Request +{ + /** + * Stores the segments of our cli "URI" command. + * + * @var array + */ + protected $segments = []; + + /** + * Command line options and their values. + * + * @var array + */ + protected $options = []; + + /** + * Command line arguments (segments and options). + * + * @var array + */ + protected $args = []; + + /** + * Set the expected HTTP verb + * + * @var string + */ + protected $method = 'CLI'; + + /** + * Constructor + */ + public function __construct(App $config) + { + if (! is_cli()) { + throw new RuntimeException(static::class . ' needs to run from the command line.'); // @codeCoverageIgnore + } + + parent::__construct($config); + + // Don't terminate the script when the cli's tty goes away + ignore_user_abort(true); + + $this->parseCommand(); + + // Set SiteURI for this request + $this->uri = new SiteURI($config, $this->getPath()); + } + + /** + * Returns the "path" of the request script so that it can be used + * in routing to the appropriate controller/method. + * + * The path is determined by treating the command line arguments + * as if it were a URL - up until we hit our first option. + * + * Example: + * php index.php users 21 profile -foo bar + * + * // Routes to /users/21/profile (index is removed for routing sake) + * // with the option foo = bar. + */ + public function getPath(): string + { + return implode('/', $this->segments); + } + + /** + * Returns an associative array of all CLI options found, with + * their values. + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Returns an array of all CLI arguments (segments and options). + */ + public function getArgs(): array + { + return $this->args; + } + + /** + * Returns the path segments. + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * Returns the value for a single CLI option that was passed in. + * + * @return string|null + */ + public function getOption(string $key) + { + return $this->options[$key] ?? null; + } + + /** + * Returns the options as a string, suitable for passing along on + * the CLI to other commands. + * + * Example: + * $options = [ + * 'foo' => 'bar', + * 'baz' => 'queue some stuff' + * ]; + * + * getOptionString() = '-foo bar -baz "queue some stuff"' + */ + public function getOptionString(bool $useLongOpts = false): string + { + if ($this->options === []) { + return ''; + } + + $out = ''; + + foreach ($this->options as $name => $value) { + if ($useLongOpts && mb_strlen($name) > 1) { + $out .= "--{$name} "; + } else { + $out .= "-{$name} "; + } + + if ($value === null) { + continue; + } + + if (mb_strpos($value, ' ') !== false) { + $out .= '"' . $value . '" '; + } else { + $out .= "{$value} "; + } + } + + return trim($out); + } + + /** + * Parses the command line it was called from and collects all options + * and valid segments. + * + * NOTE: I tried to use getopt but had it fail occasionally to find + * any options, where argv has always had our back. + * + * @return void + */ + protected function parseCommand() + { + $args = $this->getServer('argv'); + array_shift($args); // Scrap index.php + + $optionValue = false; + + foreach ($args as $i => $arg) { + if (mb_strpos($arg, '-') !== 0) { + if ($optionValue) { + $optionValue = false; + } else { + $this->segments[] = $arg; + $this->args[] = $arg; + } + + continue; + } + + $arg = ltrim($arg, '-'); + $value = null; + + if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) { + $value = $args[$i + 1]; + $optionValue = true; + } + + $this->options[$arg] = $value; + $this->args[$arg] = $value; + } + } + + /** + * Determines if this request was made from the command line (CLI). + */ + public function isCLI(): bool + { + return true; + } + + /** + * Fetch an item from GET data. + * + * @param array|string|null $index Index for item to fetch from $_GET. + * @param int|null $filter A filter name to apply. + * @param array|int|null $flags + * + * @return array|null + */ + public function getGet($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from POST. + * + * @param array|string|null $index Index for item to fetch from $_POST. + * @param int|null $filter A filter name to apply + * @param array|int|null $flags + * + * @return array|null + */ + public function getPost($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from POST data with fallback to GET. + * + * @param array|string|null $index Index for item to fetch from $_POST or $_GET + * @param int|null $filter A filter name to apply + * @param array|int|null $flags + * + * @return array|null + */ + public function getPostGet($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * Fetch an item from GET data with fallback to POST. + * + * @param array|string|null $index Index for item to be fetched from $_GET or $_POST + * @param int|null $filter A filter name to apply + * @param array|int|null $flags + * + * @return array|null + */ + public function getGetPost($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * This is a place holder for calls from cookie_helper get_cookie(). + * + * @param array|string|null $index Index for item to be fetched from $_COOKIE + * @param int|null $filter A filter name to be applied + * @param mixed $flags + * + * @return array|null + */ + public function getCookie($index = null, $filter = null, $flags = null) + { + return $this->returnNullOrEmptyArray($index); + } + + /** + * @param array|string|null $index + * + * @return array|null + */ + private function returnNullOrEmptyArray($index) + { + return ($index === null || is_array($index)) ? [] : null; + } + + /** + * Gets the current locale, with a fallback to the default + * locale if none is set. + */ + public function getLocale(): string + { + return Locale::getDefault(); + } + + /** + * Checks this request type. + * + * @param string $type HTTP verb or 'json' or 'ajax' + * @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type + */ + public function is(string $type): bool + { + return false; + } +} diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php new file mode 100755 index 0000000..e9e2d89 --- /dev/null +++ b/system/HTTP/CURLRequest.php @@ -0,0 +1,716 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\Exceptions\InvalidArgumentException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\App; +use Config\CURLRequest as ConfigCURLRequest; + +/** + * A lightweight HTTP client for sending synchronous HTTP requests via cURL. + * + * @see \CodeIgniter\HTTP\CURLRequestTest + */ +class CURLRequest extends OutgoingRequest +{ + /** + * The response object associated with this request + * + * @var ResponseInterface|null + */ + protected $response; + + /** + * The original response object associated with this request + * + * @var ResponseInterface|null + */ + protected $responseOrig; + + /** + * The URI associated with this request + * + * @var URI + */ + protected $baseURI; + + /** + * The setting values + * + * @var array + */ + protected $config; + + /** + * The default setting values + * + * @var array + */ + protected $defaultConfig = [ + 'timeout' => 0.0, + 'connect_timeout' => 150, + 'debug' => false, + 'verify' => true, + ]; + + /** + * Default values for when 'allow_redirects' + * option is true. + * + * @var array + */ + protected $redirectDefaults = [ + 'max' => 5, + 'strict' => true, + 'protocols' => [ + 'http', + 'https', + ], + ]; + + /** + * The number of milliseconds to delay before + * sending the request. + * + * @var float + */ + protected $delay = 0.0; + + /** + * The default options from the constructor. Applied to all requests. + */ + private readonly array $defaultOptions; + + /** + * Whether share options between requests or not. + * + * If true, all the options won't be reset between requests. + * It may cause an error request with unnecessary headers. + */ + private readonly bool $shareOptions; + + /** + * Takes an array of options to set the following possible class properties: + * + * - baseURI + * - timeout + * - any other request options to use as defaults. + * + * @param array $options + */ + public function __construct(App $config, URI $uri, ?ResponseInterface $response = null, array $options = []) + { + if (! function_exists('curl_version')) { + throw HTTPException::forMissingCurl(); // @codeCoverageIgnore + } + + parent::__construct(Method::GET, $uri); + + $this->responseOrig = $response ?? new Response($config); + // Remove the default Content-Type header. + $this->responseOrig->removeHeader('Content-Type'); + + $this->baseURI = $uri->useRawQueryString(); + $this->defaultOptions = $options; + + /** @var ConfigCURLRequest|null $configCURLRequest */ + $configCURLRequest = config(ConfigCURLRequest::class); + $this->shareOptions = $configCURLRequest->shareOptions ?? true; + + $this->config = $this->defaultConfig; + $this->parseOptions($options); + } + + /** + * Sends an HTTP request to the specified $url. If this is a relative + * URL, it will be merged with $this->baseURI to form a complete URL. + * + * @param string $method HTTP method + */ + public function request($method, string $url, array $options = []): ResponseInterface + { + $this->response = clone $this->responseOrig; + + $this->parseOptions($options); + + $url = $this->prepareURL($url); + + $method = esc(strip_tags($method)); + + $this->send($method, $url); + + if ($this->shareOptions === false) { + $this->resetOptions(); + } + + return $this->response; + } + + /** + * Reset all options to default. + * + * @return void + */ + protected function resetOptions() + { + // Reset headers + $this->headers = []; + $this->headerMap = []; + + // Reset body + $this->body = null; + + // Reset configs + $this->config = $this->defaultConfig; + + // Set the default options for next request + $this->parseOptions($this->defaultOptions); + } + + /** + * Convenience method for sending a GET request. + */ + public function get(string $url, array $options = []): ResponseInterface + { + return $this->request(Method::GET, $url, $options); + } + + /** + * Convenience method for sending a DELETE request. + */ + public function delete(string $url, array $options = []): ResponseInterface + { + return $this->request('DELETE', $url, $options); + } + + /** + * Convenience method for sending a HEAD request. + */ + public function head(string $url, array $options = []): ResponseInterface + { + return $this->request('HEAD', $url, $options); + } + + /** + * Convenience method for sending an OPTIONS request. + */ + public function options(string $url, array $options = []): ResponseInterface + { + return $this->request('OPTIONS', $url, $options); + } + + /** + * Convenience method for sending a PATCH request. + */ + public function patch(string $url, array $options = []): ResponseInterface + { + return $this->request('PATCH', $url, $options); + } + + /** + * Convenience method for sending a POST request. + */ + public function post(string $url, array $options = []): ResponseInterface + { + return $this->request(Method::POST, $url, $options); + } + + /** + * Convenience method for sending a PUT request. + */ + public function put(string $url, array $options = []): ResponseInterface + { + return $this->request(Method::PUT, $url, $options); + } + + /** + * Set the HTTP Authentication. + * + * @param string $type basic or digest + * + * @return $this + */ + public function setAuth(string $username, string $password, string $type = 'basic') + { + $this->config['auth'] = [ + $username, + $password, + $type, + ]; + + return $this; + } + + /** + * Set form data to be sent. + * + * @param bool $multipart Set TRUE if you are sending CURLFiles + * + * @return $this + */ + public function setForm(array $params, bool $multipart = false) + { + if ($multipart) { + $this->config['multipart'] = $params; + } else { + $this->config['form_params'] = $params; + } + + return $this; + } + + /** + * Set JSON data to be sent. + * + * @param array|bool|float|int|object|string|null $data + * + * @return $this + */ + public function setJSON($data) + { + $this->config['json'] = $data; + + return $this; + } + + /** + * Sets the correct settings based on the options array + * passed in. + * + * @return void + */ + protected function parseOptions(array $options) + { + if (array_key_exists('baseURI', $options)) { + $this->baseURI = $this->baseURI->setURI($options['baseURI']); + unset($options['baseURI']); + } + + if (array_key_exists('headers', $options) && is_array($options['headers'])) { + foreach ($options['headers'] as $name => $value) { + $this->setHeader($name, $value); + } + + unset($options['headers']); + } + + if (array_key_exists('delay', $options)) { + // Convert from the milliseconds passed in + // to the seconds that sleep requires. + $this->delay = (float) $options['delay'] / 1000; + unset($options['delay']); + } + + if (array_key_exists('body', $options)) { + $this->setBody($options['body']); + unset($options['body']); + } + + foreach ($options as $key => $value) { + $this->config[$key] = $value; + } + } + + /** + * If the $url is a relative URL, will attempt to create + * a full URL by prepending $this->baseURI to it. + */ + protected function prepareURL(string $url): string + { + // If it's a full URI, then we have nothing to do here... + if (str_contains($url, '://')) { + return $url; + } + + $uri = $this->baseURI->resolveRelativeURI($url); + + // Create the string instead of casting to prevent baseURL muddling + return URI::createURIString( + $uri->getScheme(), + $uri->getAuthority(), + $uri->getPath(), + $uri->getQuery(), + $uri->getFragment(), + ); + } + + /** + * Fires the actual cURL request. + * + * @return ResponseInterface + */ + public function send(string $method, string $url) + { + // Reset our curl options so we're on a fresh slate. + $curlOptions = []; + + if (! empty($this->config['query']) && is_array($this->config['query'])) { + // This is likely too naive a solution. + // Should look into handling when $url already + // has query vars on it. + $url .= '?' . http_build_query($this->config['query']); + unset($this->config['query']); + } + + $curlOptions[CURLOPT_URL] = $url; + $curlOptions[CURLOPT_RETURNTRANSFER] = true; + $curlOptions[CURLOPT_HEADER] = true; + $curlOptions[CURLOPT_FRESH_CONNECT] = true; + // Disable @file uploads in post data. + $curlOptions[CURLOPT_SAFE_UPLOAD] = true; + + $curlOptions = $this->setCURLOptions($curlOptions, $this->config); + $curlOptions = $this->applyMethod($method, $curlOptions); + $curlOptions = $this->applyRequestHeaders($curlOptions); + + // Do we need to delay this request? + if ($this->delay > 0) { + usleep((int) $this->delay * 1_000_000); + } + + $output = $this->sendRequest($curlOptions); + + // Set the string we want to break our response from + $breakString = "\r\n\r\n"; + + while (str_starts_with($output, 'HTTP/1.1 100 Continue')) { + $output = substr($output, strpos($output, $breakString) + 4); + } + + if (preg_match('/HTTP\/\d\.\d 200 Connection established/i', $output)) { + $output = substr($output, strpos($output, $breakString) + 4); + } + + // If request and response have Digest + if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && str_contains($output, 'WWW-Authenticate: Digest')) { + $output = substr($output, strpos($output, $breakString) + 4); + } + + // Split out our headers and body + $break = strpos($output, $breakString); + + if ($break !== false) { + // Our headers + $headers = explode("\n", substr($output, 0, $break)); + + $this->setResponseHeaders($headers); + + // Our body + $body = substr($output, $break + 4); + $this->response->setBody($body); + } else { + $this->response->setBody($output); + } + + return $this->response; + } + + /** + * Adds $this->headers to the cURL request. + */ + protected function applyRequestHeaders(array $curlOptions = []): array + { + if (empty($this->headers)) { + return $curlOptions; + } + + $set = []; + + foreach (array_keys($this->headers) as $name) { + $set[] = $name . ': ' . $this->getHeaderLine($name); + } + + $curlOptions[CURLOPT_HTTPHEADER] = $set; + + return $curlOptions; + } + + /** + * Apply method + */ + protected function applyMethod(string $method, array $curlOptions): array + { + $this->method = $method; + $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; + + $size = strlen($this->body ?? ''); + + // Have content? + if ($size > 0) { + return $this->applyBody($curlOptions); + } + + if ($method === Method::PUT || $method === Method::POST) { + // See http://tools.ietf.org/html/rfc7230#section-3.3.2 + if ($this->header('content-length') === null && ! isset($this->config['multipart'])) { + $this->setHeader('Content-Length', '0'); + } + } elseif ($method === 'HEAD') { + $curlOptions[CURLOPT_NOBODY] = 1; + } + + return $curlOptions; + } + + /** + * Apply body + */ + protected function applyBody(array $curlOptions = []): array + { + if (! empty($this->body)) { + $curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody(); + } + + return $curlOptions; + } + + /** + * Parses the header retrieved from the cURL response into + * our Response object. + * + * @return void + */ + protected function setResponseHeaders(array $headers = []) + { + foreach ($headers as $header) { + if (($pos = strpos($header, ':')) !== false) { + $title = trim(substr($header, 0, $pos)); + $value = trim(substr($header, $pos + 1)); + + if ($this->response instanceof Response) { + $this->response->addHeader($title, $value); + } else { + $this->response->setHeader($title, $value); + } + } elseif (str_starts_with($header, 'HTTP')) { + preg_match('#^HTTP\/([12](?:\.[01])?) (\d+) (.+)#', $header, $matches); + + if (isset($matches[1])) { + $this->response->setProtocolVersion($matches[1]); + } + + if (isset($matches[2])) { + $this->response->setStatusCode((int) $matches[2], $matches[3] ?? null); + } + } + } + } + + /** + * Set CURL options + * + * @return array + * + * @throws InvalidArgumentException + */ + protected function setCURLOptions(array $curlOptions = [], array $config = []) + { + // Auth Headers + if (! empty($config['auth'])) { + $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1]; + + if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') { + $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; + } else { + $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; + } + } + + // Certificate + if (! empty($config['cert'])) { + $cert = $config['cert']; + + if (is_array($cert)) { + $curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1]; + $cert = $cert[0]; + } + + if (! is_file($cert)) { + throw HTTPException::forSSLCertNotFound($cert); + } + + $curlOptions[CURLOPT_SSLCERT] = $cert; + } + + // SSL Verification + if (isset($config['verify'])) { + if (is_string($config['verify'])) { + $file = realpath($config['verify']) ?: $config['verify']; + + if (! is_file($file)) { + throw HTTPException::forInvalidSSLKey($config['verify']); + } + + $curlOptions[CURLOPT_CAINFO] = $file; + $curlOptions[CURLOPT_SSL_VERIFYPEER] = true; + $curlOptions[CURLOPT_SSL_VERIFYHOST] = 2; + } elseif (is_bool($config['verify'])) { + $curlOptions[CURLOPT_SSL_VERIFYPEER] = $config['verify']; + $curlOptions[CURLOPT_SSL_VERIFYHOST] = $config['verify'] ? 2 : 0; + } + } + + // Proxy + if (isset($config['proxy'])) { + $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true; + $curlOptions[CURLOPT_PROXY] = $config['proxy']; + } + + // Debug + if ($config['debug']) { + $curlOptions[CURLOPT_VERBOSE] = 1; + $curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb'); + } + + // Decode Content + if (! empty($config['decode_content'])) { + $accept = $this->getHeaderLine('Accept-Encoding'); + + if ($accept !== '') { + $curlOptions[CURLOPT_ENCODING] = $accept; + } else { + $curlOptions[CURLOPT_ENCODING] = ''; + $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding'; + } + } + + // Allow Redirects + if (array_key_exists('allow_redirects', $config)) { + $settings = $this->redirectDefaults; + + if (is_array($config['allow_redirects'])) { + $settings = array_merge($settings, $config['allow_redirects']); + } + + if ($config['allow_redirects'] === false) { + $curlOptions[CURLOPT_FOLLOWLOCATION] = 0; + } else { + $curlOptions[CURLOPT_FOLLOWLOCATION] = 1; + $curlOptions[CURLOPT_MAXREDIRS] = $settings['max']; + + if ($settings['strict'] === true) { + $curlOptions[CURLOPT_POSTREDIR] = 1 | 2 | 4; + } + + $protocols = 0; + + foreach ($settings['protocols'] as $proto) { + $protocols += constant('CURLPROTO_' . strtoupper($proto)); + } + + $curlOptions[CURLOPT_REDIR_PROTOCOLS] = $protocols; + } + } + + // Timeout + $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000; + + // Connection Timeout + $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000; + + // Post Data - application/x-www-form-urlencoded + if (! empty($config['form_params']) && is_array($config['form_params'])) { + $postFields = http_build_query($config['form_params']); + $curlOptions[CURLOPT_POSTFIELDS] = $postFields; + + // Ensure content-length is set, since CURL doesn't seem to + // calculate it when HTTPHEADER is set. + $this->setHeader('Content-Length', (string) strlen($postFields)); + $this->setHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + // Post Data - multipart/form-data + if (! empty($config['multipart']) && is_array($config['multipart'])) { + // setting the POSTFIELDS option automatically sets multipart + $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart']; + } + + // HTTP Errors + $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true; + + // JSON + if (isset($config['json'])) { + // Will be set as the body in `applyBody()` + $json = json_encode($config['json']); + $this->setBody($json); + $this->setHeader('Content-Type', 'application/json'); + $this->setHeader('Content-Length', (string) strlen($json)); + } + + // Resolve IP + if (array_key_exists('force_ip_resolve', $config)) { + $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) { + 'v4' => CURL_IPRESOLVE_V4, + 'v6' => CURL_IPRESOLVE_V6, + default => CURL_IPRESOLVE_WHATEVER, + }; + } + + // version + if (! empty($config['version'])) { + $version = sprintf('%.1F', $config['version']); + if ($version === '1.0') { + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; + } elseif ($version === '1.1') { + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; + } elseif ($version === '2.0') { + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; + } elseif ($version === '3.0') { + if (! defined('CURL_HTTP_VERSION_3')) { + define('CURL_HTTP_VERSION_3', 30); + } + + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3; + } + } + + // Cookie + if (isset($config['cookie'])) { + $curlOptions[CURLOPT_COOKIEJAR] = $config['cookie']; + $curlOptions[CURLOPT_COOKIEFILE] = $config['cookie']; + } + + // User Agent + if (isset($config['user_agent'])) { + $curlOptions[CURLOPT_USERAGENT] = $config['user_agent']; + } + + return $curlOptions; + } + + /** + * Does the actual work of initializing cURL, setting the options, + * and grabbing the output. + * + * @codeCoverageIgnore + */ + protected function sendRequest(array $curlOptions = []): string + { + $ch = curl_init(); + + curl_setopt_array($ch, $curlOptions); + + // Send the request and wait for a response. + $output = curl_exec($ch); + + if ($output === false) { + throw HTTPException::forCurlError((string) curl_errno($ch), curl_error($ch)); + } + + curl_close($ch); + + return $output; + } +} diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php new file mode 100755 index 0000000..a6a2b26 --- /dev/null +++ b/system/HTTP/ContentSecurityPolicy.php @@ -0,0 +1,844 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use Config\App; +use Config\ContentSecurityPolicy as ContentSecurityPolicyConfig; + +/** + * Provides tools for working with the Content-Security-Policy header + * to help defeat XSS attacks. + * + * @see http://www.w3.org/TR/CSP/ + * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + * @see http://content-security-policy.com/ + * @see https://www.owasp.org/index.php/Content_Security_Policy + * @see \CodeIgniter\HTTP\ContentSecurityPolicyTest + */ +class ContentSecurityPolicy +{ + /** + * CSP directives + * + * @var array [name => property] + */ + protected array $directives = [ + 'base-uri' => 'baseURI', + 'child-src' => 'childSrc', + 'connect-src' => 'connectSrc', + 'default-src' => 'defaultSrc', + 'font-src' => 'fontSrc', + 'form-action' => 'formAction', + 'frame-ancestors' => 'frameAncestors', + 'frame-src' => 'frameSrc', + 'img-src' => 'imageSrc', + 'media-src' => 'mediaSrc', + 'object-src' => 'objectSrc', + 'plugin-types' => 'pluginTypes', + 'script-src' => 'scriptSrc', + 'style-src' => 'styleSrc', + 'manifest-src' => 'manifestSrc', + 'sandbox' => 'sandbox', + 'report-uri' => 'reportURI', + ]; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $baseURI = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $childSrc = []; + + /** + * Used for security enforcement + * + * @var array + */ + protected $connectSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $defaultSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $fontSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $formAction = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $frameAncestors = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $frameSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $imageSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $mediaSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $objectSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $pluginTypes = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $scriptSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $styleSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $manifestSrc = []; + + /** + * Used for security enforcement + * + * @var array|string + */ + protected $sandbox = []; + + /** + * A set of endpoints to which csp violation reports will be sent when + * particular behaviors are prevented. + * + * @var string|null + */ + protected $reportURI; + + /** + * Used for security enforcement + * + * @var bool + */ + protected $upgradeInsecureRequests = false; + + /** + * Used for security enforcement + * + * @var bool + */ + protected $reportOnly = false; + + /** + * Used for security enforcement + * + * @var list + */ + protected $validSources = [ + 'self', + 'none', + 'unsafe-inline', + 'unsafe-eval', + ]; + + /** + * Used for security enforcement + * + * @var array + */ + protected $nonces = []; + + /** + * Nonce for style + * + * @var string + */ + protected $styleNonce; + + /** + * Nonce for script + * + * @var string + */ + protected $scriptNonce; + + /** + * Nonce tag for style + * + * @var string + */ + protected $styleNonceTag = '{csp-style-nonce}'; + + /** + * Nonce tag for script + * + * @var string + */ + protected $scriptNonceTag = '{csp-script-nonce}'; + + /** + * Replace nonce tag automatically + * + * @var bool + */ + protected $autoNonce = true; + + /** + * An array of header info since we have + * to build ourselves before passing to Response. + * + * @var array + */ + protected $tempHeaders = []; + + /** + * An array of header info to build + * that should only be reported. + * + * @var array + */ + protected $reportOnlyHeaders = []; + + /** + * Whether Content Security Policy is being enforced. + * + * @var bool + */ + protected $CSPEnabled = false; + + /** + * Constructor. + * + * Stores our default values from the Config file. + */ + public function __construct(ContentSecurityPolicyConfig $config) + { + $appConfig = config(App::class); + $this->CSPEnabled = $appConfig->CSPEnabled; + + foreach (get_object_vars($config) as $setting => $value) { + if (property_exists($this, $setting)) { + $this->{$setting} = $value; + } + } + + if (! is_array($this->styleSrc)) { + $this->styleSrc = [$this->styleSrc]; + } + + if (! is_array($this->scriptSrc)) { + $this->scriptSrc = [$this->scriptSrc]; + } + } + + /** + * Whether Content Security Policy is being enforced. + */ + public function enabled(): bool + { + return $this->CSPEnabled; + } + + /** + * Get the nonce for the style tag. + */ + public function getStyleNonce(): string + { + if ($this->styleNonce === null) { + $this->styleNonce = bin2hex(random_bytes(12)); + $this->styleSrc[] = 'nonce-' . $this->styleNonce; + } + + return $this->styleNonce; + } + + /** + * Get the nonce for the script tag. + */ + public function getScriptNonce(): string + { + if ($this->scriptNonce === null) { + $this->scriptNonce = bin2hex(random_bytes(12)); + $this->scriptSrc[] = 'nonce-' . $this->scriptNonce; + } + + return $this->scriptNonce; + } + + /** + * Compiles and sets the appropriate headers in the request. + * + * Should be called just prior to sending the response to the user agent. + * + * @return void + */ + public function finalize(ResponseInterface $response) + { + if ($this->autoNonce) { + $this->generateNonces($response); + } + + $this->buildHeaders($response); + } + + /** + * If TRUE, nothing will be restricted. Instead all violations will + * be reported to the reportURI for monitoring. This is useful when + * you are just starting to implement the policy, and will help + * determine what errors need to be addressed before you turn on + * all filtering. + * + * @return $this + */ + public function reportOnly(bool $value = true) + { + $this->reportOnly = $value; + + return $this; + } + + /** + * Adds a new baseURI value. Can be either a URI class or a simple string. + * + * baseURI restricts the URLs that can appear in a page's element. + * + * @see http://www.w3.org/TR/CSP/#directive-base-uri + * + * @param array|string $uri + * + * @return $this + */ + public function addBaseURI($uri, ?bool $explicitReporting = null) + { + $this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly); + + return $this; + } + + /** + * Adds a new valid endpoint for a form's action. Can be either + * a URI class or a simple string. + * + * child-src lists the URLs for workers and embedded frame contents. + * For example: child-src https://youtube.com would enable embedding + * videos from YouTube but not from other origins. + * + * @see http://www.w3.org/TR/CSP/#directive-child-src + * + * @param array|string $uri + * + * @return $this + */ + public function addChildSrc($uri, ?bool $explicitReporting = null) + { + $this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly); + + return $this; + } + + /** + * Adds a new valid endpoint for a form's action. Can be either + * a URI class or a simple string. + * + * connect-src limits the origins to which you can connect + * (via XHR, WebSockets, and EventSource). + * + * @see http://www.w3.org/TR/CSP/#directive-connect-src + * + * @param array|string $uri + * + * @return $this + */ + public function addConnectSrc($uri, ?bool $explicitReporting = null) + { + $this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly); + + return $this; + } + + /** + * Adds a new valid endpoint for a form's action. Can be either + * a URI class or a simple string. + * + * default_src is the URI that is used for many of the settings when + * no other source has been set. + * + * @see http://www.w3.org/TR/CSP/#directive-default-src + * + * @param array|string $uri + * + * @return $this + */ + public function setDefaultSrc($uri, ?bool $explicitReporting = null) + { + $this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly]; + + return $this; + } + + /** + * Adds a new valid endpoint for a form's action. Can be either + * a URI class or a simple string. + * + * font-src specifies the origins that can serve web fonts. + * + * @see http://www.w3.org/TR/CSP/#directive-font-src + * + * @param array|string $uri + * + * @return $this + */ + public function addFontSrc($uri, ?bool $explicitReporting = null) + { + $this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly); + + return $this; + } + + /** + * Adds a new valid endpoint for a form's action. Can be either + * a URI class or a simple string. + * + * @see http://www.w3.org/TR/CSP/#directive-form-action + * + * @param array|string $uri + * + * @return $this + */ + public function addFormAction($uri, ?bool $explicitReporting = null) + { + $this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly); + + return $this; + } + + /** + * Adds a new resource that should allow embedding the resource using + * ,