diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..f7e2a692a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,21 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.neon]
+indent_style = tab
+
+[phars.xml]
+indent_size = 2
+
+[*.js]
+indent_size = 2
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..ec5d2f408
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,26 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "jquery": true
+ },
+ "extends": [
+ "airbnb-base"
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "rules": {
+ "strict": 0,
+ "max-len": ["error", { "code": 150 }],
+ "no-underscore-dangle": ["error", {
+ "allow": ["__cakeDebugBlockInit", "_arguments"]
+ }],
+ "import/extensions": [0, { "": "always" }],
+ "no-param-reassign": 0,
+ "no-plusplus": 0,
+ "class-methods-use-this": 0,
+ "prefer-rest-params": 0
+ }
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..2ceb8f6be
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,45 @@
+# Define the line ending behavior of the different file extensions
+# Set default behaviour, in case users don't have core.autocrlf set.
+* text=auto
+* text eol=lf
+
+# Explicitly declare text files we want to always be normalized and converted
+# to native line endings on checkout.
+*.php text
+*.default text
+*.ctp text
+*.md text
+*.po text
+*.js text
+*.css text
+*.ini text
+*.txt text
+*.xml text
+
+# Declare files that will always have CRLF line endings on checkout.
+*.bat eol=crlf
+
+# Declare files that will always have LF line endings on checkout.
+*.pem eol=lf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
+*.gif binary
+*.ico binary
+*.mo binary
+*.mp4 binary
+
+# Remove files for archives generated using `git archive`
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.jshintrc export-ignore
+.stickler.yml export-ignore
+phpcs.xml export-ignore
+phpunit.xml.dist export-ignore
+/.github export-ignore
+/docs/static export-ignore
+/tests/TestCase export-ignore
+/tests/test_app export-ignore
+/tests/bootstrap.php export-ignore
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
new file mode 100644
index 000000000..81d159dd2
--- /dev/null
+++ b/.github/SECURITY.md
@@ -0,0 +1,25 @@
+# Security Policy
+
+## Supported Versions
+
+Security fixes are applied to all active versions listed in the
+[version map](https://github.com/cakephp/debug_kit/wiki#version-map).
+Versions marked as EOL no longer receive fixes.
+
+## Reporting a Vulnerability
+
+If you've found a security issue in CakePHP DebugKit, please use the following
+procedure instead of the normal bug reporting system. Instead of using the bug
+tracker, or one of the support forums please send an email to
+security [at] cakephp.org. Emails sent to this address go to the CakePHP core
+team on a private mailing list.
+
+For each report, we try to first confirm the vulnerability. Once confirmed,
+the CakePHP team will take the following actions:
+
+* Acknowledge to the reporter that we've received the issue, and are
+ working on a fix. We ask that the reporter keep the issue confidential until we announce it.
+* Get a fix/patch prepared.
+* Prepare a post describing the vulnerability, and the possible exploits.
+* Release new versions of all affected versions.
+* Prominently feature the problem in the release announcement
diff --git a/.github/codecov.yml b/.github/codecov.yml
new file mode 100644
index 000000000..0d79235e3
--- /dev/null
+++ b/.github/codecov.yml
@@ -0,0 +1,7 @@
+codecov:
+ require_ci_to_pass: yes
+
+coverage:
+ range: "90...100"
+
+comment: false
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..6647c4286
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+- package-ecosystem: composer
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+- package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..c2c6b791b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,23 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - 4.x
+ - 5.x
+ pull_request:
+ branches:
+ - '*'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ testsuite:
+ uses: cakephp/.github/.github/workflows/testsuite-with-db.yml@5.x
+ secrets: inherit
+
+ cs-stan:
+ uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x
+ secrets: inherit
diff --git a/.github/workflows/deploy_docs_4x.yml b/.github/workflows/deploy_docs_4x.yml
new file mode 100644
index 000000000..eb43c9bb2
--- /dev/null
+++ b/.github/workflows/deploy_docs_4x.yml
@@ -0,0 +1,27 @@
+---
+name: 'deploy_docs_4x'
+
+on:
+ push:
+ branches:
+ - 4.x
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Cloning repo
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Push to dokku
+ uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0
+ with:
+ git_remote_url: 'ssh://dokku@apps.cakephp.org:22/debugkit-docs-4'
+ git_push_flags: '-f'
+ ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
diff --git a/.github/workflows/deploy_docs_5x.yml b/.github/workflows/deploy_docs_5x.yml
new file mode 100644
index 000000000..878186360
--- /dev/null
+++ b/.github/workflows/deploy_docs_5x.yml
@@ -0,0 +1,28 @@
+---
+name: 'deploy_docs_5x'
+
+on:
+ push:
+ branches:
+ - 5.x
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Cloning repo
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Push to dokku
+ uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0
+ with:
+ git_remote_url: 'ssh://dokku@apps.cakephp.org:22/debugkit-docs-5'
+ git_push_flags: '-f'
+ ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}
+ branch: '5.x'
diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml
new file mode 100644
index 000000000..439a52c18
--- /dev/null
+++ b/.github/workflows/docs-validation.yml
@@ -0,0 +1,27 @@
+name: Documentation Validation
+
+on:
+ push:
+ branches:
+ - 5.x
+ paths:
+ - 'docs/**'
+ - '.github/**'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '.github/**'
+
+jobs:
+ validate:
+ uses: cakephp/.github/.github/workflows/docs-validation.yml@5.x
+ with:
+ docs-path: 'docs'
+ vitepress-path: 'docs/.vitepress'
+ enable-config-js-check: true
+ enable-json-lint: true
+ enable-toc-check: true
+ enable-spell-check: true
+ enable-markdown-lint: true
+ enable-link-check: true
+ tools-ref: '5.x'
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 000000000..9bb0e6202
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,29 @@
+name: Mark stale issues and pull requests
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+
+permissions:
+ contents: read
+
+jobs:
+ stale:
+
+ permissions:
+ issues: write # for actions/stale to close stale issues
+ pull-requests: write # for actions/stale to close stale PRs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/stale@v11
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: 'This issue is stale because it has been open for 120 days with no activity. Remove the `stale` label or comment or this will be closed in 15 days'
+ stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove the `stale` label or comment on this issue, or it will be closed in 15 days'
+ stale-issue-label: 'stale'
+ stale-pr-label: 'stale'
+ days-before-stale: 120
+ days-before-close: 15
+ exempt-issue-labels: 'pinned'
+ exempt-pr-labels: 'pinned'
diff --git a/.gitignore b/.gitignore
index 983a9993d..8b508fa2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,32 @@
*.diff
*.err
+*.log
*.orig
*.rej
*.swo
*.swp
*.vi
*~
+.idea/*
+nbproject/*
+.vscode
.DS_Store
.cache
+.phpunit.cache
+.phpcs.cache
.project
.settings
.svn
errors.err
-tags
\ No newline at end of file
+tags
+node_modules
+package-lock.json
+/.phpunit.result.cache
+/nbproject/
+/composer.lock
+/tools
+/vendor
+/phpunit.xml
+/webroot/css/style.css.map
+/webroot/mix.js.map
+/webroot/mix-manifest.json
diff --git a/.phive/phars.xml b/.phive/phars.xml
new file mode 100644
index 000000000..f648a9f74
--- /dev/null
+++ b/.phive/phars.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..d1f129524
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,36 @@
+# ----------------------
+# 1. Build stage
+# ----------------------
+FROM node:24-alpine AS builder
+
+# Git is required because docs/package.json pulls a dependency from GitHub.
+RUN apk add --no-cache git openssh-client
+
+WORKDIR /app/docs
+
+# Copy dependency manifests first to preserve Docker layer caching.
+COPY docs/ ./
+RUN npm ci
+
+# Increase max-old-space-size to avoid memory issues during build
+#ENV NODE_OPTIONS="--max-old-space-size=8192"
+
+# Build the site.
+RUN npm run docs:build
+
+# ----------------------
+# 2. Runtime stage (angie)
+# ----------------------
+FROM docker.angie.software/angie:latest AS runner
+
+# Copy built files
+COPY --from=builder /app/docs/.vitepress/dist /usr/share/angie/html
+
+# Expose port
+EXPOSE 80
+
+# Health check (optional)
+HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1
+
+# Start angie
+CMD ["angie", "-g", "daemon off;"]
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 000000000..7fd9d1f4b
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,28 @@
+The MIT License
+
+CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org)
+Copyright (c) 2005-present, Cake Software Foundation, Inc.
+
+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.
+
+Cake Software Foundation, Inc.
+1785 E. Sahara Avenue,
+Suite 490-204
+Las Vegas, Nevada 89104,
+United States of America.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..4c43b1454
--- /dev/null
+++ b/README.md
@@ -0,0 +1,70 @@
+# CakePHP DebugKit
+[](https://github.com/cakephp/debug_kit/actions/workflows/ci.yml)
+[](https://codecov.io/github/cakephp/debug_kit)
+[](LICENSE.txt)
+[](https://packagist.org/packages/cakephp/debug_kit)
+
+DebugKit provides a debugging toolbar and enhanced debugging tools for CakePHP
+applications. It lets you quickly see configuration data, log messages, SQL
+queries, and timing data for your application.
+
+:warning: DebugKit is only intended for use in single-user local development
+environments. You should avoid using DebugKit in shared development
+environments, staging environments, or any environment where you need to keep
+configuration data and environment variables hidden. :warning:
+
+## Requirements
+
+* SQLite (pdo_sqlite) or another database driver that CakePHP can talk to. By
+ default DebugKit will use SQLite, if you need to use a different database see the Database Configuration section in the documentation linked below.
+
+For details and older versions see [version map](https://github.com/cakephp/debug_kit/wiki#version-map).
+
+## Installation
+
+* Install the plugin with [Composer](https://getcomposer.org/) from your CakePHP Project's ROOT directory (where the **composer.json** file is located)
+```sh
+php composer.phar require --dev cakephp/debug_kit:"^5.0"
+```
+
+* [Load the plugin](https://book.cakephp.org/5/en/plugins.html#loading-a-plugin)
+```
+bin/cake plugin load DebugKit --only-debug
+```
+
+## Is DebugKit not working?
+
+If you don't see a CakePHP icon on the bottom right of your page DebugKit is not be
+working correctly. Some common problems are:
+
+1. Your PHP environment doesn't have SQLite installed. Check your application
+ logs to confirm if this happening. You can either configure DebugKit to use
+ a different database, or install the PDO SQLite 3 extension.
+2. Your hostname needs to be added to the `DebugKit.safeTld`. If your local
+ domain isn't a known development environment name, DebugKit will disable
+ itself to protect a potentially non-development environment.
+3. If you are using the [Authorization Plugin](https://github.com/cakephp/authorization)
+ you need to set `DebugKit.ignoreAuthorization` to `true` in your config.
+
+## Reporting Issues
+
+If you have a problem with DebugKit please open an issue on [GitHub](https://github.com/cakephp/debug_kit/issues).
+
+## Contributing
+
+If you'd like to contribute to DebugKit, check out the
+[roadmap](https://github.com/cakephp/debug_kit/wiki/roadmap) for any
+planned features. You can [fork](https://help.github.com/articles/fork-a-repo)
+the project, add features, and send [pull
+requests](https://help.github.com/articles/using-pull-requests) or open
+[issues](https://github.com/cakephp/debug_kit/issues).
+
+## Documentation
+
+Documentation for DebugKit can be found in the
+[CakePHP documentation](https://book.cakephp.org/debugkit/5/en/index.html).
+
+## Panels
+Panels by other plugins:
+- `L10n` by [Setup plugin](https://github.com/dereuromark/cakephp-setup) to show current localization for Date, DateTime, Time objects/values.
+- `Twig` by [Twig plugin](https://github.com/cakephp/twig-view/) to list all templates.
diff --git a/README.mdown b/README.mdown
deleted file mode 100644
index 10a386c7a..000000000
--- a/README.mdown
+++ /dev/null
@@ -1,31 +0,0 @@
-# CakePHP DebugKit
-
-DebugKit provides a debugging toolbar and enhanced debugging tools for CakePHP applications.
-
-## Installation
-
-* Clone/Copy the files in this directory into `app/plugins/debug_kit`
-* Include the toolbar component in your `app_controller.php`:
- * `var $components = array('DebugKit.Toolbar');`
-* Set debug mode to at least 1.
-* Make sure to remove the 'sql_dump' element from your layout if you want to experience the awesome that is the debug kit SQL log.
-
-## Documentation
-
-Further documentation including additional configuration and ways of extending DebugKit can be found in the [Lighthouse wiki](http://cakephp.lighthouseapp.com/projects/42880-debug-kit/overview)
-
-## Reporting issues
-
-If you have an issues with DebugKit please open a ticket on lighthouse http://cakephp.lighthouseapp.com/projects/42880-debug-kit/overview
-
-## Contributing
-
-If you'd like to contribute to DebugKit, check out the [Roadmap](http://cakephp.lighthouseapp.com/projects/42880/roadmap) for any planned features. You can fork the project add features and send pull requests, or open tickets on lighthouse.
-
-## Versions
-
-DebugKit has several versions, they are compatible with different release of CakePHP.
-
-* `1.0 -> 1.2` are compatible with CakePHP 1.2.x. These releases of DebugKit will not work with CakePHP 1.3.
-* `1.3` is compatible with CakePHP 1.3.x only. It will not work with CakePHP 1.2.
-
diff --git a/build.py b/build.py
deleted file mode 100755
index ba75dcf62..000000000
--- a/build.py
+++ /dev/null
@@ -1,130 +0,0 @@
-#! /usr/bin/env python
-
-import sys, os
-import tarfile, zipfile, gzip, bz2
-from optparse import OptionParser
-
-"""
-Builds packaged releases of DebugKit so I don't have to do things manually.
-
-Excludes itself (build.py), .gitignore, .DS_Store and the .git folder from the archives.
-"""
-def main():
- parser = OptionParser();
- parser.add_option('-o', '--output-dir', dest="output_dir",
- help="write the packages to DIR", metavar="DIR")
- parser.add_option('-p', '--prefix-name', dest="prefix",
- help="prefix used for the generated files")
- parser.add_option('-k', '--skip', dest="skip", default="",
- help="A comma separated list of files to skip")
- parser.add_option('-s', '--source-dir', dest="source", default=".",
- help="The source directory for the build process")
-
- (options, args) = parser.parse_args()
-
- if options.output_dir == '' or options.output_dir == options.source:
- print 'Requires an output dir, and that output dir cannot be the same as the source one!'
- exit()
-
- # append .git and build.py to the skip files
- skip = options.skip.split(',')
- skip.extend(['.git', '.gitignore', '.DS_Store', 'build.py'])
-
- # get list of files in top level dir.
- files = os.listdir(options.source)
-
- os.chdir(options.source)
-
- # filter the files, I couldn't figure out how to do it in a more concise way.
- for f in files[:]:
- try:
- skip.index(f)
- files.remove(f)
- except ValueError:
- pass
-
- # make a boring tar file
- destfile = ''.join([options.output_dir, options.prefix])
- tar_file_name = destfile + '.tar'
- tar = tarfile.open(tar_file_name, 'w');
- for f in files:
- tar.add(f)
- tar.close()
- print "Generated tar file"
-
- # make the gzip
- if make_gzip(tar_file_name, destfile):
- print "Generated gzip file"
- else:
- print "Could not generate gzip file"
-
- # make the bz2
- if make_bz2(tar_file_name, destfile):
- print "Generated bz2 file"
- else:
- print "Could not generate bz2 file"
-
- # make the zip file
- zip_recursive(destfile + '.zip', options.source, files)
- print "Generated zip file\n"
-
-def make_gzip(tar_file, destination):
- """
- Takes a tar_file and destination. Compressess the tar file and creates
- a .tar.gzip
- """
- tar_contents = open(tar_file, 'rb')
- gzipfile = gzip.open(destination + '.tar.gz', 'wb')
- gzipfile.writelines(tar_contents)
- gzipfile.close()
- tar_contents.close()
- return True
-
-def make_bz2(tar_file, destination):
- """
- Takes a tar_file and destination. Compressess the tar file and creates
- a .tar.bz2
- """
- tar_contents = open(tar_file, 'rb')
- bz2file = bz2.BZ2File(destination + '.tar.bz2', 'wb')
- bz2file.writelines(tar_contents)
- bz2file.close()
- tar_contents.close()
- return True
-
-def zip_recursive(destination, source_dir, rootfiles):
- """
- Recursively zips source_dir into destination.
- rootfiles should contain a list of files in the top level directory that
- are to be included. Any top level files not in rootfiles will be omitted
- from the zip file.
- """
- zipped = zipfile.ZipFile(destination, 'w', zipfile.ZIP_DEFLATED)
-
- for root, dirs, files in os.walk(source_dir):
- inRoot = False
- if root == source_dir:
- inRoot = True
-
- if inRoot:
- for d in dirs:
- try:
- rootfiles.index(d)
- except ValueError:
- dirs.remove(d)
-
- for f in files[:]:
- if inRoot:
- try:
- rootfiles.index(f)
- except ValueError:
- continue
-
- fullpath = os.path.join(root, f)
- zipped.write(fullpath)
- zipped.close()
- return destination
-
-
-if __name__ == '__main__':
- main()
\ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 000000000..2c6ce02a1
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,77 @@
+{
+ "name": "cakephp/debug_kit",
+ "description": "CakePHP Debug Kit",
+ "license": "MIT",
+ "type": "cakephp-plugin",
+ "keywords": [
+ "cakephp",
+ "debug",
+ "kit",
+ "dev"
+ ],
+ "authors": [
+ {
+ "name": "Mark Story",
+ "homepage": "https://mark-story.com",
+ "role": "Author"
+ },
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/debug_kit/graphs/contributors"
+ }
+ ],
+ "homepage": "https://github.com/cakephp/debug_kit",
+ "support": {
+ "issues": "https://github.com/cakephp/debug_kit/issues",
+ "forum": "https://stackoverflow.com/tags/cakephp",
+ "irc": "irc://irc.freenode.org/cakephp",
+ "source": "https://github.com/cakephp/debug_kit"
+ },
+ "require": {
+ "php": ">=8.1",
+ "cakephp/cakephp": "^5.1",
+ "composer/composer": "^2.7.7",
+ "doctrine/sql-formatter": "^1.1.3"
+ },
+ "require-dev": {
+ "cakephp/authorization": "^3.0",
+ "cakephp/cakephp-codesniffer": "^5.0",
+ "phpunit/phpunit": "^10.5.32 || ^11.1.3 || ^12.0.9"
+ },
+ "suggest": {
+ "ext-pdo_sqlite": "DebugKit needs to store panel data in a database. SQLite is simple and easy to use."
+ },
+ "autoload": {
+ "psr-4": {
+ "DebugKit\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/",
+ "DebugKit\\TestApp\\": "tests/test_app/",
+ "DebugKit\\Test\\": "tests/",
+ "DebugkitTestPlugin\\": "tests/test_app/Plugin/DebugkitTestPlugin/src/"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ },
+ "scripts": {
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
+ "phpstan": "tools/phpstan analyse",
+ "stan": [
+ "tools/phpstan analyse",
+ "tools/psalm"
+ ],
+ "stan-baseline": "tools/phpstan --generate-baseline",
+ "psalm-baseline": "tools/psalm --set-baseline=psalm-baseline.xml",
+ "stan-setup": "phive install",
+ "test": "phpunit"
+ },
+ "minimum-stability": "stable",
+ "prefer-stable": true
+}
diff --git a/config/bootstrap.php b/config/bootstrap.php
new file mode 100644
index 000000000..2afe7f8b5
--- /dev/null
+++ b/config/bootstrap.php
@@ -0,0 +1,76 @@
+ 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Sqlite',
+ 'database' => TMP . 'debug_kit.sqlite',
+ 'encoding' => 'utf8',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ ]);
+}
+
+if (!function_exists('sql')) {
+ /**
+ * Prints out the SQL statements generated by a Query object.
+ *
+ * This function returns the same variable that was passed.
+ * Only runs if debug mode is enabled.
+ *
+ * @param \Cake\Database\Query $query Query to show SQL statements for.
+ * @param bool $showValues Renders the SQL statement with bound variables.
+ * @param bool|null $showHtml If set to true, the method prints the debug
+ * data in a browser-friendly way.
+ * @return \Cake\Database\Query
+ */
+ function sql(Query $query, $showValues = true, $showHtml = null)
+ {
+ return DebugSql::sql($query, $showValues, $showHtml, 1);
+ }
+}
+
+if (!function_exists('sqld')) {
+ /**
+ * Prints out the SQL statements generated by a Query object and dies.
+ *
+ * Only runs if debug mode is enabled.
+ * It will otherwise just continue code execution and ignore this function.
+ *
+ * @param \Cake\Database\Query $query Query to show SQL statements for.
+ * @param bool $showValues Renders the SQL statement with bound variables.
+ * @param bool|null $showHtml If set to true, the method prints the debug
+ * data in a browser-friendly way.
+ * @return void
+ */
+ function sqld(Query $query, $showValues = true, $showHtml = null)
+ {
+ DebugSql::sqld($query, $showValues, $showHtml, 2);
+ }
+}
diff --git a/config/routes.php b/config/routes.php
new file mode 100644
index 000000000..2505a17d7
--- /dev/null
+++ b/config/routes.php
@@ -0,0 +1,56 @@
+plugin('DebugKit', ['path' => '/debug-kit'], function (RouteBuilder $routes) {
+ $routes->setExtensions('json');
+ $routes->setRouteClass(DashedRoute::class);
+
+ $routes->connect(
+ '/toolbar/clear-cache',
+ ['controller' => 'Toolbar', 'action' => 'clearCache']
+ );
+ $routes->connect(
+ '/toolbar/clear-session',
+ ['controller' => 'Toolbar', 'action' => 'clearSession']
+ );
+ $routes->connect(
+ '/toolbar/*',
+ ['controller' => 'Requests', 'action' => 'view']
+ );
+ $routes->connect(
+ '/panels/view/latest-history',
+ ['controller' => 'Panels', 'action' => 'latestHistory']
+ );
+ $routes->connect(
+ '/panels/view/*',
+ ['controller' => 'Panels', 'action' => 'view']
+ );
+ $routes->connect(
+ '/panels/*',
+ ['controller' => 'Panels', 'action' => 'index']
+ );
+
+ $routes->connect(
+ '/composer/check-dependencies',
+ ['controller' => 'Composer', 'action' => 'checkDependencies']
+ );
+
+ $routes->scope(
+ '/mail-preview',
+ ['controller' => 'MailPreview'],
+ function (RouteBuilder $routes) {
+ $routes->connect('/', ['action' => 'index']);
+ $routes->connect('/preview', ['action' => 'email']);
+ $routes->connect('/preview/*', ['action' => 'email']);
+ $routes->connect('/sent/{panel}/{id}', ['action' => 'sent'], ['pass' => ['panel', 'id']]);
+ }
+ );
+
+ $routes->get('/', ['controller' => 'Dashboard', 'action' => 'index']);
+ $routes->get('/dashboard', ['controller' => 'Dashboard', 'action' => 'index']);
+ $routes->post('/dashboard/reset', ['controller' => 'Dashboard', 'action' => 'reset']);
+ });
+};
diff --git a/controllers/components/toolbar.php b/controllers/components/toolbar.php
deleted file mode 100644
index 1066cf75f..000000000
--- a/controllers/components/toolbar.php
+++ /dev/null
@@ -1,700 +0,0 @@
- false,
- 'autoRun' => true
- );
-
-/**
- * Controller instance reference
- *
- * @var object
- */
- var $controller;
-
-/**
- * Components used by DebugToolbar
- *
- * @var array
- */
- var $components = array('RequestHandler', 'Session');
-
-/**
- * The default panels the toolbar uses.
- * which panels are used can be configured when attaching the component
- *
- * @var array
- */
- var $_defaultPanels = array('history', 'session', 'request', 'sqlLog', 'timer', 'log', 'variables');
-
-/**
- * Loaded panel objects.
- *
- * @var array
- */
- var $panels = array();
-
-/**
- * javascript files component will be using
- *
- * @var array
- **/
- var $javascript = array(
- 'behavior' => '/debug_kit/js/js_debug_toolbar'
- );
-
-/**
- * CacheKey used for the cache file.
- *
- * @var string
- **/
- var $cacheKey = 'toolbar_cache';
-
-/**
- * Duration of the debug kit history cache
- *
- * @var string
- **/
- var $cacheDuration = '+4 hours';
-
-/**
- * initialize
- *
- * If debug is off the component will be disabled and not do any further time tracking
- * or load the toolbar helper.
- *
- * @return bool
- **/
- function initialize(&$controller, $settings) {
- $this->settings = am($this->settings, $settings);
- if (!Configure::read('debug') && empty($this->settings['forceEnable'])) {
- $this->enabled = false;
- return false;
- }
- if ($this->settings['autoRun'] == false && !isset($controller->params['url']['debug'])) {
- $this->enabled = false;
- return false;
- }
- App::import('Vendor', 'DebugKit.DebugKitDebugger');
-
- DebugKitDebugger::setMemoryPoint(__d('debug_kit', 'Component initialization', true));
- DebugKitDebugger::startTimer('componentInit', __d('debug_kit', 'Component initialization and startup', true));
-
- $panels = $this->_defaultPanels;
- if (isset($settings['panels'])) {
- $panels = $this->_makePanelList($settings['panels']);
- unset($settings['panels']);
- }
-
- $this->cacheKey .= $this->Session->read('Config.userAgent');
- if (in_array('history', $panels) || (isset($settings['history']) && $settings['history'] !== false)) {
- $this->_createCacheConfig();
- }
-
- $this->_loadPanels($panels, $settings);
-
- $this->_set($settings);
- $this->controller =& $controller;
- return false;
- }
-
-/**
- * Go through user panels and remove default panels as indicated.
- *
- * @param array $userPanels The list of panels ther user has added removed.
- * @return array Array of panels to use.
- **/
- function _makePanelList($userPanels) {
- $panels = $this->_defaultPanels;
- foreach ($userPanels as $key => $value) {
- if (is_numeric($key)) {
- $panels[] = $value;
- }
- if (is_string($key) && $value === false) {
- $index = array_search($key, $panels);
- if ($index !== false) {
- unset($panels[$index]);
- }
- }
- }
- return $panels;
- }
-
-/**
- * Component Startup
- *
- * @return bool
- **/
- function startup(&$controller) {
- $currentViewClass = $controller->view;
- $this->_makeViewClass($currentViewClass);
- $controller->view = 'DebugKit.Debug';
- $isHtml = (
- !isset($controller->params['url']['ext']) ||
- (isset($controller->params['url']['ext']) && $controller->params['url']['ext'] == 'html')
- );
-
- if (!$this->RequestHandler->isAjax() && $isHtml) {
- $format = 'Html';
- } else {
- $format = 'FirePhp';
- }
- $controller->helpers['DebugKit.Toolbar'] = array(
- 'output' => sprintf('DebugKit.%sToolbar', $format),
- 'cacheKey' => $this->cacheKey,
- 'cacheConfig' => 'debug_kit',
- 'forceEnable' => $this->settings['forceEnable'],
- );
- $panels = array_keys($this->panels);
- foreach ($panels as $panelName) {
- $this->panels[$panelName]->startup($controller);
- }
- DebugKitDebugger::stopTimer('componentInit');
- DebugKitDebugger::startTimer('controllerAction', __d('debug_kit', 'Controller action', true));
- DebugKitDebugger::setMemoryPoint(__d('debug_kit', 'Controller action start', true));
- }
-
-/**
- * beforeRedirect callback
- *
- * @return void
- **/
- function beforeRedirect(&$controller) {
- if (!class_exists('DebugKitDebugger')) {
- return null;
- }
- DebugKitDebugger::stopTimer('controllerAction');
- $vars = $this->_gatherVars($controller);
- $this->_saveState($controller, $vars);
- }
-
-/**
- * beforeRender callback
- *
- * Calls beforeRender on all the panels and set the aggregate to the controller.
- *
- * @return void
- **/
- function beforeRender(&$controller) {
- if (!class_exists('DebugKitDebugger')) {
- return null;
- }
- DebugKitDebugger::stopTimer('controllerAction');
- $vars = $this->_gatherVars($controller);
- $this->_saveState($controller, $vars);
-
- $controller->set(array('debugToolbarPanels' => $vars, 'debugToolbarJavascript' => $this->javascript));
- DebugKitDebugger::startTimer('controllerRender', __d('debug_kit', 'Render Controller Action', true));
- DebugKitDebugger::setMemoryPoint(__d('debug_kit', 'Controller render start', true));
- }
-
-/**
- * Load a toolbar state from cache
- *
- * @param int $key
- * @return array
- **/
- function loadState($key) {
- $history = Cache::read($this->cacheKey, 'debug_kit');
- if (isset($history[$key])) {
- return $history[$key];
- }
- return array();
- }
-
-/**
- * Create the cache config for the history
- *
- * @return void
- * @access protected
- **/
- function _createCacheConfig() {
- if (Configure::read('Cache.disable') !== true) {
- Cache::config('debug_kit', array(
- 'duration' => $this->cacheDuration,
- 'engine' => 'File',
- 'path' => CACHE
- ));
- Cache::config('default');
- }
- }
-
-/**
- * collects the panel contents
- *
- * @return array Array of all panel beforeRender()
- * @access protected
- **/
- function _gatherVars(&$controller) {
- $vars = array();
- $panels = array_keys($this->panels);
-
- foreach ($panels as $panelName) {
- $panel =& $this->panels[$panelName];
- $panelName = Inflector::underscore($panelName);
- $vars[$panelName]['content'] = $panel->beforeRender($controller);
- $elementName = Inflector::underscore($panelName) . '_panel';
- if (isset($panel->elementName)) {
- $elementName = $panel->elementName;
- }
- $vars[$panelName]['elementName'] = $elementName;
- $vars[$panelName]['plugin'] = $panel->plugin;
- $vars[$panelName]['title'] = $panel->title;
- $vars[$panelName]['disableTimer'] = true;
- }
- return $vars;
- }
-
-/**
- * Load Panels used in the debug toolbar
- *
- * @return void
- * @access protected
- **/
- function _loadPanels($panels, $settings) {
- foreach ($panels as $panel) {
- $className = $panel . 'Panel';
- if (!class_exists($className) && !App::import('Vendor', $className)) {
- trigger_error(sprintf(__d('debug_kit', 'Could not load DebugToolbar panel %s', true), $panel), E_USER_WARNING);
- continue;
- }
- list($plugin, $className) = pluginSplit($className);
- $panelObj =& new $className($settings);
- if (is_subclass_of($panelObj, 'DebugPanel') || is_subclass_of($panelObj, 'debugpanel')) {
- list(, $panel) = pluginSplit($panel);
- $this->panels[$panel] =& $panelObj;
- }
- }
- }
-/**
- * Makes the DoppleGangerView class if it doesn't already exist.
- * This allows DebugView to be compatible with all view classes.
- *
- * @param string $baseClassName
- * @access protected
- * @return void
- */
- function _makeViewClass($baseClassName) {
- if (!class_exists('DoppelGangerView')) {
- $parent = strtolower($baseClassName) === 'view' ? false : true;
- App::import('View', $baseClassName, $parent);
- if (strpos($baseClassName, '.') !== false) {
- list($plugin, $baseClassName) = explode('.', $baseClassName);
- }
- if (strpos($baseClassName, 'View') === false) {
- $baseClassName .= 'View';
- }
- $class = "class DoppelGangerView extends $baseClassName {}";
- $this->_eval($class);
- }
- }
-
-/**
- * Method wrapper for eval() for testing uses.
- *
- * @return void
- **/
- function _eval($code) {
- eval($code);
- }
-
-/**
- * Save the current state of the toolbar varibles to the cache file.
- *
- * @param object $controller Controller instance
- * @param array $vars Vars to save.
- * @access protected
- * @return void
- **/
- function _saveState(&$controller, $vars) {
- $config = Cache::config('debug_kit');
- if (empty($config) || !isset($this->panels['history'])) {
- return;
- }
- $history = Cache::read($this->cacheKey, 'debug_kit');
- if (empty($history)) {
- $history = array();
- }
- if (count($history) == $this->panels['history']->history) {
- array_pop($history);
- }
- unset($vars['history']);
- array_unshift($history, $vars);
- Cache::write($this->cacheKey, $history, 'debug_kit');
- }
-}
-
-/**
- * Debug Panel
- *
- * Abstract class for debug panels.
- *
- * @package cake.debug_kit
- */
-class DebugPanel extends Object {
-/**
- * Defines which plugin this panel is from so the element can be located.
- *
- * @var string
- */
- var $plugin = null;
-
-/**
- * Defines the title for displaying on the toolbar. If null, the class name will be used.
- * Overriding this allows you to define a custom name in the toolbar.
- *
- * @var string
- */
- var $title = null;
-
-/**
- * Provide a custom element name for this panel. If null, the underscored version of the class
- * name will be used.
- *
- * @var string
- */
- var $elementName = null;
-
-/**
- * startup the panel
- *
- * Pull information from the controller / request
- *
- * @param object $controller Controller reference.
- * @return void
- **/
- function startup(&$controller) { }
-
-/**
- * Prepare output vars before Controller Rendering.
- *
- * @param object $controller Controller reference.
- * @return void
- **/
- function beforeRender(&$controller) { }
-}
-
-/**
- * History Panel
- *
- * Provides debug information on previous requests.
- *
- * @package cake.debug_kit.panels
- **/
-class HistoryPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * Number of history elements to keep
- *
- * @var string
- **/
- var $history = 5;
-
-/**
- * Constructor
- *
- * @param array $settings Array of settings.
- * @return void
- **/
- function __construct($settings) {
- if (isset($settings['history'])) {
- $this->history = $settings['history'];
- }
- }
-
-/**
- * beforeRender callback function
- *
- * @return array contents for panel
- **/
- function beforeRender(&$controller) {
- $cacheKey = $controller->Toolbar->cacheKey;
- $toolbarHistory = Cache::read($cacheKey, 'debug_kit');
- $historyStates = array();
- if (is_array($toolbarHistory) && !empty($toolbarHistory)) {
- $prefix = array();
- if (!empty($controller->params['prefix'])) {
- $prefix[$controller->params['prefix']] = false;
- }
- foreach ($toolbarHistory as $i => $state) {
- if (!isset($state['request']['content']['params']['url']['url'])) {
- continue;
- }
- $historyStates[] = array(
- 'title' => $state['request']['content']['params']['url']['url'],
- 'url' => array_merge($prefix, array(
- 'plugin' => 'debug_kit',
- 'controller' => 'toolbar_access',
- 'action' => 'history_state',
- $i + 1))
- );
- }
- }
- if (count($historyStates) >= $this->history) {
- array_pop($historyStates);
- }
- return $historyStates;
- }
-}
-
-/**
- * Variables Panel
- *
- * Provides debug information on the View variables.
- *
- * @package cake.debug_kit.panels
- **/
-class VariablesPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * beforeRender callback
- *
- * @return array
- **/
- function beforeRender(&$controller) {
- return array_merge($controller->viewVars, array('$this->data' => $controller->data));
- }
-}
-
-/**
- * Session Panel
- *
- * Provides debug information on the Session contents.
- *
- * @package cake.debug_kit.panels
- **/
-class SessionPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * beforeRender callback
- *
- * @param object $controller
- * @access public
- * @return array
- */
- function beforeRender(&$controller) {
- $sessions = $controller->Toolbar->Session->read();
- return $sessions;
- }
-}
-
-/**
- * Request Panel
- *
- * Provides debug information on the Current request params.
- *
- * @package cake.debug_kit.panels
- **/
-class RequestPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * beforeRender callback - grabs request params
- *
- * @return array
- **/
- function beforeRender(&$controller) {
- $out = array();
- $out['params'] = $controller->params;
- if (isset($controller->Cookie)) {
- $out['cookie'] = $controller->Cookie->read();
- }
- $out['get'] = $_GET;
- $out['currentRoute'] = Router::currentRoute();
- return $out;
- }
-}
-
-/**
- * Timer Panel
- *
- * Provides debug information on all timers used in a request.
- *
- * @package cake.debug_kit.panels
- **/
-class TimerPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * startup - add in necessary helpers
- *
- * @return void
- **/
- function startup(&$controller) {
- if (!in_array('Number', $controller->helpers)) {
- $controller->helpers[] = 'Number';
- }
- if (!in_array('SimpleGraph', $controller->helpers)) {
- $controller->helpers[] = 'DebugKit.SimpleGraph';
- }
- }
-}
-
-/**
- * SqlLog Panel
- *
- * Provides debug information on the SQL logs and provides links to an ajax explain interface.
- *
- * @package cake.debug_kit.panels
- **/
-class SqlLogPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * Minimum number of Rows Per Millisecond that must be returned by a query before an explain
- * is done.
- *
- * @var int
- **/
- var $slowRate = 20;
-
-/**
- * Gets the connection names that should have logs + dumps generated.
- *
- * @param string $controller
- * @access public
- * @return void
- */
- function beforeRender(&$controller) {
- if (!class_exists('ConnectionManager')) {
- return array();
- }
- $connections = array();
-
- $dbConfigs = ConnectionManager::sourceList();
- foreach ($dbConfigs as $configName) {
- $driver = null;
- $db =& ConnectionManager::getDataSource($configName);
- if (
- (empty($db->config['driver']) && empty($db->config['datasource'])) ||
- !$db->isInterfaceSupported('getLog')
- ) {
- continue;
- }
-
- if (isset($db->config['driver'])) {
- $driver = $db->config['driver'];
- }
- if (empty($driver) && isset($db->config['datasource'])) {
- $driver = $db->config['datasource'];
- }
- $explain = false;
- $isExplainable = ($driver === 'mysql' || $driver === 'mysqli' || $driver === 'postgres');
- if ($isExplainable) {
- $explain = true;
- }
- $connections[$configName] = $explain;
- }
- return array('connections' => $connections, 'threshold' => $this->slowRate);
- }
-}
-
-/**
- * Log Panel - Reads log entries made this request.
- *
- * @package cake.debug_kit.panels
- */
-class LogPanel extends DebugPanel {
-
- var $plugin = 'debug_kit';
-
-/**
- * Constructor - sets up the log listener.
- *
- * @return void
- */
- function __construct($settings) {
- parent::__construct();
- if (!class_exists('CakeLog')) {
- App::import('Core', 'CakeLog');
- }
- $existing = CakeLog::configured();
- if (empty($existing)) {
- CakeLog::config('default', array(
- 'engine' => 'FileLog'
- ));
- }
- CakeLog::config('debug_kit_log_panel', array(
- 'engine' => 'DebugKitLogListener',
- 'panel' => $this
- ));
- }
-
-/**
- * beforeRender Callback
- *
- * @return array
- **/
- function beforeRender(&$controller) {
- $logs = $this->logger->logs;
- return $logs;
- }
-}
-
-/**
- * A CakeLog listener which saves having to munge files or other configured loggers.
- *
- * @package debug_kit.components
- */
-class DebugKitLogListener {
-
- var $logs = array();
-
-/**
- * Makes the reverse link needed to get the logs later.
- *
- * @return void
- */
- function DebugKitLogListener($options) {
- $options['panel']->logger =& $this;
- }
-
-/**
- * Captures log messages in memory
- *
- * @return void
- */
- function write($type, $message) {
- if (!isset($this->logs[$type])) {
- $this->logs[$type] = array();
- }
- $this->logs[$type][] = array(date('Y-m-d H:i:s'), $message);
- }
-}
-
diff --git a/controllers/toolbar_access_controller.php b/controllers/toolbar_access_controller.php
deleted file mode 100644
index 5e6afdf31..000000000
--- a/controllers/toolbar_access_controller.php
+++ /dev/null
@@ -1,111 +0,0 @@
- array('output' => 'DebugKit.HtmlToolbar'),
- 'Javascript', 'Number', 'DebugKit.SimpleGraph'
- );
-
-/**
- * Components
- *
- * @var array
- **/
- var $components = array('RequestHandler', 'DebugKit.Toolbar');
-
-/**
- * Uses
- *
- * @var array
- **/
- var $uses = array('DebugKit.ToolbarAccess');
-
-/**
- * beforeFilter callback
- *
- * @return void
- **/
- function beforeFilter() {
- parent::beforeFilter();
- if (isset($this->Toolbar)) {
- $this->Toolbar->enabled = false;
- }
- $this->helpers['DebugKit.Toolbar']['cacheKey'] = $this->Toolbar->cacheKey;
- $this->helpers['DebugKit.Toolbar']['cacheConfig'] = 'debug_kit';
- }
-
-/**
- * Get a stored history state from the toolbar cache.
- *
- * @return void
- **/
- function history_state($key = null) {
- if (Configure::read('debug') == 0) {
- return $this->redirect($this->referer());
- }
- $oldState = $this->Toolbar->loadState($key);
- $this->set('toolbarState', $oldState);
- $this->set('debugKitInHistoryMode', true);
- }
-
-/**
- * Run SQL explain/profiling on queries. Checks the hash + the hashed queries,
- * if there is mismatch a 404 will be rendered. If debug == 0 a 404 will also be
- * rendered. No explain will be run if a 404 is made.
- *
- * @return void
- */
- function sql_explain() {
- if (
- !$this->RequestHandler->isPost() ||
- empty($this->data['log']['sql']) ||
- empty($this->data['log']['ds']) ||
- empty($this->data['log']['hash']) ||
- Configure::read('debug') == 0
- ) {
- $this->cakeError('error404', array(array(
- 'message' => 'Invalid parameters'
- )));
- }
- App::import('Core', 'Security');
- $hash = Security::hash($this->data['log']['sql'] . $this->data['log']['ds'], null, true);
- if ($hash !== $this->data['log']['hash']) {
- $this->cakeError('error404', array(array(
- 'message' => 'Invalid parameters'
- )));
- }
- $result = $this->ToolbarAccess->explainQuery($this->data['log']['ds'], $this->data['log']['sql']);
- $this->set(compact('result'));
- }
-}
\ No newline at end of file
diff --git a/debug_kit_app_controller.php b/debug_kit_app_controller.php
deleted file mode 100644
index 3592cb5ed..000000000
--- a/debug_kit_app_controller.php
+++ /dev/null
@@ -1,22 +0,0 @@
-
+```
+
+The `` value is returned in the response headers:
+
+```text
+X-DEBUGKIT-ID: 5ef39604-ad5d-4ca4-85d8-8595e52373bb
+```
+
+For example:
+
+```text
+http://localhost/debug-kit/toolbar/5ef39604-ad5d-4ca4-85d8-8595e52373bb
+```
diff --git a/docs/en/configuration.md b/docs/en/configuration.md
new file mode 100644
index 000000000..6be72c48b
--- /dev/null
+++ b/docs/en/configuration.md
@@ -0,0 +1,68 @@
+# Configuration
+
+DebugKit supports several configuration keys that let you tailor the toolbar for local development.
+
+* `DebugKit.panels` enables or disables individual panels:
+
+```php
+// Before loading DebugKit
+Configure::write('DebugKit.panels', ['DebugKit.Packages' => false]);
+```
+
+* `DebugKit.includeSchemaReflection` enables logging for schema reflection queries. It is disabled by default.
+* `DebugKit.safeTld` defines additional local TLDs that should be considered safe:
+
+```php
+Configure::write('DebugKit.safeTld', ['test', 'local', 'example']);
+```
+
+* `DebugKit.forceEnable` forces the toolbar to display. Whitelisting local TLDs is usually safer:
+
+```php
+Configure::write('DebugKit.forceEnable', true);
+
+Configure::write('DebugKit.forceEnable', function () {
+ return $_SERVER['REMOTE_ADDR'] === '192.168.2.182';
+});
+```
+
+* `DebugKit.ignorePathsPattern` skips requests whose URL matches a regex:
+
+```php
+Configure::write('DebugKit.ignorePathsPattern', '/\.(jpg|png|gif)$/');
+```
+
+* `DebugKit.ignoreAuthorization` tells DebugKit to ignore the Cake Authorization plugin for toolbar requests. It is disabled by default.
+* `DebugKit.maxDepth` controls how many levels of nested data are rendered in general debug output. The default is `5`.
+* `DebugKit.variablesPanelMaxDepth` controls how many levels of nested data are rendered in the Variables panel. The default is `5`.
+
+Increasing either depth value can cause out-of-memory errors:
+
+```php
+Configure::write('DebugKit.maxDepth', 8);
+Configure::write('DebugKit.variablesPanelMaxDepth', 8);
+```
+
+## Database Configuration
+
+By default DebugKit stores panel data in a SQLite database in your application's `tmp` directory. If `pdo_sqlite` is unavailable, define a `debug_kit` connection in `config/app.php`:
+
+```php
+'debug_kit' => [
+ 'className' => 'Cake\Database\Connection',
+ 'driver' => 'Cake\Database\Driver\Mysql',
+ 'persistent' => false,
+ 'host' => 'localhost',
+ //'port' => 'nonstandard_port_number',
+ 'username' => 'dbusername',
+ 'password' => 'dbpassword',
+ 'database' => 'debug_kit',
+ 'encoding' => 'utf8',
+ 'timezone' => 'UTC',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+ //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
+],
+```
+
+You can safely remove `tmp/debug_kit.sqlite` at any time. DebugKit will recreate it as needed.
diff --git a/docs/en/custom-panels.md b/docs/en/custom-panels.md
new file mode 100644
index 000000000..a8ee18188
--- /dev/null
+++ b/docs/en/custom-panels.md
@@ -0,0 +1,75 @@
+# Custom Panels
+
+You can create custom DebugKit panels to expose application-specific debugging data.
+
+## Creating a Panel Class
+
+Place panel classes in `src/Panel`. The filename must match the class name. For example, `MyCustomPanel` should live in `src/Panel/MyCustomPanel.php`:
+
+```php
+namespace App\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ // ...
+}
+```
+
+Custom panels must extend `DebugPanel`.
+
+## Callbacks
+
+Panel objects hook into the current request through the `Controller.initialize` and `Controller.shutdown` events by default. If your panel needs additional events, implement `implementedEvents()` and return the full event map your panel requires.
+
+The built-in panels are the best reference when you need examples.
+
+## Panel Elements
+
+Each panel should have a matching view element that renders the panel content. The element name is the underscored form of the class name:
+
+* `SessionPanel` maps to `session_panel.php`
+* `SqllogPanel` maps to `sqllog_panel.php`
+
+Store panel elements in `templates/element`.
+
+## Custom Titles and Elements
+
+Panels usually infer their toolbar title and element name by convention. Override these methods when you need custom behavior:
+
+* `title()` sets the label shown in the toolbar.
+* `elementName()` sets the element name used for rendering.
+
+## Panel Hook Methods
+
+You can implement these methods to customize collection and rendering:
+
+* `shutdown(EventInterface $event)` collects and prepares panel data, usually into `$this->_data`.
+* `summary()` returns short summary text for the collapsed toolbar state.
+* `data()` returns the serialized data exposed to the panel element.
+
+## Panels in Other Plugins
+
+Panels shipped by plugins work the same way as application panels, with one extra requirement: set `public string $plugin` to the plugin directory name so that the panel element can be resolved when rendering.
+
+```php
+namespace MyPlugin\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ public string $plugin = 'MyPlugin';
+}
+```
+
+To register application or plugin panels, update your DebugKit configuration:
+
+```php
+// In src/Application.php bootstrap()
+Configure::write('DebugKit.panels', ['App', 'MyPlugin.MyCustom']);
+$this->addPlugin('DebugKit', ['bootstrap' => true]);
+```
+
+This keeps the default panels enabled and adds `AppPanel` plus `MyCustomPanel` from `MyPlugin`.
diff --git a/docs/en/helper-functions.md b/docs/en/helper-functions.md
new file mode 100644
index 000000000..3e38777cc
--- /dev/null
+++ b/docs/en/helper-functions.md
@@ -0,0 +1,44 @@
+# Helper Functions
+
+DebugKit includes a few helpers for investigating queries and timing custom code paths.
+
+## SQL Helpers
+
+* `sql()` dumps the SQL for an ORM query.
+* `sqld()` dumps the SQL for an ORM query and stops execution.
+
+## Using DebugTimer
+
+Use `DebugKit\DebugTimer` when you want to measure code paths that are not covered by the default timers, such as application services, controller branches, or view rendering:
+
+```php
+use DebugKit\DebugTimer;
+
+public function view($id)
+{
+ DebugTimer::start('load_article', 'Fetching article from database');
+ // Code to measure.
+ DebugTimer::stop('load_article');
+}
+```
+
+Completed custom timers appear in the Timer panel for the request.
+
+## Tracing Query Execution
+
+When you need to know where a query originated, add `SqlTraceTrait` to a table class:
+
+```php
+use DebugKit\Model\Table\SqlTraceTrait;
+
+class CategoriesTable extends Table
+{
+ use SqlTraceTrait;
+}
+```
+
+DebugKit adds location comments to the SQL log:
+
+```text
+/* APP/Controller/CategoriesController.php (line 20) */
+```
diff --git a/docs/en/history-panel.md b/docs/en/history-panel.md
new file mode 100644
index 000000000..90648aad8
--- /dev/null
+++ b/docs/en/history-panel.md
@@ -0,0 +1,9 @@
+# History Panel
+
+The History panel is one of DebugKit's most useful features. It lets you inspect toolbar data from previous requests, including redirects and requests that ended in errors.
+
+
+
+The panel contains a list of requests. The dot on the left marks the currently active request. Selecting any earlier request loads the panel data captured for that request. When historical data is active, the panel titles transition to make it clear that you are no longer viewing the latest request.
+
+
diff --git a/docs/en/index.md b/docs/en/index.md
new file mode 100644
index 000000000..99401eb01
--- /dev/null
+++ b/docs/en/index.md
@@ -0,0 +1,45 @@
+# DebugKit
+
+DebugKit provides a debugging toolbar and enhanced debugging tools for CakePHP applications. It lets you quickly inspect configuration data, log messages, SQL queries, timers, mail previews, request history, and more while developing locally.
+
+::: warning
+DebugKit is only intended for use in single-user local development environments. Avoid using DebugKit in shared development, staging, or production-like environments where configuration and environment data must remain private.
+:::
+
+## Requirements
+
+DebugKit stores panel data in a database. The default setup uses SQLite through `pdo_sqlite`. If SQLite is not available, define a dedicated `debug_kit` datasource instead.
+
+## Installation
+
+Install the plugin with Composer from your application's root directory:
+
+```bash
+php composer.phar require --dev cakephp/debug_kit:"^5.0"
+```
+
+Then load the plugin in debug mode:
+
+```bash
+bin/cake plugin load DebugKit --only-debug
+```
+
+## Troubleshooting
+
+If the toolbar icon does not appear in the bottom-right corner of the page, check these common issues:
+
+1. SQLite is not installed, and DebugKit cannot persist panel data.
+2. Your hostname is not recognized as a safe development host. Add the TLD to `DebugKit.safeTld`.
+3. You are using the Authorization plugin and need to set `DebugKit.ignoreAuthorization` to `true`.
+
+## Documentation Map
+
+The English documentation is split into focused sections:
+
+* [Configuration](/configuration) covers the available settings and database storage.
+* [Toolbar Usage](/toolbar) introduces the built-in panels.
+* [History Panel](/history-panel) explains request history and replaying prior requests.
+* [Mail Panel](/mail-panel) shows mail capture and preview support.
+* [Custom Panels](/custom-panels) explains how to build your own panels.
+* [API Requests](/api-requests) covers toolbar access for API-only applications.
+* [Helper Functions](/helper-functions) documents the SQL helpers, timers, and query tracing.
diff --git a/docs/en/mail-panel.md b/docs/en/mail-panel.md
new file mode 100644
index 000000000..d2a1ea17e
--- /dev/null
+++ b/docs/en/mail-panel.md
@@ -0,0 +1,32 @@
+# Mail Panel
+
+The Mail panel captures all mail sent during a request so that you can inspect it safely during development.
+
+
+
+DebugKit also supports preview classes so that you can render mails without sending them.
+
+
+
+## Creating Preview Classes
+
+To preview mail before sending it, create a preview class that defines the recipient and any template variables required by the mailer method:
+
+```php
+namespace App\Mailer\Preview;
+
+use DebugKit\Mailer\MailPreview;
+
+class WelcomePreview extends MailPreview
+{
+ public function welcome(): \Cake\Mailer\Mailer
+ {
+ $mailer = $this->getMailer('Welcome');
+ // Set any template variables and recipients for the mailer.
+
+ return $mailer;
+ }
+}
+```
+
+Preview classes should live in the `Mailer\Preview` namespace of your application or plugin and use the `Preview` suffix.
diff --git a/docs/en/toolbar.md b/docs/en/toolbar.md
new file mode 100644
index 000000000..f4b87be52
--- /dev/null
+++ b/docs/en/toolbar.md
@@ -0,0 +1,21 @@
+# Toolbar Usage
+
+The DebugKit toolbar appears after you click the CakePHP icon in the lower-right corner of the browser. Each panel combines a panel class with a view element and focuses on a specific kind of runtime information.
+
+Built-in panels include:
+
+* **Cache** shows cache usage during the request and lets you clear caches.
+* **Environment** shows PHP and CakePHP environment details.
+* **History** lists previous requests and lets you inspect their panel data.
+* **Include** groups included files by type.
+* **Log** shows log entries created during the request.
+* **Packages** lists installed dependencies, their versions, and outdated packages.
+* **Mail** captures mail sent during the request and supports previews.
+* **Request** shows request data, route information, cookies, and request parameters.
+* **Session** displays the active session contents.
+* **Sql Logs** shows SQL logs for each datasource.
+* **Timer** displays timers from `DebugKit\DebugTimer` and memory readings from `DebugKit\DebugMemory`.
+* **Variables** shows view variables set in the controller.
+* **Deprecations** renders deprecation warnings in a less disruptive format.
+
+You can use the built-in panels as-is or register your own custom panels alongside them.
diff --git a/docs/fr/index.md b/docs/fr/index.md
new file mode 100644
index 000000000..8cb020767
--- /dev/null
+++ b/docs/fr/index.md
@@ -0,0 +1,128 @@
+# Debug Kit
+
+DebugKit est un plugin supporté par la core team qui fournit une toolbar et des outils de débogage avancés pour les applications CakePHP. Il permet d'inspecter rapidement la configuration, les logs, les requêtes SQL et les données de temps d'exécution.
+
+::: warning
+DebugKit est destiné uniquement aux environnements de développement local à utilisateur unique. Évitez de l'utiliser sur un environnement partagé, de préproduction ou tout autre environnement où les variables d'environnement et la configuration doivent rester privées.
+:::
+
+## Installation
+
+Par défaut, DebugKit est installé avec le squelette d'application. Si vous l'avez retiré, réinstallez-le depuis le répertoire racine de l'application :
+
+```bash
+php composer.phar require --dev cakephp/debug_kit:"^5.0"
+```
+
+Chargez ensuite le plugin :
+
+```bash
+bin/cake plugin load DebugKit --only-debug
+```
+
+## Configuration
+
+* `DebugKit.ignoreAuthorization` doit être défini à `true` si vous souhaitez ignorer le plugin Cake Authorization pour les requêtes DebugKit.
+
+## Stockage de DebugKit
+
+Par défaut, DebugKit utilise une base SQLite dans le répertoire `tmp` de l'application pour stocker les données des panneaux. Si vous souhaitez utiliser une autre base, définissez une connexion `debug_kit` dans `config/app.php`.
+
+## Utilisation de la Toolbar
+
+La toolbar DebugKit comprend plusieurs panneaux accessibles depuis l'icône CakePHP en bas à droite du navigateur.
+
+Chaque panneau inspecte un aspect différent de l'application :
+
+* **Cache** montre l'utilisation du cache et permet de le vider.
+* **Environment** affiche les variables d'environnement liées à PHP et CakePHP.
+* **History** affiche la liste des requêtes précédentes et permet de recharger leurs données.
+* **Include** montre les fichiers inclus par type.
+* **Log** affiche les écritures de log de la requête.
+* **Packages** affiche les dépendances installées et les versions obsolètes.
+* **Mail** affiche les emails envoyés pendant la requête.
+* **Request** affiche les informations de requête, de route et les cookies.
+* **Session** affiche le contenu de la session.
+* **Sql Logs** affiche les logs SQL pour chaque connexion.
+* **Timer** affiche les timers créés avec `DebugKit\\DebugTimer` ainsi que l'usage mémoire via `DebugKit\\DebugMemory`.
+* **Variables** affiche les variables de vue définies par le contrôleur.
+
+## Utiliser le panneau History
+
+Le panneau History permet de consulter les données de requêtes précédentes, y compris après une erreur ou une redirection.
+
+
+
+Lorsque des données historiques sont chargées, les titres des panneaux changent pour indiquer que vous ne regardez plus la requête active.
+
+
+
+## Utiliser le panneau Mail
+
+Le panneau Mail permet de suivre tous les emails envoyés pendant une requête.
+
+
+
+## Développer vos propres panneaux
+
+Vous pouvez créer vos propres panneaux personnalisés pour DebugKit.
+
+### Créer une classe de panneau
+
+Les classes doivent être placées dans `src/Panel` et étendre `DebugKit\\DebugPanel` :
+
+```php
+namespace App\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ // ...
+}
+```
+
+### Callbacks
+
+Par défaut, les panneaux s'abonnent aux événements `Controller.initialize` et `Controller.shutdown`. Si vous avez besoin d'autres événements, implémentez `implementedEvents()`.
+
+### Éléments de panneau
+
+Chaque panneau s'appuie sur un élément de vue. Le nom suit la convention underscore de la classe, par exemple `SessionPanel` devient `session_panel.php`.
+
+### Titres et éléments personnalisés
+
+Vous pouvez redéfinir :
+
+* `title()` pour configurer le titre de la toolbar.
+* `elementName()` pour choisir l'élément utilisé.
+
+### Méthodes de hook
+
+Les méthodes suivantes permettent de personnaliser le comportement du panneau :
+
+* `shutdown(Event $event)` collecte les données.
+* `summary()` retourne un résumé affiché dans la toolbar réduite.
+* `data()` retourne des données sérialisables pour l'élément.
+
+### Panneaux dans d'autres plugins
+
+Pour un panneau fourni par un plugin, définissez `public $plugin` avec le nom du répertoire du plugin :
+
+```php
+namespace MyPlugin\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ public string $plugin = 'MyPlugin';
+}
+```
+
+Pour enregistrer un panneau applicatif ou plugin :
+
+```php
+Configure::write('DebugKit.panels', ['App', 'MyPlugin.MyCustom']);
+$this->addPlugin('DebugKit', ['bootstrap' => true]);
+```
diff --git a/docs/ja/index.md b/docs/ja/index.md
new file mode 100644
index 000000000..4715ae4ea
--- /dev/null
+++ b/docs/ja/index.md
@@ -0,0 +1,150 @@
+# Debug Kit
+
+DebugKit は CakePHP アプリケーション向けのデバッグツールバーと拡張デバッグツールを提供します。設定データ、ログメッセージ、SQL クエリー、実行時間情報をすばやく確認できます。
+
+::: warning
+DebugKit は単一ユーザーのローカル開発環境でのみ使用してください。共有開発環境、ステージング環境、本番に近い環境では使用しないでください。
+:::
+
+## インストール
+
+アプリケーションのルートディレクトリーで次を実行します。
+
+```bash
+php composer.phar require --dev cakephp/debug_kit:"^5.0"
+```
+
+続いてプラグインを有効化します。
+
+```bash
+bin/cake plugin load DebugKit --only-debug
+```
+
+## 設定
+
+* `DebugKit.panels` で各パネルの有効化と無効化ができます。
+* `DebugKit.includeSchemaReflection` を `true` にするとスキーマリフレクションのクエリーを記録します。
+* `DebugKit.safeTld` でローカル開発用の TLD を追加できます。
+* `DebugKit.forceEnable` で DebugKit を強制表示できます。
+
+## データベース設定
+
+デフォルトでは `tmp` ディレクトリー内の SQLite データベースにパネルデータを保存します。`pdo_sqlite` が使えない場合は、`config/app.php` に `debug_kit` コネクションを定義してください。
+
+## ツールバーの使い方
+
+DebugKit ツールバーはブラウザー右下の CakePHP アイコンから開けます。各パネルはアプリケーションの異なる側面を表示します。
+
+* **Cache** キャッシュ使用状況の確認と削除。
+* **Environment** PHP と CakePHP の環境情報。
+* **History** 過去のリクエスト一覧とそのデータの再表示。
+* **Include** 読み込まれたファイル一覧。
+* **Log** リクエスト中に書かれたログ。
+* **Packages** 依存パッケージとバージョン情報。
+* **Mail** 送信メールの確認とプレビュー。
+* **Request** 現在のリクエスト情報、ルート、Cookie。
+* **Session** セッション内容。
+* **Sql Logs** 接続ごとの SQL ログ。
+* **Timer** `DebugKit\\DebugTimer` と `DebugKit\\DebugMemory` の情報。
+* **Variables** ビュー変数。
+
+## 履歴パネルを使う
+
+履歴パネルではエラーやリダイレクトを含む過去のリクエストを確認できます。
+
+
+
+過去のデータが読み込まれると、パネルタイトルが切り替わり、現在のリクエストではないことが分かります。
+
+
+
+## メールパネルを使う
+
+メールパネルではリクエスト中に送信されたすべてのメールを確認できます。
+
+
+
+メールプレビューを使うと、送信前に内容を確認できます。
+
+
+
+### プレビュークラスの作成
+
+```php
+namespace App\Mailer\Preview;
+
+use DebugKit\Mailer\MailPreview;
+
+class WelcomePreview extends MailPreview
+{
+ public function welcome(): \Cake\Mailer\Mailer
+ {
+ $mailer = $this->getMailer('Welcome');
+
+ return $mailer;
+ }
+}
+```
+
+`MailPreview` クラスは `Mailer\Preview` 名前空間に配置し、クラス名は `Preview` で終える必要があります。
+
+## 独自パネルの開発
+
+### パネルクラスを作成する
+
+パネルクラスは `src/Panel` に配置し、`DebugPanel` を継承します。
+
+```php
+namespace App\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ // ...
+}
+```
+
+### コールバック
+
+デフォルトでは `Controller.initialize` と `Controller.shutdown` を購読します。追加イベントが必要なら `implementedEvents()` を定義してください。
+
+### パネル要素
+
+パネル表示用のビュー要素を用意します。名前はクラス名のアンダースコア形式です。例えば `SessionPanel` は `session_panel.php` を使います。
+
+### カスタムタイトルとエレメント
+
+* `title()` はツールバー上のタイトルを設定します。
+* `elementName()` は使用するエレメント名を設定します。
+
+### パネルフックメソッド
+
+* `shutdown(Event $event)` はデータを収集します。
+* `summary()` は折りたたみ時のサマリー文字列を返します。
+* `data()` はシリアライズ可能なパネルデータを返します。
+
+### 他のプラグインのパネル
+
+```php
+namespace MyPlugin\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ public string $plugin = 'MyPlugin';
+}
+```
+
+アプリケーションまたはプラグインのパネルを追加するには、DebugKit 設定を更新します。
+
+```php
+Configure::write('DebugKit.panels', ['App', 'MyPlugin.MyCustom']);
+$this->addPlugin('DebugKit', ['bootstrap' => true]);
+```
+
+## ヘルパー関数
+
+* `sql()` は ORM クエリーの SQL をダンプします。
+* `sqld()` は ORM クエリーの SQL をダンプして終了します。
diff --git a/docs/package-lock.json b/docs/package-lock.json
new file mode 100644
index 000000000..568027041
--- /dev/null
+++ b/docs/package-lock.json
@@ -0,0 +1,1856 @@
+{
+ "name": "docs",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "docs",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@cakephp/docs-skeleton": "git+ssh://git@github.com:cakephp/docs-skeleton.git#node-package",
+ "vitepress": "^2.0.0-alpha.16"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@cakephp/docs-skeleton": {
+ "version": "1.0.0",
+ "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#9501c6d47a80645604fceb31328368eeb6a21bc6",
+ "bin": {
+ "cakedocs": "bin/cakedocs.js"
+ },
+ "peerDependencies": {
+ "vitepress": "^2.0.0-alpha.15"
+ }
+ },
+ "node_modules/@docsearch/css": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.7.0.tgz",
+ "integrity": "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==",
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/js": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.7.0.tgz",
+ "integrity": "sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA==",
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/sidepanel-js": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.7.0.tgz",
+ "integrity": "sha512-A8r34jCU8kcIk2viECEn2msA28ojUF1BLi/3v5OWWc5G2N3jOuuumBXoeYjfr8dA0UxgFSy5R2bt12dnFJQSyA==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify-json/simple-icons": {
+ "version": "1.2.93",
+ "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.93.tgz",
+ "integrity": "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@iconify/types": "*"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.144.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
+ "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
+ "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
+ "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
+ "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
+ "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
+ "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
+ "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
+ "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
+ "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
+ "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
+ "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
+ "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
+ "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "license": "MIT"
+ },
+ "node_modules/@shikijs/core": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz",
+ "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/primitive": "4.4.3",
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5",
+ "hast-util-to-html": "^9.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/engine-javascript": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz",
+ "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "oniguruma-to-es": "^4.3.6"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz",
+ "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz",
+ "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/primitive": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz",
+ "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz",
+ "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/transformers": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.4.3.tgz",
+ "integrity": "sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "4.4.3",
+ "@shikijs/types": "4.4.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz",
+ "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@shikijs/vscode-textmate": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
+ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.8",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
+ "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz",
+ "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/shared": "3.5.41",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz",
+ "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz",
+ "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@vue/compiler-core": "3.5.41",
+ "@vue/compiler-dom": "3.5.41",
+ "@vue/compiler-ssr": "3.5.41",
+ "@vue/shared": "3.5.41",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.19",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz",
+ "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz",
+ "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^8.2.1"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz",
+ "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^8.2.1",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "perfect-debounce": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz",
+ "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz",
+ "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz",
+ "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz",
+ "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.41",
+ "@vue/runtime-core": "3.5.41",
+ "@vue/shared": "3.5.41",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz",
+ "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.41",
+ "@vue/runtime-dom": "3.5.41",
+ "@vue/shared": "3.5.41"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz",
+ "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
+ "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.4.0",
+ "@vueuse/shared": "14.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/integrations": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.4.0.tgz",
+ "integrity": "sha512-oJz9qTgczvA7L1nXQFRU7h8tQbOCoiceqvMMhT9XYMyOGTqLJ2rEa09PON+nD2t48sZUfeOmg4eaWJXV4sZb/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "14.4.0",
+ "@vueuse/shared": "14.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "async-validator": "^4",
+ "axios": "^1",
+ "change-case": "^5",
+ "drauu": "^0.4",
+ "focus-trap": "^7 || ^8",
+ "fuse.js": "^7",
+ "idb-keyval": "^6",
+ "jwt-decode": "^4",
+ "nprogress": "^0.2",
+ "qrcode": "^1.5",
+ "sortablejs": "^1",
+ "universal-cookie": "^7 || ^8",
+ "vue": "^3.5.0"
+ },
+ "peerDependenciesMeta": {
+ "async-validator": {
+ "optional": true
+ },
+ "axios": {
+ "optional": true
+ },
+ "change-case": {
+ "optional": true
+ },
+ "drauu": {
+ "optional": true
+ },
+ "focus-trap": {
+ "optional": true
+ },
+ "fuse.js": {
+ "optional": true
+ },
+ "idb-keyval": {
+ "optional": true
+ },
+ "jwt-decode": {
+ "optional": true
+ },
+ "nprogress": {
+ "optional": true
+ },
+ "qrcode": {
+ "optional": true
+ },
+ "sortablejs": {
+ "optional": true
+ },
+ "universal-cookie": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
+ "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
+ "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/focus-trap": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.2.tgz",
+ "integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==",
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.5.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/hast-util-to-html": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "stringify-entities": "^4.0.0",
+ "zwitch": "^2.0.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mark.js": {
+ "version": "8.11.1",
+ "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
+ "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/minisearch": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz",
+ "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/oniguruma-parser": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
+ "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
+ "license": "MIT"
+ },
+ "node_modules/oniguruma-to-es": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
+ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
+ "license": "MIT",
+ "dependencies": {
+ "oniguruma-parser": "^0.12.2",
+ "regex": "^6.1.0",
+ "regex-recursion": "^6.0.2"
+ }
+ },
+ "node_modules/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-recursion": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-utilities": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
+ "license": "MIT"
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
+ "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.144.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.2.4",
+ "@rolldown/binding-darwin-arm64": "1.2.4",
+ "@rolldown/binding-darwin-x64": "1.2.4",
+ "@rolldown/binding-freebsd-x64": "1.2.4",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.4",
+ "@rolldown/binding-linux-arm64-musl": "1.2.4",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.4",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-gnu": "1.2.4",
+ "@rolldown/binding-linux-x64-musl": "1.2.4",
+ "@rolldown/binding-openharmony-arm64": "1.2.4",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.4",
+ "@rolldown/binding-win32-x64-msvc": "1.2.4"
+ }
+ },
+ "node_modules/shiki": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz",
+ "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "4.4.3",
+ "@shikijs/engine-javascript": "4.4.3",
+ "@shikijs/engine-oniguruma": "4.4.3",
+ "@shikijs/langs": "4.4.3",
+ "@shikijs/themes": "4.4.3",
+ "@shikijs/types": "4.4.3",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
+ "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
+ "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.25",
+ "rolldown": "~1.2.1",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitepress": {
+ "version": "2.0.0-alpha.19",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.19.tgz",
+ "integrity": "sha512-WnBsb0Bwr43kXKyiis+lld/7ri3hnMbthS8N3hpFtjjwsdLO4IRmiAE08D7aud4q6oMDf9uwRowxzNqRFe/amw==",
+ "license": "MIT",
+ "dependencies": {
+ "@docsearch/css": "^4.7.0",
+ "@docsearch/js": "^4.7.0",
+ "@docsearch/sidepanel-js": "^4.7.0",
+ "@iconify-json/simple-icons": "^1.2.92",
+ "@shikijs/core": "^4.4.1",
+ "@shikijs/transformers": "^4.4.1",
+ "@shikijs/types": "^4.4.1",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^6.0.8",
+ "@vue/devtools-api": "^8.2.1",
+ "@vue/shared": "^3.5.40",
+ "@vueuse/core": "^14.4.0",
+ "@vueuse/integrations": "^14.4.0",
+ "focus-trap": "^8.2.2",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.2.0",
+ "shiki": "^4.4.1",
+ "vite": "^8.2.0",
+ "vue": "^3.5.40"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.41",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz",
+ "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.41",
+ "@vue/compiler-sfc": "3.5.41",
+ "@vue/runtime-dom": "3.5.41",
+ "@vue/server-renderer": "3.5.41",
+ "@vue/shared": "3.5.41"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 000000000..f1407730a
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "docs",
+ "version": "1.0.0",
+ "description": "",
+ "main": "config.js",
+ "scripts": {
+ "docs:dev": "vitepress dev",
+ "docs:build": "vitepress build",
+ "docs:preview": "vitepress preview"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "module",
+ "dependencies": {
+ "@cakephp/docs-skeleton": "git+ssh://git@github.com:cakephp/docs-skeleton.git#node-package",
+ "vitepress": "^2.0.0-alpha.16"
+ }
+}
diff --git a/docs/pt/index.md b/docs/pt/index.md
new file mode 100644
index 000000000..460609d04
--- /dev/null
+++ b/docs/pt/index.md
@@ -0,0 +1,112 @@
+# Debug Kit
+
+DebugKit oferece uma barra de ferramentas de depuração e ferramentas avançadas para aplicações CakePHP. Ele permite visualizar rapidamente dados de configuração, mensagens de log, consultas SQL e informações de tempo de execução.
+
+::: warning
+DebugKit deve ser usado apenas em ambientes locais de desenvolvimento para um único usuário. Evite utilizá-lo em ambientes compartilhados, de homologação ou próximos de produção.
+:::
+
+## Instalação
+
+No diretório raiz da aplicação, execute:
+
+```bash
+php composer.phar require --dev cakephp/debug_kit:"^5.0"
+```
+
+Depois carregue o plugin:
+
+```bash
+bin/cake plugin load DebugKit --only-debug
+```
+
+## Armazenamento do DebugKit
+
+Por padrão, o DebugKit usa um banco SQLite no diretório `tmp` da aplicação para armazenar os dados dos painéis. Se quiser usar outro banco, defina uma conexão `debug_kit` em `config/app.php`.
+
+## Uso da barra de ferramentas
+
+A toolbar do DebugKit é exibida ao clicar no ícone do CakePHP no canto inferior direito do navegador.
+
+Cada painel mostra uma parte diferente da aplicação:
+
+* **Cache** mostra o uso de cache e permite limpá-lo.
+* **Environment** exibe variáveis de ambiente relacionadas a PHP e CakePHP.
+* **History** mostra requisições anteriores e permite recarregar seus dados.
+* **Include** exibe os arquivos incluídos por tipo.
+* **Log** mostra as entradas de log da requisição.
+* **Request** exibe dados da requisição atual, rota e cookies.
+* **Session** exibe o conteúdo da sessão.
+* **Sql Logs** mostra logs SQL por conexão.
+* **Timer** exibe timers criados com `DebugKit\\DebugTimer` e dados de memória com `DebugKit\\DebugMemory`.
+* **Variables** exibe variáveis de view definidas no controller.
+
+## Usando o painel History
+
+O painel History permite revisar dados de requisições anteriores, incluindo erros e redirecionamentos.
+
+
+
+
+
+## Desenvolvendo seus próprios painéis
+
+Você pode criar painéis personalizados para expor dados específicos da sua aplicação.
+
+### Criando uma classe de painel
+
+As classes devem ser colocadas em `src/Panel` e estender `DebugKit\DebugPanel`:
+
+```php
+namespace App\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ // ...
+}
+```
+
+### Callbacks
+
+Por padrão os painéis se inscrevem nos eventos `Controller.initialize` e `Controller.shutdown`. Se precisar de eventos adicionais, implemente `implementedEvents()`.
+
+### Elementos do painel
+
+Cada painel precisa de um elemento de view. O nome deve ser a versão underscore do nome da classe, por exemplo `session_panel.php`.
+
+### Títulos e elementos personalizados
+
+Você pode sobrescrever:
+
+* `title()` para definir o título na toolbar.
+* `elementName()` para escolher qual elemento renderizar.
+
+### Métodos de hook
+
+* `shutdown(Event $event)` coleta e prepara os dados do painel.
+* `summary()` retorna um resumo curto para a toolbar recolhida.
+* `data()` retorna dados serializáveis usados pelo elemento.
+
+### Painéis em outros plugins
+
+```php
+namespace MyPlugin\Panel;
+
+use DebugKit\DebugPanel;
+
+class MyCustomPanel extends DebugPanel
+{
+ public string $plugin = 'MyPlugin';
+}
+```
+
+Para registrar um painel do aplicativo ou de plugin:
+
+```php
+Configure::write(
+ 'DebugKit.panels',
+ array_merge(Configure::read('DebugKit.panels'), ['MyCustomPanel'])
+);
+```
diff --git a/docs/public/favicon/apple-touch-icon.png b/docs/public/favicon/apple-touch-icon.png
new file mode 100644
index 000000000..c6d073d7b
Binary files /dev/null and b/docs/public/favicon/apple-touch-icon.png differ
diff --git a/docs/public/favicon/favicon-96x96.png b/docs/public/favicon/favicon-96x96.png
new file mode 100644
index 000000000..6642e0cda
Binary files /dev/null and b/docs/public/favicon/favicon-96x96.png differ
diff --git a/docs/public/favicon/favicon.ico b/docs/public/favicon/favicon.ico
new file mode 100644
index 000000000..405aa94ce
Binary files /dev/null and b/docs/public/favicon/favicon.ico differ
diff --git a/docs/public/favicon/favicon.svg b/docs/public/favicon/favicon.svg
new file mode 100644
index 000000000..805ef4b8f
--- /dev/null
+++ b/docs/public/favicon/favicon.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/docs/public/favicon/site.webmanifest b/docs/public/favicon/site.webmanifest
new file mode 100644
index 000000000..4f23fb31d
--- /dev/null
+++ b/docs/public/favicon/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "CakePHP",
+ "short_name": "CakePHP",
+ "icons": [
+ {
+ "src": "/favicon/web-app-manifest-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/favicon/web-app-manifest-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
\ No newline at end of file
diff --git a/docs/public/favicon/web-app-manifest-192x192.png b/docs/public/favicon/web-app-manifest-192x192.png
new file mode 100644
index 000000000..b5df2990b
Binary files /dev/null and b/docs/public/favicon/web-app-manifest-192x192.png differ
diff --git a/docs/public/favicon/web-app-manifest-512x512.png b/docs/public/favicon/web-app-manifest-512x512.png
new file mode 100644
index 000000000..6a522de35
Binary files /dev/null and b/docs/public/favicon/web-app-manifest-512x512.png differ
diff --git a/docs/public/fonts/cakedingbats-webfont.eot b/docs/public/fonts/cakedingbats-webfont.eot
new file mode 100644
index 000000000..0800d1e7d
Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.eot differ
diff --git a/docs/public/fonts/cakedingbats-webfont.svg b/docs/public/fonts/cakedingbats-webfont.svg
new file mode 100644
index 000000000..d2afda5e2
--- /dev/null
+++ b/docs/public/fonts/cakedingbats-webfont.svg
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/public/fonts/cakedingbats-webfont.ttf b/docs/public/fonts/cakedingbats-webfont.ttf
new file mode 100644
index 000000000..78ad6c884
Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.ttf differ
diff --git a/docs/public/fonts/cakedingbats-webfont.woff b/docs/public/fonts/cakedingbats-webfont.woff
new file mode 100644
index 000000000..a95e1b38b
Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.woff differ
diff --git a/docs/public/fonts/cakedingbats-webfont.woff2 b/docs/public/fonts/cakedingbats-webfont.woff2
new file mode 100644
index 000000000..2cd9fdd0e
Binary files /dev/null and b/docs/public/fonts/cakedingbats-webfont.woff2 differ
diff --git a/docs/public/history-panel-use.mp4 b/docs/public/history-panel-use.mp4
new file mode 100644
index 000000000..d87dc65ab
Binary files /dev/null and b/docs/public/history-panel-use.mp4 differ
diff --git a/docs/public/history-panel.png b/docs/public/history-panel.png
new file mode 100644
index 000000000..0c6b27e20
Binary files /dev/null and b/docs/public/history-panel.png differ
diff --git a/docs/public/icons/batteries_included_chiffon.svg b/docs/public/icons/batteries_included_chiffon.svg
new file mode 100644
index 000000000..1972a9673
--- /dev/null
+++ b/docs/public/icons/batteries_included_chiffon.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/icons/build_quickly_chiffon.svg b/docs/public/icons/build_quickly_chiffon.svg
new file mode 100644
index 000000000..d834c9c43
--- /dev/null
+++ b/docs/public/icons/build_quickly_chiffon.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/icons/license_chiffon.svg b/docs/public/icons/license_chiffon.svg
new file mode 100644
index 000000000..4f4d2f362
--- /dev/null
+++ b/docs/public/icons/license_chiffon.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/icons/mvc_chiffon.svg b/docs/public/icons/mvc_chiffon.svg
new file mode 100644
index 000000000..78b621f04
--- /dev/null
+++ b/docs/public/icons/mvc_chiffon.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/icons/no_config_chiffon.svg b/docs/public/icons/no_config_chiffon.svg
new file mode 100644
index 000000000..80b5c29ab
--- /dev/null
+++ b/docs/public/icons/no_config_chiffon.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/icons/secure_chiffon.svg b/docs/public/icons/secure_chiffon.svg
new file mode 100644
index 000000000..b85f60407
--- /dev/null
+++ b/docs/public/icons/secure_chiffon.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/public/logo.svg b/docs/public/logo.svg
new file mode 100644
index 000000000..829c8e98d
--- /dev/null
+++ b/docs/public/logo.svg
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/docs/public/mail-panel.mp4 b/docs/public/mail-panel.mp4
new file mode 100644
index 000000000..02078c858
Binary files /dev/null and b/docs/public/mail-panel.mp4 differ
diff --git a/docs/public/mail-previewer.mp4 b/docs/public/mail-previewer.mp4
new file mode 100644
index 000000000..66f9fdb97
Binary files /dev/null and b/docs/public/mail-previewer.mp4 differ
diff --git a/locale/debug_kit.pot b/locale/debug_kit.pot
deleted file mode 100644
index df4988dd1..000000000
--- a/locale/debug_kit.pot
+++ /dev/null
@@ -1,138 +0,0 @@
-# LANGUAGE translation of Debug Kit Application
-# Copyright 2008 Andy Dawson
-# No version information was available in the source files.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: debug_kit-\n"
-"POT-Creation-Date: 2009-05-27 09:47+0200\n"
-"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
-"Last-Translator: Andy Dawson \n"
-"Language-Team:\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Poedit-Basepath: ../../../\n"
-
-#: controllers/components/toolbar.php:91
-msgid "Component initialization and startup"
-msgstr ""
-
-#: controllers/components/toolbar.php:140
-msgid "Controller Action"
-msgstr ""
-
-#: controllers/components/toolbar.php:167
-msgid "Render Controller Action"
-msgstr ""
-
-#: controllers/components/toolbar.php:231
-msgid "Could not load DebugToolbar panel %s"
-msgstr ""
-
-#: views/elements/debug_toolbar.ctp:25
-msgid "There are no active panels. You must enable a panel to see its output."
-msgstr ""
-
-#: views/elements/history_panel.ctp:21
-msgid "Request History"
-msgstr ""
-
-#: views/elements/history_panel.ctp:23
-msgid "No previous requests logged."
-msgstr ""
-
-#: views/elements/history_panel.ctp:25
-msgid "previous requests available"
-msgstr ""
-
-#: views/elements/history_panel.ctp:27
-msgid "Restore to current request"
-msgstr ""
-
-#: views/elements/log_panel.ctp:21
-msgid "Logs"
-msgstr ""
-
-#: views/elements/log_panel.ctp:28
-msgid "Time"
-msgstr ""
-
-#: views/elements/log_panel.ctp:28
-#: views/elements/timer_panel.ctp:54
-msgid "Message"
-msgstr ""
-
-#: views/elements/log_panel.ctp:37
-msgid "There were no log entries made this request"
-msgstr ""
-
-#: views/elements/request_panel.ctp:21
-msgid "Request"
-msgstr ""
-
-#: views/elements/request_panel.ctp:35
-msgid "Current Route"
-msgstr ""
-
-#: views/elements/session_panel.ctp:21
-msgid "Session"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:21
-msgid "Sql Logs"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:31
-msgid "toggle (%s) query explains for %s"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:39
-msgid "No slow queries!, or your database does not support EXPLAIN"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:44
-msgid "No active database connections"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:33
-msgid "Memory"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:35
-msgid "Current Memory Use"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:39
-msgid "Peak Memory Use"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:43
-msgid "Timers"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:45
-msgid "%s (ms)"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:46
-msgid "Total Request Time:"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:54
-msgid "Time in ms"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:54
-msgid "Graph"
-msgstr ""
-
-#: views/elements/variables_panel.ctp:21
-msgid "View Variables"
-msgstr ""
-
-#: views/helpers/simple_graph.php:79
-msgid "Starting %sms into the request, taking %sms"
-msgstr ""
-
diff --git a/locale/eng/LC_MESSAGES/debug_kit.po b/locale/eng/LC_MESSAGES/debug_kit.po
deleted file mode 100644
index 9aec83297..000000000
--- a/locale/eng/LC_MESSAGES/debug_kit.po
+++ /dev/null
@@ -1,135 +0,0 @@
-# LANGUAGE translation of CakePHP Application
-# Copyright YEAR NAME
-# No version information was available in the source files.
-#
-#, fuzzy
-msgid ""
-msgstr "Project-Id-Version: PROJECT VERSION\n"
- "POT-Creation-Date: 2009-05-27 09:47+0200\n"
- "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
- "Last-Translator: NAME \n"
- "Language-Team: LANGUAGE \n"
- "MIME-Version: 1.0\n"
- "Content-Type: text/plain; charset=UTF-8\n"
- "Content-Transfer-Encoding: 8bit\n"
- "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-
-#: controllers/components/toolbar.php:91
-msgid "Component initialization and startup"
-msgstr ""
-
-#: controllers/components/toolbar.php:140
-msgid "Controller Action"
-msgstr ""
-
-#: controllers/components/toolbar.php:167
-msgid "Render Controller Action"
-msgstr ""
-
-#: controllers/components/toolbar.php:231
-msgid "Could not load DebugToolbar panel %s"
-msgstr ""
-
-#: views/elements/debug_toolbar.ctp:25
-msgid "There are no active panels. You must enable a panel to see its output."
-msgstr ""
-
-#: views/elements/history_panel.ctp:21
-msgid "Request History"
-msgstr ""
-
-#: views/elements/history_panel.ctp:23
-msgid "No previous requests logged."
-msgstr ""
-
-#: views/elements/history_panel.ctp:25
-msgid "previous requests available"
-msgstr ""
-
-#: views/elements/history_panel.ctp:27
-msgid "Restore to current request"
-msgstr ""
-
-#: views/elements/log_panel.ctp:21
-msgid "Logs"
-msgstr ""
-
-#: views/elements/log_panel.ctp:28
-msgid "Time"
-msgstr ""
-
-#: views/elements/log_panel.ctp:28 views/elements/timer_panel.ctp:54
-msgid "Message"
-msgstr ""
-
-#: views/elements/log_panel.ctp:37
-msgid "There were no log entries made this request"
-msgstr ""
-
-#: views/elements/request_panel.ctp:21
-msgid "Request"
-msgstr ""
-
-#: views/elements/request_panel.ctp:35
-msgid "Current Route"
-msgstr ""
-
-#: views/elements/session_panel.ctp:21
-msgid "Session"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:21
-msgid "Sql Logs"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:31
-msgid "toggle (%s) query explains for %s"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:39
-msgid "No slow queries!, or your database does not support EXPLAIN"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:44
-msgid "No active database connections"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:33
-msgid "Memory"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:35
-msgid "Current Memory Use"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:39
-msgid "Peak Memory Use"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:43
-msgid "Timers"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:45
-msgid "%s (ms)"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:46
-msgid "Total Request Time:"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:54
-msgid "Time in ms"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:54
-msgid "Graph"
-msgstr ""
-
-#: views/elements/variables_panel.ctp:21
-msgid "View Variables"
-msgstr ""
-
-#: views/helpers/simple_graph.php:79
-msgid "Starting %sms into the request, taking %sms"
-msgstr ""
diff --git a/locale/spa/LC_MESSAGES/debug_kit.po b/locale/spa/LC_MESSAGES/debug_kit.po
deleted file mode 100644
index 0833a4942..000000000
--- a/locale/spa/LC_MESSAGES/debug_kit.po
+++ /dev/null
@@ -1,135 +0,0 @@
-# LANGUAGE translation of CakePHP Application
-# Copyright YEAR NAME
-# No version information was available in the source files.
-#
-#, fuzzy
-msgid ""
-msgstr "Project-Id-Version: PROJECT VERSION\n"
- "POT-Creation-Date: 2009-05-27 09:47+0200\n"
- "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
- "Last-Translator: NAME \n"
- "Language-Team: LANGUAGE \n"
- "MIME-Version: 1.0\n"
- "Content-Type: text/plain; charset=UTF-8\n"
- "Content-Transfer-Encoding: 8bit\n"
- "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-
-#: controllers/components/toolbar.php:91
-msgid "Component initialization and startup"
-msgstr ""
-
-#: controllers/components/toolbar.php:140
-msgid "Controller Action"
-msgstr ""
-
-#: controllers/components/toolbar.php:167
-msgid "Render Controller Action"
-msgstr ""
-
-#: controllers/components/toolbar.php:231
-msgid "Could not load DebugToolbar panel %s"
-msgstr ""
-
-#: views/elements/debug_toolbar.ctp:25
-msgid "There are no active panels. You must enable a panel to see its output."
-msgstr ""
-
-#: views/elements/history_panel.ctp:21
-msgid "Request History"
-msgstr ""
-
-#: views/elements/history_panel.ctp:23
-msgid "No previous requests logged."
-msgstr ""
-
-#: views/elements/history_panel.ctp:25
-msgid "previous requests available"
-msgstr ""
-
-#: views/elements/history_panel.ctp:27
-msgid "Restore to current request"
-msgstr ""
-
-#: views/elements/log_panel.ctp:21
-msgid "Logs"
-msgstr ""
-
-#: views/elements/log_panel.ctp:28
-msgid "Time"
-msgstr ""
-
-#: views/elements/log_panel.ctp:28 views/elements/timer_panel.ctp:54
-msgid "Message"
-msgstr ""
-
-#: views/elements/log_panel.ctp:37
-msgid "There were no log entries made this request"
-msgstr ""
-
-#: views/elements/request_panel.ctp:21
-msgid "Request"
-msgstr ""
-
-#: views/elements/request_panel.ctp:35
-msgid "Current Route"
-msgstr ""
-
-#: views/elements/session_panel.ctp:21
-msgid "Session"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:21
-msgid "Sql Logs"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:31
-msgid "toggle (%s) query explains for %s"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:39
-msgid "No slow queries!, or your database does not support EXPLAIN"
-msgstr ""
-
-#: views/elements/sql_log_panel.ctp:44
-msgid "No active database connections"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:33
-msgid "Memory"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:35
-msgid "Current Memory Use"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:39
-msgid "Peak Memory Use"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:43
-msgid "Timers"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:45
-msgid "%s (ms)"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:46
-msgid "Total Request Time:"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:54
-msgid "Time in ms"
-msgstr ""
-
-#: views/elements/timer_panel.ctp:54
-msgid "Graph"
-msgstr ""
-
-#: views/elements/variables_panel.ctp:21
-msgid "View Variables"
-msgstr ""
-
-#: views/helpers/simple_graph.php:79
-msgid "Starting %sms into the request, taking %sms"
-msgstr ""
\ No newline at end of file
diff --git a/models/behaviors/timed.php b/models/behaviors/timed.php
deleted file mode 100644
index 28eadec74..000000000
--- a/models/behaviors/timed.php
+++ /dev/null
@@ -1,105 +0,0 @@
-settings[$Model->alias] = array_merge($this->_defaults, $settings);
- } else {
- $this->settings[$Model->alias] = $this->_defaults;
- }
- }
-
-/**
- * beforeFind, starts a timer for a find operation.
- *
- * @param Model $Model
- * @param array $queryData Array of query data (not modified)
- * @return boolean true
- */
- function beforeFind(&$Model, $queryData){
- DebugKitDebugger::startTimer($Model->alias . '_find', $Model->alias . '->find()');
- return true;
- }
-
-/**
- * afterFind, stops a timer for a find operation.
- *
- * @param Model $Model
- * @param array $results Array of results
- * @return boolean true.
- */
- function afterFind(&$Model, $results){
- DebugKitDebugger::stopTimer($Model->alias . '_find');
- return true;
- }
-
-/**
- * beforeSave, starts a time before a save is initiated.
- *
- * @param Model $Model
- * @return boolean true
- */
- function beforeSave(&$Model){
- DebugKitDebugger::startTimer($Model->alias . '_save', $Model->alias . '->save()');
- return true;
- }
-
-/**
- * afterSave, stop the timer started from a save.
- *
- * @param string $Model
- * @param string $created
- * @return void
- */
- function afterSave(&$Model, $created) {
- DebugKitDebugger::stopTimer($Model->alias . '_save');
- return true;
- }
- }
-
diff --git a/models/toolbar_access.php b/models/toolbar_access.php
deleted file mode 100644
index ab1fea99b..000000000
--- a/models/toolbar_access.php
+++ /dev/null
@@ -1,56 +0,0 @@
-config['driver'];
-
- $return = array();
- if ($driver === 'mysqli' || $driver === 'mysql' || $driver === 'postgres') {
- $explained = $db->query('EXPLAIN ' . $query);
- if ($driver === 'postgres') {
- $queryPlan = array();
- foreach ($explained as $postgreValue) {
- $queryPlan[] = array($postgreValue[0]['QUERY PLAN']);
- }
- $return = array_merge(array(array('')), $queryPlan);
- } else {
- $keys = array_keys($explained[0][0]);
- foreach ($explained as $mysqlValue) {
- $queryPlan[] = array_values($mysqlValue[0]);
- }
- $return = array_merge(array($keys), $queryPlan);
- }
- }
- return $return;
- }
-}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..e683acf89
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "debug_kit",
+ "version": "1.0.0",
+ "description": "CakePHP Debug Kit",
+ "author": "CakePHP Team",
+ "license": "MIT",
+ "homepage": "https://github.com/cakephp/debug_kit#readme",
+ "bugs": {
+ "url": "https://github.com/cakephp/debug_kit/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/cakephp/debug_kit.git"
+ },
+ "keywords": [
+ "cakephp",
+ "debug",
+ "kit"
+ ],
+ "devDependencies": {
+ "eslint": "^8.17.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-plugin-import": "^2.26.0"
+ },
+ "scripts": {
+ "cs-check": "npx eslint webroot/js/main.js webroot/js/inject-iframe.js webroot/js/modules/*.js",
+ "cs-fix": "npx eslint webroot/js/main.js webroot/js/inject-iframe.js webroot/js/modules/*.js --fix"
+ }
+}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 000000000..114bdf842
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,12 @@
+
+
+ src/
+ tests/
+
+
+
+
+
+
+
+
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 000000000..ee142bcd7
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,31 @@
+parameters:
+ ignoreErrors:
+ -
+ message: '#^Parameter \#1 \$request of method DebugKit\\ToolbarService\:\:saveData\(\) expects Cake\\Http\\ServerRequest, Psr\\Http\\Message\\ServerRequestInterface given\.$#'
+ identifier: argument.type
+ count: 1
+ path: src/Middleware/DebugKitMiddleware.php
+
+ -
+ message: '#^PHPDoc tag @property for property DebugKit\\Model\\Table\\PanelsTable\:\:\$Requests contains unresolvable type\.$#'
+ identifier: propertyTag.unresolvableType
+ count: 1
+ path: src/Model/Table/PanelsTable.php
+
+ -
+ message: '#^Call to an undefined method Cake\\ORM\\Locator\\LocatorInterface\:\:genericInstances\(\)\.$#'
+ identifier: method.notFound
+ count: 1
+ path: src/Panel/SqlLogPanel.php
+
+ -
+ message: '#^Dead catch \- Cake\\Core\\Exception\\CakeException is never thrown in the try block\.$#'
+ identifier: catch.neverThrown
+ count: 1
+ path: src/ToolbarService.php
+
+ -
+ message: '#^Unreachable statement \- code above always terminates\.$#'
+ identifier: deadCode.unreachable
+ count: 1
+ path: src/ToolbarService.php
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 000000000..2afad80c3
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,14 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: 8
+ treatPhpDocTypesAsCertain: false
+ bootstrapFiles:
+ - tests/bootstrap.php
+ paths:
+ - src/
+ ignoreErrors:
+ - identifier: missingType.generics
+ - identifier: missingType.iterableValue
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 000000000..2dbb160ac
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,36 @@
+
+
+
+
+ tests/TestCase
+
+
+
+
+
+
+
+
+
+ src/
+
+
+
+
+
+
+
+
+
+
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
new file mode 100644
index 000000000..a0523438c
--- /dev/null
+++ b/psalm-baseline.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ render()]]>
+ reset()]]>
+ viewBuilder()]]>
+ viewBuilder()]]>
+ viewBuilder()->setTemplate($method)]]>
+ getHeaders($extra)]]>
+
+
+
+
+ emailLog]]>
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 000000000..565cc8755
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cache/Engine/DebugEngine.php b/src/Cache/Engine/DebugEngine.php
new file mode 100644
index 000000000..3431aa8cc
--- /dev/null
+++ b/src/Cache/Engine/DebugEngine.php
@@ -0,0 +1,380 @@
+
+ */
+ protected array $metrics = [
+ 'set' => 0,
+ 'delete' => 0,
+ 'get hit' => 0,
+ 'get miss' => 0,
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\Cache\CacheEngine|array $config Config data or the proxied adapter.
+ * @param string $name The name of the proxied cache engine.
+ * @param \Psr\Log\LoggerInterface $logger Logger for collecting cache operation logs.
+ */
+ public function __construct(CacheEngine|array $config, string $name, LoggerInterface $logger)
+ {
+ if ($config instanceof CacheEngine) {
+ $this->_engine = $config;
+ } else {
+ $this->_config = $config;
+ }
+
+ $this->logger = $logger;
+ $this->name = $name;
+ }
+
+ /**
+ * Initialize the proxied Cache Engine
+ *
+ * @param array $config Array of setting for the engine.
+ * @return bool True, this engine cannot fail to initialize.
+ */
+ public function init(array $config = []): bool
+ {
+ if (!isset($this->_engine)) {
+ $registry = new CacheRegistry();
+ $this->_engine = $registry->load('spies', $this->_config);
+ unset($registry);
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the internal engine
+ *
+ * @return \Cake\Cache\CacheEngine
+ */
+ public function engine(): CacheEngine
+ {
+ return $this->_engine;
+ }
+
+ /**
+ * Get the metrics for this object.
+ *
+ * @return array
+ */
+ public function metrics(): array
+ {
+ return $this->metrics;
+ }
+
+ /**
+ * Track a metric.
+ *
+ * @param string $metric The metric to increment.
+ * @return void
+ */
+ protected function track(string $metric): void
+ {
+ $this->metrics[$metric]++;
+ }
+
+ /**
+ * Log a cache operation
+ *
+ * @param string $operation The operation performed.
+ * @param float $duration The duration of the operation.
+ * @param string|null $key The cache key.
+ * @return void
+ */
+ protected function log(string $operation, float $duration, ?string $key = null): void
+ {
+ $key = $key ? " `{$key}`" : '';
+ $duration = number_format($duration, 5);
+ $this->logger->log('info', ":{$this->name}: {$operation}{$key} - {$duration}ms");
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function set($key, $value, $ttl = null): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->set($key, $value, $ttl);
+ $duration = microtime(true) - $start;
+
+ $this->track('set');
+ $this->log('set', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function add(string $key, mixed $value): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->add($key, $value);
+ $duration = microtime(true) - $start;
+
+ $this->track('set');
+ $this->log('add', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setMultiple($values, $ttl = null): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->setMultiple($values);
+ $duration = microtime(true) - $start;
+
+ $this->track('set');
+ $this->log('setMultiple', $duration);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get(string $key, mixed $default = null): mixed
+ {
+ $start = microtime(true);
+ $result = $this->_engine->get($key, $default);
+ $duration = microtime(true) - $start;
+ $metric = 'hit';
+ if ($result === null) {
+ $metric = 'miss';
+ }
+
+ $this->track("get {$metric}");
+ $this->log('get', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function has(string $key): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->has($key);
+ $duration = microtime(true) - $start;
+ $metric = 'hit';
+ if (!$result) {
+ $metric = 'miss';
+ }
+
+ $this->track("get {$metric}");
+ $this->log('has', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMultiple($keys, $default = null): iterable
+ {
+ $start = microtime(true);
+ $result = $this->_engine->getMultiple($keys, $default);
+ $duration = microtime(true) - $start;
+
+ $this->track('get hit');
+ $this->log('getMultiple', $duration);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function increment(string $key, int $offset = 1): int|false
+ {
+ $start = microtime(true);
+ $result = $this->_engine->increment($key, $offset);
+ $duration = microtime(true) - $start;
+
+ $this->track('set');
+ $this->log('increment', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function decrement(string $key, int $offset = 1): int|false
+ {
+ $start = microtime(true);
+ $result = $this->_engine->decrement($key, $offset);
+ $duration = microtime(true) - $start;
+
+ $this->track('set');
+ $this->log('decrement', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function delete($key): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->delete($key);
+ $duration = microtime(true) - $start;
+
+ $this->track('delete');
+ $this->log('delete', $duration, $key);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function deleteMultiple($keys): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->deleteMultiple($keys);
+ $duration = microtime(true) - $start;
+
+ $this->track('delete');
+ $this->log('deleteMultiple', $duration);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clear(): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->clear();
+ $duration = microtime(true) - $start;
+
+ $this->track('delete');
+ $this->log('clear', $duration);
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function groups(): array
+ {
+ return $this->_engine->groups();
+ }
+
+ /**
+ * Returns the config.
+ *
+ * @param string|null $key The key to get or null for the whole config.
+ * @param mixed $default The return value when the key does not exist.
+ * @return mixed Config value being read.
+ */
+ public function getConfig(?string $key = null, mixed $default = null): mixed
+ {
+ return $this->_engine->getConfig($key, $default);
+ }
+
+ /**
+ * Sets the config.
+ *
+ * @param array|string $key The key to set, or a complete array of configs.
+ * @param mixed|null $value The value to set.
+ * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
+ * @return $this
+ * @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid.
+ */
+ public function setConfig(array|string $key, mixed $value = null, bool $merge = true)
+ {
+ $this->_engine->setConfig($key, $value, $merge);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clearGroup(string $group): bool
+ {
+ $start = microtime(true);
+ $result = $this->_engine->clearGroup($group);
+ $duration = microtime(true) - $start;
+
+ $this->track('delete');
+ $this->log('clearGroup', $duration, $group);
+
+ return $result;
+ }
+
+ /**
+ * Magic __toString() method to get the CacheEngine's name
+ *
+ * @return string Returns the CacheEngine's name
+ */
+ public function __toString(): string
+ {
+ if (isset($this->_engine)) {
+ // phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable
+ [$ns, $class] = namespaceSplit(get_class($this->_engine));
+
+ return str_replace('Engine', '', $class);
+ }
+
+ return $this->_config['className'];
+ }
+}
diff --git a/src/Command/BenchmarkCommand.php b/src/Command/BenchmarkCommand.php
new file mode 100644
index 000000000..ec9432b4b
--- /dev/null
+++ b/src/Command/BenchmarkCommand.php
@@ -0,0 +1,203 @@
+io = $io;
+ /** @var string $url */
+ $url = $args->getArgumentAt(0);
+ $times = [];
+
+ $io->out(Text::insert('-> Testing :url', compact('url')));
+ $io->out('');
+ $count = 10;
+ if ($args->hasOption('n')) {
+ $count = (float)$args->getOption('n');
+ }
+ $timeout = 100;
+ if ($args->hasOption('t')) {
+ $timeout = (float)$args->getOption('t');
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ if (floor($timeout - array_sum($times)) <= 0 || $count <= 1) {
+ break;
+ }
+
+ $start = microtime(true);
+ file_get_contents($url);
+ $stop = microtime(true);
+
+ $times[] = $stop - $start;
+ }
+ $this->_results($times);
+
+ return static::CODE_SUCCESS;
+ }
+
+ /**
+ * Prints calculated results
+ *
+ * @param array $times Array of time values
+ * @return void
+ */
+ protected function _results(array $times): void
+ {
+ $duration = array_sum($times);
+ $requests = count($times);
+
+ $this->io->out(Text::insert('Total Requests made: :requests', compact('requests')));
+ $this->io->out(Text::insert('Total Time elapsed: :duration (seconds)', compact('duration')));
+
+ $this->io->out('');
+
+ $this->io->out(Text::insert('Requests/Second: :rps req/sec', [
+ 'rps' => round($requests / $duration, 3),
+ ]));
+
+ $this->io->out(Text::insert('Average request time: :average-time seconds', [
+ 'average-time' => round($duration / $requests, 3),
+ ]));
+
+ $this->io->out(Text::insert('Standard deviation of average request time: :std-dev', [
+ 'std-dev' => round($this->_deviation($times, true), 3),
+ ]));
+
+ if (!empty($times)) {
+ $this->io->out(Text::insert('Longest/shortest request: :longest sec/:shortest sec', [
+ 'longest' => round(max($times), 3),
+ 'shortest' => round(min($times), 3),
+ ]));
+ }
+
+ $this->io->out('');
+ }
+
+ /**
+ * One-pass, numerically stable calculation of population variance.
+ *
+ * Donald E. Knuth (1998).
+ * The Art of Computer Programming, volume 2: Seminumerical Algorithms, 3rd edn.,
+ * p. 232. Boston: Addison-Wesley.
+ *
+ * @param array $times Array of values
+ * @param bool $sample If true, calculates an unbiased estimate of the population
+ * variance from a finite sample.
+ * @return float Variance
+ */
+ protected function _variance(array $times, bool $sample = true): float
+ {
+ $n = $mean = $M2 = 0;
+
+ foreach ($times as $time) {
+ $n += 1;
+ $delta = $time - $mean;
+ $mean = $mean + $delta / $n;
+ $M2 = $M2 + $delta * ($time - $mean);
+ }
+
+ if ($sample) {
+ $n -= 1;
+ }
+
+ return $M2 / $n;
+ }
+
+ /**
+ * Calculate the standard deviation.
+ *
+ * @param array $times Array of values
+ * @param bool $sample ''
+ * @return float Standard deviation
+ */
+ protected function _deviation(array $times, bool $sample = true): float
+ {
+ return sqrt($this->_variance($times, $sample));
+ }
+
+ /**
+ * Gets the option parser instance and configures it.
+ *
+ * @param \Cake\Console\ConsoleOptionParser $parser The parser to build
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser->setDescription(
+ 'Allows you to obtain some rough benchmarking statistics' .
+ 'about a fully qualified URL.',
+ )
+ ->addArgument('url', [
+ 'help' => 'The URL to request.',
+ 'required' => true,
+ ])
+ ->addOption('n', [
+ 'default' => '10',
+ 'help' => 'Number of iterations to perform.',
+ ])
+ ->addOption('t', [
+ 'default' => '100',
+ 'help' =>
+ 'Maximum total time for all iterations, in seconds. ' .
+ 'If a single iteration takes more than the timeout, only one request will be made',
+ ])
+ ->setEpilog(
+ 'Example Use: `cake benchmark --n 10 --t 100 http://localhost/testsite`. ' .
+ 'Note: this benchmark does not include browser render times.',
+ );
+
+ return $parser;
+ }
+}
diff --git a/src/Controller/ComposerController.php b/src/Controller/ComposerController.php
new file mode 100644
index 000000000..6697b07f2
--- /dev/null
+++ b/src/Controller/ComposerController.php
@@ -0,0 +1,109 @@
+viewBuilder()->setClassName(JsonView::class);
+ }
+
+ /**
+ * Check outdated composer dependencies
+ *
+ * @return void
+ * @throws \RuntimeException
+ */
+ public function checkDependencies(): void
+ {
+ $this->request->allowMethod('post');
+
+ $input = new ArrayInput([
+ 'command' => 'outdated',
+ '--no-interaction' => true,
+ '--direct' => filter_var($this->request->getData('direct'), FILTER_VALIDATE_BOOLEAN),
+ ]);
+
+ $output = $this->executeComposerCommand($input);
+ $dependencies = array_filter(explode("\n", $output->fetch()));
+ $semverCompatible = [];
+ $bcBreaks = [];
+ foreach ($dependencies as $dependency) {
+ if (str_contains($dependency, 'php_network_getaddresses')) {
+ throw new RuntimeException('You have to be connected to the internet');
+ }
+ if (str_contains($dependency, '')) {
+ $semverCompatible[] = $dependency;
+ continue;
+ }
+ $bcBreaks[] = $dependency;
+ }
+ $packages = [];
+ if ($semverCompatible) {
+ $packages['semverCompatible'] = trim(implode("\n", $semverCompatible));
+ }
+ if ($bcBreaks) {
+ $packages['bcBreaks'] = trim(implode("\n", $bcBreaks));
+ }
+
+ $this->viewBuilder()->setOption('serialize', ['packages']);
+ $this->set('packages', $packages);
+ }
+
+ /**
+ * @param \Symfony\Component\Console\Input\ArrayInput $input An array describing the command input
+ * @return \Symfony\Component\Console\Output\BufferedOutput Aa Console command buffered result
+ */
+ private function executeComposerCommand(ArrayInput $input): BufferedOutput
+ {
+ $bin = implode(DIRECTORY_SEPARATOR, [ROOT, 'vendor', 'bin', 'composer']);
+ putenv('COMPOSER_HOME=' . $bin);
+ putenv('COMPOSER_CACHE_DIR=' . CACHE);
+
+ $dir = (string)getcwd();
+ chdir(ROOT);
+ $timeLimit = ini_get('max_execution_time');
+ set_time_limit(300);
+ $memoryLimit = ini_get('memory_limit');
+ ini_set('memory_limit', '512M');
+
+ $output = new BufferedOutput();
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->run($input, $output);
+
+ // Restore environment
+ chdir($dir);
+ set_time_limit((int)$timeLimit);
+ ini_set('memory_limit', $memoryLimit);
+
+ return $output;
+ }
+}
diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php
new file mode 100644
index 000000000..5d9bd0d24
--- /dev/null
+++ b/src/Controller/DashboardController.php
@@ -0,0 +1,74 @@
+viewBuilder()->setLayout('dashboard');
+ }
+
+ /**
+ * Dashboard.
+ *
+ * @return void
+ * @throws \Cake\Http\Exception\NotFoundException
+ */
+ public function index(): void
+ {
+ $requestsModel = $this->fetchTable('DebugKit.Requests');
+
+ $data = [
+ 'driver' => get_class($requestsModel->getConnection()->getDriver()),
+ 'rows' => $requestsModel->find()->count(),
+ ];
+
+ $this->set('connection', $data);
+ }
+
+ /**
+ * Reset SQLite DB.
+ *
+ * @return \Cake\Http\Response|null
+ */
+ public function reset(): ?Response
+ {
+ $this->request->allowMethod('post');
+ /** @var \DebugKit\Model\Table\RequestsTable $requestsModel */
+ $requestsModel = $this->fetchTable('DebugKit.Requests');
+
+ $requestsModel->Panels->deleteAll('1=1');
+ $requestsModel->deleteAll('1=1');
+
+ return $this->redirect(['action' => 'index']);
+ }
+}
diff --git a/src/Controller/DebugKitController.php b/src/Controller/DebugKitController.php
new file mode 100644
index 000000000..d9d3a05b8
--- /dev/null
+++ b/src/Controller/DebugKitController.php
@@ -0,0 +1,57 @@
+getRequest()->getAttribute('authorization');
+ if ($authorizationService instanceof AuthorizationService) {
+ if (Configure::read('DebugKit.ignoreAuthorization')) {
+ $authorizationService->skipAuthorization();
+ } else {
+ Log::info(
+ 'Cake Authorization plugin is enabled. If you would like ' .
+ 'to force DebugKit to ignore it, set `DebugKit.ignoreAuthorization` ' .
+ ' Configure option to true.',
+ );
+ }
+ }
+ }
+}
diff --git a/src/Controller/MailPreviewController.php b/src/Controller/MailPreviewController.php
new file mode 100644
index 000000000..ec5c6bffa
--- /dev/null
+++ b/src/Controller/MailPreviewController.php
@@ -0,0 +1,296 @@
+viewBuilder()->setLayout('DebugKit.mailer');
+ }
+
+ /**
+ * Handles mail-preview/index
+ *
+ * @return void
+ */
+ public function index(): void
+ {
+ $this->set('mailPreviews', $this->getMailPreviews()->toArray());
+ }
+
+ /**
+ * Handles the viewing of an already sent email that was logged in the Mail panel
+ * for DebugKit
+ *
+ * @param string $panelId The Mail panel id where the email data is stored.
+ * @param string $number The email number as stored in the logs.
+ * @return \Psr\Http\Message\ResponseInterface|null
+ */
+ public function sent(string $panelId, string $number): ?ResponseInterface
+ {
+ /** @var \DebugKit\Model\Entity\Panel $panel */
+ $panel = $this->fetchTable('DebugKit.Panels')->get($panelId);
+
+ // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
+ $content = @unserialize($panel->content);
+
+ if (empty($content['emails'][$number])) {
+ throw new NotFoundException('No emails found in this request');
+ }
+
+ $email = $content['emails'][$number];
+ $email = new SentMailResult(array_filter($email['headers']), $email['message']);
+
+ /** @var string $partType */
+ $partType = $this->request->getQuery('part');
+ if ($partType) {
+ return $this->respondWithPart($email, $partType);
+ }
+
+ /** @var string $part */
+ $part = $this->request->getQuery('part');
+ $this->set('noHeader', true);
+ $this->set('email', $email);
+ $this->set('plugin', '');
+ $this->set('part', $this->findPreferredPart($email, $part));
+ $this->viewBuilder()->setTemplate('email');
+
+ return null;
+ }
+
+ /**
+ * Handles mail-preview/email
+ *
+ * @param string $name The Mailer name
+ * @param string $method The mailer preview method
+ * @return \Psr\Http\Message\ResponseInterface|null
+ */
+ public function email(string $name, string $method): ?ResponseInterface
+ {
+ $restore = Router::getRequest();
+ // Clear the plugin attribute from the request instance
+ // Router is holding onto so that we can render mail previews
+ // in a plugin less request context.
+ Router::setRequest($this->request->withParam('plugin', null));
+
+ /** @var string $plugin */
+ $plugin = $this->request->getQuery('plugin');
+ $email = $this->findPreview($name, $method, $plugin);
+ $partType = $this->request->getQuery('part');
+
+ $this->viewBuilder()->disableAutoLayout();
+
+ if ($partType) {
+ $result = $this->respondWithPart($email, $partType);
+ if ($restore) {
+ Router::setRequest($restore);
+ }
+
+ return $result;
+ }
+
+ $humanName = Inflector::humanize(Inflector::underscore($name) . "_$method");
+ /** @var string $part */
+ $part = $this->request->getQuery('part');
+ $this->set('title', $humanName);
+ $this->set('email', $email);
+ $this->set('plugin', $plugin);
+ $this->set('part', $this->findPreferredPart($email, $part));
+
+ if ($restore) {
+ Router::setRequest($restore);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a response object with the requested part type for the
+ * email or throws an exception, if no such part exists.
+ *
+ * @param \DebugKit\Mailer\AbstractResult $email the email to preview
+ * @param string $partType The email part to retrieve
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ protected function respondWithPart(AbstractResult $email, string $partType): ResponseInterface
+ {
+ $part = $this->findPart($email, $partType);
+
+ if ($part === null) {
+ throw new NotFoundException(sprintf("Email part '%s' not found in email", $partType));
+ }
+
+ $response = $this->response->withType($partType);
+ if ($partType === 'text') {
+ $part = '' . $part . ' ';
+ }
+
+ return $response->withStringBody($part);
+ }
+
+ /**
+ * Retrieves an array of MailPreview objects
+ *
+ * @return \Cake\Collection\CollectionInterface
+ */
+ protected function getMailPreviews(): CollectionInterface
+ {
+ return $this->getMailPreviewClasses()->groupBy('plugin');
+ }
+
+ /**
+ * Returns an array of MailPreview class names for the app and plugins
+ *
+ * @return \Cake\Collection\CollectionInterface
+ */
+ protected function getMailPreviewClasses(): CollectionInterface
+ {
+ $pluginPaths = collection(CorePlugin::loaded())
+ ->reject(function ($plugin) {
+ return $plugin === 'DebugKit';
+ })
+ ->map(function ($plugin) {
+ return [[CorePlugin::classPath($plugin) . 'Mailer/Preview/'], "$plugin."];
+ });
+
+ $appPaths = [App::classPath('Mailer/Preview'), ''];
+
+ return collection([$appPaths])
+ ->append($pluginPaths)
+ ->unfold(function ($pairs) {
+ [$paths, $plugin] = $pairs;
+ foreach ($paths as $path) {
+ yield $plugin => $path;
+ }
+ })
+ ->unfold(function ($path, $plugin) {
+ /** @var list $files */
+ $files = glob($path . '*Preview.php');
+ foreach ($files as $file) {
+ $base = str_replace('.php', '', basename($file));
+ $class = App::className($plugin . $base, 'Mailer/Preview');
+ if ($class) {
+ yield ['plugin' => trim($plugin, '.'), 'class' => new $class()];
+ }
+ }
+ });
+ }
+
+ /**
+ * Finds a specified email part
+ *
+ * @param \DebugKit\Mailer\AbstractResult $email The result of the email preview
+ * @param string $partType The name of a part
+ * @return string|null
+ */
+ protected function findPart(AbstractResult $email, string $partType): ?string
+ {
+ foreach ($email->getParts() as $part => $content) {
+ if ($part === $partType) {
+ return $content;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds a specified email part or the first part available
+ *
+ * @param \DebugKit\Mailer\AbstractResult $email The result of the email preview
+ * @param string|null $partType The name of a part
+ * @return string|null
+ */
+ protected function findPreferredPart(AbstractResult $email, ?string $partType): ?string
+ {
+ $parts = $email->getParts();
+
+ if ($partType === null && !empty($parts['html'])) {
+ return 'html';
+ }
+
+ if ($partType === null) {
+ foreach ($email->getParts() as $part => $content) {
+ return $part;
+ }
+ }
+
+ return $this->findPart($email, (string)$partType) ?: null;
+ }
+
+ /**
+ * Returns a matching MailPreview object with name
+ *
+ * @param string $previewName The Mailer name
+ * @param string $emailName The mailer preview method
+ * @param ?string $plugin The plugin where the mailer preview should be found
+ * @return \DebugKit\Mailer\PreviewResult The result of the email preview
+ * @throws \Cake\Http\Exception\NotFoundException
+ */
+ protected function findPreview(string $previewName, string $emailName, ?string $plugin = null): PreviewResult
+ {
+ if ($plugin) {
+ $plugin = "$plugin.";
+ }
+ if (str_contains($previewName, '\\')) {
+ throw new NotFoundException("Mailer preview $previewName not found");
+ }
+
+ $realClass = App::className($plugin . $previewName, 'Mailer/Preview');
+ if (!$realClass || !is_subclass_of($realClass, MailPreview::class, true)) {
+ throw new NotFoundException("Mailer preview $previewName not found");
+ }
+ /** @var \DebugKit\Mailer\MailPreview $mailPreview */
+ $mailPreview = new $realClass();
+
+ $email = $mailPreview->find($emailName);
+ if ($email === null) {
+ throw new NotFoundException(sprintf(
+ 'Mailer preview `%s::%s` not found',
+ $previewName,
+ $emailName,
+ ));
+ }
+
+ return new PreviewResult($mailPreview->$email(), $email);
+ }
+}
diff --git a/src/Controller/PanelsController.php b/src/Controller/PanelsController.php
new file mode 100644
index 000000000..2aec295dc
--- /dev/null
+++ b/src/Controller/PanelsController.php
@@ -0,0 +1,121 @@
+viewBuilder()
+ ->addHelpers([
+ 'Form', 'Html', 'Number', 'Url', 'DebugKit.Toolbar',
+ 'DebugKit.Credentials', 'DebugKit.SimpleGraph',
+ ])
+ ->setLayout('DebugKit.toolbar');
+
+ if (!$this->request->is('json')) {
+ $this->viewBuilder()->setClassName('DebugKit.Ajax');
+ }
+ }
+
+ /**
+ * Index method that lets you get requests by panelid.
+ *
+ * @param string $requestId Request id
+ * @return void
+ * @throws \Cake\Http\Exception\NotFoundException
+ */
+ public function index(?string $requestId = null): void
+ {
+ $query = $this->Panels->find('byRequest', requestId: $requestId);
+ $panels = $query->toArray();
+ if (empty($panels)) {
+ throw new NotFoundException();
+ }
+ $this->set([
+ 'panels' => $panels,
+ ]);
+ $this->viewBuilder()->setOption('serialize', ['panels']);
+ }
+
+ /**
+ * View a panel's data.
+ *
+ * @param string $id The id.
+ * @return void
+ */
+ public function view(?string $id = null): void
+ {
+ $this->set('sort', $this->request->getCookie('debugKit_sort'));
+ $panel = $this->Panels->get($id, ...['contain' => ['Requests']]);
+
+ $this->set('panel', $panel);
+ // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
+ $this->set(@unserialize($panel->content));
+ }
+
+ /**
+ * Get Latest request history panel
+ *
+ * @return \Cake\Http\Response|null
+ */
+ public function latestHistory(): ?Response
+ {
+ /** @var array{id:string}|null $request */
+ $request = $this->Panels->Requests->find('recent')
+ ->select(['id'])
+ ->disableHydration()
+ ->first();
+ if (!$request) {
+ throw new NotFoundException('No requests found');
+ }
+ /** @var array{id:string}|null $historyPanel */
+ $historyPanel = $this->Panels->find('byRequest', requestId: $request['id'])
+ ->where(['title' => 'History'])
+ ->select(['id'])
+ ->first();
+ if (!$historyPanel) {
+ throw new NotFoundException('History Panel from latest request not found');
+ }
+
+ return $this->redirect([
+ 'action' => 'view', $historyPanel['id'],
+ ]);
+ }
+}
diff --git a/src/Controller/RequestsController.php b/src/Controller/RequestsController.php
new file mode 100644
index 000000000..8493747ac
--- /dev/null
+++ b/src/Controller/RequestsController.php
@@ -0,0 +1,63 @@
+response = $this->response->withHeader('Content-Security-Policy', '');
+ }
+
+ /**
+ * Before render handler.
+ *
+ * @param \Cake\Event\EventInterface $event The event.
+ * @return void
+ */
+ public function beforeRender(EventInterface $event): void
+ {
+ $this->viewBuilder()
+ ->setLayout('DebugKit.toolbar')
+ ->setClassName('DebugKit.Ajax');
+ }
+
+ /**
+ * View a request's data.
+ *
+ * @param string $id The id.
+ * @return void
+ */
+ public function view(?string $id = null): void
+ {
+ $toolbar = $this->Requests->get($id, contain: 'Panels');
+ $this->set('toolbar', $toolbar);
+ }
+}
diff --git a/src/Controller/ToolbarController.php b/src/Controller/ToolbarController.php
new file mode 100644
index 000000000..abf2b8614
--- /dev/null
+++ b/src/Controller/ToolbarController.php
@@ -0,0 +1,76 @@
+viewBuilder()->setClassName(JsonView::class);
+ }
+
+ /**
+ * Clear a named cache.
+ *
+ * @return void
+ * @throws \Cake\Http\Exception\NotFoundException
+ */
+ public function clearCache(): void
+ {
+ $this->request->allowMethod('post');
+ $name = $this->request->getData('name');
+ if (!$name) {
+ throw new NotFoundException('Invalid cache engine name.');
+ }
+ if (!Cache::getConfig($name)) {
+ throw new NotFoundException(sprintf('Unknown cache engine "%s".', $name));
+ }
+ $success = Cache::clear($name);
+ $message = $success ?
+ sprintf('%s cache cleared.', $name) :
+ sprintf('%s cache could not be cleared.', $name);
+ $this->set(compact('success', 'message'));
+ $this->viewBuilder()->setOption('serialize', ['success', 'message']);
+ }
+
+ /**
+ * Clear the session.
+ *
+ * @return void
+ */
+ public function clearSession(): void
+ {
+ $this->request->allowMethod('post');
+ $session = $this->request->getSession();
+ $session->destroy();
+
+ $success = true;
+ $message = 'Session cleared.';
+ $this->set(compact('success', 'message'));
+ $this->viewBuilder()->setOption('serialize', ['success', 'message']);
+ }
+}
diff --git a/src/Database/Log/DebugLog.php b/src/Database/Log/DebugLog.php
new file mode 100644
index 000000000..979e5cede
--- /dev/null
+++ b/src/Database/Log/DebugLog.php
@@ -0,0 +1,223 @@
+_logger = $logger;
+ $this->_connectionName = $name;
+ $this->_includeSchema = $includeSchema;
+ }
+
+ /**
+ * Set the schema include flag.
+ *
+ * @param bool $value Set
+ * @return $this
+ */
+ public function setIncludeSchema(bool $value)
+ {
+ $this->_includeSchema = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get the connection name.
+ *
+ * @return string
+ */
+ public function name(): string
+ {
+ return $this->_connectionName;
+ }
+
+ /**
+ * Get the stored logs.
+ *
+ * @return array
+ */
+ public function queries(): array
+ {
+ return $this->_queries;
+ }
+
+ /**
+ * Get the total time
+ *
+ * @return float
+ */
+ public function totalTime(): float
+ {
+ return $this->_totalTime;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function log($level, string|Stringable $message, array $context = []): void
+ {
+ /** @var \Cake\Database\Log\LoggedQuery|object|null $query */
+ $query = $context['query'] ?? null;
+
+ if ($this->_logger) {
+ $this->_logger->log($level, $message, $context);
+ }
+
+ // This specific to Elastic Search
+ if (!$query instanceof LoggedQuery && isset($context['request']) && isset($context['response'])) {
+ $took = $context['response']['took'] ?? 0;
+ $this->_totalTime += $took;
+
+ $this->_queries[] = [
+ 'query' => json_encode([
+ 'method' => $context['request']['method'],
+ 'path' => $context['request']['path'],
+ 'data' => $context['request']['data'],
+ ], JSON_PRETTY_PRINT),
+ 'took' => $took,
+ 'rows' => $context['response']['hits']['total']['value'] ?? $context['response']['hits']['total'] ?? 0,
+ 'inTransaction' => $this->inTransaction,
+ 'isCommitOrRollback' => false,
+ 'role' => '',
+ ];
+
+ return;
+ }
+
+ if (
+ !$query instanceof LoggedQuery ||
+ ($this->_includeSchema === false && $this->isSchemaQuery($query))
+ ) {
+ return;
+ }
+
+ $data = $query->jsonSerialize();
+
+ $this->_totalTime += $data['took'];
+
+ $sql = (string)$query;
+ $isBegin = $sql === 'BEGIN';
+ $isCommitOrRollback = $sql === 'COMMIT' || $sql === 'ROLLBACK';
+
+ if ($isBegin) {
+ $this->inTransaction = true;
+ }
+
+ $this->_queries[] = [
+ 'query' => $sql,
+ 'took' => $data['took'],
+ 'rows' => $data['numRows'],
+ 'inTransaction' => $this->inTransaction,
+ 'isCommitOrRollback' => $isCommitOrRollback,
+ 'role' => $query->getContext()['role'],
+ ];
+
+ if ($isCommitOrRollback) {
+ $this->inTransaction = false;
+ }
+ }
+
+ /**
+ * Sniff SQL statements for things only found in schema reflection.
+ *
+ * @param \Cake\Database\Log\LoggedQuery $query The query to check.
+ * @return bool
+ */
+ protected function isSchemaQuery(LoggedQuery $query): bool
+ {
+ $querystring = $query->jsonSerialize()['query'];
+
+ return // Multiple engines
+ strpos($querystring, 'FROM information_schema') !== false ||
+ // Postgres
+ strpos($querystring, 'FROM pg_catalog') !== false ||
+ // MySQL
+ strpos($querystring, 'SHOW TABLE') === 0 ||
+ strpos($querystring, 'SHOW FULL COLUMNS') === 0 ||
+ strpos($querystring, 'SHOW INDEXES') === 0 ||
+ // Sqlite
+ strpos($querystring, 'FROM sqlite_master') !== false ||
+ strpos($querystring, 'PRAGMA') === 0 ||
+ // Sqlserver
+ strpos($querystring, 'FROM INFORMATION_SCHEMA') !== false ||
+ strpos($querystring, 'FROM sys.') !== false;
+ }
+}
diff --git a/src/DebugInclude.php b/src/DebugInclude.php
new file mode 100644
index 000000000..944c5308f
--- /dev/null
+++ b/src/DebugInclude.php
@@ -0,0 +1,199 @@
+
+ */
+ protected array $_pluginPaths = [];
+
+ /**
+ * The list of Composer packages
+ *
+ * @var array
+ */
+ protected array $_composerPaths = [];
+
+ /**
+ * File Types
+ *
+ * @var array
+ */
+ protected array $_fileTypes = [
+ 'Auth', 'Cache', 'Collection', 'Config', 'Configure', 'Console', 'Component', 'Controller',
+ 'Behavior', 'Database', 'Datasource', 'Model', 'Template', 'View', 'Utility',
+ 'Network', 'Routing', 'I18n', 'Log', 'Error', 'Event', 'Form', 'Filesystem',
+ 'ORM', 'Filter', 'Validation',
+ ];
+
+ /**
+ * Get a list of plugins on construct for later use
+ */
+ public function __construct()
+ {
+ foreach (CorePlugin::loaded() as $plugin) {
+ $this->_pluginPaths[$plugin] = str_replace('/', DIRECTORY_SEPARATOR, CorePlugin::path($plugin));
+ }
+
+ $lockFile = new JsonFile(ROOT . DIRECTORY_SEPARATOR . 'composer.lock');
+ if ($lockFile->exists()) {
+ $lockContent = $lockFile->read();
+
+ $vendorDir = ROOT . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR;
+ $packages = array_merge($lockContent['packages'], $lockContent['packages-dev']);
+
+ foreach ($packages as $package) {
+ /** @var string $name */
+ $name = $package['name'];
+ $this->_composerPaths[$name] = $vendorDir
+ . str_replace('/', DIRECTORY_SEPARATOR, $name)
+ . DIRECTORY_SEPARATOR;
+ }
+ }
+ }
+
+ /**
+ * Get the possible include paths
+ *
+ * @return array
+ */
+ public function includePaths(): array
+ {
+ $paths = explode(PATH_SEPARATOR, (string)get_include_path());
+ $paths = array_filter($paths, function ($path) {
+ if ($path === '.' || strlen($path) === 0) {
+ return false;
+ }
+
+ return true;
+ });
+
+ return array_values($paths);
+ }
+
+ /**
+ * Check if a path is part of CakePHP
+ *
+ * @param string $file File to check
+ * @return bool
+ */
+ public function isCakeFile(string $file): bool
+ {
+ return strpos($file, CAKE) === 0;
+ }
+
+ /**
+ * Check if a path is from APP but not a plugin
+ *
+ * @param string $file File to check
+ * @return bool
+ */
+ public function isAppFile(string $file): bool
+ {
+ return strpos($file, APP) === 0;
+ }
+
+ /**
+ * Detect plugin the file belongs to
+ *
+ * @param string $file File to check
+ * @return string|bool plugin name, or false if not plugin
+ */
+ public function getPluginName(string $file): string|bool
+ {
+ foreach ($this->_pluginPaths as $plugin => $path) {
+ if (strpos($file, $path) === 0) {
+ return $plugin;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Detect Composer package the file belongs to
+ *
+ * @param string $file File to check
+ * @return string|bool package name, or false if not Composer package
+ */
+ public function getComposerPackageName(string $file): string|bool
+ {
+ foreach ($this->_composerPaths as $package => $path) {
+ if (strpos($file, $path) === 0) {
+ return $package;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Replace the path with APP, CAKE, ROOT or the plugin name
+ *
+ * @param string $file File to check
+ * @param string $type The file type
+ * @param string|null $name plugin name or composer package
+ * @return string Path with replaced prefix
+ */
+ public function niceFileName(string $file, string $type, ?string $name = null): string
+ {
+ switch ($type) {
+ case 'app':
+ return str_replace(APP, 'APP' . DIRECTORY_SEPARATOR, $file);
+
+ case 'cake':
+ return str_replace(CAKE, 'CAKE' . DIRECTORY_SEPARATOR, $file);
+
+ case 'root':
+ return str_replace(ROOT, 'ROOT', $file);
+
+ case 'plugin':
+ return str_replace($this->_pluginPaths[$name], $name . DIRECTORY_SEPARATOR, $file);
+
+ case 'vendor':
+ return str_replace($this->_composerPaths[$name], '', $file);
+ }
+
+ throw new InvalidArgumentException("Type `{$type}` is not supported.");
+ }
+
+ /**
+ * Get the type of file (model, controller etc)
+ *
+ * @param string $file File to check.
+ * @return string
+ */
+ public function getFileType(string $file): string
+ {
+ foreach ($this->_fileTypes as $type) {
+ if (stripos($file, DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR) !== false) {
+ return $type;
+ }
+ }
+
+ return 'other';
+ }
+}
diff --git a/src/DebugKitPlugin.php b/src/DebugKitPlugin.php
new file mode 100644
index 000000000..ab2a7ee8d
--- /dev/null
+++ b/src/DebugKitPlugin.php
@@ -0,0 +1,123 @@
+isEnabled()) {
+ return;
+ }
+
+ $this->service = $service;
+ $this->setDeprecationHandler($service);
+
+ // will load `config/bootstrap.php`.
+ parent::bootstrap($app);
+ }
+
+ /**
+ * Add middleware for the plugin.
+ *
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
+ * @return \Cake\Http\MiddlewareQueue
+ */
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Only insert middleware if Toolbar Service is available (not in phpunit run)
+ if ($this->service) {
+ $middlewareQueue->insertAt(0, new DebugKitMiddleware($this->service));
+ }
+
+ return $middlewareQueue;
+ }
+
+ /**
+ * Add console commands for the plugin.
+ *
+ * @param \Cake\Console\CommandCollection $commands The command collection to update
+ * @return \Cake\Console\CommandCollection
+ */
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ return $commands->add('benchmark', BenchmarkCommand::class);
+ }
+
+ /**
+ * set deprecation handler
+ *
+ * @param \DebugKit\ToolbarService $service The toolbar service instance
+ * @return void
+ */
+ public function setDeprecationHandler(ToolbarService $service): void
+ {
+ if (!empty($service->getConfig('panels')['DebugKit.Deprecations'])) {
+ EventManager::instance()->on('Error.beforeRender', function (EventInterface $event, PhpError $error): void {
+ $code = $error->getCode();
+ if ($code !== E_USER_DEPRECATED && $code !== E_DEPRECATED) {
+ return;
+ }
+ $file = $error->getFile();
+ $line = $error->getLine();
+
+ // Extract the line/file from the message as deprecationWarning
+ // will calculate the application frame when generating the message.
+ preg_match('/\\n([^\n,]+?), line: (\d+)\\n/', $error->getMessage(), $matches);
+ if ($matches) {
+ $file = $matches[1];
+ $line = $matches[2];
+ }
+
+ DeprecationsPanel::addDeprecatedError([
+ 'code' => $code,
+ 'message' => $error->getMessage(),
+ 'file' => $file,
+ 'line' => $line,
+ ]);
+ $event->stopPropagation();
+ });
+ }
+ }
+}
diff --git a/src/DebugMemory.php b/src/DebugMemory.php
new file mode 100644
index 000000000..40e7a78b3
--- /dev/null
+++ b/src/DebugMemory.php
@@ -0,0 +1,107 @@
+plugin) {
+ return $this->plugin . '.' . Inflector::underscore($name);
+ }
+
+ return Inflector::underscore($name);
+ }
+
+ /**
+ * Get the data a panel has collected.
+ *
+ * @return array
+ */
+ public function data(): array
+ {
+ return $this->_data;
+ }
+
+ /**
+ * Get the summary data for a panel.
+ *
+ * This data is displayed in the toolbar even when the panel is collapsed.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ return '';
+ }
+
+ /**
+ * Initialize hook method.
+ *
+ * @return void
+ */
+ public function initialize(): void
+ {
+ }
+
+ /**
+ * Shutdown callback
+ *
+ * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event The event.
+ * @return void
+ */
+ public function shutdown(EventInterface $event): void
+ {
+ }
+
+ /**
+ * Get the events this panels supports.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ return [
+ 'Controller.shutdown' => 'shutdown',
+ ];
+ }
+}
diff --git a/src/DebugSql.php b/src/DebugSql.php
new file mode 100644
index 000000000..4b26d03ed
--- /dev/null
+++ b/src/DebugSql.php
@@ -0,0 +1,226 @@
+
+%s
+
+%s
+
+
+HTML;
+
+ /**
+ * Template used for CLI and text output.
+ *
+ * @var string
+ */
+ private static string $templateText = <<getValueBinder()->bindings());
+ }
+
+ /** @var array $trace */
+ $trace = Debugger::trace(['start' => 0, 'depth' => $stackDepth + 1, 'format' => 'array']);
+ $file = isset($trace[$stackDepth]) ? $trace[$stackDepth]['file'] : 'n/a';
+ $line = isset($trace[$stackDepth]) ? $trace[$stackDepth]['line'] : 0;
+ $lineInfo = '';
+ if ($file) {
+ $search = [];
+ if (defined('ROOT')) {
+ $search = [ROOT];
+ }
+ if (defined('CAKE_CORE_INCLUDE_PATH')) {
+ array_unshift($search, CAKE_CORE_INCLUDE_PATH);
+ }
+ /** @var string $file */
+ $file = str_replace($search, '', $file);
+ }
+
+ $template = self::$templateHtml;
+ if (static::isCli() || $showHtml === false) {
+ $template = self::$templateText;
+ if ($file && $line) {
+ $lineInfo = sprintf('%s (line %s)', $file, $line);
+ }
+ }
+ if ($showHtml === null && $template !== self::$templateText) {
+ $showHtml = true;
+ }
+
+ if (static::isCli() && !$showHtml) {
+ $highlighter = new CliHighlighter([
+ CliHighlighter::HIGHLIGHT_QUOTE => "\x1b[33;1m",
+ CliHighlighter::HIGHLIGHT_WORD => "\x1b[36;1m",
+ CliHighlighter::HIGHLIGHT_VARIABLE => "\x1b[33;1m",
+ ]);
+ } elseif ($showHtml) {
+ $highlighter = new HtmlHighlighter(
+ [
+ HtmlHighlighter::HIGHLIGHT_QUOTE => 'style="color: #004d40;"',
+ HtmlHighlighter::HIGHLIGHT_BACKTICK_QUOTE => 'style="color: #26a69a;"',
+ HtmlHighlighter::HIGHLIGHT_NUMBER => 'style="color: #ec407a;"',
+ HtmlHighlighter::HIGHLIGHT_WORD => 'style="color: #9c27b0;"',
+ HtmlHighlighter::HIGHLIGHT_PRE => 'style="color: #222; background-color: transparent;"',
+ ],
+ false,
+ );
+ } else {
+ $highlighter = new NullHighlighter();
+ }
+
+ $var = (new SqlFormatter($highlighter))->format($sql);
+ $var = trim($var);
+
+ if ($showHtml) {
+ $template = self::$templateHtml;
+ if ($file && $line) {
+ $lineInfo = sprintf('%s (line %s ) ', $file, $line);
+ }
+ }
+
+ printf($template, $lineInfo, $var);
+
+ return $query;
+ }
+
+ /**
+ * Prints out the SQL statements generated by a Query object and dies.
+ *
+ * Only runs if debug mode is enabled.
+ * It will otherwise just continue code execution and ignore this function.
+ *
+ * @param \Cake\Database\Query $query Query to show SQL statements for.
+ * @param bool $showValues Renders the SQL statement with bound variables.
+ * @param bool|null $showHtml If set to true, the method prints the debug
+ * data in a browser-friendly way.
+ * @param int $stackDepth Provides a hint as to which file in the call stack to reference.
+ * @return void
+ */
+ public static function sqld(
+ Query $query,
+ bool $showValues = true,
+ ?bool $showHtml = null,
+ int $stackDepth = 1,
+ ): void {
+ static::sql($query, $showValues, $showHtml, $stackDepth);
+ die(1);
+ }
+
+ /**
+ * Checks whether the current environment is CLI based.
+ *
+ * @return bool
+ */
+ protected static function isCli(): bool
+ {
+ return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
+ }
+
+ /**
+ * Helper function used to replace query placeholders by the real
+ * params used to execute the query.
+ *
+ * @param string $sql The SQL statement
+ * @param array $bindings The Query bindings
+ * @return string
+ */
+ private static function interpolate(string $sql, array $bindings): string
+ {
+ $params = array_map(function ($binding) {
+ $p = $binding['value'];
+
+ if ($p === null) {
+ return 'NULL';
+ }
+ if (is_bool($p)) {
+ return $p ? '1' : '0';
+ }
+
+ if (is_string($p)) {
+ $replacements = [
+ '$' => '\\$',
+ '\\' => '\\\\\\\\',
+ "'" => "''",
+ ];
+
+ $p = strtr($p, $replacements);
+
+ return "'$p'";
+ }
+
+ return $p;
+ }, $bindings);
+
+ $keys = [];
+ $limit = is_int(key($params)) ? 1 : -1;
+ foreach ($params as $key => $param) {
+ $keys[] = is_string($key) ? "/$key\b/" : '/[?]/';
+ }
+
+ return (string)preg_replace($keys, $params, $sql, $limit);
+ }
+}
diff --git a/src/DebugTimer.php b/src/DebugTimer.php
new file mode 100644
index 000000000..31ee3dd5d
--- /dev/null
+++ b/src/DebugTimer.php
@@ -0,0 +1,219 @@
+ 1) {
+ $message .= ' #' . $i;
+ }
+
+ self::$_timers[$name] = [
+ 'start' => $start,
+ 'message' => $message,
+ 'named' => $named,
+ ];
+
+ return true;
+ }
+
+ /**
+ * Stop a benchmarking timer.
+ *
+ * $name should be the same as the $name used in startTimer().
+ *
+ * @param string $name The name of the timer to end.
+ * @return bool true if timer was ended, false if timer was not started.
+ */
+ public static function stop(?string $name = null): bool
+ {
+ $end = microtime(true);
+ if (!$name) {
+ $names = array_reverse(array_keys(self::$_timers));
+ foreach ($names as $name) {
+ if (!empty(self::$_timers[$name]['end'])) {
+ continue;
+ }
+ if (empty(self::$_timers[$name]['named'])) {
+ break;
+ }
+ }
+ } else {
+ $i = 1;
+ $_name = $name;
+ while (isset(self::$_timers[$name])) {
+ if (empty(self::$_timers[$name]['end'])) {
+ break;
+ }
+ $i++;
+ $name = $_name . ' #' . $i;
+ }
+ }
+ if (!isset(self::$_timers[$name])) {
+ return false;
+ }
+ self::$_timers[$name]['end'] = $end;
+
+ return true;
+ }
+
+ /**
+ * Get all timers that have been started and stopped.
+ * Calculates elapsed time for each timer. If clear is true, will delete existing timers
+ *
+ * @param bool $clear false
+ * @return array
+ */
+ public static function getAll(bool $clear = false): array
+ {
+ $start = self::requestStartTime();
+ $now = microtime(true);
+
+ $times = [];
+ if (!empty(self::$_timers)) {
+ $firstTimer = reset(self::$_timers);
+ $_end = $firstTimer['start'];
+ } else {
+ $_end = $now;
+ }
+ $times['Core Processing (Derived from $_SERVER["REQUEST_TIME"])'] = [
+ 'message' => 'Core Processing (Derived from $_SERVER["REQUEST_TIME"])',
+ 'start' => 0,
+ 'end' => $_end - $start,
+ 'time' => round($_end - $start, 6),
+ 'named' => null,
+ ];
+ foreach (self::$_timers as $name => $timer) {
+ if (!isset($timer['end'])) {
+ $timer['end'] = $now;
+ }
+ $times[$name] = array_merge($timer, [
+ 'start' => $timer['start'] - $start,
+ 'end' => $timer['end'] - $start,
+ 'time' => self::elapsedTime($name),
+ ]);
+ }
+ if ($clear) {
+ self::$_timers = [];
+ }
+
+ return $times;
+ }
+
+ /**
+ * Clear all existing timers
+ *
+ * @return bool true
+ */
+ public static function clear(): bool
+ {
+ self::$_timers = [];
+
+ return true;
+ }
+
+ /**
+ * Get the difference in time between the timer start and timer end.
+ *
+ * @param string $name the name of the timer you want elapsed time for.
+ * @param int $precision the number of decimal places to return, defaults to 5.
+ * @return float number of seconds elapsed for timer name, 0 on missing key
+ */
+ public static function elapsedTime(string $name = 'default', int $precision = 5): float
+ {
+ if (!isset(self::$_timers[$name]['start']) || !isset(self::$_timers[$name]['end'])) {
+ return 0;
+ }
+
+ return round(self::$_timers[$name]['end'] - self::$_timers[$name]['start'], $precision);
+ }
+
+ /**
+ * Get the total execution time until this point
+ *
+ * @return float elapsed time in seconds since script start.
+ */
+ public static function requestTime(): float
+ {
+ $start = self::requestStartTime();
+ $now = microtime(true);
+
+ return $now - $start;
+ }
+
+ /**
+ * get the time the current request started.
+ *
+ * @return float time of request start
+ */
+ public static function requestStartTime(): float
+ {
+ if (defined('TIME_START')) {
+ $startTime = TIME_START;
+ } elseif (isset($GLOBALS['TIME_START'])) {
+ $startTime = $GLOBALS['TIME_START'];
+ } else {
+ $startTime = env('REQUEST_TIME');
+ }
+
+ return (float)$startTime;
+ }
+}
diff --git a/src/Log/Engine/DebugKitLog.php b/src/Log/Engine/DebugKitLog.php
new file mode 100644
index 000000000..32416a3d3
--- /dev/null
+++ b/src/Log/Engine/DebugKitLog.php
@@ -0,0 +1,79 @@
+_logs[$level])) {
+ $this->_logs[$level] = [];
+ }
+ $this->_logs[$level][] = [date('Y-m-d H:i:s'), $this->interpolate($message)];
+ }
+
+ /**
+ * Get the logs.
+ *
+ * @return array
+ */
+ public function all(): array
+ {
+ return $this->_logs;
+ }
+
+ /**
+ * Get the number of log entries.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return array_reduce($this->_logs, function ($sum, $v) {
+ return $sum + count($v);
+ }, 0);
+ }
+
+ /**
+ * Check if there are no logs.
+ *
+ * @return bool
+ */
+ public function noLogs(): bool
+ {
+ return empty($this->_logs);
+ }
+}
diff --git a/src/Mailer/AbstractResult.php b/src/Mailer/AbstractResult.php
new file mode 100644
index 000000000..2b51937f7
--- /dev/null
+++ b/src/Mailer/AbstractResult.php
@@ -0,0 +1,57 @@
+headers;
+ }
+
+ /**
+ * Returns the rendered parts in th email
+ *
+ * @return array
+ */
+ public function getParts(): array
+ {
+ return $this->parts;
+ }
+}
diff --git a/src/Mailer/MailPreview.php b/src/Mailer/MailPreview.php
new file mode 100644
index 000000000..7294d8ebf
--- /dev/null
+++ b/src/Mailer/MailPreview.php
@@ -0,0 +1,104 @@
+validEmail($email)) {
+ return $email;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of valid emails
+ *
+ * @return array
+ */
+ public function getEmails(): array
+ {
+ $emails = [];
+ foreach (get_class_methods($this) as $methodName) {
+ if (!$this->validEmail($methodName)) {
+ continue;
+ }
+
+ $emails[] = $methodName;
+ }
+
+ return $emails;
+ }
+
+ /**
+ * Returns the name of this preview
+ *
+ * @return string
+ */
+ public function name(): string
+ {
+ $classname = static::class;
+ $pos = strrpos($classname, '\\');
+
+ return substr($classname, $pos + 1);
+ }
+
+ /**
+ * Returns whether or not a specified email is valid
+ * for this MailPreview instance
+ *
+ * @param string $email Name of email
+ * @return bool
+ */
+ protected function validEmail(string $email): bool
+ {
+ if (empty($email)) {
+ return false;
+ }
+
+ $baseClass = new ReflectionClass(self::class);
+ if ($baseClass->hasMethod($email)) {
+ return false;
+ }
+
+ try {
+ $method = new ReflectionMethod($this, $email);
+ } catch (ReflectionException $e) {
+ return false;
+ }
+
+ return $method->isPublic();
+ }
+}
diff --git a/src/Mailer/PreviewResult.php b/src/Mailer/PreviewResult.php
new file mode 100644
index 000000000..b5f019f23
--- /dev/null
+++ b/src/Mailer/PreviewResult.php
@@ -0,0 +1,61 @@
+processMailer(clone $mailer, $method);
+ $mailer->reset();
+ }
+
+ /**
+ * Executes the mailer and extracts the relevant information from the generated email
+ *
+ * @param \Cake\Mailer\Mailer $mailer The mailer instance to execute and extract the email data from
+ * @param string $method The method to execute in the mailer
+ * @return void
+ */
+ protected function processMailer(Mailer $mailer, string $method): void
+ {
+ if (!$mailer->viewBuilder()->getTemplate()) {
+ $mailer->viewBuilder()->setTemplate($method);
+ }
+
+ $mailer->render();
+ $message = $mailer->getMessage();
+ $this->parts = [
+ 'html' => $message->getBodyHtml(),
+ 'text' => $message->getBodyText(),
+ ];
+
+ $extra = ['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'subject'];
+ $this->headers = array_filter($message->getHeaders($extra));
+ }
+}
diff --git a/src/Mailer/SentMailResult.php b/src/Mailer/SentMailResult.php
new file mode 100644
index 000000000..790b7c6a9
--- /dev/null
+++ b/src/Mailer/SentMailResult.php
@@ -0,0 +1,35 @@
+headers = $headers;
+ $this->parts = $parts;
+ }
+}
diff --git a/src/Mailer/Transport/DebugKitTransport.php b/src/Mailer/Transport/DebugKitTransport.php
new file mode 100644
index 000000000..f764bb66d
--- /dev/null
+++ b/src/Mailer/Transport/DebugKitTransport.php
@@ -0,0 +1,161 @@
+emailLog = $config['debugKitLog'];
+
+ if ($originalTransport !== null) {
+ $this->originalTransport = $originalTransport;
+
+ return;
+ }
+
+ $className = false;
+ if (!empty($config['originalClassName'])) {
+ /** @var class-string<\Cake\Mailer\AbstractTransport> $className */
+ $className = App::className(
+ $config['originalClassName'],
+ 'Mailer/Transport',
+ 'Transport',
+ );
+ }
+
+ if ($className) {
+ unset($config['originalClassName'], $config['debugKitLog']);
+ $this->originalTransport = new $className($config);
+ }
+ }
+
+ /**
+ * Capture the message into the in-memory email log and optionally forward
+ * to a wrapped real transport.
+ *
+ * Overrides the parent return shape: DebugKit stores the headers as an
+ * associative array (so the panel can render rows) and splits the body
+ * into text/html parts. Callers that consume this transport's return
+ * value directly must account for this richer shape.
+ *
+ * @param \Cake\Mailer\Message $message The message to capture.
+ * @return array
+ * @phpstan-return array{headers: array, message: array{text: string, html: string}}|array
+ */
+ public function send(Message $message): array
+ {
+ $headers = $message->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc']);
+ $parts = [
+ 'text' => $message->getBodyText(),
+ 'html' => $message->getBodyHtml(),
+ ];
+
+ $headers['Subject'] = $message->getOriginalSubject();
+ $result = ['headers' => $headers, 'message' => $parts];
+ $this->emailLog[] = $result;
+
+ if ($this->originalTransport !== null) {
+ return $this->originalTransport->send($message);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Proxy unknown methods to the wrapped object
+ *
+ * @param string $method The method to call
+ * @param array $args The args to call $method with.
+ * @return mixed
+ */
+ public function __call(string $method, array $args): mixed
+ {
+ /** @var callable $callable */
+ $callable = [$this->originalTransport, $method];
+
+ return call_user_func_array($callable, $args);
+ }
+
+ /**
+ * Proxy property reads to the wrapped object
+ *
+ * @param string $name The property to read.
+ * @return mixed
+ */
+ public function __get(string $name): mixed
+ {
+ return $this->originalTransport->{$name};
+ }
+
+ /**
+ * Proxy property changes to the wrapped object
+ *
+ * @param string $name The property to read.
+ * @param mixed $value The property value.
+ * @return void
+ */
+ public function __set(string $name, mixed $value): void
+ {
+ $this->originalTransport->{$name} = $value;
+ }
+
+ /**
+ * Proxy property changes to the wrapped object
+ *
+ * @param string $name The property to read.
+ * @return bool
+ */
+ public function __isset(string $name): bool
+ {
+ return isset($this->originalTransport->{$name});
+ }
+
+ /**
+ * Proxy property changes to the wrapped object
+ *
+ * @param string $name The property to delete.
+ * @return void
+ */
+ public function __unset(string $name): void
+ {
+ unset($this->originalTransport->{$name});
+ }
+}
diff --git a/src/Middleware/DebugKitMiddleware.php b/src/Middleware/DebugKitMiddleware.php
new file mode 100644
index 000000000..39f3f07a9
--- /dev/null
+++ b/src/Middleware/DebugKitMiddleware.php
@@ -0,0 +1,73 @@
+service = $service;
+ }
+
+ /**
+ * Invoke the middleware.
+ *
+ * DebugKit will augment the response and add the toolbar if possible.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ if ($this->service->isEnabled()) {
+ $this->service->loadPanels();
+ $this->service->initializePanels();
+ }
+ $response = $handler->handle($request);
+
+ if (!$this->service->isEnabled()) {
+ return $response;
+ }
+
+ $row = $this->service->saveData($request, $response);
+ if (!$row) {
+ return $response;
+ }
+
+ return $this->service->injectScripts($row, $response);
+ }
+}
diff --git a/src/Model/Behavior/TimedBehavior.php b/src/Model/Behavior/TimedBehavior.php
new file mode 100644
index 000000000..e25961d7c
--- /dev/null
+++ b/src/Model/Behavior/TimedBehavior.php
@@ -0,0 +1,78 @@
+getSubject();
+ $alias = $table->getAlias();
+ DebugTimer::start($alias . '_find', $alias . '->find()');
+
+ $formattedQuery = $query->formatResults(function ($results) use ($alias) {
+ DebugTimer::stop($alias . '_find');
+
+ return $results;
+ });
+
+ $event->setResult($formattedQuery);
+ }
+
+ /**
+ * beforeSave, starts a time before a save is initiated.
+ *
+ * @param \Cake\Event\EventInterface $event The beforeSave event
+ * @return void
+ */
+ public function beforeSave(EventInterface $event): void
+ {
+ /** @var \Cake\Datasource\RepositoryInterface $table */
+ $table = $event->getSubject();
+ $alias = $table->getAlias();
+ DebugTimer::start($alias . '_save', $alias . '->save()');
+ }
+
+ /**
+ * afterSave, stop the timer started from a save.
+ *
+ * @param \Cake\Event\EventInterface $event The afterSave event
+ * @return void
+ */
+ public function afterSave(EventInterface $event): void
+ {
+ /** @var \Cake\Datasource\RepositoryInterface $table */
+ $table = $event->getSubject();
+ $alias = $table->getAlias();
+ DebugTimer::stop($alias . '_save');
+ }
+}
diff --git a/src/Model/Entity/Panel.php b/src/Model/Entity/Panel.php
new file mode 100644
index 000000000..49f3bf02d
--- /dev/null
+++ b/src/Model/Entity/Panel.php
@@ -0,0 +1,82 @@
+
+ */
+ protected array $_hidden = ['content'];
+
+ /**
+ * Read the stream contents or inflate deflated data.
+ *
+ * Over certain sizes PDO will return file handles.
+ * For backwards compatibility and consistency we smooth over that difference here.
+ *
+ * @param mixed $content Content
+ * @return string
+ */
+ protected function _getContent(mixed $content): string
+ {
+ if (is_resource($content)) {
+ $content = (string)stream_get_contents($content);
+ }
+
+ if (is_string($content) && function_exists('gzinflate')) {
+ // phpcs:disable
+ $contentInflated = @gzinflate($content);
+ // phpcs:enable
+ if ($contentInflated !== false) {
+ return $contentInflated;
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Deflate the string data before saving it into database
+ *
+ * @param mixed $content Content
+ * @return mixed
+ */
+ protected function _setContent(mixed $content): mixed
+ {
+ if (is_string($content) && function_exists('gzdeflate')) {
+ $contentDeflated = gzdeflate($content, 9);
+ if ($contentDeflated !== false) {
+ $content = $contentDeflated;
+ }
+ }
+
+ return $content;
+ }
+}
diff --git a/src/Model/Entity/Request.php b/src/Model/Entity/Request.php
new file mode 100644
index 000000000..42880ef00
--- /dev/null
+++ b/src/Model/Entity/Request.php
@@ -0,0 +1,16 @@
+getConnection();
+ $schema = $connection->getSchemaCollection();
+
+ try {
+ $existing = $schema->listTables();
+ } catch (PDOException $e) {
+ // Handle errors when SQLite blows up if the schema has changed.
+ if (strpos($e->getMessage(), 'schema has changed') !== false) {
+ $existing = $schema->listTables();
+ } else {
+ throw $e;
+ }
+ }
+
+ try {
+ $config = require dirname(dirname(__DIR__)) . '/schema.php';
+ $driver = $connection->getDriver();
+ foreach ($config as $table) {
+ if (in_array($table['table'], $existing, true)) {
+ continue;
+ }
+ if (!in_array($table['table'], $fixtures, true)) {
+ continue;
+ }
+
+ // Use Database/Schema primitives to generate dialect specific
+ // CREATE TABLE statements and run them.
+ $schema = new TableSchema($table['table'], $table['columns']);
+ foreach ($table['constraints'] as $name => $itemConfig) {
+ $schema->addConstraint($name, $itemConfig);
+ }
+ foreach ($schema->createSql($connection) as $sql) {
+ $driver->execute($sql);
+ }
+ }
+ } catch (PDOException $e) {
+ if (strpos($e->getMessage(), 'unable to open')) {
+ throw new RuntimeException(
+ 'Could not create a SQLite database. ' .
+ 'Ensure that your webserver has write access to the database file and folder it is in.',
+ );
+ }
+ throw $e;
+ }
+ }
+}
diff --git a/src/Model/Table/PanelsTable.php b/src/Model/Table/PanelsTable.php
new file mode 100644
index 000000000..4cfc7b460
--- /dev/null
+++ b/src/Model/Table/PanelsTable.php
@@ -0,0 +1,72 @@
+belongsTo('DebugKit.Requests');
+ $this->ensureTables(['requests', 'panels']);
+ }
+
+ /**
+ * Find panels by request id
+ *
+ * @param \Cake\ORM\Query\SelectQuery $query The query
+ * @param string|int $requestId The request id
+ * @return \Cake\ORM\Query\SelectQuery The query.
+ */
+ public function findByRequest(SelectQuery $query, string|int $requestId): SelectQuery
+ {
+ return $query->where(['Panels.request_id' => $requestId])
+ ->orderBy(['Panels.title' => 'ASC']);
+ }
+
+ /**
+ * DebugKit tables are special.
+ *
+ * @return string
+ */
+ public static function defaultConnectionName(): string
+ {
+ return 'debug_kit';
+ }
+}
diff --git a/src/Model/Table/RequestsTable.php b/src/Model/Table/RequestsTable.php
new file mode 100644
index 000000000..38fb02d62
--- /dev/null
+++ b/src/Model/Table/RequestsTable.php
@@ -0,0 +1,173 @@
+hasMany('DebugKit.Panels', [
+ 'sort' => ['Panels.title' => 'ASC'],
+ ]);
+ $this->addBehavior('Timestamp', [
+ 'events' => [
+ 'Model.beforeSave' => ['requested_at' => 'new'],
+ ],
+ ]);
+ $this->ensureTables(['requests', 'panels']);
+ }
+
+ /**
+ * DebugKit tables are special.
+ *
+ * @return string
+ */
+ public static function defaultConnectionName(): string
+ {
+ return 'debug_kit';
+ }
+
+ /**
+ * Finder method to get recent requests as a simple array
+ *
+ * @param \Cake\ORM\Query\SelectQuery $query The query
+ * @return \Cake\ORM\Query\SelectQuery The query.
+ */
+ public function findRecent(SelectQuery $query): SelectQuery
+ {
+ return $query->orderBy(['Requests.requested_at' => 'DESC'])
+ ->limit(10);
+ }
+
+ /**
+ * Check if garbage collection should be run
+ *
+ * @return bool
+ */
+ protected function shouldGc(): bool
+ {
+ return rand(1, 10) === 10;
+ }
+
+ /**
+ * Check if garbage collection vacuum should be run
+ *
+ * @return bool
+ */
+ protected function shouldGcVacuum(): bool
+ {
+ return rand(1, 10) === 10;
+ }
+
+ /**
+ * Garbage collect old request data.
+ *
+ * Delete request data that is older than latest 20 requests.
+ * You can use the `DebugKit.requestCount` config to change this limit.
+ * This method will only trigger periodically.
+ *
+ * @return void
+ */
+ public function gc(): void
+ {
+ if (!$this->shouldGc()) {
+ return;
+ }
+
+ try {
+ $noPurge = $this->find()
+ ->select(['id'])
+ ->enableHydration(false)
+ ->orderBy(['requested_at' => 'desc'])
+ ->limit(Configure::read('DebugKit.requestCount') ?: 20)
+ ->all()
+ ->extract('id')
+ ->toArray();
+
+ if (empty($noPurge)) {
+ return;
+ }
+
+ $query = $this->Panels->deleteQuery()
+ ->where(['request_id NOT IN' => $noPurge]);
+ $statement = $query->execute();
+ $statement->closeCursor();
+
+ $query = $this->deleteQuery()
+ ->where(['id NOT IN' => $noPurge]);
+
+ $statement = $query->execute();
+ $statement->closeCursor();
+
+ $conn = $this->getConnection();
+ if ($conn->getDriver() instanceof Sqlite) {
+ $conn->execute('
+ PRAGMA auto_vacuum = FULL;
+ PRAGMA journal_mode = OFF;
+ PRAGMA synchronous = OFF;
+ PRAGMA foreign_keys = OFF;
+ PRAGMA temp_store = MEMORY;
+ PRAGMA automatic_index = OFF;
+ ');
+
+ if (!$this->shouldGcVacuum()) {
+ return;
+ }
+
+ try {
+ $conn->execute('VACUUM;');
+ } catch (PDOException $e) {
+ Log::warning(
+ 'Unable to run VACUUM on debug kit SQLite database. ' .
+ 'Please manually remove the database file',
+ );
+ Log::warning((string)$e);
+ }
+ }
+ } catch (PDOException $e) {
+ Log::warning('Unable to garbage collect requests table. This is probably due to concurrent requests.');
+ Log::warning((string)$e);
+ }
+ }
+}
diff --git a/src/Model/Table/SqlTraceTrait.php b/src/Model/Table/SqlTraceTrait.php
new file mode 100644
index 000000000..a9d79c712
--- /dev/null
+++ b/src/Model/Table/SqlTraceTrait.php
@@ -0,0 +1,95 @@
+fileStamp(parent::selectQuery());
+ }
+
+ /**
+ * Overwrite parent table method to inject SQL comment
+ */
+ public function updateQuery(): UpdateQuery
+ {
+ return $this->fileStamp(parent::updateQuery());
+ }
+
+ /**
+ * Overwrite parent table method to inject SQL comment
+ */
+ public function deleteQuery(): DeleteQuery
+ {
+ return $this->fileStamp(parent::deleteQuery());
+ }
+
+ /**
+ * Applies a comment to a query about which file created it.
+ *
+ * @template T of \Cake\ORM\Query\SelectQuery|\Cake\ORM\Query\UpdateQuery|\Cake\ORM\Query\DeleteQuery
+ * @param \Cake\ORM\Query\SelectQuery|\Cake\ORM\Query\UpdateQuery|\Cake\ORM\Query\DeleteQuery $query The Query to insert a comment into.
+ * @phpstan-param T $query
+ * @param int $start How many entries in the stack trace to skip.
+ * @param bool $debugOnly False to always stamp queries with a comment.
+ * @return \Cake\ORM\Query\SelectQuery|\Cake\ORM\Query\UpdateQuery|\Cake\ORM\Query\DeleteQuery
+ * @phpstan-return T
+ */
+ protected function fileStamp(
+ SelectQuery|UpdateQuery|DeleteQuery $query,
+ int $start = 1,
+ bool $debugOnly = true,
+ ): SelectQuery|UpdateQuery|DeleteQuery {
+ if (!Configure::read('debug') && $debugOnly === true) {
+ return $query;
+ }
+
+ $traces = Debugger::trace(['start' => $start, 'format' => 'array']);
+ $file = '[unknown]';
+ $line = '??';
+
+ if (is_array($traces)) {
+ foreach ($traces as $trace) {
+ $path = $trace['file'];
+ $line = $trace['line'];
+ $file = Debugger::trimPath($path);
+ if ($path === '[internal]') {
+ continue;
+ }
+ if (defined('CAKE_CORE_INCLUDE_PATH') && strpos($path, CAKE_CORE_INCLUDE_PATH) !== 0) {
+ break;
+ }
+ }
+ }
+
+ return $query->comment(sprintf('%s (line %s)', $file, $line));
+ }
+}
diff --git a/src/Panel/CachePanel.php b/src/Panel/CachePanel.php
new file mode 100644
index 000000000..fef295994
--- /dev/null
+++ b/src/Panel/CachePanel.php
@@ -0,0 +1,96 @@
+
+ */
+ protected array $instances = [];
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->logger = new ArrayLog();
+ }
+
+ /**
+ * Initialize - install cache spies.
+ *
+ * @return void
+ */
+ public function initialize(): void
+ {
+ foreach (Cache::configured() as $name) {
+ /** @var array $config */
+ $config = Cache::getConfig($name);
+ if (isset($config['className']) && $config['className'] instanceof DebugEngine) {
+ $instance = $config['className'];
+ } elseif (isset($config['className'])) {
+ /** @var \Cake\Cache\CacheEngine $engine */
+ $engine = Cache::pool($name);
+ // Unload from the cache registry so that subsequence calls to
+ // Cache::pool($name) use the new config with DebugEngine instance set below.
+ Cache::getRegistry()->unload($name);
+
+ $instance = new DebugEngine($engine, $name, $this->logger);
+ $instance->init();
+ $config['className'] = $instance;
+
+ Cache::drop($name);
+ Cache::setConfig($name, $config);
+ }
+ if (isset($instance)) {
+ $this->instances[$name] = $instance;
+ }
+ }
+ }
+
+ /**
+ * Get the data for this panel
+ *
+ * @return array
+ */
+ public function data(): array
+ {
+ $metrics = [];
+ foreach ($this->instances as $name => $instance) {
+ $metrics[$name] = $instance->metrics();
+ }
+ $logs = $this->logger->read();
+ $this->logger->clear();
+
+ return [
+ 'metrics' => $metrics,
+ 'logs' => $logs,
+ ];
+ }
+}
diff --git a/src/Panel/DeprecationsPanel.php b/src/Panel/DeprecationsPanel.php
new file mode 100644
index 000000000..b3013a935
--- /dev/null
+++ b/src/Panel/DeprecationsPanel.php
@@ -0,0 +1,161 @@
+_debug = new DebugInclude();
+ }
+
+ /**
+ * Get a list of files that were deprecated and split them out into the various parts of the app
+ *
+ * @return array
+ */
+ protected function _prepare(): array
+ {
+ $errors = static::$deprecatedErrors;
+ $return = ['cake' => [], 'app' => [], 'plugins' => [], 'vendor' => [], 'other' => []];
+
+ foreach ($errors as $error) {
+ $file = $error['file'];
+ $line = $error['line'];
+
+ $errorData = [
+ 'file' => $file,
+ 'line' => $line,
+ 'message' => $error['message'],
+ ];
+
+ $pluginName = $this->_debug->getPluginName($file);
+ /** @var string|false $pluginName */
+ if ($pluginName) {
+ $errorData['niceFile'] = $this->_debug->niceFileName($file, 'plugin', $pluginName);
+ $return['plugins'][$pluginName][] = $errorData;
+ } elseif ($this->_debug->isAppFile($file)) {
+ $errorData['niceFile'] = $this->_debug->niceFileName($file, 'app');
+ $return['app'][] = $errorData;
+ } elseif ($this->_debug->isCakeFile($file)) {
+ $errorData['niceFile'] = $this->_debug->niceFileName($file, 'cake');
+ $return['cake'][] = $errorData;
+ } else {
+ /** @var string|false $vendorName */
+ $vendorName = $this->_debug->getComposerPackageName($file);
+
+ if ($vendorName) {
+ $errorData['niceFile'] = $this->_debug->niceFileName($file, 'vendor', $vendorName);
+ $return['vendor'][$vendorName][] = $errorData;
+ } else {
+ $errorData['niceFile'] = $this->_debug->niceFileName($file, 'root');
+ $return['other'][] = $errorData;
+ }
+ }
+ }
+
+ ksort($return['plugins']);
+ ksort($return['vendor']);
+
+ return $return;
+ }
+
+ /**
+ * Add a error
+ *
+ * @param array $error The deprecated error
+ * @return void
+ */
+ public static function addDeprecatedError(array $error): void
+ {
+ static::$deprecatedErrors[] = $error;
+ }
+
+ /**
+ * Reset the tracked errors.
+ *
+ * @return void
+ */
+ public static function clearDeprecatedErrors(): void
+ {
+ static::$deprecatedErrors = [];
+ }
+
+ /**
+ * Get the number of files deprecated in this request.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ $data = $this->_data;
+ if (empty($data)) {
+ $data = $this->_prepare();
+ }
+
+ return (string)array_reduce($data, function ($carry, $item) {
+ if (empty($item)) {
+ return $carry;
+ }
+ // app, cake, or other groups
+ if (Hash::dimensions($item) == 2) {
+ return $carry + count($item);
+ }
+
+ // plugin and vendor groups
+ foreach ($item as $group) {
+ $carry += count($group);
+ }
+
+ return $carry;
+ }, 0);
+ }
+
+ /**
+ * Shutdown callback
+ *
+ * @param \Cake\Event\EventInterface $event Event
+ * @return void
+ */
+ public function shutdown(EventInterface $event): void
+ {
+ $this->_data = $this->_prepare();
+ }
+}
diff --git a/src/Panel/EnvironmentPanel.php b/src/Panel/EnvironmentPanel.php
new file mode 100644
index 000000000..87ae1b612
--- /dev/null
+++ b/src/Panel/EnvironmentPanel.php
@@ -0,0 +1,169 @@
+_debug = new DebugInclude();
+ }
+
+ /**
+ * Get necessary data about environment to pass back to controller
+ *
+ * @return array
+ */
+ protected function _prepare(): array
+ {
+ $return = [];
+ // PHP Data
+ $phpVer = phpversion();
+ $return['php'] = array_merge(
+ ['PHP_VERSION' => $phpVer],
+ $_SERVER,
+ );
+ unset($return['php']['argv']);
+
+ // ini Data
+ $return['ini'] = [
+ 'intl.default_locale' => ini_get('intl.default_locale'),
+ 'memory_limit' => ini_get('memory_limit'),
+ 'error_reporting' => ini_get('error_reporting'),
+ 'upload_max_filesize' => ini_get('upload_max_filesize'),
+ 'post_max_size' => ini_get('post_max_size'),
+ 'zend.assertions' => ini_get('zend.assertions'),
+ ];
+
+ // CakePHP Data
+ $return['cake'] = [
+ 'APP' => APP,
+ 'APP_DIR' => APP_DIR,
+ 'CACHE' => CACHE,
+ 'CAKE' => CAKE,
+ 'CAKE_CORE_INCLUDE_PATH' => CAKE_CORE_INCLUDE_PATH,
+ 'CONFIG' => CONFIG,
+ 'CORE_PATH' => CORE_PATH,
+ 'DS' => DS,
+ 'LOGS' => LOGS,
+ 'ROOT' => ROOT,
+ 'TESTS' => TESTS,
+ 'TMP' => TMP,
+ 'WWW_ROOT' => WWW_ROOT,
+ ];
+
+ $hiddenCakeConstants = array_fill_keys(
+ [
+ 'TIME_START', 'SECOND', 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR',
+ ],
+ '',
+ );
+ $var = get_defined_constants(true);
+ $return['app'] = array_diff_key($var['user'], $return['cake'], $hiddenCakeConstants);
+
+ $includePaths = $this->_debug->includePaths();
+ foreach ($includePaths as $k => $v) {
+ $includePaths[$k] = Debugger::exportVarAsNodes($v);
+ }
+
+ // Included files data
+ $return['includePaths'] = $includePaths;
+ $return['includedFiles'] = $this->prepareIncludedFiles();
+
+ return $return;
+ }
+
+ /**
+ * Shutdown callback
+ *
+ * @param \Cake\Event\EventInterface $event Event
+ * @return void
+ */
+ public function shutdown(EventInterface $event): void
+ {
+ $this->_data = $this->_prepare();
+ }
+
+ /**
+ * Build the list of files segmented by app, cake, plugins, vendor and other
+ *
+ * @return array
+ */
+ protected function prepareIncludedFiles(): array
+ {
+ $return = ['cake' => [], 'app' => [], 'plugins' => [], 'vendor' => [], 'other' => []];
+
+ foreach (get_included_files() as $file) {
+ /** @var string|false $pluginName */
+ $pluginName = $this->_debug->getPluginName($file);
+
+ if ($pluginName) {
+ $return['plugins'][$pluginName][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName(
+ $file,
+ 'plugin',
+ $pluginName,
+ );
+ } elseif ($this->_debug->isAppFile($file)) {
+ $return['app'][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName($file, 'app');
+ } elseif ($this->_debug->isCakeFile($file)) {
+ $return['cake'][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName($file, 'cake');
+ } else {
+ /** @var string|false $vendorName */
+ $vendorName = $this->_debug->getComposerPackageName($file);
+
+ if ($vendorName) {
+ $return['vendor'][$vendorName][] = $this->_debug->niceFileName($file, 'vendor', $vendorName);
+ } else {
+ $return['other'][] = $this->_debug->niceFileName($file, 'root');
+ }
+ }
+ }
+
+ $return['paths'] = $this->_debug->includePaths();
+
+ ksort($return['app']);
+ ksort($return['cake']);
+ ksort($return['plugins']);
+ ksort($return['vendor']);
+
+ foreach ($return['plugins'] as &$plugin) {
+ ksort($plugin);
+ }
+
+ foreach ($return as $k => $v) {
+ $return[$k] = Debugger::exportVarAsNodes($v);
+ }
+
+ return $return;
+ }
+}
diff --git a/src/Panel/HistoryPanel.php b/src/Panel/HistoryPanel.php
new file mode 100644
index 000000000..850648b90
--- /dev/null
+++ b/src/Panel/HistoryPanel.php
@@ -0,0 +1,51 @@
+fetchTable('DebugKit.Requests');
+ $recent = $table->find('recent');
+
+ return [
+ 'requests' => $recent->toArray(),
+ ];
+ }
+
+ /**
+ * Gets the initial text for the history summary
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ return '0 xhr';
+ }
+}
diff --git a/src/Panel/IncludePanel.php b/src/Panel/IncludePanel.php
new file mode 100644
index 000000000..3056c5f42
--- /dev/null
+++ b/src/Panel/IncludePanel.php
@@ -0,0 +1,131 @@
+_debug = new DebugInclude();
+ deprecationWarning(
+ '5.1.0',
+ 'Include panel is deprecated. Remove it from your panel configuration, and use Environment Panel instead.',
+ );
+ }
+
+ /**
+ * Get a list of files that were included and split them out into the various parts of the app
+ *
+ * @return array
+ */
+ protected function _prepare(): array
+ {
+ $return = ['cake' => [], 'app' => [], 'plugins' => [], 'vendor' => [], 'other' => []];
+
+ foreach (get_included_files() as $file) {
+ /** @var string|false $pluginName */
+ $pluginName = $this->_debug->getPluginName($file);
+
+ if ($pluginName) {
+ $return['plugins'][$pluginName][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName(
+ $file,
+ 'plugin',
+ $pluginName,
+ );
+ } elseif ($this->_debug->isAppFile($file)) {
+ $return['app'][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName($file, 'app');
+ } elseif ($this->_debug->isCakeFile($file)) {
+ $return['cake'][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName($file, 'cake');
+ } else {
+ /** @var string|false $vendorName */
+ $vendorName = $this->_debug->getComposerPackageName($file);
+
+ if ($vendorName) {
+ $return['vendor'][$vendorName][] = $this->_debug->niceFileName($file, 'vendor', $vendorName);
+ } else {
+ $return['other'][] = $this->_debug->niceFileName($file, 'root');
+ }
+ }
+ }
+
+ $return['paths'] = $this->_debug->includePaths();
+
+ ksort($return['app']);
+ ksort($return['cake']);
+ ksort($return['plugins']);
+ ksort($return['vendor']);
+
+ foreach ($return['plugins'] as &$plugin) {
+ ksort($plugin);
+ }
+
+ foreach ($return as $k => $v) {
+ $return[$k] = Debugger::exportVarAsNodes($v);
+ }
+
+ return $return;
+ }
+
+ /**
+ * Get the number of files included in this request.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ $data = $this->_data;
+ if (empty($data)) {
+ $data = $this->_prepare();
+ }
+
+ unset($data['paths']);
+ $data = array_filter($data, function ($v, $k) {
+ return !empty($v);
+ }, ARRAY_FILTER_USE_BOTH);
+
+ return (string)count(Hash::flatten($data));
+ }
+
+ /**
+ * Shutdown callback
+ *
+ * @param \Cake\Event\EventInterface $event Event
+ * @return void
+ */
+ public function shutdown(EventInterface $event): void
+ {
+ $this->_data = $this->_prepare();
+ }
+}
diff --git a/src/Panel/LogPanel.php b/src/Panel/LogPanel.php
new file mode 100644
index 000000000..0b23db269
--- /dev/null
+++ b/src/Panel/LogPanel.php
@@ -0,0 +1,67 @@
+ 'DebugKit.DebugKit',
+ ]);
+ }
+
+ /**
+ * Get the panel data
+ *
+ * @return array
+ */
+ public function data(): array
+ {
+ return [
+ 'logger' => Log::engine('debug_kit_log_panel'),
+ ];
+ }
+
+ /**
+ * Get the summary data.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ /** @var \DebugKit\Log\Engine\DebugKitLog|null $logger */
+ $logger = Log::engine('debug_kit_log_panel');
+ if (!$logger) {
+ return '0';
+ }
+
+ return (string)$logger->count();
+ }
+}
diff --git a/src/Panel/MailPanel.php b/src/Panel/MailPanel.php
new file mode 100644
index 000000000..c44a4f79c
--- /dev/null
+++ b/src/Panel/MailPanel.php
@@ -0,0 +1,97 @@
+getProperty('_config');
+ /** @var array<\Cake\Mailer\AbstractTransport|array> $configs */
+ $configs = $property->getValue();
+
+ $log = $this->emailLog = new ArrayObject();
+
+ foreach ($configs as $name => $transport) {
+ if (is_object($transport)) {
+ if (!$transport instanceof DebugKitTransport) {
+ $configs[$name] = new DebugKitTransport(['debugKitLog' => $log], $transport);
+ }
+ continue;
+ }
+
+ $className = App::className($transport['className'], 'Mailer/Transport', 'Transport');
+ if (!$className || $className === DebugKitTransport::class) {
+ continue;
+ }
+
+ $transport['originalClassName'] = $transport['className'];
+ $transport['className'] = 'DebugKit.DebugKit';
+ $transport['debugKitLog'] = $log;
+
+ $configs[$name] = $transport;
+ }
+ $reflection->setStaticPropertyValue('_config', $configs);
+ }
+
+ /**
+ * Get the data this panel wants to store.
+ *
+ * @return array
+ */
+ public function data(): array
+ {
+ return [
+ 'emails' => isset($this->emailLog) ? $this->emailLog->getArrayCopy() : [],
+ ];
+ }
+
+ /**
+ * Get summary data from the queries run.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ if (empty($this->emailLog)) {
+ return '';
+ }
+
+ return (string)count($this->emailLog);
+ }
+}
diff --git a/src/Panel/PackagesPanel.php b/src/Panel/PackagesPanel.php
new file mode 100644
index 000000000..81750a0b5
--- /dev/null
+++ b/src/Panel/PackagesPanel.php
@@ -0,0 +1,47 @@
+exists()) {
+ $lockContent = $lockFile->read();
+ $packages = $lockContent['packages'];
+ $devPackages = $lockContent['packages-dev'];
+ }
+
+ return [
+ 'packages' => $packages,
+ 'devPackages' => $devPackages,
+ ];
+ }
+}
diff --git a/src/Panel/PanelRegistry.php b/src/Panel/PanelRegistry.php
new file mode 100644
index 000000000..3c268695d
--- /dev/null
+++ b/src/Panel/PanelRegistry.php
@@ -0,0 +1,95 @@
+
+ */
+class PanelRegistry extends ObjectRegistry implements EventDispatcherInterface
+{
+ use EventDispatcherTrait;
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\Event\EventManager $eventManager Event Manager that panels should bind to.
+ * Typically this is the global manager.
+ */
+ public function __construct(EventManager $eventManager)
+ {
+ $this->setEventManager($eventManager);
+ }
+
+ /**
+ * Resolve a panel class name.
+ *
+ * Part of the template method for Cake\Utility\ObjectRegistry::load()
+ *
+ * @param string $class Partial class name to resolve.
+ * @return string|null Either the correct class name, null if the class is not found.
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ return App::className($class, 'Panel', 'Panel');
+ }
+
+ /**
+ * Throws an exception when a component is missing.
+ *
+ * Part of the template method for Cake\Utility\ObjectRegistry::load()
+ *
+ * @param string $class The classname that is missing.
+ * @param string $plugin The plugin the component is missing in.
+ * @return void
+ * @throws \RuntimeException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ throw new RuntimeException(sprintf("Unable to find '%s' panel.", $class));
+ }
+
+ /**
+ * Create the panels instance.
+ *
+ * Part of the template method for Cake\Utility\ObjectRegistry::load()
+ *
+ * @param \DebugKit\DebugPanel|class-string<\DebugKit\DebugPanel> $class The classname to create.
+ * @param string $alias The alias of the panel.
+ * @param array $config An array of config to use for the panel.
+ * @return \DebugKit\DebugPanel The constructed panel class.
+ */
+ protected function _create(object|string $class, string $alias, array $config): DebugPanel
+ {
+ if (is_string($class)) {
+ $instance = new $class();
+ } else {
+ $instance = $class;
+ }
+
+ $this->getEventManager()->on($instance);
+
+ return $instance;
+ }
+}
diff --git a/src/Panel/PluginsPanel.php b/src/Panel/PluginsPanel.php
new file mode 100644
index 000000000..b8011740e
--- /dev/null
+++ b/src/Panel/PluginsPanel.php
@@ -0,0 +1,62 @@
+_data['hasEmptyAppConfig'] = empty($config);
+ $plugins = [];
+
+ foreach ($config as $pluginName => $options) {
+ $plugins[$pluginName] = [
+ 'isLoaded' => $loadedPluginsCollection->has($pluginName),
+ 'onlyDebug' => $options['onlyDebug'] ?? false,
+ 'onlyCli' => $options['onlyCli'] ?? false,
+ 'optional' => $options['optional'] ?? false,
+ ];
+ }
+
+ $this->_data['plugins'] = $plugins;
+ }
+
+ /**
+ * Get summary data for the plugins panel.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ if (!isset($this->_data['plugins'])) {
+ return '0';
+ }
+
+ return (string)count($this->_data['plugins']);
+ }
+}
diff --git a/src/Panel/RequestPanel.php b/src/Panel/RequestPanel.php
new file mode 100644
index 000000000..377dd970b
--- /dev/null
+++ b/src/Panel/RequestPanel.php
@@ -0,0 +1,67 @@
+getSubject();
+ $request = $controller->getRequest();
+ $maxDepth = Configure::read('DebugKit.maxDepth', 5);
+
+ $attributes = [];
+ foreach ($request->getAttributes() as $attr => $value) {
+ try {
+ serialize($value);
+ } catch (Exception $e) {
+ $value = "Could not serialize `{$attr}`. It failed with {$e->getMessage()}";
+ }
+ $attributes[$attr] = Debugger::exportVarAsNodes($value, $maxDepth);
+ }
+
+ $this->_data = [
+ 'params' => $request->getAttribute('params'),
+ 'attributes' => $attributes,
+ 'query' => Debugger::exportVarAsNodes($request->getQueryParams(), $maxDepth),
+ 'data' => Debugger::exportVarAsNodes($request->getData(), $maxDepth),
+ 'cookie' => Debugger::exportVarAsNodes($request->getCookieParams(), $maxDepth),
+ 'get' => Debugger::exportVarAsNodes($request->getQueryParams(), $maxDepth),
+ 'session' => Debugger::exportVarAsNodes($request->getSession()->read(), $maxDepth),
+ 'matchedRoute' => $request->getParam('_matchedRoute'),
+ 'headers' => [
+ 'response' => headers_sent($file, $line),
+ 'file' => $file,
+ 'line' => $line,
+ ],
+ ];
+ }
+}
diff --git a/src/Panel/RoutesPanel.php b/src/Panel/RoutesPanel.php
new file mode 100644
index 000000000..ef9ee979a
--- /dev/null
+++ b/src/Panel/RoutesPanel.php
@@ -0,0 +1,53 @@
+defaults['plugin']) || $route->defaults['plugin'] !== 'DebugKit';
+ });
+
+ return (string)count($routes);
+ }
+
+ /**
+ * Data collection callback.
+ *
+ * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event The shutdown event.
+ * @return void
+ */
+ public function shutdown(EventInterface $event): void
+ {
+ $controller = $event->getSubject();
+ $this->_data = [
+ 'matchedRoute' => $controller->getRequest()->getParam('_matchedRoute'),
+ ];
+ }
+}
diff --git a/src/Panel/SessionPanel.php b/src/Panel/SessionPanel.php
new file mode 100644
index 000000000..84b73ffb7
--- /dev/null
+++ b/src/Panel/SessionPanel.php
@@ -0,0 +1,48 @@
+getSubject();
+ $request = $controller->getRequest();
+
+ $maxDepth = Configure::read('DebugKit.maxDepth', 5);
+ $content = Debugger::exportVarAsNodes($request->getSession()->read(), $maxDepth);
+ $this->_data = compact('content');
+ }
+}
diff --git a/src/Panel/SqlLogPanel.php b/src/Panel/SqlLogPanel.php
new file mode 100644
index 000000000..231a25005
--- /dev/null
+++ b/src/Panel/SqlLogPanel.php
@@ -0,0 +1,131 @@
+configName() === 'debug_kit') {
+ return;
+ }
+ $driver = $connection->getDriver();
+
+ if (!method_exists($driver, 'setLogger')) {
+ return;
+ }
+
+ $logger = null;
+ if ($driver instanceof Driver) {
+ $logger = $driver->getLogger();
+ } elseif (method_exists($connection, 'getLogger')) {
+ // ElasticSearch connection holds the logger, not the Elastica Driver
+ $logger = $connection->getLogger();
+ }
+
+ if ($logger instanceof DebugLog) {
+ $logger->setIncludeSchema($includeSchemaReflection);
+ static::$_loggers[] = $logger;
+
+ return;
+ }
+ $logger = new DebugLog($logger, $name, $includeSchemaReflection);
+
+ /** @var \Cake\Database\Driver $driver */
+ $driver->setLogger($logger);
+
+ static::$_loggers[] = $logger;
+ }
+
+ /**
+ * Get the data this panel wants to store.
+ *
+ * @return array
+ */
+ public function data(): array
+ {
+ return [
+ 'tables' => array_map(function (Table $table) {
+ return $table->getAlias();
+ }, $this->getTableLocator()->genericInstances()),
+ 'loggers' => static::$_loggers,
+ ];
+ }
+
+ /**
+ * Get summary data from the queries run.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ $count = $time = 0;
+ foreach (static::$_loggers as $logger) {
+ $count += count($logger->queries());
+ $time += $logger->totalTime();
+ }
+ if (!$count) {
+ return '0';
+ }
+
+ return "$count / $time ms";
+ }
+}
diff --git a/src/Panel/TimerPanel.php b/src/Panel/TimerPanel.php
new file mode 100644
index 000000000..7b3dc01df
--- /dev/null
+++ b/src/Panel/TimerPanel.php
@@ -0,0 +1,130 @@
+
+ */
+ public function implementedEvents(): array
+ {
+ $before = function ($name) {
+ return function () use ($name): void {
+ DebugTimer::start($name);
+ };
+ };
+ $after = function ($name) {
+ return function () use ($name): void {
+ DebugTimer::stop($name);
+ };
+ };
+ $both = function ($name) use ($before, $after) {
+ return [
+ ['priority' => 0, 'callable' => $before('Event: ' . $name)],
+ ['priority' => 999, 'callable' => $after('Event: ' . $name)],
+ ];
+ };
+
+ return [
+ 'Controller.initialize' => [
+ ['priority' => 0, 'callable' => function (): void {
+ DebugMemory::record('Controller initialization');
+ }],
+ ['priority' => 0, 'callable' => $before('Event: Controller.initialize')],
+ ['priority' => 999, 'callable' => $after('Event: Controller.initialize')],
+ ],
+ 'Controller.startup' => [
+ ['priority' => 0, 'callable' => $before('Event: Controller.startup')],
+ ['priority' => 999, 'callable' => $after('Event: Controller.startup')],
+ ['priority' => 999, 'callable' => function (): void {
+ DebugMemory::record('Controller action start');
+ DebugTimer::start('Controller: action');
+ }],
+ ],
+ 'Controller.beforeRender' => [
+ ['priority' => 0, 'callable' => function (): void {
+ DebugTimer::stop('Controller: action');
+ }],
+ ['priority' => 0, 'callable' => $before('Event: Controller.beforeRender')],
+ ['priority' => 999, 'callable' => $after('Event: Controller.beforeRender')],
+ ['priority' => 999, 'callable' => function (): void {
+ DebugMemory::record('View Render start');
+ DebugTimer::start('View: Render');
+ }],
+ ],
+ 'View.beforeRender' => $both('View.beforeRender'),
+ 'View.afterRender' => $both('View.afterRender'),
+ 'View.beforeLayout' => $both('View.beforeLayout'),
+ 'View.afterLayout' => $both('View.afterLayout'),
+ 'View.beforeRenderFile' => [
+ ['priority' => 0, 'callable' => function ($event, $filename): void {
+ DebugTimer::start('Render File: ' . $filename);
+ }],
+ ],
+ 'View.afterRenderFile' => [
+ ['priority' => 0, 'callable' => function ($event, $filename): void {
+ DebugTimer::stop('Render File: ' . $filename);
+ }],
+ ],
+ 'Controller.shutdown' => [
+ ['priority' => 0, 'callable' => $before('Event: Controller.shutdown')],
+ ['priority' => 0, 'callable' => function (): void {
+ DebugTimer::stop('View: Render');
+ DebugMemory::record('Controller shutdown');
+ }],
+ ['priority' => 999, 'callable' => $after('Event: Controller.shutdown')],
+ ],
+ ];
+ }
+
+ /**
+ * Get the data for the panel.
+ *
+ * @return array
+ */
+ public function data(): array
+ {
+ return [
+ 'requestTime' => DebugTimer::requestTime(),
+ 'timers' => DebugTimer::getAll(),
+ 'memory' => DebugMemory::getAll(),
+ 'peakMemory' => DebugMemory::getPeak(),
+ ];
+ }
+
+ /**
+ * Get the summary for the panel.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ $time = Number::precision(DebugTimer::requestTime(), 2) . ' s';
+ $memory = Number::toReadableSize(DebugMemory::getPeak());
+
+ return "$time / $memory";
+ }
+}
diff --git a/src/Panel/VariablesPanel.php b/src/Panel/VariablesPanel.php
new file mode 100644
index 000000000..40f0ad242
--- /dev/null
+++ b/src/Panel/VariablesPanel.php
@@ -0,0 +1,131 @@
+getErrors();
+
+ foreach ($entity->getVisible() as $property) {
+ $v = $entity[$property];
+ if ($v instanceof EntityInterface) {
+ $errors[$property] = $this->_getErrors($v);
+ } elseif (is_array($v)) {
+ foreach ($v as $key => $varValue) {
+ if ($varValue instanceof EntityInterface) {
+ $errors[$property][$key] = $this->_getErrors($varValue);
+ }
+ }
+ }
+ }
+
+ return Hash::filter($errors);
+ }
+
+ /**
+ * Safely retrieves debug information from an object
+ * and applies a callback.
+ *
+ * @param callable $walker The walker to apply on the debug info array.
+ * @param object $item The item whose debug info to retrieve.
+ * @return array|string
+ */
+ protected function _walkDebugInfo(callable $walker, object $item): array|string
+ {
+ try {
+ /** @phpstan-ignore method.notFound */
+ $info = $item->__debugInfo();
+ } catch (Exception $exception) {
+ return sprintf(
+ 'Could not retrieve debug info - %s. Error: %s in %s, line %d',
+ get_class($item),
+ $exception->getMessage(),
+ $exception->getFile(),
+ $exception->getLine(),
+ );
+ }
+
+ return array_map($walker, $info);
+ }
+
+ /**
+ * Shutdown event
+ *
+ * @param \Cake\Event\EventInterface $event The event
+ * @return void
+ */
+ public function shutdown(EventInterface $event): void
+ {
+ /** @var \Cake\Controller\Controller $controller */
+ $controller = $event->getSubject();
+ $errors = [];
+ $content = [];
+ $vars = $controller->viewBuilder()->getVars();
+ $varsMaxDepth = (int)Configure::read('DebugKit.variablesPanelMaxDepth', 5);
+
+ foreach ($vars as $k => $v) {
+ // Get the validation errors for Entity
+ if ($v instanceof EntityInterface) {
+ $errors[$k] = Debugger::exportVarAsNodes($this->_getErrors($v), $varsMaxDepth);
+ } elseif ($v instanceof Form) {
+ $formErrors = $v->getErrors();
+ if ($formErrors) {
+ $errors[$k] = Debugger::exportVarAsNodes($formErrors, $varsMaxDepth);
+ }
+ }
+ $content[$k] = Debugger::exportVarAsNodes($v, $varsMaxDepth);
+ }
+
+ $this->_data = [
+ 'variables' => $content,
+ 'errors' => $errors,
+ 'varsMaxDepth' => $varsMaxDepth,
+ ];
+ }
+
+ /**
+ * Get summary data for the variables panel.
+ *
+ * @return string
+ */
+ public function summary(): string
+ {
+ if (!isset($this->_data['variables'])) {
+ return '0';
+ }
+
+ return (string)count($this->_data['variables']);
+ }
+}
diff --git a/src/ToolbarService.php b/src/ToolbarService.php
new file mode 100644
index 000000000..590c2a195
--- /dev/null
+++ b/src/ToolbarService.php
@@ -0,0 +1,418 @@
+ [
+ 'DebugKit.Cache' => true,
+ 'DebugKit.Request' => true,
+ 'DebugKit.SqlLog' => true,
+ 'DebugKit.Timer' => true,
+ 'DebugKit.Log' => true,
+ 'DebugKit.Variables' => true,
+ 'DebugKit.Environment' => true,
+ 'DebugKit.History' => true,
+ 'DebugKit.Routes' => true,
+ 'DebugKit.Packages' => true,
+ 'DebugKit.Mail' => true,
+ 'DebugKit.Deprecations' => true,
+ 'DebugKit.Plugins' => true,
+ ],
+ 'forceEnable' => false,
+ 'safeTld' => [],
+ 'ignorePathsPattern' => null,
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\Event\EventManager $events The event manager to use defaults to the global manager
+ * @param array $config The configuration data for DebugKit.
+ */
+ public function __construct(EventManager $events, array $config)
+ {
+ $this->setConfig($config);
+ $this->registry = new PanelRegistry($events);
+ }
+
+ /**
+ * Fetch the PanelRegistry
+ *
+ * @return \DebugKit\Panel\PanelRegistry
+ */
+ public function registry(): PanelRegistry
+ {
+ return $this->registry;
+ }
+
+ /**
+ * Check whether or not debug kit is enabled.
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ if (!isset($GLOBALS['FORCE_DEBUGKIT_TOOLBAR'])) {
+ $GLOBALS['FORCE_DEBUGKIT_TOOLBAR'] = false;
+ }
+ if (
+ defined('PHPUNIT_COMPOSER_INSTALL') &&
+ !$GLOBALS['FORCE_DEBUGKIT_TOOLBAR']
+ ) {
+ return false;
+ }
+ $enabled = (bool)Configure::read('debug')
+ && !$this->isSuspiciouslyProduction()
+ && php_sapi_name() !== 'phpdbg';
+
+ if ($enabled) {
+ return true;
+ }
+ $force = $this->getConfig('forceEnable');
+ if (is_callable($force)) {
+ return $force();
+ }
+
+ return $force;
+ }
+
+ /**
+ * Returns true if this application is being executed on a domain with a TLD
+ * that is commonly associated with a production environment, or if the IP
+ * address is not in a private or reserved range.
+ *
+ * Private IPv4 = 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16
+ * Reserved IPv4 = 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8 and 240.0.0.0/4
+ *
+ * Private IPv6 = fc00::/7
+ * Reserved IPv6 = ::1/128, ::/128, ::ffff:0:0/96 and fe80::/10
+ *
+ * @return bool
+ */
+ protected function isSuspiciouslyProduction(): bool
+ {
+ $host = parse_url('http://' . env('HTTP_HOST'), PHP_URL_HOST);
+ if ($host === false || $host === null) {
+ return false;
+ }
+
+ // IPv6 addresses in URLs are enclosed in brackets. Remove them.
+ $host = trim($host, '[]');
+
+ // Check if the host is a private or reserved IPv4/6 address.
+ $isIp = filter_var($host, FILTER_VALIDATE_IP) !== false;
+ if ($isIp) {
+ $flags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
+
+ return filter_var($host, FILTER_VALIDATE_IP, $flags) !== false;
+ }
+
+ // So it's not an IP address. It must be a domain name.
+ $parts = explode('.', $host);
+ if (count($parts) == 1) {
+ return false;
+ }
+
+ // Check if the TLD is in the list of safe TLDs.
+ $tld = end($parts);
+ $safeTlds = ['localhost', 'invalid', 'test', 'example', 'local', 'internal'];
+ $safeTlds = array_merge($safeTlds, (array)$this->getConfig('safeTld'));
+
+ if (in_array($tld, $safeTlds, true)) {
+ return false;
+ }
+
+ // Don't log a warning if forceEnable is set.
+ if (!$this->getConfig('forceEnable')) {
+ $safeList = implode(', ', $safeTlds);
+ Log::warning(
+ "DebugKit is disabling itself as your host `{$host}` " .
+ "is not in the known safe list of top-level-domains ({$safeList}). " .
+ 'If you would like to force DebugKit on use the `DebugKit.forceEnable` Configure option.',
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the list of loaded panels
+ *
+ * @return array
+ */
+ public function loadedPanels(): array
+ {
+ return $this->registry->loaded();
+ }
+
+ /**
+ * Get the a loaded panel
+ *
+ * @param string $name The name of the panel you want to get.
+ * @return \DebugKit\DebugPanel|null The panel or null.
+ */
+ public function panel(string $name): ?DebugPanel
+ {
+ return $this->registry->{$name};
+ }
+
+ /**
+ * Load all the panels being used
+ *
+ * @return void
+ */
+ public function loadPanels(): void
+ {
+ foreach ($this->getConfig('panels') as $panel => $enabled) {
+ [$panel, $enabled] = is_numeric($panel) ? [$enabled, true] : [$panel, $enabled];
+ if ($enabled) {
+ $this->registry->load($panel);
+ }
+ }
+ }
+
+ /**
+ * Call the initialize method onl all the loaded panels.
+ *
+ * @return void
+ */
+ public function initializePanels(): void
+ {
+ foreach ($this->registry->loaded() as $panel) {
+ $this->registry->{$panel}->initialize();
+ }
+ }
+
+ /**
+ * Save the toolbar state.
+ *
+ * @param \Cake\Http\ServerRequest $request The request
+ * @param \Psr\Http\Message\ResponseInterface $response The response
+ * @return \DebugKit\Model\Entity\Request|false Saved request data.
+ */
+ public function saveData(ServerRequest $request, ResponseInterface $response): Request|false
+ {
+ $path = $request->getUri()->getPath();
+ $dashboardUrl = '/debug-kit';
+ if (strpos($path, 'debug_kit') !== false || strpos($path, 'debug-kit') !== false) {
+ if (!($path === $dashboardUrl || $path === $dashboardUrl . '/')) {
+ // internal debug-kit request
+ return false;
+ }
+ // debug-kit dashboard, save request and show toolbar
+ }
+
+ $ignorePathsPattern = $this->getConfig('ignorePathsPattern');
+ $statusCode = $response->getStatusCode();
+ if (
+ $ignorePathsPattern &&
+ $statusCode >= 200 &&
+ $statusCode <= 299 &&
+ preg_match($ignorePathsPattern, $path)
+ ) {
+ return false;
+ }
+
+ $data = [
+ 'url' => $request->getUri()->getPath(),
+ 'content_type' => $response->getHeaderLine('Content-Type'),
+ 'method' => $request->getMethod(),
+ 'status_code' => $response->getStatusCode(),
+ 'requested_at' => $request->getEnv('REQUEST_TIME'),
+ 'panels' => [],
+ ];
+ try {
+ /** @var \DebugKit\Model\Table\RequestsTable $requests */
+ $requests = $this->fetchTable('DebugKit.Requests');
+ $requests->gc();
+ } catch (MissingDatasourceConfigException $e) {
+ Log::warning(
+ 'Unable to save request. Check your debug_kit datasource connection ' .
+ 'or ensure that PDO SQLite extension is enabled.',
+ );
+ Log::warning($e->getMessage());
+
+ return false;
+ }
+
+ $row = $requests->newEntity($data);
+ $row->setNew(true);
+
+ foreach ($this->registry->loaded() as $name) {
+ $panel = $this->registry->{$name};
+ $data = null;
+ $handlerInstalled = false;
+ try {
+ $data = $panel->data();
+
+ // Catch only warnings/notices raised during serialization; fatals
+ // and exceptions in __sleep/__serialize already surface as throws.
+ set_error_handler(
+ function ($errno, $errstr) use ($name): bool {
+ throw new Exception("Serialization error in panel '{$name}': {$errstr}");
+ },
+ E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE,
+ );
+ $handlerInstalled = true;
+
+ $content = serialize($data);
+ } catch (Exception $e) {
+ $errorMessage = sprintf(
+ 'Failed to serialize data for panel "%s": %s',
+ $name,
+ $e->getMessage(),
+ );
+
+ Log::warning($errorMessage);
+ Log::debug('Panel data type: ' . gettype($data));
+
+ $content = serialize([
+ 'error' => $errorMessage,
+ 'panel' => $name,
+ ]);
+ } finally {
+ if ($handlerInstalled) {
+ restore_error_handler();
+ }
+ }
+ $row->panels[] = $requests->Panels->newEntity([
+ 'panel' => $name,
+ 'element' => $panel->elementName(),
+ 'title' => $panel->title(),
+ 'summary' => $panel->summary(),
+ 'content' => $content,
+ ]);
+ }
+
+ try {
+ return $requests->save($row);
+ } catch (CakeException $e) {
+ Log::warning('Unable to save request. This is probably due to concurrent requests.');
+ Log::warning($e->getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Reads the modified date of a file in the webroot, and returns the integer
+ *
+ * @return string
+ */
+ public function getToolbarUrl(): string
+ {
+ $url = 'js/inject-iframe.js';
+ $filePaths = [
+ str_replace('/', DIRECTORY_SEPARATOR, WWW_ROOT . 'debug_kit/' . $url),
+ str_replace('/', DIRECTORY_SEPARATOR, CorePlugin::path('DebugKit') . 'webroot/' . $url),
+ ];
+ $url = '/debug_kit/' . $url;
+ foreach ($filePaths as $filePath) {
+ if (file_exists($filePath)) {
+ return $url . '?' . filemtime($filePath);
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * Injects the JS to build the toolbar.
+ *
+ * The toolbar will only be injected if the response's content type
+ * contains HTML and there is a
+ = $this->fetch('content') ?>
+
+ = $this->Html->image('DebugKit./img/cake.icon.png', ['class' => 'o-loader__indicator'])?>
+
+ tag.
+ *
+ * @param \DebugKit\Model\Entity\Request $row The request data to inject.
+ * @param \Psr\Http\Message\ResponseInterface $response The response to augment.
+ * @return \Psr\Http\Message\ResponseInterface The modified response
+ */
+ public function injectScripts(Request $row, ResponseInterface $response): ResponseInterface
+ {
+ $response = $response->withHeader('X-DEBUGKIT-ID', (string)$row->id);
+ if (strpos($response->getHeaderLine('Content-Type'), 'html') === false) {
+ return $response;
+ }
+ $body = $response->getBody();
+ if (!$body->isSeekable() || !$body->isWritable()) {
+ return $response;
+ }
+ $body->rewind();
+ $contents = $body->getContents();
+
+ $pos = strrpos($contents, '');
+ if ($pos === false) {
+ return $response;
+ }
+ // Use Router to get the request so that we can see the
+ // state after other middleware have been applied.
+ $request = Router::getRequest();
+ $nonce = '';
+ if ($request && $request->getAttribute('cspScriptNonce')) {
+ $nonce = sprintf(' nonce="%s"', $request->getAttribute('cspScriptNonce'));
+ }
+
+ $url = Router::url('/', true);
+ $script = sprintf(
+ '',
+ $row->id,
+ $url,
+ Router::url($this->getToolbarUrl()),
+ $nonce,
+ );
+ $contents = substr($contents, 0, $pos) . $script . substr($contents, $pos);
+ $body->rewind();
+ $body->write($contents);
+
+ return $response->withBody($body);
+ }
+}
diff --git a/src/View/AjaxView.php b/src/View/AjaxView.php
new file mode 100644
index 000000000..c2844efdc
--- /dev/null
+++ b/src/View/AjaxView.php
@@ -0,0 +1,42 @@
+response = $this->response->withType('ajax');
+ }
+}
diff --git a/src/View/Helper/CredentialsHelper.php b/src/View/Helper/CredentialsHelper.php
new file mode 100644
index 000000000..3df741016
--- /dev/null
+++ b/src/View/Helper/CredentialsHelper.php
@@ -0,0 +1,67 @@
+>
+ */
+ public array $helpers = ['Html', 'DebugKit.Toolbar'];
+
+ /**
+ * Replace credentials in url's by *****
+ * Example mysql://username:password@localhost/my_db -> mysql://******@localhost/my_db
+ *
+ * @param mixed $in variable to filter
+ * @return mixed
+ */
+ public function filter(mixed $in): mixed
+ {
+ $regexp = '/^([^:;]+:\/\/)([^:;]+:?.*?)@(.*)$/i';
+ if (!is_string($in) || empty($in)) {
+ return $in;
+ }
+ preg_match_all($regexp, $in, $tokens);
+ if ($tokens[0] === []) {
+ return h($in);
+ }
+ $protocol = Hash::get($tokens, '1.0');
+ $credentials = Hash::get($tokens, '2.0');
+ $tail = Hash::get($tokens, '3.0');
+ $link = $this->Html->tag('a', '******', [
+ 'class' => 'filtered-credentials',
+ 'title' => h($credentials),
+ 'onclick' => 'this.textContent = this.title',
+ ]);
+
+ return h($protocol) . $link . '@' . h($tail);
+ }
+}
diff --git a/src/View/Helper/SimpleGraphHelper.php b/src/View/Helper/SimpleGraphHelper.php
new file mode 100644
index 000000000..1489f828b
--- /dev/null
+++ b/src/View/Helper/SimpleGraphHelper.php
@@ -0,0 +1,79 @@
+ (int) Maximum value in the graphs
+ * - width => (int)
+ * - valueType => string (value, percentage)
+ * - style => array
+ *
+ * @var array
+ */
+ protected array $_defaultSettings = [
+ 'max' => 100,
+ 'width' => 350,
+ 'valueType' => 'value',
+ ];
+
+ /**
+ * bar method
+ *
+ * @param float|int $value Value to be graphed
+ * @param float|int $offset how much indentation
+ * @param array $options Graph options
+ * @return string Html graph
+ */
+ public function bar(float|int $value, float|int $offset, array $options = []): string
+ {
+ $settings = array_merge($this->_defaultSettings, $options);
+ $max = $settings['max'];
+ $width = $settings['width'];
+ $valueType = $settings['valueType'];
+
+ $graphValue = $value / $max * $width;
+ $graphValue = max(round($graphValue), 1);
+
+ if ($valueType === 'percentage') {
+ $graphOffset = 0;
+ } else {
+ $graphOffset = $offset / $max * $width;
+ $graphOffset = round($graphOffset);
+ }
+
+ return sprintf(
+ '
',
+ "width: {$width}px",
+ "margin-left: {$graphOffset}px; width: {$graphValue}px",
+ "Starting {$offset}ms into the request, taking {$value}ms",
+ );
+ }
+}
diff --git a/src/View/Helper/ToolbarHelper.php b/src/View/Helper/ToolbarHelper.php
new file mode 100644
index 000000000..55e1db481
--- /dev/null
+++ b/src/View/Helper/ToolbarHelper.php
@@ -0,0 +1,102 @@
+>
+ */
+ public array $helpers = ['Html', 'Form', 'Url'];
+
+ /**
+ * Whether or not the top level keys should be sorted.
+ *
+ * @var bool
+ */
+ protected bool $sort = false;
+
+ /**
+ * set sorting of values
+ *
+ * @param bool $sort Whether or not sort values by key
+ * @return void
+ */
+ public function setSort(bool $sort): void
+ {
+ $this->sort = $sort;
+ }
+
+ /**
+ * Dump an array of nodes
+ *
+ * @param array<\Cake\Error\Debug\NodeInterface> $nodes An array of dumped variables.
+ * Variables should be keyed by the name they had in the view.
+ * @return string Formatted HTML
+ */
+ public function dumpNodes(array $nodes): string
+ {
+ $formatter = new HtmlFormatter();
+ if ($this->sort) {
+ ksort($nodes);
+ }
+ $items = [];
+ foreach ($nodes as $key => $value) {
+ $items[] = new ArrayItemNode(new ScalarNode('string', $key), $value);
+ }
+ $root = new ArrayNode($items);
+
+ return implode([
+ '
',
+ ]);
+ }
+
+ /**
+ * Dump an error node
+ *
+ * @param \Cake\Error\Debug\NodeInterface $node A error node containing dumped variables.
+ * @return string Formatted HTML
+ */
+ public function dumpNode(NodeInterface $node): string
+ {
+ $formatter = new HtmlFormatter();
+
+ return implode([
+ '
',
+ ]);
+ }
+}
diff --git a/src/schema.php b/src/schema.php
new file mode 100644
index 000000000..40a062861
--- /dev/null
+++ b/src/schema.php
@@ -0,0 +1,50 @@
+ 'requests',
+ 'columns' => [
+ 'id' => ['type' => 'uuid', 'null' => false],
+ 'url' => ['type' => 'text', 'null' => false],
+ 'content_type' => ['type' => 'string'],
+ 'status_code' => ['type' => 'integer'],
+ 'method' => ['type' => 'string'],
+ 'requested_at' => ['type' => 'datetime', 'null' => false],
+ ],
+ 'constraints' => [
+ 'primary' => ['type' => 'primary', 'columns' => ['id']],
+ ],
+ ],
+ [
+ 'table' => 'panels',
+ 'columns' => [
+ 'id' => ['type' => 'uuid'],
+ 'request_id' => ['type' => 'uuid', 'null' => false],
+ 'panel' => ['type' => 'string'],
+ 'title' => ['type' => 'string'],
+ 'element' => ['type' => 'string'],
+ 'summary' => ['type' => 'string'],
+ 'content' => ['type' => 'binary', 'length' => TableSchema::LENGTH_LONG],
+ ],
+ 'constraints' => [
+ 'primary' => ['type' => 'primary', 'columns' => ['id']],
+ 'unique_panel' => ['type' => 'unique', 'columns' => ['request_id', 'panel']],
+ 'request_id_fk' => [
+ 'type' => 'foreign',
+ 'columns' => ['request_id'],
+ 'references' => ['requests', 'id'],
+ ],
+ ],
+ ],
+];
diff --git a/templates/Dashboard/index.php b/templates/Dashboard/index.php
new file mode 100644
index 000000000..56c25e9b7
--- /dev/null
+++ b/templates/Dashboard/index.php
@@ -0,0 +1,28 @@
+
+
+
+ = $this->Form->postLink(
+ 'Reset database',
+ ['_method' => 'POST', 'action' => 'reset'],
+ ['confirm' => 'Are you sure?']
+ ); ?>
+
+
+
diff --git a/templates/MailPreview/email.php b/templates/MailPreview/email.php
new file mode 100644
index 000000000..d105038e9
--- /dev/null
+++ b/templates/MailPreview/email.php
@@ -0,0 +1,36 @@
+
+
+
+request->getQuery('part')) : ?>
+ = $this->element('preview_header'); ?>
+
+
+
+
+
+
You are trying to preview an email that does not have any content.
+
+
+
diff --git a/templates/MailPreview/index.php b/templates/MailPreview/index.php
new file mode 100644
index 000000000..0c2bada07
--- /dev/null
+++ b/templates/MailPreview/index.php
@@ -0,0 +1,95 @@
+
+ $previews) : ?>
+
diff --git a/templates/Panels/view.php b/templates/Panels/view.php
new file mode 100644
index 000000000..deb4557f7
--- /dev/null
+++ b/templates/Panels/view.php
@@ -0,0 +1,11 @@
+
+
diff --git a/templates/Requests/view.php b/templates/Requests/view.php
new file mode 100644
index 000000000..f01fa135d
--- /dev/null
+++ b/templates/Requests/view.php
@@ -0,0 +1,67 @@
+
+
+Html->script('DebugKit./js/jquery', [
+ 'block' => true,
+ ]);
+ $this->Html->script('DebugKit./js/main', [
+ 'type' => 'module',
+ 'block' => true,
+ 'id' => '__debug_kit_app',
+ 'data-id' => $toolbar->id,
+ 'data-url' => Router::url('/', true),
+ 'data-webroot' => $this->getRequest()->getAttribute('webroot'),
+ ]);
+ ?>
diff --git a/templates/element/cache_panel.php b/templates/element/cache_panel.php
new file mode 100644
index 000000000..2b142d5ff
--- /dev/null
+++ b/templates/element/cache_panel.php
@@ -0,0 +1,99 @@
+
+
diff --git a/templates/element/deprecations_panel.php b/templates/element/deprecations_panel.php
new file mode 100644
index 000000000..38da14bd5
--- /dev/null
+++ b/templates/element/deprecations_panel.php
@@ -0,0 +1,72 @@
+
+
diff --git a/templates/element/environment_panel.php b/templates/element/environment_panel.php
new file mode 100644
index 000000000..c84912499
--- /dev/null
+++ b/templates/element/environment_panel.php
@@ -0,0 +1,151 @@
+Toolbar
+ * @var \DebugKit\View\Helper\CredentialsHelper $this->Credentials
+ */
+?>
+
diff --git a/templates/element/history_panel.php b/templates/element/history_panel.php
new file mode 100644
index 000000000..7be822638
--- /dev/null
+++ b/templates/element/history_panel.php
@@ -0,0 +1,110 @@
+
+
diff --git a/templates/element/include_panel.php b/templates/element/include_panel.php
new file mode 100644
index 000000000..8f5cc6772
--- /dev/null
+++ b/templates/element/include_panel.php
@@ -0,0 +1,36 @@
+ $paths
+ * @var array<\Cake\Error\Debug\NodeInterface> $app
+ * @var array<\Cake\Error\Debug\NodeInterface> $cake
+ * @var array<\Cake\Error\Debug\NodeInterface> $plugins
+ * @var array<\Cake\Error\Debug\NodeInterface> $vendor
+ * @var array<\Cake\Error\Debug\NodeInterface> $other
+ */
+
+// Backwards compat for old DebugKit data.
+if (!isset($cake) && isset($core)) {
+ $cake = $core;
+}
+?>
+
diff --git a/templates/element/log_panel.php b/templates/element/log_panel.php
new file mode 100644
index 000000000..9010810a6
--- /dev/null
+++ b/templates/element/log_panel.php
@@ -0,0 +1,46 @@
+
+
diff --git a/templates/element/mail_panel.php b/templates/element/mail_panel.php
new file mode 100644
index 000000000..251cbdbb1
--- /dev/null
+++ b/templates/element/mail_panel.php
@@ -0,0 +1,58 @@
+
+
diff --git a/templates/element/packages_panel.php b/templates/element/packages_panel.php
new file mode 100644
index 000000000..34579ef17
--- /dev/null
+++ b/templates/element/packages_panel.php
@@ -0,0 +1,117 @@
+
+
diff --git a/templates/element/plugins_panel.php b/templates/element/plugins_panel.php
new file mode 100644
index 000000000..f76067c80
--- /dev/null
+++ b/templates/element/plugins_panel.php
@@ -0,0 +1,51 @@
+
+
diff --git a/templates/element/preview_header.php b/templates/element/preview_header.php
new file mode 100644
index 000000000..9ce050135
--- /dev/null
+++ b/templates/element/preview_header.php
@@ -0,0 +1,30 @@
+
+
+
+
+ View As
+
+ getParts()) ?>
+ = $this->Form->control('part', [
+ 'type' => 'select',
+ 'label' => false,
+ 'value' => $this->request->getQuery('part') ?: 'html',
+ 'onChange' => 'formatChanged(this);',
+ 'options' => array_combine($partNames, $partNames),
+ ]);
+ ?>
+
+
+ getHeaders() as $name => $header) :?>
+
+ = h($name) ?>
+ = h($header) ?>
+
+
+
+
diff --git a/templates/element/request_panel.php b/templates/element/request_panel.php
new file mode 100644
index 000000000..1e967848f
--- /dev/null
+++ b/templates/element/request_panel.php
@@ -0,0 +1,122 @@
+
+
diff --git a/templates/element/routes_panel.php b/templates/element/routes_panel.php
new file mode 100644
index 000000000..bae904ea4
--- /dev/null
+++ b/templates/element/routes_panel.php
@@ -0,0 +1,96 @@
+defaults['plugin'] ?? 'app';
+ if (!array_key_exists($group, $amountOfRoutesPerGroup)) {
+ $amountOfRoutesPerGroup[$group] = 0;
+ }
+ $amountOfRoutesPerGroup[$group]++;
+
+ if (!array_key_exists($route->template, $duplicateRoutes)) {
+ $duplicateRoutes[$route->template] = 0;
+ }
+ $duplicateRoutes[$route->template]++;
+}
+
+$pluginNames = [];
+foreach (CorePlugin::loaded() as $pluginName) {
+ if (!empty($amountOfRoutesPerGroup[$pluginName])) {
+ $name = sprintf('%s (%s)', $pluginName, $amountOfRoutesPerGroup[$pluginName]);
+ $pluginNames[$name] = Text::slug($pluginName);
+ }
+}
+
+?>
+
diff --git a/templates/element/session_panel.php b/templates/element/session_panel.php
new file mode 100644
index 000000000..d7d13f383
--- /dev/null
+++ b/templates/element/session_panel.php
@@ -0,0 +1,22 @@
+
+
diff --git a/templates/element/sql_log_panel.php b/templates/element/sql_log_panel.php
new file mode 100644
index 000000000..83336081f
--- /dev/null
+++ b/templates/element/sql_log_panel.php
@@ -0,0 +1,112 @@
+
+
+
diff --git a/templates/element/timer_panel.php b/templates/element/timer_panel.php
new file mode 100644
index 000000000..31dda79cf
--- /dev/null
+++ b/templates/element/timer_panel.php
@@ -0,0 +1,104 @@
+
+
diff --git a/templates/element/variables_panel.php b/templates/element/variables_panel.php
new file mode 100644
index 000000000..6fbf1ebc3
--- /dev/null
+++ b/templates/element/variables_panel.php
@@ -0,0 +1,60 @@
+
+
diff --git a/templates/layout/dashboard.php b/templates/layout/dashboard.php
new file mode 100644
index 000000000..31757f987
--- /dev/null
+++ b/templates/layout/dashboard.php
@@ -0,0 +1,13 @@
+extend('toolbar') ?>
+
+
diff --git a/templates/layout/mailer.php b/templates/layout/mailer.php
new file mode 100644
index 000000000..3e0e86b0b
--- /dev/null
+++ b/templates/layout/mailer.php
@@ -0,0 +1,16 @@
+extend('toolbar');
+?>
+
+
diff --git a/templates/layout/toolbar.php b/templates/layout/toolbar.php
new file mode 100644
index 000000000..3c23d3780
--- /dev/null
+++ b/templates/layout/toolbar.php
@@ -0,0 +1,21 @@
+
+
+
+
+ = $this->Html->css('DebugKit./css/reset') ?>
+ = $this->Html->css('DebugKit./css/style') ?>
+
+