commit 0429395219a209a83e3f40759c8862a8b500a1b0 Author: CHIEFSOFT\ameye Date: Tue Jun 18 14:00:40 2024 -0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a47574c --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +#------------------------- +# Operating Specific Junk Files +#------------------------- + +# OS X +.DS_Store +.AppleDouble +.LSOverride + +apache_log +apache_log/* + + +# OS X Thumbnails +._* + +# Windows image file caches +Thumbs.db +ehthumbs.db +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +#------------------------- +# Environment Files +#------------------------- +# These should never be under version control, +# as it poses a security risk. +.env +.vagrant +Vagrantfile + +#------------------------- +# Temporary Files +#------------------------- +writable/cache/* +!writable/cache/index.html + +writable/logs/* +!writable/logs/index.html + +writable/session/* +!writable/session/index.html + +writable/uploads/* +!writable/uploads/index.html + +writable/debugbar/* +!writable/debugbar/.gitkeep + +php_errors.log + +#------------------------- +# User Guide Temp Files +#------------------------- +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* + +#------------------------- +# Test Files +#------------------------- +tests/coverage* + +# Don't save phpunit under version control. +phpunit + +#------------------------- +# Composer +#------------------------- +vendor/ + +#------------------------- +# IDE / Development Files +#------------------------- + +# Modules Testing +_modules/* + +# phpenv local config +.php-version + +# Jetbrains editors (PHPStorm, etc) +.idea/ +*.iml + +# Netbeans +nbproject/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml +.nb-gradle/ + +# Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project +.phpintel +/api/ + +# Visual Studio Code +.vscode/ + +/results/ +/phpunit*.xml +/.phpunit.*.cache + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..148e7f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 British Columbia Institute of Technology +Copyright (c) 2019-2024 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 100644 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 100644 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 100644 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 100644 index 0000000..76cd926 --- /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 = []; +} diff --git a/app/Config/Boot/development.php b/app/Config/Boot/development.php new file mode 100644 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 100644 index 0000000..47b92f8 --- /dev/null +++ b/app/Config/Constants.php @@ -0,0 +1,94 @@ +|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 ``, `', + srcAction: "iframe_src", + patterns: { + youtube: { + index: "youtube.com", + id: "v=", + src: "//www.youtube.com/embed/%id%?autoplay=1" + }, + vimeo: { + index: "vimeo.com/", + id: "/", + src: "//player.vimeo.com/video/%id%?autoplay=1" + }, + gmaps: { + index: "//maps.google.", + src: "%id%&output=embed" + } + } + }, + proto: { + initIframe: function () { + b.types.push(P), w("BeforeChange", function (a, b, c) { + b !== c && (b === P ? R() : c === P && R(!0)) + }), w(h + "." + P, function () { + R() + }) + }, + getIframe: function (c, d) { + var e = c.src, + f = b.st.iframe; + a.each(f.patterns, function () { + return e.indexOf(this.index) > -1 ? (this.id && (e = "string" == typeof this.id ? e.substr(e.lastIndexOf(this.id) + this.id.length, e.length) : this.id.call(this, e)), e = this.src.replace("%id%", e), !1) : void 0 + }); + var g = {}; + return f.srcAction && (g[f.srcAction] = e), b._parseMarkup(d, g, c), b.updateStatus("ready"), d + } + } + }); + var S = function (a) { + var c = b.items.length; + return a > c - 1 ? a - c : 0 > a ? c + a : a + }, + T = function (a, b, c) { + return a.replace(/%curr%/gi, b + 1).replace(/%total%/gi, c) + }; + a.magnificPopup.registerModule("gallery", { + options: { + enabled: !1, + arrowMarkup: '', + preload: [0, 2], + navigateByImgClick: !0, + arrows: !0, + tPrev: "Previous (Left arrow key)", + tNext: "Next (Right arrow key)", + tCounter: "%curr% of %total%" + }, + proto: { + initGallery: function () { + var c = b.st.gallery, + e = ".mfp-gallery"; + return b.direction = !0, c && c.enabled ? (f += " mfp-gallery", w(m + e, function () { + c.navigateByImgClick && b.wrap.on("click" + e, ".mfp-img", function () { + return b.items.length > 1 ? (b.next(), !1) : void 0 + }), d.on("keydown" + e, function (a) { + 37 === a.keyCode ? b.prev() : 39 === a.keyCode && b.next() + }) + }), w("UpdateStatus" + e, function (a, c) { + c.text && (c.text = T(c.text, b.currItem.index, b.items.length)) + }), w(l + e, function (a, d, e, f) { + var g = b.items.length; + e.counter = g > 1 ? T(c.tCounter, f.index, g) : "" + }), w("BuildControls" + e, function () { + if (b.items.length > 1 && c.arrows && !b.arrowLeft) { + var d = c.arrowMarkup, + e = b.arrowLeft = a(d.replace(/%title%/gi, c.tPrev).replace(/%dir%/gi, "left")).addClass(s), + f = b.arrowRight = a(d.replace(/%title%/gi, c.tNext).replace(/%dir%/gi, "right")).addClass(s); + e.click(function () { + b.prev() + }), f.click(function () { + b.next() + }), b.container.append(e.add(f)) + } + }), w(n + e, function () { + b._preloadTimeout && clearTimeout(b._preloadTimeout), b._preloadTimeout = setTimeout(function () { + b.preloadNearbyImages(), b._preloadTimeout = null + }, 16) + }), void w(h + e, function () { + d.off(e), b.wrap.off("click" + e), b.arrowRight = b.arrowLeft = null + })) : !1 + }, + next: function () { + b.direction = !0, b.index = S(b.index + 1), b.updateItemHTML() + }, + prev: function () { + b.direction = !1, b.index = S(b.index - 1), b.updateItemHTML() + }, + goTo: function (a) { + b.direction = a >= b.index, b.index = a, b.updateItemHTML() + }, + preloadNearbyImages: function () { + var a, c = b.st.gallery.preload, + d = Math.min(c[0], b.items.length), + e = Math.min(c[1], b.items.length); + for (a = 1; a <= (b.direction ? e : d); a++) b._preloadItem(b.index + a); + for (a = 1; a <= (b.direction ? d : e); a++) b._preloadItem(b.index - a) + }, + _preloadItem: function (c) { + if (c = S(c), !b.items[c].preloaded) { + var d = b.items[c]; + d.parsed || (d = b.parseEl(c)), y("LazyLoad", d), "image" === d.type && (d.img = a('').on("load.mfploader", function () { + d.hasSize = !0 + }).on("error.mfploader", function () { + d.hasSize = !0, d.loadError = !0, y("LazyLoadError", d) + }).attr("src", d.src)), d.preloaded = !0 + } + } + } + }); + var U = "retina"; + a.magnificPopup.registerModule(U, { + options: { + replaceSrc: function (a) { + return a.src.replace(/\.\w+$/, function (a) { + return "@2x" + a + }) + }, + ratio: 1 + }, + proto: { + initRetina: function () { + if (window.devicePixelRatio > 1) { + var a = b.st.retina, + c = a.ratio; + c = isNaN(c) ? c() : c, c > 1 && (w("ImageHasSize." + U, function (a, b) { + b.img.css({ + "max-width": b.img[0].naturalWidth / c, + width: "100%" + }) + }), w("ElementParse." + U, function (b, d) { + d.src = a.replaceSrc(d, c) + })) + } + } + } + }), A() +}); \ No newline at end of file diff --git a/public/assets/js/main.js b/public/assets/js/main.js new file mode 100644 index 0000000..b4330b6 --- /dev/null +++ b/public/assets/js/main.js @@ -0,0 +1,743 @@ +(function ($) { + "user strict"; + // Preloader Js + $(window).on('load', function () { + $("[data-paroller-factor]").paroller(); + $('.preloader').fadeOut(1000); + var img = $('.bg_img'); + img.css('background-image', function () { + var bg = ('url(' + $(this).data('background') + ')'); + return bg; + }); + }); + $(document).ready(function () { + // Nice Select + $('.select-bar').niceSelect(); + // PoPuP + $('.popup').magnificPopup({ + disableOn: 700, + type: 'iframe', + mainClass: 'mfp-fade', + removalDelay: 160, + preloader: true, + fixedContentPos: false, + disableOn: 300 + }); + $("body").each(function () { + $(this).find(".img-pop").magnificPopup({ + type: "image", + gallery: { + enabled: true + } + }); + }); + // aos js active + new WOW().init() + //Faq + $('.faq-wrapper .faq-title').on('click', function (e) { + var element = $(this).parent('.faq-item'); + if (element.hasClass('open')) { + element.removeClass('open'); + element.find('.faq-content').removeClass('open'); + element.find('.faq-content').slideUp(300, "swing"); + } else { + element.addClass('open'); + element.children('.faq-content').slideDown(300, "swing"); + element.siblings('.faq-item').children('.faq-content').slideUp(300, "swing"); + element.siblings('.faq-item').removeClass('open'); + element.siblings('.faq-item').find('.faq-title').removeClass('open'); + element.siblings('.faq-item').find('.faq-content').slideUp(300, "swing"); + } + }); + //Faq + $('.faq--area .faq-title').on('click', function (e) { + var element = $(this).parent('.faq--item'); + if (element.hasClass('open')) { + element.removeClass('open'); + element.find('.faq-content').removeClass('open'); + element.find('.faq-content').slideUp(300, "swing"); + } else { + element.addClass('open'); + element.children('.faq-content').slideDown(300, "swing"); + element.siblings('.faq--item').children('.faq-content').slideUp(300, "swing"); + element.siblings('.faq--item').removeClass('open'); + element.siblings('.faq--item').find('.faq-title').removeClass('open'); + element.siblings('.faq--item').find('.faq-content').slideUp(300, "swing"); + } + }); + //Menu Dropdown Icon Adding + $("ul>li>.submenu").parent("li").addClass("menu-item-has-children"); + // drop down menu width overflow problem fix + $('.submenu').parent('li').hover(function () { + var menu = $(this).find("ul"); + var menupos = $(menu).offset(); + if (menupos.left + menu.width() > $(window).width()) { + var newpos = -$(menu).width(); + menu.css({ + left: newpos + }); + } + }); + $('.menu li a').on('click', function (e) { + var element = $(this).parent('li'); + if (element.hasClass('open')) { + element.removeClass('open'); + element.find('li').removeClass('open'); + element.find('ul').slideUp(300, "swing"); + } else { + element.addClass('open'); + element.children('ul').slideDown(300, "swing"); + element.siblings('li').children('ul').slideUp(300, "swing"); + element.siblings('li').removeClass('open'); + element.siblings('li').find('li').removeClass('open'); + element.siblings('li').find('ul').slideUp(300, "swing"); + } + }) + //Widget Slider + $('.widget-slider').owlCarousel({ + loop:true, + nav:false, + dots: false, + items:1, + autoplay:true, + autoplayTimeout:2500, + autoplayHoverPause:true, + margin: 30, + }); + var owlTutu = $('.widget-slider'); + owlTutu.owlCarousel(); + // Go to the next item + $('.widget-next').on('click', function() { + owlTutu.trigger('next.owl.carousel'); + }) + // Go to the previous item + $('.widget-prev').on('click', function() { + owlTutu.trigger('prev.owl.carousel', [300]); + }) + // Scroll To Top + var scrollTop = $(".scrollToTop"); + $(window).on('scroll', function () { + if ($(this).scrollTop() < 500) { + scrollTop.removeClass("active"); + } else { + scrollTop.addClass("active"); + } + }); + //Click event to scroll to top + $('.scrollToTop').on('click', function () { + $('html, body').animate({ + scrollTop: 0 + }, 500); + return false; + }); + //Header Bar + $('.header-bar').on('click', function () { + $(this).toggleClass('active'); + $('.overlay').toggleClass('active'); + $('.menu').toggleClass('active'); + }) + $('.overlay').on('click', function () { + $(this).removeClass('active'); + $('.header-bar').removeClass('active'); + $('.menu').removeClass('active'); + }) + // Header Sticky Herevar prevScrollpos = window.pageYOffset; + var scrollPosition = window.scrollY; + if (scrollPosition >= 1) { + $(".header-bottom").addClass('active'); + $(".header-section-2").removeClass('plan-header'); + } + var header = $(".header-section"); + $(window).on('scroll', function () { + if ($(this).scrollTop() < 1) { + header.removeClass("active"); + } else { + header.addClass("active"); + } + }); + $('.tab ul.tab-menu li').on('click', function (g) { + var tab = $(this).closest('.tab'), + index = $(this).closest('li').index(); + tab.find('li').siblings('li').removeClass('active'); + $(this).closest('li').addClass('active'); + tab.find('.tab-area').find('div.tab-item').not('div.tab-item:eq(' + index + ')').hide(10); + tab.find('.tab-area').find('div.tab-item:eq(' + index + ')').fadeIn(10); + g.preventDefault(); + }); + $('.tab-up ul.tab-menu li').on('click', function (g) { + var tabT = $(this).closest('.tab-up'), + indexT = $(this).closest('li').index(); + tabT.find('li').siblings('li').removeClass('active'); + $(this).closest('li').addClass('active'); + tabT.find('.tab-area').find('div.tab-item').not('div.tab-item:eq(' + indexT + ')').slideUp(400); + tabT.find('.tab-area').find('div.tab-item:eq(' + indexT + ')').slideDown(400); + g.preventDefault(); + }); + // counter + $('.counter').countUp({ + 'time': 1500, + 'delay': 10 + }); + $('.social-icons li a').on('mouseover', function(e) { + var social = $(this).parent('li'); + if(social.children('a').hasClass('active')) { + social.siblings('li').children('a').removeClass('active'); + $(this).addClass('active'); + } else { + social.siblings('li').children('a').removeClass('active'); + $(this).addClass('active'); + } + }); + //Widget Slider + $('.testimonial-slider').owlCarousel({ + loop:true, + nav:false, + dots: false, + items:1, + autoplay:true, + autoplayTimeout:2500, + autoplayHoverPause:true, + margin: 0, + mouseDrag: false, + touchDrag: true, + }); + var owlBela = $('.testimonial-slider'); + owlBela.owlCarousel(); + // Go to the next item + $('.testi-next').on('click', function() { + owlBela.trigger('prev.owl.carousel'); + }) + // Go to the previous item + $('.testi-prev').on('click', function() { + owlBela.trigger('next.owl.carousel', [300]); + }) + //Widget Slider + $('.mobile-slider-16').owlCarousel({ + loop:true, + nav:false, + dots: true, + items:1, + autoplay:true, + autoplayTimeout: 2500, + autoplayHoverPause:false, + margin: 0, + mouseDrag: false, + touchDrag: false, + }); + //Widget Slider + $('.mobile-slider').owlCarousel({ + loop:true, + nav:false, + dots: false, + items:1, + autoplay:true, + autoplayTimeout: 4000, + autoplayHoverPause:false, + margin: 0, + mouseDrag: false, + touchDrag: false, + }); + var owlC = $('.mobile-slider'); + owlC.owlCarousel(); + // Go to the next item + $('.cola-next').on('click', function() { + owlC.trigger('next.owl.carousel'); + }) + // Go to the previous item + $('.cola-prev').on('click', function() { + owlC.trigger('prev.owl.carousel', [300]); + }) + //Widget Slider + $('.colaboration-slider').owlCarousel({ + loop:true, + nav:false, + dots: false, + items:1, + autoplay:true, + autoplayTimeout: 4000, + autoplayHoverPause:false, + margin: 0, + mouseDrag: false, + touchDrag: false, + }); + var owlF = $('.colaboration-slider'); + owlF.owlCarousel(); + // Go to the next item + $('.cola-next').on('click', function() { + owlF.trigger('next.owl.carousel'); + }) + // Go to the previous item + $('.cola-prev').on('click', function() { + owlF.trigger('prev.owl.carousel', [300]); + }) + //Widget Slider + $('.banner-4-slider').owlCarousel({ + loop:true, + nav:false, + dots: false, + items:1, + autoplay:true, + autoplayTimeout:4000, + autoplayHoverPause:false, + margin: 0, + mouseDrag: false, + touchDrag: true, + }); + var owlD = $('.banner-4-slider'); + owlD.owlCarousel(); + // Go to the next item + $('.ban-next').on('click', function() { + owlD.trigger('next.owl.carousel'); + }) + // Go to the previous item + $('.ban-prev').on('click', function() { + owlD.trigger('prev.owl.carousel', [300]); + }) + //Widget Slider + $('.banner-1-slider').owlCarousel({ + loop:true, + nav:false, + dots: false, + items:1, + autoplay:false, + margin: 0, + mouseDrag: false, + touchDrag: false, + animateOut: 'fadeOut', + animateIn: 'fadeIn', + // animateOut: 'slideOutUp', + // animateIn: 'slideInDown', + }); + var owlE = $('.banner-1-slider'); + owlE.owlCarousel(); + // Go to the next item + $('.ban-click').on('click', function() { + owlE.trigger('next.owl.carousel'); + }) + //Range Slider + $( function() { + $( "#usd-range" ).slider({ + range: "min", + value: 250, + min: 1, + max: 500, + slide: function( event, ui ) { + $( "#usd-amount" ).val( ui.value + " Users" ); + } + }); + $( "#usd-amount" ).val( $( "#usd-range" ).slider( "value" ) + " Users"); + } ); + //Sponsor Slider + $('.sponsor-slider').owlCarousel({ + loop: true, + margin: 0, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:2, + }, + 480:{ + items:3, + }, + 768:{ + items:4, + } + } + }) + $('.sponsor-slider-two').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:2, + }, + 480:{ + items:3, + }, + 768:{ + items:5, + }, + 992:{ + items:3, + }, + 1200:{ + items:4, + }, + } + }) + $('.sponsor-slider-3').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:2, + }, + 480:{ + items:3, + }, + 768:{ + items:4, + }, + 992:{ + items:5, + }, + 1200:{ + items:6, + }, + } + }) + $('.sponsor-slider-4').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:2, + }, + 480:{ + items:3, + }, + 768:{ + items:5, + }, + 992:{ + items:6, + }, + 1200:{ + items:7, + }, + } + }) + $('.logo-slider').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:2, + }, + 480:{ + items:3, + }, + 768:{ + items:4, + }, + 992:{ + items:5, + }, + 1200:{ + items:6, + }, + } + }) + $('.client-slider').owlCarousel({ + loop: true, + margin: 0, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:1, + }, + 500:{ + items:2, + }, + 992:{ + items:3, + } + } + }) + $('.history-slider').owlCarousel({ + loop: true, + margin: 0, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + center: true, + responsive:{ + 0:{ + items:1, + }, + 767:{ + items:3, + }, + 1199:{ + items:5, + }, + } + }) + $('.tool-slider').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: true, + navText: ["",""], + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:1, + }, + 500:{ + items:2, + }, + 768:{ + items:3, + }, + 992:{ + items:2, + } + } + }) + //feature-item-2-slider + $('.feature-item-2-slider').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:1, + }, + 768:{ + items:2, + }, + 1200:{ + items:3, + } + } + }) + //Pricing SLider + $('.pricing-slider').owlCarousel({ + loop: true, + margin: 0, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:1, + }, + 768:{ + items:2, + }, + 992:{ + items:3, + }, + 1200:{ + items:4, + } + } + }) + if ($('.feat-slider').length) { + $('.feat-slider').owlCarousel({ + center: true, + items: 1, + // autoplay: true, + // autoplayTimeout: 3000, + loop: true, + margin: 0, + singleItem: true, + nav: false, + dots: false, + thumbs: true, + mouseDrag: false, + touchDrag: true, + thumbsPrerendered: true, + animateOut: 'fadeOut', + animateIn: 'fadeIn', + }); + } + var owlFeat = $('.feat-slider, .app-screenshot-slider-20, .feature-content-slider-20'); + // Go to the next item + $('.feat-prev').on('click', function() { + owlFeat.trigger('prev.owl.carousel'); + }) + // Go to the previous item + $('.feat-next').on('click', function() { + owlFeat.trigger('next.owl.carousel', [300]); + }) + if ($('.work-slider').length) { + $('.work-slider').owlCarousel({ + center: true, + items: 1, + // autoplay: true, + // autoplayTimeout: 2500, + loop: false, + margin: 0, + singleItem: true, + nav: true, + dots: false, + thumbs: true, + mouseDrag: false, + touchDrag: true, + thumbsPrerendered: true, + // animateOut: 'fadeOut', + // animateIn: 'fadeIn', + }); + } + + // JS 4.0.1 Starts + $('.screenshot-slider, .screenshot-slider-3').owlCarousel({ + items: 1, + loop: false, + margin: 30, + singleItem: true, + nav: false, + dots: false, + }); + var owlSrn = $('.screenshot-slider, .screenshot-slider-3'); + // Go to the next item + $('.app-prev').on('click', function() { + owlSrn.trigger('prev.owl.carousel'); + }) + // Go to the previous item + $('.app-next').on('click', function() { + owlSrn.trigger('next.owl.carousel', [300]); + }) + $('.screenshot-slider-2').owlCarousel({ + items: 1, + loop: false, + margin: 30, + singleItem: true, + nav: false, + dots: true, + dotsContainer: '.dots-2' + }); + $('.app-screenshot-slider-20, .how-slider-20').owlCarousel({ + items: 1, + loop: false, + margin: 30, + singleItem: true, + nav: false, + dots: true, + dotsContainer: '.dots-2', + animateOut: 'fadeOut', + animateIn: 'fadeIn', + mouseDrag: false, + }); + $('.client-slider-2, .feature-content-slider-20').owlCarousel({ + items: 1, + loop: false, + margin: 30, + singleItem: true, + nav: false, + dots: false, + }); + $('.amazing-slider').owlCarousel({ + loop: true, + margin: 30, + responsiveClass: true, + nav: false, + dots: false, + loop: true, + autoplay: true, + autoplayTimeout: 2000, + autoplayHoverPause: true, + responsive:{ + 0:{ + items:1, + }, + 450:{ + items:2, + }, + 992:{ + items:3, + }, + 1200:{ + items:3, + margin: 70, + } + } + }) + + $('.up--down--overflow').prev('*').addClass('pb-lg-200'); + $('.up--down--overflow').next('*').addClass('pt-lg-200'); + $('.up--down--overflow').addClass('pt-lg-0 pb-lg-0'); + + // Current Year + $(".currentYear").text(new Date().getFullYear()); + + // Navbar Auto Active Class + var curUrl = $(location).attr('href'); + var terSegments = curUrl.split("/"); + var desired_segment = terSegments[terSegments.length - 1]; + var removeGarbage = desired_segment.split(".html")[0] + ".html"; + var checkLink = $('.menu a[href="' + removeGarbage + '"]'); + var targetClass = checkLink.addClass('active'); + targetClass.parents('.menu-item-has-children').addClass('active-parents'); + $('.active-parents > a').addClass('active'); + + // JS 4.0.1 Ends + $('.chorka').on('click', function(){ + $('.swap-area').toggleClass('active'); + }) + + $('.client-item-16 .client-thumb').on('mouseover', function (e) { + var element = $(this).parent('.client-item-16'); + if (element.hasClass('open')) { + element.removeClass('open'); + element.removeClass('active'); + } else { + element.siblings('.client-item-16').find('.client-content').removeClass('active'); + element.siblings('.client-item-16').removeClass('active'); + element.children('.client-content').addClass('active'); + element.addClass('active'); + } + }) + }); + })(jQuery); + \ No newline at end of file diff --git a/public/assets/js/map.js b/public/assets/js/map.js new file mode 100644 index 0000000..eb5b542 --- /dev/null +++ b/public/assets/js/map.js @@ -0,0 +1,87 @@ +var styleArray = [{ + "featureType": "administrative", + "elementType": "labels.text.fill", + "stylers": [{ + "color": "#001b83" + }] +}, +{ + "featureType": "landscape", + "elementType": "all", + "stylers": [{ + "color": "#f0f8ff" + }] +}, +{ + "featureType": "poi", + "elementType": "all", + "stylers": [{ + "visibility": "off" + }] +}, +{ + "featureType": "road", + "elementType": "all", + "stylers": [{ + "saturation": -100 + }, + { + "lightness": 45 + } + ] +}, +{ + "featureType": "road.highway", + "elementType": "all", + "stylers": [{ + "visibility": "simplified" + }] +}, +{ + "featureType": "road.arterial", + "elementType": "labels.icon", + "stylers": [{ + "visibility": "off" + }] +}, +{ + "featureType": "transit", + "elementType": "all", + "stylers": [{ + "visibility": "off" + }] +}, +{ + "featureType": "water", + "elementType": "all", + "stylers": [{ + "color": "#4c5fa8" + }, + { + "visibility": "on" + } + ] +} +] + +var mapOptions = { +center: new google.maps.LatLng(23.874936, 90.385821), +zoom: 5, +styles: styleArray, +scrollwheel: false, +backgroundColor: '#001b83', +mapTypeControl: false, +mapTypeId: google.maps.MapTypeId.ROADMAP +}; + +var map = new google.maps.Map(document.getElementsByClassName("maps")[0], +mapOptions); +var myLatlng = new google.maps.LatLng(23.874936, 90.385821); +var focusplace = {lat: 55.864237, lng: -4.251806}; +var marker = new google.maps.Marker({ +position: myLatlng, +map: map, +icon: { + url: "assets/images/map-marker.png" +} +}) \ No newline at end of file diff --git a/public/assets/js/minified.js b/public/assets/js/minified.js new file mode 100644 index 0000000..da6e9d7 --- /dev/null +++ b/public/assets/js/minified.js @@ -0,0 +1 @@ +!function(t,e){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=t.document?e(t,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return e(t)}:e(t)}("undefined"!=typeof window?window:this,function(t,e){"use strict";var i=[],n=t.document,s=Object.getPrototypeOf,o=i.slice,r=i.concat,a=i.push,l=i.indexOf,h={},c=h.toString,u=h.hasOwnProperty,d=u.toString,p=d.call(Object),f={},g=function(t){return"function"==typeof t&&"number"!=typeof t.nodeType},m=function(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function _(t,e,i){var s,o=(e=e||n).createElement("script");if(o.text=t,i)for(s in v)i[s]&&(o[s]=i[s]);e.head.appendChild(o).parentNode.removeChild(o)}function y(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?h[c.call(t)]||"object":typeof t}var b=function(t,e){return new b.fn.init(t,e)},w=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function x(t){var e=!!t&&"length"in t&&t.length,i=y(t);return!g(t)&&!m(t)&&("array"===i||0===e||"number"==typeof e&&e>0&&e-1 in t)}b.fn=b.prototype={jquery:"3.3.1",constructor:b,length:0,toArray:function(){return o.call(this)},get:function(t){return null==t?o.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var e=b.merge(this.constructor(),t);return e.prevObject=this,e},each:function(t){return b.each(this,t)},map:function(t){return this.pushStack(b.map(this,function(e,i){return t.call(e,i,e)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(t){var e=this.length,i=+t+(t<0?e:0);return this.pushStack(i>=0&&i+~]|"+z+")"+z+"*"),$=new RegExp("="+z+"*([^\\]'\"]*?)"+z+"*\\]","g"),U=new RegExp(j),Y=new RegExp("^"+L+"$"),K={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+j),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+z+"*(even|odd|(([+-]|)(\\d*)n|)"+z+"*(?:([+-]|)"+z+"*(\\d+)|))"+z+"*\\)|)","i"),bool:new RegExp("^(?:"+H+")$","i"),needsContext:new RegExp("^"+z+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+z+"*((?:-\\d)?\\d*)"+z+"*\\)|)(?=[^-]|$)","i")},V=/^(?:input|select|textarea|button)$/i,Q=/^h\d$/i,X=/^[^{]+\{\s*\[native \w/,G=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/[+~]/,J=new RegExp("\\\\([\\da-f]{1,6}"+z+"?|("+z+")|.)","ig"),tt=function(t,e,i){var n="0x"+e-65536;return n!=n||i?e:n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320)},et=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,it=function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t},nt=function(){d()},st=_t(function(t){return!0===t.disabled&&("form"in t||"label"in t)},{dir:"parentNode",next:"legend"});try{O.apply(I=N.call(w.childNodes),w.childNodes),I[w.childNodes.length].nodeType}catch(t){O={apply:I.length?function(t,e){P.apply(t,N.call(e))}:function(t,e){for(var i=t.length,n=0;t[i++]=e[n++];);t.length=i-1}}}function ot(t,e,n,s){var o,a,h,c,u,f,v,_=e&&e.ownerDocument,x=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==x&&9!==x&&11!==x)return n;if(!s&&((e?e.ownerDocument||e:w)!==p&&d(e),e=e||p,g)){if(11!==x&&(u=G.exec(t)))if(o=u[1]){if(9===x){if(!(h=e.getElementById(o)))return n;if(h.id===o)return n.push(h),n}else if(_&&(h=_.getElementById(o))&&y(e,h)&&h.id===o)return n.push(h),n}else{if(u[2])return O.apply(n,e.getElementsByTagName(t)),n;if((o=u[3])&&i.getElementsByClassName&&e.getElementsByClassName)return O.apply(n,e.getElementsByClassName(o)),n}if(i.qsa&&!D[t+" "]&&(!m||!m.test(t))){if(1!==x)_=e,v=t;else if("object"!==e.nodeName.toLowerCase()){for((c=e.getAttribute("id"))?c=c.replace(et,it):e.setAttribute("id",c=b),a=(f=r(t)).length;a--;)f[a]="#"+c+" "+vt(f[a]);v=f.join(","),_=Z.test(t)&>(e.parentNode)||e}if(v)try{return O.apply(n,_.querySelectorAll(v)),n}catch(t){}finally{c===b&&e.removeAttribute("id")}}}return l(t.replace(F,"$1"),e,n,s)}function rt(){var t=[];return function e(i,s){return t.push(i+" ")>n.cacheLength&&delete e[t.shift()],e[i+" "]=s}}function at(t){return t[b]=!0,t}function lt(t){var e=p.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function ht(t,e){for(var i=t.split("|"),s=i.length;s--;)n.attrHandle[i[s]]=e}function ct(t,e){var i=e&&t,n=i&&1===t.nodeType&&1===e.nodeType&&t.sourceIndex-e.sourceIndex;if(n)return n;if(i)for(;i=i.nextSibling;)if(i===e)return-1;return t?1:-1}function ut(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function dt(t){return function(e){var i=e.nodeName.toLowerCase();return("input"===i||"button"===i)&&e.type===t}}function pt(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&st(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ft(t){return at(function(e){return e=+e,at(function(i,n){for(var s,o=t([],i.length,e),r=o.length;r--;)i[s=o[r]]&&(i[s]=!(n[s]=i[s]))})})}function gt(t){return t&&void 0!==t.getElementsByTagName&&t}for(e in i=ot.support={},o=ot.isXML=function(t){var e=t&&(t.ownerDocument||t).documentElement;return!!e&&"HTML"!==e.nodeName},d=ot.setDocument=function(t){var e,s,r=t?t.ownerDocument||t:w;return r!==p&&9===r.nodeType&&r.documentElement?(f=(p=r).documentElement,g=!o(p),w!==p&&(s=p.defaultView)&&s.top!==s&&(s.addEventListener?s.addEventListener("unload",nt,!1):s.attachEvent&&s.attachEvent("onunload",nt)),i.attributes=lt(function(t){return t.className="i",!t.getAttribute("className")}),i.getElementsByTagName=lt(function(t){return t.appendChild(p.createComment("")),!t.getElementsByTagName("*").length}),i.getElementsByClassName=X.test(p.getElementsByClassName),i.getById=lt(function(t){return f.appendChild(t).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),i.getById?(n.filter.ID=function(t){var e=t.replace(J,tt);return function(t){return t.getAttribute("id")===e}},n.find.ID=function(t,e){if(void 0!==e.getElementById&&g){var i=e.getElementById(t);return i?[i]:[]}}):(n.filter.ID=function(t){var e=t.replace(J,tt);return function(t){var i=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return i&&i.value===e}},n.find.ID=function(t,e){if(void 0!==e.getElementById&&g){var i,n,s,o=e.getElementById(t);if(o){if((i=o.getAttributeNode("id"))&&i.value===t)return[o];for(s=e.getElementsByName(t),n=0;o=s[n++];)if((i=o.getAttributeNode("id"))&&i.value===t)return[o]}return[]}}),n.find.TAG=i.getElementsByTagName?function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):i.qsa?e.querySelectorAll(t):void 0}:function(t,e){var i,n=[],s=0,o=e.getElementsByTagName(t);if("*"===t){for(;i=o[s++];)1===i.nodeType&&n.push(i);return n}return o},n.find.CLASS=i.getElementsByClassName&&function(t,e){if(void 0!==e.getElementsByClassName&&g)return e.getElementsByClassName(t)},v=[],m=[],(i.qsa=X.test(p.querySelectorAll))&&(lt(function(t){f.appendChild(t).innerHTML="",t.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+z+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||m.push("\\["+z+"*(?:value|"+H+")"),t.querySelectorAll("[id~="+b+"-]").length||m.push("~="),t.querySelectorAll(":checked").length||m.push(":checked"),t.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]")}),lt(function(t){t.innerHTML="";var e=p.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&m.push("name"+z+"*[*^$|!~]?="),2!==t.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),f.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),m.push(",.*:")})),(i.matchesSelector=X.test(_=f.matches||f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&<(function(t){i.disconnectedMatch=_.call(t,"*"),_.call(t,"[s!='']:x"),v.push("!=",j)}),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),e=X.test(f.compareDocumentPosition),y=e||X.test(f.contains)?function(t,e){var i=9===t.nodeType?t.documentElement:t,n=e&&e.parentNode;return t===n||!(!n||1!==n.nodeType||!(i.contains?i.contains(n):t.compareDocumentPosition&&16&t.compareDocumentPosition(n)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},S=e?function(t,e){if(t===e)return u=!0,0;var n=!t.compareDocumentPosition-!e.compareDocumentPosition;return n||(1&(n=(t.ownerDocument||t)===(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!i.sortDetached&&e.compareDocumentPosition(t)===n?t===p||t.ownerDocument===w&&y(w,t)?-1:e===p||e.ownerDocument===w&&y(w,e)?1:c?M(c,t)-M(c,e):0:4&n?-1:1)}:function(t,e){if(t===e)return u=!0,0;var i,n=0,s=t.parentNode,o=e.parentNode,r=[t],a=[e];if(!s||!o)return t===p?-1:e===p?1:s?-1:o?1:c?M(c,t)-M(c,e):0;if(s===o)return ct(t,e);for(i=t;i=i.parentNode;)r.unshift(i);for(i=e;i=i.parentNode;)a.unshift(i);for(;r[n]===a[n];)n++;return n?ct(r[n],a[n]):r[n]===w?-1:a[n]===w?1:0},p):p},ot.matches=function(t,e){return ot(t,null,null,e)},ot.matchesSelector=function(t,e){if((t.ownerDocument||t)!==p&&d(t),e=e.replace($,"='$1']"),i.matchesSelector&&g&&!D[e+" "]&&(!v||!v.test(e))&&(!m||!m.test(e)))try{var n=_.call(t,e);if(n||i.disconnectedMatch||t.document&&11!==t.document.nodeType)return n}catch(t){}return ot(e,p,null,[t]).length>0},ot.contains=function(t,e){return(t.ownerDocument||t)!==p&&d(t),y(t,e)},ot.attr=function(t,e){(t.ownerDocument||t)!==p&&d(t);var s=n.attrHandle[e.toLowerCase()],o=s&&E.call(n.attrHandle,e.toLowerCase())?s(t,e,!g):void 0;return void 0!==o?o:i.attributes||!g?t.getAttribute(e):(o=t.getAttributeNode(e))&&o.specified?o.value:null},ot.escape=function(t){return(t+"").replace(et,it)},ot.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},ot.uniqueSort=function(t){var e,n=[],s=0,o=0;if(u=!i.detectDuplicates,c=!i.sortStable&&t.slice(0),t.sort(S),u){for(;e=t[o++];)e===t[o]&&(s=n.push(o));for(;s--;)t.splice(n[s],1)}return c=null,t},s=ot.getText=function(t){var e,i="",n=0,o=t.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)i+=s(t)}else if(3===o||4===o)return t.nodeValue}else for(;e=t[n++];)i+=s(e);return i},(n=ot.selectors={cacheLength:50,createPseudo:at,match:K,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(J,tt),t[3]=(t[3]||t[4]||t[5]||"").replace(J,tt),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||ot.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&ot.error(t[0]),t},PSEUDO:function(t){var e,i=!t[6]&&t[2];return K.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":i&&U.test(i)&&(e=r(i,!0))&&(e=i.indexOf(")",i.length-e)-i.length)&&(t[0]=t[0].slice(0,e),t[2]=i.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(J,tt).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=k[t+" "];return e||(e=new RegExp("(^|"+z+")"+t+"("+z+"|$)"))&&k(t,function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")})},ATTR:function(t,e,i){return function(n){var s=ot.attr(n,t);return null==s?"!="===e:!e||(s+="","="===e?s===i:"!="===e?s!==i:"^="===e?i&&0===s.indexOf(i):"*="===e?i&&s.indexOf(i)>-1:"$="===e?i&&s.slice(-i.length)===i:"~="===e?(" "+s.replace(R," ")+" ").indexOf(i)>-1:"|="===e&&(s===i||s.slice(0,i.length+1)===i+"-"))}},CHILD:function(t,e,i,n,s){var o="nth"!==t.slice(0,3),r="last"!==t.slice(-4),a="of-type"===e;return 1===n&&0===s?function(t){return!!t.parentNode}:function(e,i,l){var h,c,u,d,p,f,g=o!==r?"nextSibling":"previousSibling",m=e.parentNode,v=a&&e.nodeName.toLowerCase(),_=!l&&!a,y=!1;if(m){if(o){for(;g;){for(d=e;d=d[g];)if(a?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;f=g="only"===t&&!f&&"nextSibling"}return!0}if(f=[r?m.firstChild:m.lastChild],r&&_){for(y=(p=(h=(c=(u=(d=m)[b]||(d[b]={}))[d.uniqueID]||(u[d.uniqueID]={}))[t]||[])[0]===x&&h[1])&&h[2],d=p&&m.childNodes[p];d=++p&&d&&d[g]||(y=p=0)||f.pop();)if(1===d.nodeType&&++y&&d===e){c[t]=[x,p,y];break}}else if(_&&(y=p=(h=(c=(u=(d=e)[b]||(d[b]={}))[d.uniqueID]||(u[d.uniqueID]={}))[t]||[])[0]===x&&h[1]),!1===y)for(;(d=++p&&d&&d[g]||(y=p=0)||f.pop())&&((a?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++y||(_&&((c=(u=d[b]||(d[b]={}))[d.uniqueID]||(u[d.uniqueID]={}))[t]=[x,y]),d!==e)););return(y-=s)===n||y%n==0&&y/n>=0}}},PSEUDO:function(t,e){var i,s=n.pseudos[t]||n.setFilters[t.toLowerCase()]||ot.error("unsupported pseudo: "+t);return s[b]?s(e):s.length>1?(i=[t,t,"",e],n.setFilters.hasOwnProperty(t.toLowerCase())?at(function(t,i){for(var n,o=s(t,e),r=o.length;r--;)t[n=M(t,o[r])]=!(i[n]=o[r])}):function(t){return s(t,0,i)}):s}},pseudos:{not:at(function(t){var e=[],i=[],n=a(t.replace(F,"$1"));return n[b]?at(function(t,e,i,s){for(var o,r=n(t,null,s,[]),a=t.length;a--;)(o=r[a])&&(t[a]=!(e[a]=o))}):function(t,s,o){return e[0]=t,n(e,null,o,i),e[0]=null,!i.pop()}}),has:at(function(t){return function(e){return ot(t,e).length>0}}),contains:at(function(t){return t=t.replace(J,tt),function(e){return(e.textContent||e.innerText||s(e)).indexOf(t)>-1}}),lang:at(function(t){return Y.test(t||"")||ot.error("unsupported lang: "+t),t=t.replace(J,tt).toLowerCase(),function(e){var i;do{if(i=g?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(i=i.toLowerCase())===t||0===i.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var i=t.location&&t.location.hash;return i&&i.slice(1)===e.id},root:function(t){return t===f},focus:function(t){return t===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:pt(!1),disabled:pt(!0),checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!n.pseudos.empty(t)},header:function(t){return Q.test(t.nodeName)},input:function(t){return V.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){var e;return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:ft(function(){return[0]}),last:ft(function(t,e){return[e-1]}),eq:ft(function(t,e,i){return[i<0?i+e:i]}),even:ft(function(t,e){for(var i=0;i=0;)t.push(n);return t}),gt:ft(function(t,e,i){for(var n=i<0?i+e:i;++n1?function(e,i,n){for(var s=t.length;s--;)if(!t[s](e,i,n))return!1;return!0}:t[0]}function bt(t,e,i,n,s){for(var o,r=[],a=0,l=t.length,h=null!=e;a-1&&(o[h]=!(r[h]=u))}}else v=bt(v===r?v.splice(f,v.length):v),s?s(null,r,v,l):O.apply(r,v)})}function xt(t){for(var e,i,s,o=t.length,r=n.relative[t[0].type],a=r||n.relative[" "],l=r?1:0,c=_t(function(t){return t===e},a,!0),u=_t(function(t){return M(e,t)>-1},a,!0),d=[function(t,i,n){var s=!r&&(n||i!==h)||((e=i).nodeType?c(t,i,n):u(t,i,n));return e=null,s}];l1&&yt(d),l>1&&vt(t.slice(0,l-1).concat({value:" "===t[l-2].type?"*":""})).replace(F,"$1"),i,l0,s=t.length>0,o=function(o,r,a,l,c){var u,f,m,v=0,_="0",y=o&&[],b=[],w=h,C=o||s&&n.find.TAG("*",c),k=x+=null==w?1:Math.random()||.1,T=C.length;for(c&&(h=r===p||r||c);_!==T&&null!=(u=C[_]);_++){if(s&&u){for(f=0,r||u.ownerDocument===p||(d(u),a=!g);m=t[f++];)if(m(u,r||p,a)){l.push(u);break}c&&(x=k)}i&&((u=!m&&u)&&v--,o&&y.push(u))}if(v+=_,i&&_!==v){for(f=0;m=e[f++];)m(y,b,r,a);if(o){if(v>0)for(;_--;)y[_]||b[_]||(b[_]=A.call(l));b=bt(b)}O.apply(l,b),c&&!o&&b.length>0&&v+e.length>1&&ot.uniqueSort(l)}return c&&(x=k,h=w),y};return i?at(o):o}return mt.prototype=n.filters=n.pseudos,n.setFilters=new mt,r=ot.tokenize=function(t,e){var i,s,o,r,a,l,h,c=T[t+" "];if(c)return e?0:c.slice(0);for(a=t,l=[],h=n.preFilter;a;){for(r in i&&!(s=q.exec(a))||(s&&(a=a.slice(s[0].length)||a),l.push(o=[])),i=!1,(s=B.exec(a))&&(i=s.shift(),o.push({value:i,type:s[0].replace(F," ")}),a=a.slice(i.length)),n.filter)!(s=K[r].exec(a))||h[r]&&!(s=h[r](s))||(i=s.shift(),o.push({value:i,type:r,matches:s}),a=a.slice(i.length));if(!i)break}return e?a.length:a?ot.error(t):T(t,l).slice(0)},a=ot.compile=function(t,e){var i,n=[],s=[],o=D[t+" "];if(!o){for(e||(e=r(t)),i=e.length;i--;)(o=xt(e[i]))[b]?n.push(o):s.push(o);(o=D(t,Ct(s,n))).selector=t}return o},l=ot.select=function(t,e,i,s){var o,l,h,c,u,d="function"==typeof t&&t,p=!s&&r(t=d.selector||t);if(i=i||[],1===p.length){if((l=p[0]=p[0].slice(0)).length>2&&"ID"===(h=l[0]).type&&9===e.nodeType&&g&&n.relative[l[1].type]){if(!(e=(n.find.ID(h.matches[0].replace(J,tt),e)||[])[0]))return i;d&&(e=e.parentNode),t=t.slice(l.shift().value.length)}for(o=K.needsContext.test(t)?0:l.length;o--&&(h=l[o],!n.relative[c=h.type]);)if((u=n.find[c])&&(s=u(h.matches[0].replace(J,tt),Z.test(l[0].type)&>(e.parentNode)||e))){if(l.splice(o,1),!(t=s.length&&vt(l)))return O.apply(i,s),i;break}}return(d||a(t,p))(s,e,!g,i,!e||Z.test(t)&>(e.parentNode)||e),i},i.sortStable=b.split("").sort(S).join("")===b,i.detectDuplicates=!!u,d(),i.sortDetached=lt(function(t){return 1&t.compareDocumentPosition(p.createElement("fieldset"))}),lt(function(t){return t.innerHTML="","#"===t.firstChild.getAttribute("href")})||ht("type|href|height|width",function(t,e,i){if(!i)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)}),i.attributes&<(function(t){return t.innerHTML="",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")})||ht("value",function(t,e,i){if(!i&&"input"===t.nodeName.toLowerCase())return t.defaultValue}),lt(function(t){return null==t.getAttribute("disabled")})||ht(H,function(t,e,i){var n;if(!i)return!0===t[e]?e.toLowerCase():(n=t.getAttributeNode(e))&&n.specified?n.value:null}),ot}(t);b.find=C,b.expr=C.selectors,b.expr[":"]=b.expr.pseudos,b.uniqueSort=b.unique=C.uniqueSort,b.text=C.getText,b.isXMLDoc=C.isXML,b.contains=C.contains,b.escapeSelector=C.escape;var k=function(t,e,i){for(var n=[],s=void 0!==i;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(s&&b(t).is(i))break;n.push(t)}return n},T=function(t,e){for(var i=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&i.push(t);return i},D=b.expr.match.needsContext;function S(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}var E=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function I(t,e,i){return g(e)?b.grep(t,function(t,n){return!!e.call(t,n,t)!==i}):e.nodeType?b.grep(t,function(t){return t===e!==i}):"string"!=typeof e?b.grep(t,function(t){return l.call(e,t)>-1!==i}):b.filter(e,t,i)}b.filter=function(t,e,i){var n=e[0];return i&&(t=":not("+t+")"),1===e.length&&1===n.nodeType?b.find.matchesSelector(n,t)?[n]:[]:b.find.matches(t,b.grep(e,function(t){return 1===t.nodeType}))},b.fn.extend({find:function(t){var e,i,n=this.length,s=this;if("string"!=typeof t)return this.pushStack(b(t).filter(function(){for(e=0;e1?b.uniqueSort(i):i},filter:function(t){return this.pushStack(I(this,t||[],!1))},not:function(t){return this.pushStack(I(this,t||[],!0))},is:function(t){return!!I(this,"string"==typeof t&&D.test(t)?b(t):t||[],!1).length}});var A,P=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(b.fn.init=function(t,e,i){var s,o;if(!t)return this;if(i=i||A,"string"==typeof t){if(!(s="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:P.exec(t))||!s[1]&&e)return!e||e.jquery?(e||i).find(t):this.constructor(e).find(t);if(s[1]){if(e=e instanceof b?e[0]:e,b.merge(this,b.parseHTML(s[1],e&&e.nodeType?e.ownerDocument||e:n,!0)),E.test(s[1])&&b.isPlainObject(e))for(s in e)g(this[s])?this[s](e[s]):this.attr(s,e[s]);return this}return(o=n.getElementById(s[2]))&&(this[0]=o,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):g(t)?void 0!==i.ready?i.ready(t):t(b):b.makeArray(t,this)}).prototype=b.fn,A=b(n);var O=/^(?:parents|prev(?:Until|All))/,N={children:!0,contents:!0,next:!0,prev:!0};function M(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}b.fn.extend({has:function(t){var e=b(t,this),i=e.length;return this.filter(function(){for(var t=0;t-1:1===i.nodeType&&b.find.matchesSelector(i,t))){o.push(i);break}return this.pushStack(o.length>1?b.uniqueSort(o):o)},index:function(t){return t?"string"==typeof t?l.call(b(t),this[0]):l.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(b.uniqueSort(b.merge(this.get(),b(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),b.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return k(t,"parentNode")},parentsUntil:function(t,e,i){return k(t,"parentNode",i)},next:function(t){return M(t,"nextSibling")},prev:function(t){return M(t,"previousSibling")},nextAll:function(t){return k(t,"nextSibling")},prevAll:function(t){return k(t,"previousSibling")},nextUntil:function(t,e,i){return k(t,"nextSibling",i)},prevUntil:function(t,e,i){return k(t,"previousSibling",i)},siblings:function(t){return T((t.parentNode||{}).firstChild,t)},children:function(t){return T(t.firstChild)},contents:function(t){return S(t,"iframe")?t.contentDocument:(S(t,"template")&&(t=t.content||t),b.merge([],t.childNodes))}},function(t,e){b.fn[t]=function(i,n){var s=b.map(this,e,i);return"Until"!==t.slice(-5)&&(n=i),n&&"string"==typeof n&&(s=b.filter(n,s)),this.length>1&&(N[t]||b.uniqueSort(s),O.test(t)&&s.reverse()),this.pushStack(s)}});var H=/[^\x20\t\r\n\f]+/g;function z(t){return t}function L(t){throw t}function W(t,e,i,n){var s;try{t&&g(s=t.promise)?s.call(t).done(e).fail(i):t&&g(s=t.then)?s.call(t,e,i):e.apply(void 0,[t].slice(n))}catch(t){i.apply(void 0,[t])}}b.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return b.each(t.match(H)||[],function(t,i){e[i]=!0}),e}(t):b.extend({},t);var e,i,n,s,o=[],r=[],a=-1,l=function(){for(s=s||t.once,n=e=!0;r.length;a=-1)for(i=r.shift();++a-1;)o.splice(i,1),i<=a&&a--}),this},has:function(t){return t?b.inArray(t,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return s=r=[],o=i="",this},disabled:function(){return!o},lock:function(){return s=r=[],i||e||(o=i=""),this},locked:function(){return!!s},fireWith:function(t,i){return s||(i=[t,(i=i||[]).slice?i.slice():i],r.push(i),e||l()),this},fire:function(){return h.fireWith(this,arguments),this},fired:function(){return!!n}};return h},b.extend({Deferred:function(e){var i=[["notify","progress",b.Callbacks("memory"),b.Callbacks("memory"),2],["resolve","done",b.Callbacks("once memory"),b.Callbacks("once memory"),0,"resolved"],["reject","fail",b.Callbacks("once memory"),b.Callbacks("once memory"),1,"rejected"]],n="pending",s={state:function(){return n},always:function(){return o.done(arguments).fail(arguments),this},catch:function(t){return s.then(null,t)},pipe:function(){var t=arguments;return b.Deferred(function(e){b.each(i,function(i,n){var s=g(t[n[4]])&&t[n[4]];o[n[1]](function(){var t=s&&s.apply(this,arguments);t&&g(t.promise)?t.promise().progress(e.notify).done(e.resolve).fail(e.reject):e[n[0]+"With"](this,s?[t]:arguments)})}),t=null}).promise()},then:function(e,n,s){var o=0;function r(e,i,n,s){return function(){var a=this,l=arguments,h=function(){var t,h;if(!(e=o&&(n!==L&&(a=void 0,l=[t]),i.rejectWith(a,l))}};e?c():(b.Deferred.getStackHook&&(c.stackTrace=b.Deferred.getStackHook()),t.setTimeout(c))}}return b.Deferred(function(t){i[0][3].add(r(0,t,g(s)?s:z,t.notifyWith)),i[1][3].add(r(0,t,g(e)?e:z)),i[2][3].add(r(0,t,g(n)?n:L))}).promise()},promise:function(t){return null!=t?b.extend(t,s):s}},o={};return b.each(i,function(t,e){var r=e[2],a=e[5];s[e[1]]=r.add,a&&r.add(function(){n=a},i[3-t][2].disable,i[3-t][3].disable,i[0][2].lock,i[0][3].lock),r.add(e[3].fire),o[e[0]]=function(){return o[e[0]+"With"](this===o?void 0:this,arguments),this},o[e[0]+"With"]=r.fireWith}),s.promise(o),e&&e.call(o,o),o},when:function(t){var e=arguments.length,i=e,n=Array(i),s=o.call(arguments),r=b.Deferred(),a=function(t){return function(i){n[t]=this,s[t]=arguments.length>1?o.call(arguments):i,--e||r.resolveWith(n,s)}};if(e<=1&&(W(t,r.done(a(i)).resolve,r.reject,!e),"pending"===r.state()||g(s[i]&&s[i].then)))return r.then();for(;i--;)W(s[i],a(i),r.reject);return r.promise()}});var j=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;b.Deferred.exceptionHook=function(e,i){t.console&&t.console.warn&&e&&j.test(e.name)&&t.console.warn("jQuery.Deferred exception: "+e.message,e.stack,i)},b.readyException=function(e){t.setTimeout(function(){throw e})};var R=b.Deferred();function F(){n.removeEventListener("DOMContentLoaded",F),t.removeEventListener("load",F),b.ready()}b.fn.ready=function(t){return R.then(t).catch(function(t){b.readyException(t)}),this},b.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--b.readyWait:b.isReady)||(b.isReady=!0,!0!==t&&--b.readyWait>0||R.resolveWith(n,[b]))}}),b.ready.then=R.then,"complete"===n.readyState||"loading"!==n.readyState&&!n.documentElement.doScroll?t.setTimeout(b.ready):(n.addEventListener("DOMContentLoaded",F),t.addEventListener("load",F));var q=function(t,e,i,n,s,o,r){var a=0,l=t.length,h=null==i;if("object"===y(i))for(a in s=!0,i)q(t,e,a,i[a],!0,o,r);else if(void 0!==n&&(s=!0,g(n)||(r=!0),h&&(r?(e.call(t,n),e=null):(h=e,e=function(t,e,i){return h.call(b(t),i)})),e))for(;a1,null,!0)},removeData:function(t){return this.each(function(){X.remove(this,t)})}}),b.extend({queue:function(t,e,i){var n;if(t)return e=(e||"fx")+"queue",n=Q.get(t,e),i&&(!n||Array.isArray(i)?n=Q.access(t,e,b.makeArray(i)):n.push(i)),n||[]},dequeue:function(t,e){e=e||"fx";var i=b.queue(t,e),n=i.length,s=i.shift(),o=b._queueHooks(t,e);"inprogress"===s&&(s=i.shift(),n--),s&&("fx"===e&&i.unshift("inprogress"),delete o.stop,s.call(t,function(){b.dequeue(t,e)},o)),!n&&o&&o.empty.fire()},_queueHooks:function(t,e){var i=e+"queueHooks";return Q.get(t,i)||Q.access(t,i,{empty:b.Callbacks("once memory").add(function(){Q.remove(t,[e+"queue",i])})})}}),b.fn.extend({queue:function(t,e){var i=2;return"string"!=typeof t&&(e=t,t="fx",i--),arguments.length\x20\t\r\n\f]+)/i,ut=/^$|^module$|\/(?:java|ecma)script/i,dt={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function pt(t,e){var i;return i=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&S(t,e)?b.merge([t],i):i}function ft(t,e){for(var i=0,n=t.length;i-1)s&&s.push(o);else if(h=b.contains(o.ownerDocument,o),r=pt(u.appendChild(o),"script"),h&&ft(r),i)for(c=0;o=r[c++];)ut.test(o.type||"")&&i.push(o);return u}!function(){var t=n.createDocumentFragment().appendChild(n.createElement("div")),e=n.createElement("input");e.setAttribute("type","radio"),e.setAttribute("checked","checked"),e.setAttribute("name","t"),t.appendChild(e),f.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",f.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var vt=n.documentElement,_t=/^key/,yt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,bt=/^([^.]*)(?:\.(.+)|)/;function wt(){return!0}function xt(){return!1}function Ct(){try{return n.activeElement}catch(t){}}function kt(t,e,i,n,s,o){var r,a;if("object"==typeof e){for(a in"string"!=typeof i&&(n=n||i,i=void 0),e)kt(t,a,i,n,e[a],o);return t}if(null==n&&null==s?(s=i,n=i=void 0):null==s&&("string"==typeof i?(s=n,n=void 0):(s=n,n=i,i=void 0)),!1===s)s=xt;else if(!s)return t;return 1===o&&(r=s,(s=function(t){return b().off(t),r.apply(this,arguments)}).guid=r.guid||(r.guid=b.guid++)),t.each(function(){b.event.add(this,e,s,n,i)})}b.event={global:{},add:function(t,e,i,n,s){var o,r,a,l,h,c,u,d,p,f,g,m=Q.get(t);if(m)for(i.handler&&(i=(o=i).handler,s=o.selector),s&&b.find.matchesSelector(vt,s),i.guid||(i.guid=b.guid++),(l=m.events)||(l=m.events={}),(r=m.handle)||(r=m.handle=function(e){return void 0!==b&&b.event.triggered!==e.type?b.event.dispatch.apply(t,arguments):void 0}),h=(e=(e||"").match(H)||[""]).length;h--;)p=g=(a=bt.exec(e[h])||[])[1],f=(a[2]||"").split(".").sort(),p&&(u=b.event.special[p]||{},p=(s?u.delegateType:u.bindType)||p,u=b.event.special[p]||{},c=b.extend({type:p,origType:g,data:n,handler:i,guid:i.guid,selector:s,needsContext:s&&b.expr.match.needsContext.test(s),namespace:f.join(".")},o),(d=l[p])||((d=l[p]=[]).delegateCount=0,u.setup&&!1!==u.setup.call(t,n,f,r)||t.addEventListener&&t.addEventListener(p,r)),u.add&&(u.add.call(t,c),c.handler.guid||(c.handler.guid=i.guid)),s?d.splice(d.delegateCount++,0,c):d.push(c),b.event.global[p]=!0)},remove:function(t,e,i,n,s){var o,r,a,l,h,c,u,d,p,f,g,m=Q.hasData(t)&&Q.get(t);if(m&&(l=m.events)){for(h=(e=(e||"").match(H)||[""]).length;h--;)if(p=g=(a=bt.exec(e[h])||[])[1],f=(a[2]||"").split(".").sort(),p){for(u=b.event.special[p]||{},d=l[p=(n?u.delegateType:u.bindType)||p]||[],a=a[2]&&new RegExp("(^|\\.)"+f.join("\\.(?:.*\\.|)")+"(\\.|$)"),r=o=d.length;o--;)c=d[o],!s&&g!==c.origType||i&&i.guid!==c.guid||a&&!a.test(c.namespace)||n&&n!==c.selector&&("**"!==n||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,u.remove&&u.remove.call(t,c));r&&!d.length&&(u.teardown&&!1!==u.teardown.call(t,f,m.handle)||b.removeEvent(t,p,m.handle),delete l[p])}else for(p in l)b.event.remove(t,p+e[h],i,n,!0);b.isEmptyObject(l)&&Q.remove(t,"handle events")}},dispatch:function(t){var e,i,n,s,o,r,a=b.event.fix(t),l=new Array(arguments.length),h=(Q.get(this,"events")||{})[a.type]||[],c=b.event.special[a.type]||{};for(l[0]=a,e=1;e=1))for(;h!==this;h=h.parentNode||this)if(1===h.nodeType&&("click"!==t.type||!0!==h.disabled)){for(o=[],r={},i=0;i-1:b.find(s,this,null,[h]).length),r[s]&&o.push(n);o.length&&a.push({elem:h,handlers:o})}return h=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,Dt=/\s*$/g;function It(t,e){return S(t,"table")&&S(11!==e.nodeType?e:e.firstChild,"tr")&&b(t).children("tbody")[0]||t}function At(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function Pt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function Ot(t,e){var i,n,s,o,r,a,l,h;if(1===e.nodeType){if(Q.hasData(t)&&(o=Q.access(t),r=Q.set(e,o),h=o.events))for(s in delete r.handle,r.events={},h)for(i=0,n=h[s].length;i1&&"string"==typeof m&&!f.checkClone&&St.test(m))return t.each(function(s){var o=t.eq(s);v&&(e[0]=m.call(this,s,o.html())),Mt(o,e,i,n)});if(d&&(o=(s=mt(e,t[0].ownerDocument,!1,t,n)).firstChild,1===s.childNodes.length&&(s=o),o||n)){for(l=(a=b.map(pt(s,"script"),At)).length;u")},clone:function(t,e,i){var n,s,o,r,a=t.cloneNode(!0),l=b.contains(t.ownerDocument,t);if(!(f.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||b.isXMLDoc(t)))for(r=pt(a),n=0,s=(o=pt(t)).length;n0&&ft(r,!l&&pt(t,"script")),a},cleanData:function(t){for(var e,i,n,s=b.event.special,o=0;void 0!==(i=t[o]);o++)if(K(i)){if(e=i[Q.expando]){if(e.events)for(n in e.events)s[n]?b.event.remove(i,n):b.removeEvent(i,n,e.handle);i[Q.expando]=void 0}i[X.expando]&&(i[X.expando]=void 0)}}}),b.fn.extend({detach:function(t){return Ht(this,t,!0)},remove:function(t){return Ht(this,t)},text:function(t){return q(this,function(t){return void 0===t?b.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return Mt(this,arguments,function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||It(this,t).appendChild(t)})},prepend:function(){return Mt(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=It(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return Mt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return Mt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(b.cleanData(pt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return b.clone(this,t,e)})},html:function(t){return q(this,function(t){var e=this[0]||{},i=0,n=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!Dt.test(t)&&!dt[(ct.exec(t)||["",""])[1].toLowerCase()]){t=b.htmlPrefilter(t);try{for(;i=0&&(l+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-o-l-a-.5))),l}function Xt(t,e,i){var n=Lt(t),s=jt(t,e,n),o="border-box"===b.css(t,"boxSizing",!1,n),r=o;if(zt.test(s)){if(!i)return s;s="auto"}return r=r&&(f.boxSizingReliable()||s===t.style[e]),("auto"===s||!parseFloat(s)&&"inline"===b.css(t,"display",!1,n))&&(s=t["offset"+e[0].toUpperCase()+e.slice(1)],r=!0),(s=parseFloat(s)||0)+Qt(t,e,i||(o?"border":"content"),r,n,s)+"px"}function Gt(t,e,i,n,s){return new Gt.prototype.init(t,e,i,n,s)}b.extend({cssHooks:{opacity:{get:function(t,e){if(e){var i=jt(t,"opacity");return""===i?"1":i}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,e,i,n){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var s,o,r,a=Y(e),l=qt.test(e),h=t.style;if(l||(e=Kt(a)),r=b.cssHooks[e]||b.cssHooks[a],void 0===i)return r&&"get"in r&&void 0!==(s=r.get(t,!1,n))?s:h[e];"string"==(o=typeof i)&&(s=et.exec(i))&&s[1]&&(i=ot(t,e,s),o="number"),null!=i&&i==i&&("number"===o&&(i+=s&&s[3]||(b.cssNumber[a]?"":"px")),f.clearCloneStyle||""!==i||0!==e.indexOf("background")||(h[e]="inherit"),r&&"set"in r&&void 0===(i=r.set(t,i,n))||(l?h.setProperty(e,i):h[e]=i))}},css:function(t,e,i,n){var s,o,r,a=Y(e);return qt.test(e)||(e=Kt(a)),(r=b.cssHooks[e]||b.cssHooks[a])&&"get"in r&&(s=r.get(t,!0,i)),void 0===s&&(s=jt(t,e,n)),"normal"===s&&e in $t&&(s=$t[e]),""===i||i?(o=parseFloat(s),!0===i||isFinite(o)?o||0:s):s}}),b.each(["height","width"],function(t,e){b.cssHooks[e]={get:function(t,i,n){if(i)return!Ft.test(b.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?Xt(t,e,n):st(t,Bt,function(){return Xt(t,e,n)})},set:function(t,i,n){var s,o=Lt(t),r="border-box"===b.css(t,"boxSizing",!1,o),a=n&&Qt(t,e,n,r,o);return r&&f.scrollboxSize()===o.position&&(a-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(o[e])-Qt(t,e,"border",!1,o)-.5)),a&&(s=et.exec(i))&&"px"!==(s[3]||"px")&&(t.style[e]=i,i=b.css(t,e)),Vt(0,i,a)}}}),b.cssHooks.marginLeft=Rt(f.reliableMarginLeft,function(t,e){if(e)return(parseFloat(jt(t,"marginLeft"))||t.getBoundingClientRect().left-st(t,{marginLeft:0},function(){return t.getBoundingClientRect().left}))+"px"}),b.each({margin:"",padding:"",border:"Width"},function(t,e){b.cssHooks[t+e]={expand:function(i){for(var n=0,s={},o="string"==typeof i?i.split(" "):[i];n<4;n++)s[t+it[n]+e]=o[n]||o[n-2]||o[0];return s}},"margin"!==t&&(b.cssHooks[t+e].set=Vt)}),b.fn.extend({css:function(t,e){return q(this,function(t,e,i){var n,s,o={},r=0;if(Array.isArray(e)){for(n=Lt(t),s=e.length;r1)}}),b.Tween=Gt,Gt.prototype={constructor:Gt,init:function(t,e,i,n,s,o){this.elem=t,this.prop=i,this.easing=s||b.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=n,this.unit=o||(b.cssNumber[i]?"":"px")},cur:function(){var t=Gt.propHooks[this.prop];return t&&t.get?t.get(this):Gt.propHooks._default.get(this)},run:function(t){var e,i=Gt.propHooks[this.prop];return this.options.duration?this.pos=e=b.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),i&&i.set?i.set(this):Gt.propHooks._default.set(this),this}},Gt.prototype.init.prototype=Gt.prototype,Gt.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=b.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){b.fx.step[t.prop]?b.fx.step[t.prop](t):1!==t.elem.nodeType||null==t.elem.style[b.cssProps[t.prop]]&&!b.cssHooks[t.prop]?t.elem[t.prop]=t.now:b.style(t.elem,t.prop,t.now+t.unit)}}},Gt.propHooks.scrollTop=Gt.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},b.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},b.fx=Gt.prototype.init,b.fx.step={};var Zt,Jt,te=/^(?:toggle|show|hide)$/,ee=/queueHooks$/;function ie(){Jt&&(!1===n.hidden&&t.requestAnimationFrame?t.requestAnimationFrame(ie):t.setTimeout(ie,b.fx.interval),b.fx.tick())}function ne(){return t.setTimeout(function(){Zt=void 0}),Zt=Date.now()}function se(t,e){var i,n=0,s={height:t};for(e=e?1:0;n<4;n+=2-e)s["margin"+(i=it[n])]=s["padding"+i]=t;return e&&(s.opacity=s.width=t),s}function oe(t,e,i){for(var n,s=(re.tweeners[e]||[]).concat(re.tweeners["*"]),o=0,r=s.length;o1)},removeAttr:function(t){return this.each(function(){b.removeAttr(this,t)})}}),b.extend({attr:function(t,e,i){var n,s,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===t.getAttribute?b.prop(t,e,i):(1===o&&b.isXMLDoc(t)||(s=b.attrHooks[e.toLowerCase()]||(b.expr.match.bool.test(e)?ae:void 0)),void 0!==i?null===i?void b.removeAttr(t,e):s&&"set"in s&&void 0!==(n=s.set(t,i,e))?n:(t.setAttribute(e,i+""),i):s&&"get"in s&&null!==(n=s.get(t,e))?n:null==(n=b.find.attr(t,e))?void 0:n)},attrHooks:{type:{set:function(t,e){if(!f.radioValue&&"radio"===e&&S(t,"input")){var i=t.value;return t.setAttribute("type",e),i&&(t.value=i),e}}}},removeAttr:function(t,e){var i,n=0,s=e&&e.match(H);if(s&&1===t.nodeType)for(;i=s[n++];)t.removeAttribute(i)}}),ae={set:function(t,e,i){return!1===e?b.removeAttr(t,i):t.setAttribute(i,i),i}},b.each(b.expr.match.bool.source.match(/\w+/g),function(t,e){var i=le[e]||b.find.attr;le[e]=function(t,e,n){var s,o,r=e.toLowerCase();return n||(o=le[r],le[r]=s,s=null!=i(t,e,n)?r:null,le[r]=o),s}});var he=/^(?:input|select|textarea|button)$/i,ce=/^(?:a|area)$/i;function ue(t){return(t.match(H)||[]).join(" ")}function de(t){return t.getAttribute&&t.getAttribute("class")||""}function pe(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(H)||[]}b.fn.extend({prop:function(t,e){return q(this,b.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[b.propFix[t]||t]})}}),b.extend({prop:function(t,e,i){var n,s,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&b.isXMLDoc(t)||(e=b.propFix[e]||e,s=b.propHooks[e]),void 0!==i?s&&"set"in s&&void 0!==(n=s.set(t,i,e))?n:t[e]=i:s&&"get"in s&&null!==(n=s.get(t,e))?n:t[e]},propHooks:{tabIndex:{get:function(t){var e=b.find.attr(t,"tabindex");return e?parseInt(e,10):he.test(t.nodeName)||ce.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),f.optSelected||(b.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),b.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){b.propFix[this.toLowerCase()]=this}),b.fn.extend({addClass:function(t){var e,i,n,s,o,r,a,l=0;if(g(t))return this.each(function(e){b(this).addClass(t.call(this,e,de(this)))});if((e=pe(t)).length)for(;i=this[l++];)if(s=de(i),n=1===i.nodeType&&" "+ue(s)+" "){for(r=0;o=e[r++];)n.indexOf(" "+o+" ")<0&&(n+=o+" ");s!==(a=ue(n))&&i.setAttribute("class",a)}return this},removeClass:function(t){var e,i,n,s,o,r,a,l=0;if(g(t))return this.each(function(e){b(this).removeClass(t.call(this,e,de(this)))});if(!arguments.length)return this.attr("class","");if((e=pe(t)).length)for(;i=this[l++];)if(s=de(i),n=1===i.nodeType&&" "+ue(s)+" "){for(r=0;o=e[r++];)for(;n.indexOf(" "+o+" ")>-1;)n=n.replace(" "+o+" "," ");s!==(a=ue(n))&&i.setAttribute("class",a)}return this},toggleClass:function(t,e){var i=typeof t,n="string"===i||Array.isArray(t);return"boolean"==typeof e&&n?e?this.addClass(t):this.removeClass(t):g(t)?this.each(function(i){b(this).toggleClass(t.call(this,i,de(this),e),e)}):this.each(function(){var e,s,o,r;if(n)for(s=0,o=b(this),r=pe(t);e=r[s++];)o.hasClass(e)?o.removeClass(e):o.addClass(e);else void 0!==t&&"boolean"!==i||((e=de(this))&&Q.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":Q.get(this,"__className__")||""))})},hasClass:function(t){var e,i,n=0;for(e=" "+t+" ";i=this[n++];)if(1===i.nodeType&&(" "+ue(de(i))+" ").indexOf(e)>-1)return!0;return!1}});var fe=/\r/g;b.fn.extend({val:function(t){var e,i,n,s=this[0];return arguments.length?(n=g(t),this.each(function(i){var s;1===this.nodeType&&(null==(s=n?t.call(this,i,b(this).val()):t)?s="":"number"==typeof s?s+="":Array.isArray(s)&&(s=b.map(s,function(t){return null==t?"":t+""})),(e=b.valHooks[this.type]||b.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,s,"value")||(this.value=s))})):s?(e=b.valHooks[s.type]||b.valHooks[s.nodeName.toLowerCase()])&&"get"in e&&void 0!==(i=e.get(s,"value"))?i:"string"==typeof(i=s.value)?i.replace(fe,""):null==i?"":i:void 0}}),b.extend({valHooks:{option:{get:function(t){var e=b.find.attr(t,"value");return null!=e?e:ue(b.text(t))}},select:{get:function(t){var e,i,n,s=t.options,o=t.selectedIndex,r="select-one"===t.type,a=r?null:[],l=r?o+1:s.length;for(n=o<0?l:r?o:0;n-1)&&(i=!0);return i||(t.selectedIndex=-1),o}}}}),b.each(["radio","checkbox"],function(){b.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=b.inArray(b(t).val(),e)>-1}},f.checkOn||(b.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}),f.focusin="onfocusin"in t;var ge=/^(?:focusinfocus|focusoutblur)$/,me=function(t){t.stopPropagation()};b.extend(b.event,{trigger:function(e,i,s,o){var r,a,l,h,c,d,p,f,v=[s||n],_=u.call(e,"type")?e.type:e,y=u.call(e,"namespace")?e.namespace.split("."):[];if(a=f=l=s=s||n,3!==s.nodeType&&8!==s.nodeType&&!ge.test(_+b.event.triggered)&&(_.indexOf(".")>-1&&(_=(y=_.split(".")).shift(),y.sort()),c=_.indexOf(":")<0&&"on"+_,(e=e[b.expando]?e:new b.Event(_,"object"==typeof e&&e)).isTrigger=o?2:3,e.namespace=y.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+y.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=s),i=null==i?[e]:b.makeArray(i,[e]),p=b.event.special[_]||{},o||!p.trigger||!1!==p.trigger.apply(s,i))){if(!o&&!p.noBubble&&!m(s)){for(h=p.delegateType||_,ge.test(h+_)||(a=a.parentNode);a;a=a.parentNode)v.push(a),l=a;l===(s.ownerDocument||n)&&v.push(l.defaultView||l.parentWindow||t)}for(r=0;(a=v[r++])&&!e.isPropagationStopped();)f=a,e.type=r>1?h:p.bindType||_,(d=(Q.get(a,"events")||{})[e.type]&&Q.get(a,"handle"))&&d.apply(a,i),(d=c&&a[c])&&d.apply&&K(a)&&(e.result=d.apply(a,i),!1===e.result&&e.preventDefault());return e.type=_,o||e.isDefaultPrevented()||p._default&&!1!==p._default.apply(v.pop(),i)||!K(s)||c&&g(s[_])&&!m(s)&&((l=s[c])&&(s[c]=null),b.event.triggered=_,e.isPropagationStopped()&&f.addEventListener(_,me),s[_](),e.isPropagationStopped()&&f.removeEventListener(_,me),b.event.triggered=void 0,l&&(s[c]=l)),e.result}},simulate:function(t,e,i){var n=b.extend(new b.Event,i,{type:t,isSimulated:!0});b.event.trigger(n,null,e)}}),b.fn.extend({trigger:function(t,e){return this.each(function(){b.event.trigger(t,e,this)})},triggerHandler:function(t,e){var i=this[0];if(i)return b.event.trigger(t,e,i,!0)}}),f.focusin||b.each({focus:"focusin",blur:"focusout"},function(t,e){var i=function(t){b.event.simulate(e,t.target,b.event.fix(t))};b.event.special[e]={setup:function(){var n=this.ownerDocument||this,s=Q.access(n,e);s||n.addEventListener(t,i,!0),Q.access(n,e,(s||0)+1)},teardown:function(){var n=this.ownerDocument||this,s=Q.access(n,e)-1;s?Q.access(n,e,s):(n.removeEventListener(t,i,!0),Q.remove(n,e))}}});var ve=t.location,_e=Date.now(),ye=/\?/;b.parseXML=function(e){var i;if(!e||"string"!=typeof e)return null;try{i=(new t.DOMParser).parseFromString(e,"text/xml")}catch(t){i=void 0}return i&&!i.getElementsByTagName("parsererror").length||b.error("Invalid XML: "+e),i};var be=/\[\]$/,we=/\r?\n/g,xe=/^(?:submit|button|image|reset|file)$/i,Ce=/^(?:input|select|textarea|keygen)/i;function ke(t,e,i,n){var s;if(Array.isArray(e))b.each(e,function(e,s){i||be.test(t)?n(t,s):ke(t+"["+("object"==typeof s&&null!=s?e:"")+"]",s,i,n)});else if(i||"object"!==y(e))n(t,e);else for(s in e)ke(t+"["+s+"]",e[s],i,n)}b.param=function(t,e){var i,n=[],s=function(t,e){var i=g(e)?e():e;n[n.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==i?"":i)};if(Array.isArray(t)||t.jquery&&!b.isPlainObject(t))b.each(t,function(){s(this.name,this.value)});else for(i in t)ke(i,t[i],e,s);return n.join("&")},b.fn.extend({serialize:function(){return b.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=b.prop(this,"elements");return t?b.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!b(this).is(":disabled")&&Ce.test(this.nodeName)&&!xe.test(t)&&(this.checked||!ht.test(t))}).map(function(t,e){var i=b(this).val();return null==i?null:Array.isArray(i)?b.map(i,function(t){return{name:e.name,value:t.replace(we,"\r\n")}}):{name:e.name,value:i.replace(we,"\r\n")}}).get()}});var Te=/%20/g,De=/#.*$/,Se=/([?&])_=[^&]*/,Ee=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ie=/^(?:GET|HEAD)$/,Ae=/^\/\//,Pe={},Oe={},Ne="*/".concat("*"),Me=n.createElement("a");function He(t){return function(e,i){"string"!=typeof e&&(i=e,e="*");var n,s=0,o=e.toLowerCase().match(H)||[];if(g(i))for(;n=o[s++];)"+"===n[0]?(n=n.slice(1)||"*",(t[n]=t[n]||[]).unshift(i)):(t[n]=t[n]||[]).push(i)}}function ze(t,e,i,n){var s={},o=t===Oe;function r(a){var l;return s[a]=!0,b.each(t[a]||[],function(t,a){var h=a(e,i,n);return"string"!=typeof h||o||s[h]?o?!(l=h):void 0:(e.dataTypes.unshift(h),r(h),!1)}),l}return r(e.dataTypes[0])||!s["*"]&&r("*")}function Le(t,e){var i,n,s=b.ajaxSettings.flatOptions||{};for(i in e)void 0!==e[i]&&((s[i]?t:n||(n={}))[i]=e[i]);return n&&b.extend(!0,t,n),t}Me.href=ve.href,b.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:ve.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(ve.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ne,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":b.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Le(Le(t,b.ajaxSettings),e):Le(b.ajaxSettings,t)},ajaxPrefilter:He(Pe),ajaxTransport:He(Oe),ajax:function(e,i){"object"==typeof e&&(i=e,e=void 0),i=i||{};var s,o,r,a,l,h,c,u,d,p,f=b.ajaxSetup({},i),g=f.context||f,m=f.context&&(g.nodeType||g.jquery)?b(g):b.event,v=b.Deferred(),_=b.Callbacks("once memory"),y=f.statusCode||{},w={},x={},C="canceled",k={readyState:0,getResponseHeader:function(t){var e;if(c){if(!a)for(a={};e=Ee.exec(r);)a[e[1].toLowerCase()]=e[2];e=a[t.toLowerCase()]}return null==e?null:e},getAllResponseHeaders:function(){return c?r:null},setRequestHeader:function(t,e){return null==c&&(t=x[t.toLowerCase()]=x[t.toLowerCase()]||t,w[t]=e),this},overrideMimeType:function(t){return null==c&&(f.mimeType=t),this},statusCode:function(t){var e;if(t)if(c)k.always(t[k.status]);else for(e in t)y[e]=[y[e],t[e]];return this},abort:function(t){var e=t||C;return s&&s.abort(e),T(0,e),this}};if(v.promise(k),f.url=((e||f.url||ve.href)+"").replace(Ae,ve.protocol+"//"),f.type=i.method||i.type||f.method||f.type,f.dataTypes=(f.dataType||"*").toLowerCase().match(H)||[""],null==f.crossDomain){h=n.createElement("a");try{h.href=f.url,h.href=h.href,f.crossDomain=Me.protocol+"//"+Me.host!=h.protocol+"//"+h.host}catch(t){f.crossDomain=!0}}if(f.data&&f.processData&&"string"!=typeof f.data&&(f.data=b.param(f.data,f.traditional)),ze(Pe,f,i,k),c)return k;for(d in(u=b.event&&f.global)&&0==b.active++&&b.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!Ie.test(f.type),o=f.url.replace(De,""),f.hasContent?f.data&&f.processData&&0===(f.contentType||"").indexOf("application/x-www-form-urlencoded")&&(f.data=f.data.replace(Te,"+")):(p=f.url.slice(o.length),f.data&&(f.processData||"string"==typeof f.data)&&(o+=(ye.test(o)?"&":"?")+f.data,delete f.data),!1===f.cache&&(o=o.replace(Se,"$1"),p=(ye.test(o)?"&":"?")+"_="+_e+++p),f.url=o+p),f.ifModified&&(b.lastModified[o]&&k.setRequestHeader("If-Modified-Since",b.lastModified[o]),b.etag[o]&&k.setRequestHeader("If-None-Match",b.etag[o])),(f.data&&f.hasContent&&!1!==f.contentType||i.contentType)&&k.setRequestHeader("Content-Type",f.contentType),k.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Ne+"; q=0.01":""):f.accepts["*"]),f.headers)k.setRequestHeader(d,f.headers[d]);if(f.beforeSend&&(!1===f.beforeSend.call(g,k,f)||c))return k.abort();if(C="abort",_.add(f.complete),k.done(f.success),k.fail(f.error),s=ze(Oe,f,i,k)){if(k.readyState=1,u&&m.trigger("ajaxSend",[k,f]),c)return k;f.async&&f.timeout>0&&(l=t.setTimeout(function(){k.abort("timeout")},f.timeout));try{c=!1,s.send(w,T)}catch(t){if(c)throw t;T(-1,t)}}else T(-1,"No Transport");function T(e,i,n,a){var h,d,p,w,x,C=i;c||(c=!0,l&&t.clearTimeout(l),s=void 0,r=a||"",k.readyState=e>0?4:0,h=e>=200&&e<300||304===e,n&&(w=function(t,e,i){for(var n,s,o,r,a=t.contents,l=t.dataTypes;"*"===l[0];)l.shift(),void 0===n&&(n=t.mimeType||e.getResponseHeader("Content-Type"));if(n)for(s in a)if(a[s]&&a[s].test(n)){l.unshift(s);break}if(l[0]in i)o=l[0];else{for(s in i){if(!l[0]||t.converters[s+" "+l[0]]){o=s;break}r||(r=s)}o=o||r}if(o)return o!==l[0]&&l.unshift(o),i[o]}(f,k,n)),w=function(t,e,i,n){var s,o,r,a,l,h={},c=t.dataTypes.slice();if(c[1])for(r in t.converters)h[r.toLowerCase()]=t.converters[r];for(o=c.shift();o;)if(t.responseFields[o]&&(i[t.responseFields[o]]=e),!l&&n&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(!(r=h[l+" "+o]||h["* "+o]))for(s in h)if((a=s.split(" "))[1]===o&&(r=h[l+" "+a[0]]||h["* "+a[0]])){!0===r?r=h[s]:!0!==h[s]&&(o=a[0],c.unshift(a[1]));break}if(!0!==r)if(r&&t.throws)e=r(e);else try{e=r(e)}catch(t){return{state:"parsererror",error:r?t:"No conversion from "+l+" to "+o}}}return{state:"success",data:e}}(f,w,k,h),h?(f.ifModified&&((x=k.getResponseHeader("Last-Modified"))&&(b.lastModified[o]=x),(x=k.getResponseHeader("etag"))&&(b.etag[o]=x)),204===e||"HEAD"===f.type?C="nocontent":304===e?C="notmodified":(C=w.state,d=w.data,h=!(p=w.error))):(p=C,!e&&C||(C="error",e<0&&(e=0))),k.status=e,k.statusText=(i||C)+"",h?v.resolveWith(g,[d,C,k]):v.rejectWith(g,[k,C,p]),k.statusCode(y),y=void 0,u&&m.trigger(h?"ajaxSuccess":"ajaxError",[k,f,h?d:p]),_.fireWith(g,[k,C]),u&&(m.trigger("ajaxComplete",[k,f]),--b.active||b.event.trigger("ajaxStop")))}return k},getJSON:function(t,e,i){return b.get(t,e,i,"json")},getScript:function(t,e){return b.get(t,void 0,e,"script")}}),b.each(["get","post"],function(t,e){b[e]=function(t,i,n,s){return g(i)&&(s=s||n,n=i,i=void 0),b.ajax(b.extend({url:t,type:e,dataType:s,data:i,success:n},b.isPlainObject(t)&&t))}}),b._evalUrl=function(t){return b.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},b.fn.extend({wrapAll:function(t){var e;return this[0]&&(g(t)&&(t=t.call(this[0])),e=b(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(t){return g(t)?this.each(function(e){b(this).wrapInner(t.call(this,e))}):this.each(function(){var e=b(this),i=e.contents();i.length?i.wrapAll(t):e.append(t)})},wrap:function(t){var e=g(t);return this.each(function(i){b(this).wrapAll(e?t.call(this,i):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){b(this).replaceWith(this.childNodes)}),this}}),b.expr.pseudos.hidden=function(t){return!b.expr.pseudos.visible(t)},b.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},b.ajaxSettings.xhr=function(){try{return new t.XMLHttpRequest}catch(t){}};var We={0:200,1223:204},je=b.ajaxSettings.xhr();f.cors=!!je&&"withCredentials"in je,f.ajax=je=!!je,b.ajaxTransport(function(e){var i,n;if(f.cors||je&&!e.crossDomain)return{send:function(s,o){var r,a=e.xhr();if(a.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(r in e.xhrFields)a[r]=e.xhrFields[r];for(r in e.mimeType&&a.overrideMimeType&&a.overrideMimeType(e.mimeType),e.crossDomain||s["X-Requested-With"]||(s["X-Requested-With"]="XMLHttpRequest"),s)a.setRequestHeader(r,s[r]);i=function(t){return function(){i&&(i=n=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,"abort"===t?a.abort():"error"===t?"number"!=typeof a.status?o(0,"error"):o(a.status,a.statusText):o(We[a.status]||a.status,a.statusText,"text"!==(a.responseType||"text")||"string"!=typeof a.responseText?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=i(),n=a.onerror=a.ontimeout=i("error"),void 0!==a.onabort?a.onabort=n:a.onreadystatechange=function(){4===a.readyState&&t.setTimeout(function(){i&&n()})},i=i("abort");try{a.send(e.hasContent&&e.data||null)}catch(t){if(i)throw t}},abort:function(){i&&i()}}}),b.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),b.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return b.globalEval(t),t}}}),b.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),b.ajaxTransport("script",function(t){var e,i;if(t.crossDomain)return{send:function(s,o){e=b("' + . '' + . '' + . $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 = Services::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 100644 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 100644 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 100644 index 0000000..ee9f829 --- /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) { + $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) => $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 100644 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 100644 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 100644 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 100644 index 0000000..2194526 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Logs.php @@ -0,0 +1,97 @@ + + * + * 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 Config\Services; + +/** + * 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 = Services::logger(true)->logCache ?? []; + } +} diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php new file mode 100644 index 0000000..b6862dc --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -0,0 +1,170 @@ + + * + * 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 Config\Services; +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 = Services::routes(true); + $router = Services::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 = Services::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 100644 index 0000000..163c9f5 --- /dev/null +++ b/system/Debug/Toolbar/Collectors/Timers.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\Debug\Toolbar\Collectors; + +use Config\Services; + +/** + * 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 = Services::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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 index 0000000..2e165b8 --- /dev/null +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -0,0 +1,862 @@ +/** + * 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-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 { + 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 { + 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 100644 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 100644 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 100644 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 100644 index 0000000..b82779e --- /dev/null +++ b/system/Email/Email.php @@ -0,0 +1,2274 @@ + + * + * 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) { + $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)) { + $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)) { + $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 100644 index 0000000..2d01ad7 --- /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); + } + + 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 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 100644 index 0000000..5de8a42 --- /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) => ! 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 100644 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 100644 index 0000000..a06bd79 --- /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 = config(Modules::class); + $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) { + 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/ConfigException.php b/system/Exceptions/ConfigException.php new file mode 100644 index 0000000..d8849b8 --- /dev/null +++ b/system/Exceptions/ConfigException.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +/** + * Exception for automatic logging. + */ +class ConfigException extends CriticalError 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 100644 index 0000000..756393d --- /dev/null +++ b/system/Exceptions/CriticalError.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +use Error; + +/** + * Error: Critical conditions, like component unavailable, etc. + */ +class CriticalError extends Error +{ +} diff --git a/system/Exceptions/DebugTraceableTrait.php b/system/Exceptions/DebugTraceableTrait.php new file mode 100644 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 100644 index 0000000..df78127 --- /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; + +use RuntimeException; + +/** + * Class DownloadException + */ +class DownloadException extends RuntimeException implements ExceptionInterface +{ + 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')); + } + + /** + * @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 100644 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 100644 index 0000000..1fe47be --- /dev/null +++ b/system/Exceptions/FrameworkException.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +use RuntimeException; + +/** + * Class FrameworkException + * + * A collection of exceptions thrown by the framework + * that can only be determined at run time. + */ +class FrameworkException extends RuntimeException implements ExceptionInterface +{ + 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 100644 index 0000000..1974d63 --- /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 +{ +} diff --git a/system/Exceptions/HasExitCodeInterface.php b/system/Exceptions/HasExitCodeInterface.php new file mode 100644 index 0000000..1557c82 --- /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 +{ + /** + * Returns exit status code. + */ + public function getExitCode(): int; +} diff --git a/system/Exceptions/ModelException.php b/system/Exceptions/ModelException.php new file mode 100644 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 100644 index 0000000..b1af079 --- /dev/null +++ b/system/Exceptions/PageNotFoundException.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Exceptions; + +use Config\Services; +use OutOfBoundsException; + +class PageNotFoundException extends OutOfBoundsException implements ExceptionInterface, 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 = Services::language(null, false); + + return $lang->getLine($line, $args); + } +} diff --git a/system/Exceptions/TestException.php b/system/Exceptions/TestException.php new file mode 100644 index 0000000..f533dc2 --- /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 for automatic logging. + */ +class TestException extends CriticalError +{ + use DebugTraceableTrait; + + /** + * @return static + */ + public static function forInvalidMockClass(string $name) + { + return new static(lang('Test.invalidMockClass', [$name])); + } +} diff --git a/system/Files/Exceptions/FileException.php b/system/Files/Exceptions/FileException.php new file mode 100644 index 0000000..5feb979 --- /dev/null +++ b/system/Files/Exceptions/FileException.php @@ -0,0 +1,55 @@ + + * + * 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\ExceptionInterface; +use 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 100644 index 0000000..86b2262 --- /dev/null +++ b/system/Files/Exceptions/FileNotFoundException.php @@ -0,0 +1,31 @@ + + * + * 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\ExceptionInterface; +use 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 100644 index 0000000..02c8f18 --- /dev/null +++ b/system/Files/File.php @@ -0,0 +1,191 @@ + + * + * 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. + * + * @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. + * + * @return false|int|string + */ + public function getSizeByUnit(string $unit = 'b') + { + return match (strtolower($unit)) { + 'kb' => number_format($this->getSize() / 1024, 3), + 'mb' => number_format(($this->getSize() / 1024) / 1024, 3), + 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; + } +} diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php new file mode 100644 index 0000000..f131079 --- /dev/null +++ b/system/Files/FileCollection.php @@ -0,0 +1,370 @@ + + * + * 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 Countable; +use Generator; +use InvalidArgumentException; +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) 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))); + } + + // -------------------------------------------------------------------- + // 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/Filters/CSRF.php b/system/Filters/CSRF.php new file mode 100644 index 0000000..90ccb9b --- /dev/null +++ b/system/Filters/CSRF.php @@ -0,0 +1,77 @@ + + * + * 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; + +/** + * CSRF filter. + * + * This filter is not intended to be used from the command line. + * + * @codeCoverageIgnore + * @see \CodeIgniter\Filters\CSRFTest + */ +class CSRF implements 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 RedirectResponse|void + * + * @throws SecurityException + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return; + } + + $security = service('security'); + + try { + $security->verify($request); + } catch (SecurityException $e) { + if ($security->shouldRedirect() && ! $request->isAJAX()) { + return redirect()->back()->with('error', $e->getMessage()); + } + + throw $e; + } + } + + /** + * We don't have anything to do here. + * + * @param list|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + } +} diff --git a/system/Filters/Cors.php b/system/Filters/Cors.php new file mode 100644 index 0000000..93ca551 --- /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|string|void + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return; + } + + $this->createCorsService($arguments); + + if (! $this->cors->isPreflightRequest($request)) { + return; + } + + /** @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|void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return; + } + + $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 100644 index 0000000..9f864ee --- /dev/null +++ b/system/Filters/DebugToolbar.php @@ -0,0 +1,45 @@ + + * + * 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) + { + } + + /** + * 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); + } +} diff --git a/system/Filters/Exceptions/FilterException.php b/system/Filters/Exceptions/FilterException.php new file mode 100644 index 0000000..1226ba3 --- /dev/null +++ b/system/Filters/Exceptions/FilterException.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\Filters\Exceptions; + +use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\ExceptionInterface; + +/** + * FilterException + */ +class FilterException extends ConfigException implements ExceptionInterface +{ + /** + * 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 100644 index 0000000..0353fb2 --- /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|void + */ + 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|void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null); +} diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php new file mode 100644 index 0000000..d8e2283 --- /dev/null +++ b/system/Filters/Filters.php @@ -0,0 +1,869 @@ + + * + * 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\Exceptions\ConfigException; +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 original config file + * + * @var FiltersConfig + */ + protected $config; + + /** + * The active IncomingRequest or CLIRequest + * + * @var RequestInterface + */ + protected $request; + + /** + * The active Response instance + * + * @var ResponseInterface + */ + protected $response; + + /** + * Handle to the modules config. + * + * @var Modules + */ + protected $modules; + + /** + * Whether we've done initial processing + * on the filter lists. + * + * @var bool + */ + protected $initialized = false; + + /** + * The processed filters that will + * be used to check against. + * + * This does not include "Required Filters". + * + * @var array + */ + protected $filters = [ + 'before' => [], + 'after' => [], + ]; + + /** + * The collection of filters' class names that will + * be used to execute in each position. + * + * This does not include "Required Filters". + * + * @var array + */ + protected $filtersClass = [ + 'before' => [], + 'after' => [], + ]; + + /** + * Any arguments to be passed to filters. + * + * @var array|null> [name => params] + */ + protected $arguments = []; + + /** + * Any arguments to be passed to filtersClass. + * + * @var array|null> [classname => arguments] + */ + 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 ?? config(Modules::class); + + 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 of the 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]); + } + + /** + * @return RequestInterface|ResponseInterface|string + */ + private function runBefore(array $filterClasses) + { + foreach ($filterClasses as $className) { + $class = new $className(); + + if (! $class instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($class::class); + } + + $result = $class->before( + $this->request, + $this->argumentsClass[$className] ?? null + ); + + 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; + } + + private function runAfter(array $filterClasses): ResponseInterface + { + foreach ($filterClasses as $className) { + $class = new $className(); + + if (! $class instanceof FilterInterface) { + throw FilterException::forIncorrectInterface($class::class); + } + + $result = $class->after( + $this->request, + $this->response, + $this->argumentsClass[$className] ?? null + ); + + if ($result instanceof ResponseInterface) { + $this->response = $result; + + continue; + } + } + + return $this->response; + } + + /** + * Runs "Required Filters" for the specified position. + * + * @return RequestInterface|ResponseInterface|string|null + * + * @phpstan-param 'before'|'after' $position + * + * @throws FilterException + * + * @internal + */ + public function runRequired(string $position = 'before') + { + [$filters, $aliases] = $this->getRequiredFilters($position); + + if ($filters === []) { + return $position === 'before' ? $this->request : $this->response; + } + + $filterClasses = []; + + foreach ($filters as $alias) { + if (is_array($aliases[$alias])) { + $filterClasses[$position] = array_merge($filterClasses[$position], $aliases[$alias]); + } else { + $filterClasses[$position][] = $aliases[$alias]; + } + } + + if ($position === 'before') { + return $this->runBefore($filterClasses[$position]); + } + + // After + return $this->runAfter($filterClasses[$position]); + } + + /** + * 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 + */ + 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']); + + $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". + */ + public function getFilters(): array + { + return $this->filters; + } + + /** + * Returns the filtersClass array. + * This does not include "Required Filters". + */ + 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. + * + * @return $this + */ + public function addFilter(string $class, ?string $alias = null, string $when = 'before', string $section = 'globals') + { + $alias ??= md5($class); + + if (! isset($this->config->{$section})) { + $this->config->{$section} = []; + } + + if (! isset($this->config->{$section}[$when])) { + $this->config->{$section}[$when] = []; + } + + $this->config->aliases[$alias] = $class; + + $this->config->{$section}[$when][] = $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 $name filter_name or filter_name:arguments like 'role:admin,manager' + */ + private function enableFilter(string $name, string $when = 'before'): void + { + // Get arguments and clean name + [$name, $arguments] = $this->getCleanName($name); + $this->arguments[$name] = ($arguments !== []) ? $arguments : null; + + if (class_exists($name)) { + $this->config->aliases[$name] = $name; + } elseif (! array_key_exists($name, $this->config->aliases)) { + throw FilterException::forNoAlias($name); + } + + $classNames = (array) $this->config->aliases[$name]; + + foreach ($classNames as $className) { + $this->argumentsClass[$className] = $this->arguments[$name] ?? null; + } + + if (! isset($this->filters[$when][$name])) { + $this->filters[$when][] = $name; + $this->filtersClass[$when] = array_merge($this->filtersClass[$when], $classNames); + } + } + + /** + * Get clean name and arguments + * + * @param string $name filter_name or filter_name:arguments like 'role:admin,manager' + * + * @return array{0: string, 1: list} [name, arguments] + */ + private function getCleanName(string $name): array + { + $arguments = []; + + if (str_contains($name, ':')) { + [$name, $arguments] = explode(':', $name); + + $arguments = explode(',', $arguments); + array_walk($arguments, static function (&$item) { + $item = trim($item); + }); + } + + return [$name, $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. + * + * @params array $names filter_name or filter_name:arguments like 'role:admin,manager' + * + * @return Filters + */ + public function enableFilters(array $names, string $when = 'before') + { + foreach ($names as $filter) { + $this->enableFilter($filter, $when); + } + + return $this; + } + + /** + * Returns the arguments for a specified key, or all. + * + * @return array|string + */ + public function getArguments(?string $key = null) + { + return $key === null ? $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 $alias => $settings) { + // Look for inclusion rules + if (isset($settings['before'])) { + $path = $settings['before']; + + if ($this->pathApplies($uri, $path)) { + // Get arguments and clean name + [$name, $arguments] = $this->getCleanName($alias); + + $filters['before'][] = $name; + + $this->registerArguments($name, $arguments); + } + } + + if (isset($settings['after'])) { + $path = $settings['after']; + + if ($this->pathApplies($uri, $path)) { + // Get arguments and clean name + [$name, $arguments] = $this->getCleanName($alias); + + $filters['after'][] = $name; + + // The arguments may have already been registered in the before filter. + // So disable check. + $this->registerArguments($name, $arguments, false); + } + } + } + + $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']); + } + } + + /** + * @param string $name filter alias + * @param array $arguments filter arguments + * @param bool $check if true, check if already defined + */ + private function registerArguments(string $name, array $arguments, bool $check = true): void + { + if ($arguments !== []) { + if ($check && array_key_exists($name, $this->arguments)) { + throw new ConfigException( + '"' . $name . '" already has arguments: ' + . (($this->arguments[$name] === null) ? 'null' : implode(',', $this->arguments[$name])) + ); + } + + $this->arguments[$name] = $arguments; + } + + $classNames = (array) $this->config->aliases[$name]; + + foreach ($classNames as $className) { + $this->argumentsClass[$className] = $this->arguments[$name] ?? null; + } + } + + /** + * Maps filter aliases to the equivalent filter classes + * + * @return void + * + * @throws FilterException + */ + protected function processAliasesToClass(string $position) + { + $filterClasses = []; + + foreach ($this->filters[$position] as $alias => $rules) { + if (is_numeric($alias) && is_string($rules)) { + $alias = $rules; + } + + if (! array_key_exists($alias, $this->config->aliases)) { + throw FilterException::forNoAlias($alias); + } + + if (is_array($this->config->aliases[$alias])) { + $filterClasses = [...$filterClasses, ...$this->config->aliases[$alias]]; + } else { + $filterClasses[] = $this->config->aliases[$alias]; + } + } + + // when using enableFilter() we already write the class name in $filterClasses as well as the + // alias in $filters. This leads to duplicates when using route filters. + if ($position === 'before') { + $this->filtersClass[$position] = array_merge($filterClasses, $this->filtersClass[$position]); + } else { + $this->filtersClass[$position] = array_merge($this->filtersClass[$position], $filterClasses); + } + + // Since some filters like rate limiters rely on being executed once a request we filter em here. + $this->filtersClass[$position] = array_values(array_unique($this->filtersClass[$position])); + } + + /** + * 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 100644 index 0000000..f415bd0 --- /dev/null +++ b/system/Filters/ForceHTTPS.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\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|void + */ + public function before(RequestInterface $request, $arguments = null) + { + $config = config(App::class); + + if ($config->forceGlobalSecureRequests !== true) { + return; + } + + $response = service('response'); + + try { + force_https(YEAR, $request, $response); + } catch (RedirectException $e) { + return $e->getResponse(); + } + } + + /** + * We don't have anything to do here. + * + * @param array|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + } +} diff --git a/system/Filters/Honeypot.php b/system/Filters/Honeypot.php new file mode 100644 index 0000000..c2fb98c --- /dev/null +++ b/system/Filters/Honeypot.php @@ -0,0 +1,57 @@ + + * + * 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; +use Config\Services; + +/** + * 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; + } + + if (Services::honeypot()->hasContent($request)) { + throw HoneypotException::isBot(); + } + } + + /** + * Attach a honeypot to the current response. + * + * @param list|null $arguments + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + service('honeypot')->attachHoneypot($response); + } +} diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php new file mode 100644 index 0000000..542b12d --- /dev/null +++ b/system/Filters/InvalidChars.php @@ -0,0 +1,128 @@ + + * + * 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 + * + * @return void + */ + public function before(RequestInterface $request, $arguments = null) + { + if (! $request instanceof IncomingRequest) { + return; + } + + $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); + } + } + + /** + * We don't have anything to do here. + * + * @param list|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = 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 100644 index 0000000..a3d3af8 --- /dev/null +++ b/system/Filters/PageCache.php @@ -0,0 +1,77 @@ + + * + * 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|void + */ + 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; + } + } + + /** + * Cache the page. + * + * @param array|null $arguments + * + * @return void + */ + 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); + } + } +} diff --git a/system/Filters/PerformanceMetrics.php b/system/Filters/PerformanceMetrics.php new file mode 100644 index 0000000..f2371c7 --- /dev/null +++ b/system/Filters/PerformanceMetrics.php @@ -0,0 +1,62 @@ + + * + * 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) + { + } + + /** + * Replaces the performance metrics. + * + * @param array|null $arguments + * + * @return void + */ + 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); + } + } +} diff --git a/system/Filters/SecureHeaders.php b/system/Filters/SecureHeaders.php new file mode 100644 index 0000000..b8bd6e0 --- /dev/null +++ b/system/Filters/SecureHeaders.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\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 + * + * @return void + */ + public function before(RequestInterface $request, $arguments = null) + { + } + + /** + * Add security headers. + * + * @param list|null $arguments + * + * @return void + */ + public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) + { + foreach ($this->headers as $header => $value) { + $response->setHeader($header, $value); + } + } +} diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php new file mode 100644 index 0000000..5a8c2d2 --- /dev/null +++ b/system/Format/Exceptions/FormatException.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\Format\Exceptions; + +use CodeIgniter\Exceptions\DebugTraceableTrait; +use CodeIgniter\Exceptions\ExceptionInterface; +use RuntimeException; + +/** + * FormatException + */ +class FormatException extends RuntimeException implements ExceptionInterface +{ + 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 100644 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 100644 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 100644 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 100644 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 100644 index 0000000..0b2c573 --- /dev/null +++ b/system/HTTP/CLIRequest.php @@ -0,0 +1,327 @@ + + * + * 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 Locale; +use RuntimeException; + +/** + * 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 + { + $path = implode('/', $this->segments); + + return ($path === '') ? '' : $path; + } + + /** + * 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 100644 index 0000000..4b1c9c6 --- /dev/null +++ b/system/HTTP/CURLRequest.php @@ -0,0 +1,700 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\App; +use Config\CURLRequest as ConfigCURLRequest; +use InvalidArgumentException; + +/** + * 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 (str_starts_with($output, 'HTTP/1.1 200 Connection established')) { + $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)); + } + + // version + if (! empty($config['version'])) { + if ($config['version'] === 1.0) { + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; + } elseif ($config['version'] === 1.1) { + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; + } elseif ($config['version'] === 2.0) { + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; + } + } + + // 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 100644 index 0000000..945c3e0 --- /dev/null +++ b/system/HTTP/ContentSecurityPolicy.php @@ -0,0 +1,840 @@ + + * + * 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 + */ + 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 = []; + + /** + * Used for security enforcement + * + * @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 array + */ + 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 ourself 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 base_uri value. Can be either a URI class or a simple string. + * + * base_uri 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 + * ,