├── .eslintrc.json ├── .phive └── phars.xml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── composer.json ├── config ├── bootstrap.php └── routes.php ├── docs.Dockerfile ├── docs ├── _static │ ├── history-panel-use.mp4 │ ├── history-panel.png │ ├── mail-panel.mp4 │ └── mail-previewer.mp4 ├── config │ ├── __init__.py │ └── all.py ├── en │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── fr │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── ja │ ├── conf.py │ ├── contents.rst │ └── index.rst └── pt │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── package.json ├── phpstan-baseline.neon ├── phpstan.neon ├── psalm-baseline.xml ├── psalm.xml ├── src ├── Cache │ └── Engine │ │ └── DebugEngine.php ├── Command │ └── BenchmarkCommand.php ├── Controller │ ├── ComposerController.php │ ├── DashboardController.php │ ├── DebugKitController.php │ ├── MailPreviewController.php │ ├── PanelsController.php │ ├── RequestsController.php │ └── ToolbarController.php ├── Database │ └── Log │ │ └── DebugLog.php ├── DebugInclude.php ├── DebugKitPlugin.php ├── DebugMemory.php ├── DebugPanel.php ├── DebugSql.php ├── DebugTimer.php ├── Log │ └── Engine │ │ └── DebugKitLog.php ├── Mailer │ ├── AbstractResult.php │ ├── MailPreview.php │ ├── PreviewResult.php │ ├── SentMailResult.php │ └── Transport │ │ └── DebugKitTransport.php ├── Middleware │ └── DebugKitMiddleware.php ├── Model │ ├── Behavior │ │ └── TimedBehavior.php │ ├── Entity │ │ ├── Panel.php │ │ └── Request.php │ └── Table │ │ ├── LazyTableTrait.php │ │ ├── PanelsTable.php │ │ ├── RequestsTable.php │ │ └── SqlTraceTrait.php ├── Panel │ ├── CachePanel.php │ ├── DeprecationsPanel.php │ ├── EnvironmentPanel.php │ ├── HistoryPanel.php │ ├── IncludePanel.php │ ├── LogPanel.php │ ├── MailPanel.php │ ├── PackagesPanel.php │ ├── PanelRegistry.php │ ├── PluginsPanel.php │ ├── RequestPanel.php │ ├── RoutesPanel.php │ ├── SessionPanel.php │ ├── SqlLogPanel.php │ ├── TimerPanel.php │ └── VariablesPanel.php ├── ToolbarService.php ├── View │ ├── AjaxView.php │ └── Helper │ │ ├── CredentialsHelper.php │ │ ├── SimpleGraphHelper.php │ │ └── ToolbarHelper.php └── schema.php ├── templates ├── Dashboard │ └── index.php ├── MailPreview │ ├── email.php │ └── index.php ├── Panels │ └── view.php ├── Requests │ └── view.php ├── element │ ├── cache_panel.php │ ├── deprecations_panel.php │ ├── environment_panel.php │ ├── history_panel.php │ ├── include_panel.php │ ├── log_panel.php │ ├── mail_panel.php │ ├── packages_panel.php │ ├── plugins_panel.php │ ├── preview_header.php │ ├── request_panel.php │ ├── routes_panel.php │ ├── session_panel.php │ ├── sql_log_panel.php │ ├── timer_panel.php │ └── variables_panel.php └── layout │ ├── dashboard.php │ ├── mailer.php │ └── toolbar.php ├── tests ├── Fixture │ ├── PanelsFixture.php │ └── RequestsFixture.php └── schema.php └── webroot ├── css ├── raleway-regular.eot ├── raleway-regular.svg ├── raleway-regular.ttf ├── raleway-regular.woff ├── reset.css └── style.css ├── img ├── cake-red.svg └── cake.icon.png └── js ├── inject-iframe.js ├── jquery.js ├── main.js └── modules ├── Helper.js ├── Keyboard.js ├── Panels ├── CachePanel.js ├── HistoryPanel.js ├── MailPanel.js ├── PackagesPanel.js ├── RoutesPanel.js └── VariablesPanel.js ├── Start.js └── Toolbar.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jquery": true 6 | }, 7 | "extends": [ 8 | "airbnb-base" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "strict": 0, 16 | "max-len": ["error", { "code": 150 }], 17 | "no-underscore-dangle": ["error", { 18 | "allow": ["__cakeDebugBlockInit", "_arguments"] 19 | }], 20 | "import/extensions": [0, { "": "always" }], 21 | "no-param-reassign": 0, 22 | "no-plusplus": 0, 23 | "class-methods-use-this": 0, 24 | "prefer-rest-params": 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.phive/phars.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Basic docker based environment 2 | # Necessary to trick dokku into building the documentation 3 | # using dockerfile instead of herokuish 4 | FROM ubuntu:22.04 5 | 6 | # Add basic tools 7 | RUN apt-get update && \ 8 | apt-get install -y build-essential \ 9 | software-properties-common \ 10 | curl \ 11 | git \ 12 | libxml2 \ 13 | libffi-dev \ 14 | libssl-dev 15 | 16 | # Prevent interactive timezone input 17 | ENV DEBIAN_FRONTEND=noninteractive 18 | RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ 19 | apt-get update && \ 20 | apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite 21 | 22 | WORKDIR /code 23 | 24 | VOLUME ["/code"] 25 | 26 | CMD [ '/bin/bash' ] 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) 4 | Copyright (c) 2005-present, Cake Software Foundation, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | 24 | Cake Software Foundation, Inc. 25 | 1785 E. Sahara Avenue, 26 | Suite 490-204 27 | Las Vegas, Nevada 89104, 28 | United States of America. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP DebugKit 2 | [![CI](https://github.com/cakephp/debug_kit/actions/workflows/ci.yml/badge.svg)](https://github.com/cakephp/debug_kit/actions/workflows/ci.yml) 3 | [![Coverage Status](https://img.shields.io/codecov/c/github/cakephp/debug_kit.svg?style=flat-square)](https://codecov.io/github/cakephp/debug_kit) 4 | [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.txt) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/cakephp.svg?style=flat-square)](https://packagist.org/packages/cakephp/debug_kit) 6 | 7 | DebugKit provides a debugging toolbar and enhanced debugging tools for CakePHP 8 | applications. It lets you quickly see configuration data, log messages, SQL 9 | queries, and timing data for your application. 10 | 11 | :warning: DebugKit is only intended for use in single-user local development 12 | environments. You should avoid using DebugKit in shared development 13 | environments, staging environments, or any environment where you need to keep 14 | configuration data and environment variables hidden. :warning: 15 | 16 | ## Requirements 17 | 18 | * SQLite (pdo_sqlite) or another database driver that CakePHP can talk to. By 19 | default DebugKit will use SQLite, if you need to use a different database see the Database Configuration section in the documentation linked below. 20 | 21 | For details and older versions see [version map](https://github.com/cakephp/debug_kit/wiki#version-map). 22 | 23 | ## Installation 24 | 25 | * Install the plugin with [Composer](https://getcomposer.org/) from your CakePHP Project's ROOT directory (where the **composer.json** file is located) 26 | ```sh 27 | php composer.phar require --dev cakephp/debug_kit:"^5.0" 28 | ``` 29 | 30 | * [Load the plugin](https://book.cakephp.org/5/en/plugins.html#loading-a-plugin) 31 | ``` 32 | bin/cake plugin load DebugKit --only-debug 33 | ``` 34 | 35 | ## Is DebugKit not working? 36 | 37 | If you don't see a CakePHP icon on the bottom right of your page DebugKit is not be 38 | working correctly. Some common problems are: 39 | 40 | 1. Your PHP environment doesn't have SQLite installed. Check your application 41 | logs to confirm if this happening. You can either configure DebugKit to use 42 | a different database, or install the PDO SQLite 3 extension. 43 | 2. Your hostname needs to be added to the `DebugKit.safeTld`. If your local 44 | domain isn't a known development environment name, DebugKit will disable 45 | itself to protect a potentially non-development environment. 46 | 3. If you are using the [Authorization Plugin](https://github.com/cakephp/authorization) 47 | you need to set `DebugKit.ignoreAuthorization` to `true` in your config. 48 | 49 | ## Reporting Issues 50 | 51 | If you have a problem with DebugKit please open an issue on [GitHub](https://github.com/cakephp/debug_kit/issues). 52 | 53 | ## Contributing 54 | 55 | If you'd like to contribute to DebugKit, check out the 56 | [roadmap](https://github.com/cakephp/debug_kit/wiki/roadmap) for any 57 | planned features. You can [fork](https://help.github.com/articles/fork-a-repo) 58 | the project, add features, and send [pull 59 | requests](https://help.github.com/articles/using-pull-requests) or open 60 | [issues](https://github.com/cakephp/debug_kit/issues). 61 | 62 | ## Documentation 63 | 64 | Documentation for DebugKit can be found in the 65 | [CakePHP documentation](https://book.cakephp.org/debugkit/5/en/index.html). 66 | 67 | ## Panels 68 | Panels by other plugins: 69 | - `L10n` by [Setup plugin](https://github.com/dereuromark/cakephp-setup) to show current localization for Date, DateTime, Time objects/values. 70 | - `Twig` by [Twig plugin](https://github.com/cakephp/twig-view/) to list all templates. 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/debug_kit", 3 | "description": "CakePHP Debug Kit", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "cakephp", 8 | "debug", 9 | "kit", 10 | "dev" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Mark Story", 15 | "homepage": "https://mark-story.com", 16 | "role": "Author" 17 | }, 18 | { 19 | "name": "CakePHP Community", 20 | "homepage": "https://github.com/cakephp/debug_kit/graphs/contributors" 21 | } 22 | ], 23 | "homepage": "https://github.com/cakephp/debug_kit", 24 | "support": { 25 | "issues": "https://github.com/cakephp/debug_kit/issues", 26 | "forum": "https://stackoverflow.com/tags/cakephp", 27 | "irc": "irc://irc.freenode.org/cakephp", 28 | "source": "https://github.com/cakephp/debug_kit" 29 | }, 30 | "require": { 31 | "php": ">=8.1", 32 | "cakephp/cakephp": "^5.1", 33 | "composer/composer": "^2.0", 34 | "doctrine/sql-formatter": "^1.1.3" 35 | }, 36 | "require-dev": { 37 | "cakephp/authorization": "^3.0", 38 | "cakephp/cakephp-codesniffer": "^5.0", 39 | "phpunit/phpunit": "^10.5.5 || ^11.1.3" 40 | }, 41 | "suggest": { 42 | "ext-pdo_sqlite": "DebugKit needs to store panel data in a database. SQLite is simple and easy to use." 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "DebugKit\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Cake\\Test\\": "vendor/cakephp/cakephp/tests/", 52 | "DebugKit\\TestApp\\": "tests/test_app/", 53 | "DebugKit\\Test\\": "tests/", 54 | "DebugkitTestPlugin\\": "tests/test_app/Plugin/DebugkitTestPlugin/src/" 55 | } 56 | }, 57 | "config": { 58 | "allow-plugins": { 59 | "dealerdirect/phpcodesniffer-composer-installer": true 60 | } 61 | }, 62 | "scripts": { 63 | "cs-check": "phpcs --colors --parallel=16 -p src/ tests/", 64 | "cs-fix": "phpcbf --colors --parallel=16 -p src/ tests/", 65 | "phpstan": "tools/phpstan analyse", 66 | "stan": "@phpstan", 67 | "stan-baseline": "tools/phpstan --generate-baseline", 68 | "stan-setup": "phive install", 69 | "test": "phpunit" 70 | }, 71 | "minimum-stability": "dev", 72 | "prefer-stable": true 73 | } 74 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'Cake\Database\Connection', 32 | 'driver' => 'Cake\Database\Driver\Sqlite', 33 | 'database' => TMP . 'debug_kit.sqlite', 34 | 'encoding' => 'utf8', 35 | 'cacheMetadata' => true, 36 | 'quoteIdentifiers' => false, 37 | ]); 38 | } 39 | 40 | if (!function_exists('sql')) { 41 | /** 42 | * Prints out the SQL statements generated by a Query object. 43 | * 44 | * This function returns the same variable that was passed. 45 | * Only runs if debug mode is enabled. 46 | * 47 | * @param \Cake\Database\Query $query Query to show SQL statements for. 48 | * @param bool $showValues Renders the SQL statement with bound variables. 49 | * @param bool|null $showHtml If set to true, the method prints the debug 50 | * data in a browser-friendly way. 51 | * @return \Cake\Database\Query 52 | */ 53 | function sql(Query $query, $showValues = true, $showHtml = null) 54 | { 55 | return DebugSql::sql($query, $showValues, $showHtml, 1); 56 | } 57 | } 58 | 59 | if (!function_exists('sqld')) { 60 | /** 61 | * Prints out the SQL statements generated by a Query object and dies. 62 | * 63 | * Only runs if debug mode is enabled. 64 | * It will otherwise just continue code execution and ignore this function. 65 | * 66 | * @param \Cake\Database\Query $query Query to show SQL statements for. 67 | * @param bool $showValues Renders the SQL statement with bound variables. 68 | * @param bool|null $showHtml If set to true, the method prints the debug 69 | * data in a browser-friendly way. 70 | * @return void 71 | */ 72 | function sqld(Query $query, $showValues = true, $showHtml = null) 73 | { 74 | DebugSql::sqld($query, $showValues, $showHtml, 2); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | plugin('DebugKit', ['path' => '/debug-kit'], function (RouteBuilder $routes) { 8 | $routes->setExtensions('json'); 9 | $routes->setRouteClass(DashedRoute::class); 10 | 11 | $routes->connect( 12 | '/toolbar/clear-cache', 13 | ['controller' => 'Toolbar', 'action' => 'clearCache'] 14 | ); 15 | $routes->connect( 16 | '/toolbar/*', 17 | ['controller' => 'Requests', 'action' => 'view'] 18 | ); 19 | $routes->connect( 20 | '/panels/view/latest-history', 21 | ['controller' => 'Panels', 'action' => 'latestHistory'] 22 | ); 23 | $routes->connect( 24 | '/panels/view/*', 25 | ['controller' => 'Panels', 'action' => 'view'] 26 | ); 27 | $routes->connect( 28 | '/panels/*', 29 | ['controller' => 'Panels', 'action' => 'index'] 30 | ); 31 | 32 | $routes->connect( 33 | '/composer/check-dependencies', 34 | ['controller' => 'Composer', 'action' => 'checkDependencies'] 35 | ); 36 | 37 | $routes->scope( 38 | '/mail-preview', 39 | ['controller' => 'MailPreview'], 40 | function (RouteBuilder $routes) { 41 | $routes->connect('/', ['action' => 'index']); 42 | $routes->connect('/preview', ['action' => 'email']); 43 | $routes->connect('/preview/*', ['action' => 'email']); 44 | $routes->connect('/sent/{panel}/{id}', ['action' => 'sent'], ['pass' => ['panel', 'id']]); 45 | } 46 | ); 47 | 48 | $routes->get('/', ['controller' => 'Dashboard', 'action' => 'index']); 49 | $routes->get('/dashboard', ['controller' => 'Dashboard', 'action' => 'index']); 50 | $routes->post('/dashboard/reset', ['controller' => 'Dashboard', 'action' => 'reset']); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /docs.Dockerfile: -------------------------------------------------------------------------------- 1 | # Generate the HTML output. 2 | FROM ghcr.io/cakephp/docs-builder as builder 3 | 4 | RUN pip install sphinxcontrib-video 5 | 6 | # Copy entire repo in with .git so we can build all versions in one image. 7 | COPY docs /data/docs 8 | ENV LANGS="en fr ja pt" 9 | 10 | # Make docs 11 | RUN cd /data/docs-builder \ 12 | && make website LANGS="$LANGS" SOURCE=/data/docs DEST=/data/website \ 13 | # Move media files into the output directory so video elements work. 14 | && mkdir -p /data/website/html/_static \ 15 | && cp /data/docs/_static/* /data/website/html/_static/ 16 | 17 | # Build a small nginx container with just the static site in it. 18 | FROM ghcr.io/cakephp/docs-builder:runtime as runtime 19 | 20 | # Configure search index script 21 | ENV LANGS="en fr ja pt" 22 | ENV SEARCH_SOURCE="/usr/share/nginx/html" 23 | ENV SEARCH_URL_PREFIX="/debugkit/5" 24 | 25 | COPY --from=builder /data/docs /data/docs 26 | COPY --from=builder /data/website /data/website 27 | COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf 28 | 29 | # Move files into final location 30 | RUN cp -R /data/website/html/* /usr/share/nginx/html \ 31 | && rm -rf /data/website/ 32 | -------------------------------------------------------------------------------- /docs/_static/history-panel-use.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/docs/_static/history-panel-use.mp4 -------------------------------------------------------------------------------- /docs/_static/history-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/docs/_static/history-panel.png -------------------------------------------------------------------------------- /docs/_static/mail-panel.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/docs/_static/mail-panel.mp4 -------------------------------------------------------------------------------- /docs/_static/mail-previewer.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/docs/_static/mail-previewer.mp4 -------------------------------------------------------------------------------- /docs/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/docs/config/__init__.py -------------------------------------------------------------------------------- /docs/config/all.py: -------------------------------------------------------------------------------- 1 | # Global configuration information used across all the 2 | # translations of documentation. 3 | # 4 | # Import the base theme configuration 5 | from cakephpsphinx.config.all import * 6 | 7 | # The version info for the project you're documenting, acts as replacement for 8 | # |version| and |release|, also used in various other places throughout the 9 | # built documents. 10 | # 11 | 12 | # The full version, including alpha/beta/rc tags. 13 | release = '5.x' 14 | 15 | # The search index version. 16 | search_version = 'debugkit-5' 17 | 18 | # The marketing display name for the book. 19 | version_name = '' 20 | 21 | # Project name shown in the black header bar 22 | project = 'CakePHP DebugKit' 23 | 24 | # Other versions that display in the version picker menu. 25 | version_list = [ 26 | {'name': '3.x', 'number': 'debugkit/3.x', 'title': '3.x'}, 27 | {'name': '4.x', 'number': 'debugkit/4.x', 'title': '4.x'}, 28 | {'name': '5.x', 'number': 'debugkit/5.x', 'title': '5.x', 'current': True}, 29 | ] 30 | 31 | # Languages available. 32 | languages = ['en', 'fr', 'ja', 'pt'] 33 | 34 | # The GitHub branch name for this version of the docs 35 | # for edit links to point at. 36 | branch = '5.x' 37 | 38 | # Current version being built 39 | version = '5.x' 40 | 41 | # Language in use for this directory. 42 | language = 'en' 43 | 44 | show_root_link = True 45 | 46 | repository = 'cakephp/debug_kit' 47 | 48 | source_path = 'docs/' 49 | 50 | hide_page_contents = ('search', '404', 'contents') 51 | 52 | # DebugKit docs use mp4 videos to show the UI 53 | extensions.append('sphinxcontrib.video') 54 | -------------------------------------------------------------------------------- /docs/en/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'en' 10 | -------------------------------------------------------------------------------- /docs/en/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP DebugKit 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /docs/fr/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'fr' 10 | -------------------------------------------------------------------------------- /docs/fr/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP DebugKit 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /docs/ja/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'ja' 10 | -------------------------------------------------------------------------------- /docs/ja/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP DebugKit 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /docs/pt/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | # Append the top level directory of the docs, so we can import from the config dir. 4 | sys.path.insert(0, os.path.abspath('..')) 5 | 6 | # Pull in all the configuration options defined in the global config file.. 7 | from config.all import * 8 | 9 | language = 'pt' 10 | -------------------------------------------------------------------------------- /docs/pt/contents.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | :caption: CakePHP DebugKit 4 | 5 | /index 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debug_kit", 3 | "version": "1.0.0", 4 | "description": "CakePHP Debug Kit", 5 | "author": "CakePHP Team", 6 | "license": "MIT", 7 | "homepage": "https://github.com/cakephp/debug_kit#readme", 8 | "bugs": { 9 | "url": "https://github.com/cakephp/debug_kit/issues" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/cakephp/debug_kit.git" 14 | }, 15 | "keywords": [ 16 | "cakephp", 17 | "debug", 18 | "kit" 19 | ], 20 | "devDependencies": { 21 | "eslint": "^8.17.0", 22 | "eslint-config-airbnb-base": "^15.0.0", 23 | "eslint-plugin-import": "^2.26.0" 24 | }, 25 | "scripts": { 26 | "cs-check": "npx eslint webroot/js/main.js webroot/js/inject-iframe.js webroot/js/modules/*.js", 27 | "cs-fix": "npx eslint webroot/js/main.js webroot/js/inject-iframe.js webroot/js/modules/*.js --fix" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: '#^Method DebugKit\\Mailer\\Transport\\DebugKitTransport\:\:send\(\) should return array\{headers\: string, message\: string\} but returns array\{headers\: non\-empty\-array\, message\: array\{text\: string, html\: string\}\}\.$#' 5 | identifier: return.type 6 | count: 1 7 | path: src/Mailer/Transport/DebugKitTransport.php 8 | 9 | - 10 | message: '#^Parameter \#1 \$request of method DebugKit\\ToolbarService\:\:saveData\(\) expects Cake\\Http\\ServerRequest, Psr\\Http\\Message\\ServerRequestInterface given\.$#' 11 | identifier: argument.type 12 | count: 1 13 | path: src/Middleware/DebugKitMiddleware.php 14 | 15 | - 16 | message: '#^PHPDoc tag @property for property DebugKit\\Model\\Table\\PanelsTable\:\:\$Requests contains unresolvable type\.$#' 17 | identifier: propertyTag.unresolvableType 18 | count: 1 19 | path: src/Model/Table/PanelsTable.php 20 | 21 | - 22 | message: '#^Call to an undefined method Cake\\ORM\\Locator\\LocatorInterface\:\:genericInstances\(\)\.$#' 23 | identifier: method.notFound 24 | count: 1 25 | path: src/Panel/SqlLogPanel.php 26 | 27 | - 28 | message: '#^Dead catch \- Cake\\Core\\Exception\\CakeException is never thrown in the try block\.$#' 29 | identifier: catch.neverThrown 30 | count: 1 31 | path: src/ToolbarService.php 32 | 33 | - 34 | message: '#^Unreachable statement \- code above always terminates\.$#' 35 | identifier: deadCode.unreachable 36 | count: 1 37 | path: src/ToolbarService.php 38 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 8 6 | treatPhpDocTypesAsCertain: false 7 | bootstrapFiles: 8 | - tests/bootstrap.php 9 | paths: 10 | - src/ 11 | ignoreErrors: 12 | - identifier: missingType.generics 13 | - identifier: missingType.iterableValue 14 | 15 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | _composerPaths]]> 12 | _pluginPaths]]> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | emailLog]]> 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Controller/ComposerController.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->setClassName(JsonView::class); 36 | } 37 | 38 | /** 39 | * Check outdated composer dependencies 40 | * 41 | * @return void 42 | * @throws \RuntimeException 43 | */ 44 | public function checkDependencies(): void 45 | { 46 | $this->request->allowMethod('post'); 47 | 48 | $input = new ArrayInput([ 49 | 'command' => 'outdated', 50 | '--no-interaction' => true, 51 | '--direct' => filter_var($this->request->getData('direct'), FILTER_VALIDATE_BOOLEAN), 52 | ]); 53 | 54 | $output = $this->executeComposerCommand($input); 55 | $dependencies = array_filter(explode("\n", $output->fetch())); 56 | $packages = []; 57 | foreach ($dependencies as $dependency) { 58 | if (strpos($dependency, 'php_network_getaddresses') !== false) { 59 | throw new RuntimeException('You have to be connected to the internet'); 60 | } 61 | if (strpos($dependency, '') !== false) { 62 | $packages['semverCompatible'][] = $dependency; 63 | continue; 64 | } 65 | $packages['bcBreaks'][] = $dependency; 66 | } 67 | if (!empty($packages['semverCompatible'])) { 68 | $packages['semverCompatible'] = trim(implode("\n", $packages['semverCompatible'])); 69 | } 70 | if (!empty($packages['bcBreaks'])) { 71 | $packages['bcBreaks'] = trim(implode("\n", $packages['bcBreaks'])); 72 | } 73 | 74 | $this->viewBuilder()->setOption('serialize', ['packages']); 75 | $this->set('packages', $packages); 76 | } 77 | 78 | /** 79 | * @param \Symfony\Component\Console\Input\ArrayInput $input An array describing the command input 80 | * @return \Symfony\Component\Console\Output\BufferedOutput Aa Console command buffered result 81 | */ 82 | private function executeComposerCommand(ArrayInput $input): BufferedOutput 83 | { 84 | $bin = implode(DIRECTORY_SEPARATOR, [ROOT, 'vendor', 'bin', 'composer']); 85 | putenv('COMPOSER_HOME=' . $bin); 86 | putenv('COMPOSER_CACHE_DIR=' . CACHE); 87 | 88 | $dir = (string)getcwd(); 89 | chdir(ROOT); 90 | $timeLimit = ini_get('max_execution_time'); 91 | set_time_limit(300); 92 | $memoryLimit = ini_get('memory_limit'); 93 | ini_set('memory_limit', '512M'); 94 | 95 | $output = new BufferedOutput(); 96 | $application = new Application(); 97 | $application->setAutoExit(false); 98 | $application->run($input, $output); 99 | 100 | // Restore environment 101 | chdir($dir); 102 | set_time_limit((int)$timeLimit); 103 | ini_set('memory_limit', $memoryLimit); 104 | 105 | return $output; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Controller/DashboardController.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->setLayout('dashboard'); 38 | } 39 | 40 | /** 41 | * Dashboard. 42 | * 43 | * @return void 44 | * @throws \Cake\Http\Exception\NotFoundException 45 | */ 46 | public function index(): void 47 | { 48 | $requestsModel = $this->fetchTable('DebugKit.Requests'); 49 | 50 | $data = [ 51 | 'driver' => get_class($requestsModel->getConnection()->getDriver()), 52 | 'rows' => $requestsModel->find()->count(), 53 | ]; 54 | 55 | $this->set('connection', $data); 56 | } 57 | 58 | /** 59 | * Reset SQLite DB. 60 | * 61 | * @return \Cake\Http\Response|null 62 | */ 63 | public function reset(): ?Response 64 | { 65 | $this->request->allowMethod('post'); 66 | /** @var \DebugKit\Model\Table\RequestsTable $requestsModel */ 67 | $requestsModel = $this->fetchTable('DebugKit.Requests'); 68 | 69 | $requestsModel->Panels->deleteAll('1=1'); 70 | $requestsModel->deleteAll('1=1'); 71 | 72 | return $this->redirect(['action' => 'index']); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Controller/DebugKitController.php: -------------------------------------------------------------------------------- 1 | getRequest()->getAttribute('authorization'); 45 | if ($authorizationService instanceof AuthorizationService) { 46 | if (Configure::read('DebugKit.ignoreAuthorization')) { 47 | $authorizationService->skipAuthorization(); 48 | } else { 49 | Log::info( 50 | 'Cake Authorization plugin is enabled. If you would like ' . 51 | 'to force DebugKit to ignore it, set `DebugKit.ignoreAuthorization` ' . 52 | ' Configure option to true.', 53 | ); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Controller/PanelsController.php: -------------------------------------------------------------------------------- 1 | viewBuilder() 46 | ->addHelpers([ 47 | 'Form', 'Html', 'Number', 'Url', 'DebugKit.Toolbar', 48 | 'DebugKit.Credentials', 'DebugKit.SimpleGraph', 49 | ]) 50 | ->setLayout('DebugKit.toolbar'); 51 | 52 | if (!$this->request->is('json')) { 53 | $this->viewBuilder()->setClassName('DebugKit.Ajax'); 54 | } 55 | } 56 | 57 | /** 58 | * Index method that lets you get requests by panelid. 59 | * 60 | * @param string $requestId Request id 61 | * @return void 62 | * @throws \Cake\Http\Exception\NotFoundException 63 | */ 64 | public function index(?string $requestId = null): void 65 | { 66 | $query = $this->Panels->find('byRequest', requestId: $requestId); 67 | $panels = $query->toArray(); 68 | if (empty($panels)) { 69 | throw new NotFoundException(); 70 | } 71 | $this->set([ 72 | 'panels' => $panels, 73 | ]); 74 | $this->viewBuilder()->setOption('serialize', ['panels']); 75 | } 76 | 77 | /** 78 | * View a panel's data. 79 | * 80 | * @param string $id The id. 81 | * @return void 82 | */ 83 | public function view(?string $id = null): void 84 | { 85 | $this->set('sort', $this->request->getCookie('debugKit_sort')); 86 | $panel = $this->Panels->get($id, ...['contain' => ['Requests']]); 87 | 88 | $this->set('panel', $panel); 89 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged 90 | $this->set(@unserialize($panel->content)); 91 | } 92 | 93 | /** 94 | * Get Latest request history panel 95 | * 96 | * @return \Cake\Http\Response|null 97 | */ 98 | public function latestHistory(): ?Response 99 | { 100 | /** @var array{id:string}|null $request */ 101 | $request = $this->Panels->Requests->find('recent') 102 | ->select(['id']) 103 | ->disableHydration() 104 | ->first(); 105 | if (!$request) { 106 | throw new NotFoundException('No requests found'); 107 | } 108 | /** @var array{id:string}|null $historyPanel */ 109 | $historyPanel = $this->Panels->find('byRequest', requestId: $request['id']) 110 | ->where(['title' => 'History']) 111 | ->select(['id']) 112 | ->first(); 113 | if (!$historyPanel) { 114 | throw new NotFoundException('History Panel from latest request not found'); 115 | } 116 | 117 | return $this->redirect([ 118 | 'action' => 'view', $historyPanel['id'], 119 | ]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Controller/RequestsController.php: -------------------------------------------------------------------------------- 1 | response = $this->response->withHeader('Content-Security-Policy', ''); 37 | } 38 | 39 | /** 40 | * Before render handler. 41 | * 42 | * @param \Cake\Event\EventInterface $event The event. 43 | * @return void 44 | */ 45 | public function beforeRender(EventInterface $event): void 46 | { 47 | $this->viewBuilder() 48 | ->setLayout('DebugKit.toolbar') 49 | ->setClassName('DebugKit.Ajax'); 50 | } 51 | 52 | /** 53 | * View a request's data. 54 | * 55 | * @param string $id The id. 56 | * @return void 57 | */ 58 | public function view(?string $id = null): void 59 | { 60 | $toolbar = $this->Requests->get($id, contain: 'Panels'); 61 | $this->set('toolbar', $toolbar); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Controller/ToolbarController.php: -------------------------------------------------------------------------------- 1 | viewBuilder()->setClassName(JsonView::class); 34 | } 35 | 36 | /** 37 | * Clear a named cache. 38 | * 39 | * @return void 40 | * @throws \Cake\Http\Exception\NotFoundException 41 | */ 42 | public function clearCache(): void 43 | { 44 | $this->request->allowMethod('post'); 45 | $name = $this->request->getData('name'); 46 | if (!$name) { 47 | throw new NotFoundException('Invalid cache engine name.'); 48 | } 49 | $success = Cache::clear($name); 50 | $message = $success ? 51 | sprintf('%s cache cleared.', $name) : 52 | sprintf('%s cache could not be cleared.', $name); 53 | $this->set(compact('success', 'message')); 54 | $this->viewBuilder()->setOption('serialize', ['success', 'message']); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DebugKitPlugin.php: -------------------------------------------------------------------------------- 1 | isEnabled()) { 51 | return; 52 | } 53 | 54 | $this->service = $service; 55 | $this->setDeprecationHandler($service); 56 | 57 | // will load `config/bootstrap.php`. 58 | parent::bootstrap($app); 59 | } 60 | 61 | /** 62 | * Add middleware for the plugin. 63 | * 64 | * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update. 65 | * @return \Cake\Http\MiddlewareQueue 66 | */ 67 | public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue 68 | { 69 | // Only insert middleware if Toolbar Service is available (not in phpunit run) 70 | if ($this->service) { 71 | $middlewareQueue->insertAt(0, new DebugKitMiddleware($this->service)); 72 | } 73 | 74 | return $middlewareQueue; 75 | } 76 | 77 | /** 78 | * Add console commands for the plugin. 79 | * 80 | * @param \Cake\Console\CommandCollection $commands The command collection to update 81 | * @return \Cake\Console\CommandCollection 82 | */ 83 | public function console(CommandCollection $commands): CommandCollection 84 | { 85 | return $commands->add('benchmark', BenchmarkCommand::class); 86 | } 87 | 88 | /** 89 | * set deprecation handler 90 | * 91 | * @param \DebugKit\ToolbarService $service The toolbar service instance 92 | * @return void 93 | */ 94 | public function setDeprecationHandler(ToolbarService $service): void 95 | { 96 | if (!empty($service->getConfig('panels')['DebugKit.Deprecations'])) { 97 | EventManager::instance()->on('Error.beforeRender', function (EventInterface $event, PhpError $error): void { 98 | $code = $error->getCode(); 99 | if ($code !== E_USER_DEPRECATED && $code !== E_DEPRECATED) { 100 | return; 101 | } 102 | $file = $error->getFile(); 103 | $line = $error->getLine(); 104 | 105 | // Extract the line/file from the message as deprecationWarning 106 | // will calculate the application frame when generating the message. 107 | preg_match('/\\n([^\n,]+?), line: (\d+)\\n/', $error->getMessage(), $matches); 108 | if ($matches) { 109 | $file = $matches[1]; 110 | $line = $matches[2]; 111 | } 112 | 113 | DeprecationsPanel::addDeprecatedError([ 114 | 'code' => $code, 115 | 'message' => $error->getMessage(), 116 | 'file' => $file, 117 | 'line' => $line, 118 | ]); 119 | $event->stopPropagation(); 120 | }); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/DebugMemory.php: -------------------------------------------------------------------------------- 1 | plugin) { 68 | return $this->plugin . '.' . Inflector::underscore($name); 69 | } 70 | 71 | return Inflector::underscore($name); 72 | } 73 | 74 | /** 75 | * Get the data a panel has collected. 76 | * 77 | * @return array 78 | */ 79 | public function data(): array 80 | { 81 | return $this->_data; 82 | } 83 | 84 | /** 85 | * Get the summary data for a panel. 86 | * 87 | * This data is displayed in the toolbar even when the panel is collapsed. 88 | * 89 | * @return string 90 | */ 91 | public function summary(): string 92 | { 93 | return ''; 94 | } 95 | 96 | /** 97 | * Initialize hook method. 98 | * 99 | * @return void 100 | */ 101 | public function initialize(): void 102 | { 103 | } 104 | 105 | /** 106 | * Shutdown callback 107 | * 108 | * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event The event. 109 | * @return void 110 | */ 111 | public function shutdown(EventInterface $event): void 112 | { 113 | } 114 | 115 | /** 116 | * Get the events this panels supports. 117 | * 118 | * @return array 119 | */ 120 | public function implementedEvents(): array 121 | { 122 | return [ 123 | 'Controller.shutdown' => 'shutdown', 124 | ]; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Log/Engine/DebugKitLog.php: -------------------------------------------------------------------------------- 1 | _logs[$level])) { 43 | $this->_logs[$level] = []; 44 | } 45 | $this->_logs[$level][] = [date('Y-m-d H:i:s'), $this->interpolate($message)]; 46 | } 47 | 48 | /** 49 | * Get the logs. 50 | * 51 | * @return array 52 | */ 53 | public function all(): array 54 | { 55 | return $this->_logs; 56 | } 57 | 58 | /** 59 | * Get the number of log entries. 60 | * 61 | * @return int 62 | */ 63 | public function count(): int 64 | { 65 | return array_reduce($this->_logs, function ($sum, $v) { 66 | return $sum + count($v); 67 | }, 0); 68 | } 69 | 70 | /** 71 | * Check if there are no logs. 72 | * 73 | * @return bool 74 | */ 75 | public function noLogs(): bool 76 | { 77 | return empty($this->_logs); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Mailer/AbstractResult.php: -------------------------------------------------------------------------------- 1 | headers; 44 | } 45 | 46 | /** 47 | * Returns the rendered parts in th email 48 | * 49 | * @return array 50 | */ 51 | public function getParts(): array 52 | { 53 | return $this->parts; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Mailer/MailPreview.php: -------------------------------------------------------------------------------- 1 | validEmail($email)) { 40 | return $email; 41 | } 42 | 43 | return null; 44 | } 45 | 46 | /** 47 | * Returns a list of valid emails 48 | * 49 | * @return array 50 | */ 51 | public function getEmails(): array 52 | { 53 | $emails = []; 54 | foreach (get_class_methods($this) as $methodName) { 55 | if (!$this->validEmail($methodName)) { 56 | continue; 57 | } 58 | 59 | $emails[] = $methodName; 60 | } 61 | 62 | return $emails; 63 | } 64 | 65 | /** 66 | * Returns the name of this preview 67 | * 68 | * @return string 69 | */ 70 | public function name(): string 71 | { 72 | $classname = static::class; 73 | $pos = strrpos($classname, '\\'); 74 | 75 | return substr($classname, $pos + 1); 76 | } 77 | 78 | /** 79 | * Returns whether or not a specified email is valid 80 | * for this MailPreview instance 81 | * 82 | * @param string $email Name of email 83 | * @return bool 84 | */ 85 | protected function validEmail(string $email): bool 86 | { 87 | if (empty($email)) { 88 | return false; 89 | } 90 | 91 | $baseClass = new ReflectionClass(self::class); 92 | if ($baseClass->hasMethod($email)) { 93 | return false; 94 | } 95 | 96 | try { 97 | $method = new ReflectionMethod($this, $email); 98 | } catch (ReflectionException $e) { 99 | return false; 100 | } 101 | 102 | return $method->isPublic(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Mailer/PreviewResult.php: -------------------------------------------------------------------------------- 1 | processMailer(clone $mailer, $method); 33 | $mailer->reset(); 34 | } 35 | 36 | /** 37 | * Executes the mailer and extracts the relevant information from the generated email 38 | * 39 | * @param \Cake\Mailer\Mailer $mailer The mailer instance to execute and extract the email data from 40 | * @param string $method The method to execute in the mailer 41 | * @return void 42 | */ 43 | protected function processMailer(Mailer $mailer, string $method): void 44 | { 45 | if (!$mailer->viewBuilder()->getTemplate()) { 46 | $mailer->viewBuilder()->setTemplate($method); 47 | } 48 | 49 | $mailer->render(); 50 | $message = $mailer->getMessage(); 51 | $this->parts = [ 52 | 'html' => $message->getBodyHtml(), 53 | 'text' => $message->getBodyText(), 54 | ]; 55 | 56 | $extra = ['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'subject']; 57 | $this->headers = array_filter($message->getHeaders($extra)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Mailer/SentMailResult.php: -------------------------------------------------------------------------------- 1 | headers = $headers; 31 | $this->parts = $parts; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Mailer/Transport/DebugKitTransport.php: -------------------------------------------------------------------------------- 1 | emailLog = $config['debugKitLog']; 41 | 42 | if ($originalTransport !== null) { 43 | $this->originalTransport = $originalTransport; 44 | 45 | return; 46 | } 47 | 48 | $className = false; 49 | if (!empty($config['originalClassName'])) { 50 | /** @var class-string<\Cake\Mailer\AbstractTransport> $className */ 51 | $className = App::className( 52 | $config['originalClassName'], 53 | 'Mailer/Transport', 54 | 'Transport', 55 | ); 56 | } 57 | 58 | if ($className) { 59 | unset($config['originalClassName'], $config['debugKitLog']); 60 | $this->originalTransport = new $className($config); 61 | } 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | public function send(Message $message): array 68 | { 69 | $headers = $message->getHeaders(['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc']); 70 | $parts = [ 71 | 'text' => $message->getBodyText(), 72 | 'html' => $message->getBodyHtml(), 73 | ]; 74 | 75 | $headers['Subject'] = $message->getOriginalSubject(); 76 | $result = ['headers' => $headers, 'message' => $parts]; 77 | $this->emailLog[] = $result; 78 | 79 | if ($this->originalTransport !== null) { 80 | return $this->originalTransport->send($message); 81 | } 82 | 83 | return $result; 84 | } 85 | 86 | /** 87 | * Proxy unknown methods to the wrapped object 88 | * 89 | * @param string $method The method to call 90 | * @param array $args The args to call $method with. 91 | * @return mixed 92 | */ 93 | public function __call(string $method, array $args): mixed 94 | { 95 | /** @var callable $callable */ 96 | $callable = [$this->originalTransport, $method]; 97 | 98 | return call_user_func_array($callable, $args); 99 | } 100 | 101 | /** 102 | * Proxy property reads to the wrapped object 103 | * 104 | * @param string $name The property to read. 105 | * @return mixed 106 | */ 107 | public function __get(string $name): mixed 108 | { 109 | return $this->originalTransport->{$name}; 110 | } 111 | 112 | /** 113 | * Proxy property changes to the wrapped object 114 | * 115 | * @param string $name The property to read. 116 | * @param mixed $value The property value. 117 | * @return void 118 | */ 119 | public function __set(string $name, mixed $value): void 120 | { 121 | $this->originalTransport->{$name} = $value; 122 | } 123 | 124 | /** 125 | * Proxy property changes to the wrapped object 126 | * 127 | * @param string $name The property to read. 128 | * @return bool 129 | */ 130 | public function __isset(string $name): bool 131 | { 132 | return isset($this->originalTransport->{$name}); 133 | } 134 | 135 | /** 136 | * Proxy property changes to the wrapped object 137 | * 138 | * @param string $name The property to delete. 139 | * @return void 140 | */ 141 | public function __unset(string $name): void 142 | { 143 | unset($this->originalTransport->{$name}); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Middleware/DebugKitMiddleware.php: -------------------------------------------------------------------------------- 1 | service = $service; 43 | } 44 | 45 | /** 46 | * Invoke the middleware. 47 | * 48 | * DebugKit will augment the response and add the toolbar if possible. 49 | * 50 | * @param \Psr\Http\Message\ServerRequestInterface $request The request. 51 | * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler. 52 | * @return \Psr\Http\Message\ResponseInterface A response. 53 | */ 54 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 55 | { 56 | if ($this->service->isEnabled()) { 57 | $this->service->loadPanels(); 58 | $this->service->initializePanels(); 59 | } 60 | $response = $handler->handle($request); 61 | 62 | if (!$this->service->isEnabled()) { 63 | return $response; 64 | } 65 | 66 | $row = $this->service->saveData($request, $response); 67 | if (!$row) { 68 | return $response; 69 | } 70 | 71 | return $this->service->injectScripts($row, $response); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Model/Behavior/TimedBehavior.php: -------------------------------------------------------------------------------- 1 | getSubject(); 39 | $alias = $table->getAlias(); 40 | DebugTimer::start($alias . '_find', $alias . '->find()'); 41 | 42 | $formattedQuery = $query->formatResults(function ($results) use ($alias) { 43 | DebugTimer::stop($alias . '_find'); 44 | 45 | return $results; 46 | }); 47 | 48 | $event->setResult($formattedQuery); 49 | } 50 | 51 | /** 52 | * beforeSave, starts a time before a save is initiated. 53 | * 54 | * @param \Cake\Event\EventInterface $event The beforeSave event 55 | * @return void 56 | */ 57 | public function beforeSave(EventInterface $event): void 58 | { 59 | /** @var \Cake\Datasource\RepositoryInterface $table */ 60 | $table = $event->getSubject(); 61 | $alias = $table->getAlias(); 62 | DebugTimer::start($alias . '_save', $alias . '->save()'); 63 | } 64 | 65 | /** 66 | * afterSave, stop the timer started from a save. 67 | * 68 | * @param \Cake\Event\EventInterface $event The afterSave event 69 | * @return void 70 | */ 71 | public function afterSave(EventInterface $event): void 72 | { 73 | /** @var \Cake\Datasource\RepositoryInterface $table */ 74 | $table = $event->getSubject(); 75 | $alias = $table->getAlias(); 76 | DebugTimer::stop($alias . '_save'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Model/Entity/Panel.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $_hidden = ['content']; 37 | 38 | /** 39 | * Read the stream contents or inflate deflated data. 40 | * 41 | * Over certain sizes PDO will return file handles. 42 | * For backwards compatibility and consistency we smooth over that difference here. 43 | * 44 | * @param mixed $content Content 45 | * @return string 46 | */ 47 | protected function _getContent(mixed $content): string 48 | { 49 | if (is_resource($content)) { 50 | $content = (string)stream_get_contents($content); 51 | } 52 | 53 | if (is_string($content) && function_exists('gzinflate')) { 54 | // phpcs:disable 55 | $contentInflated = @gzinflate($content); 56 | // phpcs:enable 57 | if ($contentInflated !== false) { 58 | return $contentInflated; 59 | } 60 | } 61 | 62 | return $content; 63 | } 64 | 65 | /** 66 | * Deflate the string data before saving it into database 67 | * 68 | * @param mixed $content Content 69 | * @return mixed 70 | */ 71 | protected function _setContent(mixed $content): mixed 72 | { 73 | if (is_string($content) && function_exists('gzdeflate')) { 74 | $contentDeflated = gzdeflate($content, 9); 75 | if ($contentDeflated !== false) { 76 | $content = $contentDeflated; 77 | } 78 | } 79 | 80 | return $content; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Model/Entity/Request.php: -------------------------------------------------------------------------------- 1 | getConnection(); 44 | $schema = $connection->getSchemaCollection(); 45 | 46 | try { 47 | $existing = $schema->listTables(); 48 | } catch (PDOException $e) { 49 | // Handle errors when SQLite blows up if the schema has changed. 50 | if (strpos($e->getMessage(), 'schema has changed') !== false) { 51 | $existing = $schema->listTables(); 52 | } else { 53 | throw $e; 54 | } 55 | } 56 | 57 | try { 58 | $config = require dirname(dirname(__DIR__)) . '/schema.php'; 59 | $driver = $connection->getDriver(); 60 | foreach ($config as $table) { 61 | if (in_array($table['table'], $existing, true)) { 62 | continue; 63 | } 64 | if (!in_array($table['table'], $fixtures, true)) { 65 | continue; 66 | } 67 | 68 | // Use Database/Schema primitives to generate dialect specific 69 | // CREATE TABLE statements and run them. 70 | $schema = new TableSchema($table['table'], $table['columns']); 71 | foreach ($table['constraints'] as $name => $itemConfig) { 72 | $schema->addConstraint($name, $itemConfig); 73 | } 74 | foreach ($schema->createSql($connection) as $sql) { 75 | $driver->execute($sql); 76 | } 77 | } 78 | } catch (PDOException $e) { 79 | if (strpos($e->getMessage(), 'unable to open')) { 80 | throw new RuntimeException( 81 | 'Could not create a SQLite database. ' . 82 | 'Ensure that your webserver has write access to the database file and folder it is in.', 83 | ); 84 | } 85 | throw $e; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Model/Table/PanelsTable.php: -------------------------------------------------------------------------------- 1 | belongsTo('DebugKit.Requests'); 47 | $this->ensureTables(['requests', 'panels']); 48 | } 49 | 50 | /** 51 | * Find panels by request id 52 | * 53 | * @param \Cake\ORM\Query\SelectQuery $query The query 54 | * @param string|int $requestId The request id 55 | * @return \Cake\ORM\Query\SelectQuery The query. 56 | */ 57 | public function findByRequest(SelectQuery $query, string|int $requestId): SelectQuery 58 | { 59 | return $query->where(['Panels.request_id' => $requestId]) 60 | ->orderBy(['Panels.title' => 'ASC']); 61 | } 62 | 63 | /** 64 | * DebugKit tables are special. 65 | * 66 | * @return string 67 | */ 68 | public static function defaultConnectionName(): string 69 | { 70 | return 'debug_kit'; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Model/Table/SqlTraceTrait.php: -------------------------------------------------------------------------------- 1 | fileStamp(parent::selectQuery()); 37 | } 38 | 39 | /** 40 | * Overwrite parent table method to inject SQL comment 41 | */ 42 | public function updateQuery(): UpdateQuery 43 | { 44 | return $this->fileStamp(parent::updateQuery()); 45 | } 46 | 47 | /** 48 | * Overwrite parent table method to inject SQL comment 49 | */ 50 | public function deleteQuery(): DeleteQuery 51 | { 52 | return $this->fileStamp(parent::deleteQuery()); 53 | } 54 | 55 | /** 56 | * Applies a comment to a query about which file created it. 57 | * 58 | * @template T of \Cake\ORM\Query\SelectQuery|\Cake\ORM\Query\UpdateQuery|\Cake\ORM\Query\DeleteQuery 59 | * @param \Cake\ORM\Query\SelectQuery|\Cake\ORM\Query\UpdateQuery|\Cake\ORM\Query\DeleteQuery $query The Query to insert a comment into. 60 | * @phpstan-param T $query 61 | * @param int $start How many entries in the stack trace to skip. 62 | * @param bool $debugOnly False to always stamp queries with a comment. 63 | * @return \Cake\ORM\Query\SelectQuery|\Cake\ORM\Query\UpdateQuery|\Cake\ORM\Query\DeleteQuery 64 | * @phpstan-return T 65 | */ 66 | protected function fileStamp( 67 | SelectQuery|UpdateQuery|DeleteQuery $query, 68 | int $start = 1, 69 | bool $debugOnly = true, 70 | ): SelectQuery|UpdateQuery|DeleteQuery { 71 | if (!Configure::read('debug') && $debugOnly === true) { 72 | return $query; 73 | } 74 | 75 | $traces = Debugger::trace(['start' => $start, 'format' => 'array']); 76 | $file = '[unknown]'; 77 | $line = '??'; 78 | 79 | if (is_array($traces)) { 80 | foreach ($traces as $trace) { 81 | $path = $trace['file']; 82 | $line = $trace['line']; 83 | $file = Debugger::trimPath($path); 84 | if ($path === '[internal]') { 85 | continue; 86 | } 87 | if (defined('CAKE_CORE_INCLUDE_PATH') && strpos($path, CAKE_CORE_INCLUDE_PATH) !== 0) { 88 | break; 89 | } 90 | } 91 | } 92 | 93 | return $query->comment(sprintf('%s (line %s)', $file, $line)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Panel/CachePanel.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected array $instances = []; 36 | 37 | /** 38 | * Constructor 39 | */ 40 | public function __construct() 41 | { 42 | $this->logger = new ArrayLog(); 43 | } 44 | 45 | /** 46 | * Initialize - install cache spies. 47 | * 48 | * @return void 49 | */ 50 | public function initialize(): void 51 | { 52 | foreach (Cache::configured() as $name) { 53 | /** @var array $config */ 54 | $config = Cache::getConfig($name); 55 | if (isset($config['className']) && $config['className'] instanceof DebugEngine) { 56 | $instance = $config['className']; 57 | } elseif (isset($config['className'])) { 58 | /** @var \Cake\Cache\CacheEngine $engine */ 59 | $engine = Cache::pool($name); 60 | // Unload from the cache registry so that subsequence calls to 61 | // Cache::pool($name) use the new config with DebugEngine instance set below. 62 | Cache::getRegistry()->unload($name); 63 | 64 | $instance = new DebugEngine($engine, $name, $this->logger); 65 | $instance->init(); 66 | $config['className'] = $instance; 67 | 68 | Cache::drop($name); 69 | Cache::setConfig($name, $config); 70 | } 71 | if (isset($instance)) { 72 | $this->instances[$name] = $instance; 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Get the data for this panel 79 | * 80 | * @return array 81 | */ 82 | public function data(): array 83 | { 84 | $metrics = []; 85 | foreach ($this->instances as $name => $instance) { 86 | $metrics[$name] = $instance->metrics(); 87 | } 88 | $logs = $this->logger->read(); 89 | $this->logger->clear(); 90 | 91 | return [ 92 | 'metrics' => $metrics, 93 | 'logs' => $logs, 94 | ]; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Panel/DeprecationsPanel.php: -------------------------------------------------------------------------------- 1 | _debug = new DebugInclude(); 47 | } 48 | 49 | /** 50 | * Get a list of files that were deprecated and split them out into the various parts of the app 51 | * 52 | * @return array 53 | */ 54 | protected function _prepare(): array 55 | { 56 | $errors = static::$deprecatedErrors; 57 | $return = ['cake' => [], 'app' => [], 'plugins' => [], 'vendor' => [], 'other' => []]; 58 | 59 | foreach ($errors as $error) { 60 | $file = $error['file']; 61 | $line = $error['line']; 62 | 63 | $errorData = [ 64 | 'file' => $file, 65 | 'line' => $line, 66 | 'message' => $error['message'], 67 | ]; 68 | 69 | $pluginName = $this->_debug->getPluginName($file); 70 | /** @var string|false $pluginName */ 71 | if ($pluginName) { 72 | $errorData['niceFile'] = $this->_debug->niceFileName($file, 'plugin', $pluginName); 73 | $return['plugins'][$pluginName][] = $errorData; 74 | } elseif ($this->_debug->isAppFile($file)) { 75 | $errorData['niceFile'] = $this->_debug->niceFileName($file, 'app'); 76 | $return['app'][] = $errorData; 77 | } elseif ($this->_debug->isCakeFile($file)) { 78 | $errorData['niceFile'] = $this->_debug->niceFileName($file, 'cake'); 79 | $return['cake'][] = $errorData; 80 | } else { 81 | /** @var string|false $vendorName */ 82 | $vendorName = $this->_debug->getComposerPackageName($file); 83 | 84 | if ($vendorName) { 85 | $errorData['niceFile'] = $this->_debug->niceFileName($file, 'vendor', $vendorName); 86 | $return['vendor'][$vendorName][] = $errorData; 87 | } else { 88 | $errorData['niceFile'] = $this->_debug->niceFileName($file, 'root'); 89 | $return['other'][] = $errorData; 90 | } 91 | } 92 | } 93 | 94 | ksort($return['plugins']); 95 | ksort($return['vendor']); 96 | 97 | return $return; 98 | } 99 | 100 | /** 101 | * Add a error 102 | * 103 | * @param array $error The deprecated error 104 | * @return void 105 | */ 106 | public static function addDeprecatedError(array $error): void 107 | { 108 | static::$deprecatedErrors[] = $error; 109 | } 110 | 111 | /** 112 | * Reset the tracked errors. 113 | * 114 | * @return void 115 | */ 116 | public static function clearDeprecatedErrors(): void 117 | { 118 | static::$deprecatedErrors = []; 119 | } 120 | 121 | /** 122 | * Get the number of files deprecated in this request. 123 | * 124 | * @return string 125 | */ 126 | public function summary(): string 127 | { 128 | $data = $this->_data; 129 | if (empty($data)) { 130 | $data = $this->_prepare(); 131 | } 132 | 133 | return (string)array_reduce($data, function ($carry, $item) { 134 | if (empty($item)) { 135 | return $carry; 136 | } 137 | // app, cake, or other groups 138 | if (Hash::dimensions($item) == 2) { 139 | return $carry + count($item); 140 | } 141 | 142 | // plugin and vendor groups 143 | foreach ($item as $group) { 144 | $carry += count($group); 145 | } 146 | 147 | return $carry; 148 | }, 0); 149 | } 150 | 151 | /** 152 | * Shutdown callback 153 | * 154 | * @param \Cake\Event\EventInterface $event Event 155 | * @return void 156 | */ 157 | public function shutdown(EventInterface $event): void 158 | { 159 | $this->_data = $this->_prepare(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Panel/HistoryPanel.php: -------------------------------------------------------------------------------- 1 | fetchTable('DebugKit.Requests'); 35 | $recent = $table->find('recent'); 36 | 37 | return [ 38 | 'requests' => $recent->toArray(), 39 | ]; 40 | } 41 | 42 | /** 43 | * Gets the initial text for the history summary 44 | * 45 | * @return string 46 | */ 47 | public function summary(): string 48 | { 49 | return '0 xhr'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Panel/IncludePanel.php: -------------------------------------------------------------------------------- 1 | _debug = new DebugInclude(); 42 | deprecationWarning( 43 | '5.1.0', 44 | 'Include panel is deprecated. Remove it from your panel configuration, and use Environment Panel instead.', 45 | ); 46 | } 47 | 48 | /** 49 | * Get a list of files that were included and split them out into the various parts of the app 50 | * 51 | * @return array 52 | */ 53 | protected function _prepare(): array 54 | { 55 | $return = ['cake' => [], 'app' => [], 'plugins' => [], 'vendor' => [], 'other' => []]; 56 | 57 | foreach (get_included_files() as $file) { 58 | /** @var string|false $pluginName */ 59 | $pluginName = $this->_debug->getPluginName($file); 60 | 61 | if ($pluginName) { 62 | $return['plugins'][$pluginName][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName( 63 | $file, 64 | 'plugin', 65 | $pluginName, 66 | ); 67 | } elseif ($this->_debug->isAppFile($file)) { 68 | $return['app'][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName($file, 'app'); 69 | } elseif ($this->_debug->isCakeFile($file)) { 70 | $return['cake'][$this->_debug->getFileType($file)][] = $this->_debug->niceFileName($file, 'cake'); 71 | } else { 72 | /** @var string|false $vendorName */ 73 | $vendorName = $this->_debug->getComposerPackageName($file); 74 | 75 | if ($vendorName) { 76 | $return['vendor'][$vendorName][] = $this->_debug->niceFileName($file, 'vendor', $vendorName); 77 | } else { 78 | $return['other'][] = $this->_debug->niceFileName($file, 'root'); 79 | } 80 | } 81 | } 82 | 83 | $return['paths'] = $this->_debug->includePaths(); 84 | 85 | ksort($return['app']); 86 | ksort($return['cake']); 87 | ksort($return['plugins']); 88 | ksort($return['vendor']); 89 | 90 | foreach ($return['plugins'] as &$plugin) { 91 | ksort($plugin); 92 | } 93 | 94 | foreach ($return as $k => $v) { 95 | $return[$k] = Debugger::exportVarAsNodes($v); 96 | } 97 | 98 | return $return; 99 | } 100 | 101 | /** 102 | * Get the number of files included in this request. 103 | * 104 | * @return string 105 | */ 106 | public function summary(): string 107 | { 108 | $data = $this->_data; 109 | if (empty($data)) { 110 | $data = $this->_prepare(); 111 | } 112 | 113 | unset($data['paths']); 114 | $data = array_filter($data, function ($v, $k) { 115 | return !empty($v); 116 | }, ARRAY_FILTER_USE_BOTH); 117 | 118 | return (string)count(Hash::flatten($data)); 119 | } 120 | 121 | /** 122 | * Shutdown callback 123 | * 124 | * @param \Cake\Event\EventInterface $event Event 125 | * @return void 126 | */ 127 | public function shutdown(EventInterface $event): void 128 | { 129 | $this->_data = $this->_prepare(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Panel/LogPanel.php: -------------------------------------------------------------------------------- 1 | 'DebugKit.DebugKit', 37 | ]); 38 | } 39 | 40 | /** 41 | * Get the panel data 42 | * 43 | * @return array 44 | */ 45 | public function data(): array 46 | { 47 | return [ 48 | 'logger' => Log::engine('debug_kit_log_panel'), 49 | ]; 50 | } 51 | 52 | /** 53 | * Get the summary data. 54 | * 55 | * @return string 56 | */ 57 | public function summary(): string 58 | { 59 | /** @var \DebugKit\Log\Engine\DebugKitLog|null $logger */ 60 | $logger = Log::engine('debug_kit_log_panel'); 61 | if (!$logger) { 62 | return '0'; 63 | } 64 | 65 | return (string)$logger->count(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Panel/MailPanel.php: -------------------------------------------------------------------------------- 1 | getProperty('_config'); 45 | $property->setAccessible(true); 46 | /** @var array<\Cake\Mailer\AbstractTransport|array> $configs */ 47 | $configs = $property->getValue(); 48 | 49 | $log = $this->emailLog = new ArrayObject(); 50 | 51 | foreach ($configs as $name => $transport) { 52 | if (is_object($transport)) { 53 | if (!$transport instanceof DebugKitTransport) { 54 | $configs[$name] = new DebugKitTransport(['debugKitLog' => $log], $transport); 55 | } 56 | continue; 57 | } 58 | 59 | $className = App::className($transport['className'], 'Mailer/Transport', 'Transport'); 60 | if (!$className || $className === DebugKitTransport::class) { 61 | continue; 62 | } 63 | 64 | $transport['originalClassName'] = $transport['className']; 65 | $transport['className'] = 'DebugKit.DebugKit'; 66 | $transport['debugKitLog'] = $log; 67 | 68 | $configs[$name] = $transport; 69 | } 70 | $reflection->setStaticPropertyValue('_config', $configs); 71 | } 72 | 73 | /** 74 | * Get the data this panel wants to store. 75 | * 76 | * @return array 77 | */ 78 | public function data(): array 79 | { 80 | return [ 81 | 'emails' => isset($this->emailLog) ? $this->emailLog->getArrayCopy() : [], 82 | ]; 83 | } 84 | 85 | /** 86 | * Get summary data from the queries run. 87 | * 88 | * @return string 89 | */ 90 | public function summary(): string 91 | { 92 | if (empty($this->emailLog)) { 93 | return ''; 94 | } 95 | 96 | return (string)count($this->emailLog); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Panel/PackagesPanel.php: -------------------------------------------------------------------------------- 1 | exists()) { 37 | $lockContent = $lockFile->read(); 38 | $packages = $lockContent['packages']; 39 | $devPackages = $lockContent['packages-dev']; 40 | } 41 | 42 | return [ 43 | 'packages' => $packages, 44 | 'devPackages' => $devPackages, 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Panel/PanelRegistry.php: -------------------------------------------------------------------------------- 1 | 29 | * @implements \Cake\Event\EventDispatcherInterface 30 | */ 31 | class PanelRegistry extends ObjectRegistry implements EventDispatcherInterface 32 | { 33 | /** 34 | * @use \Cake\Event\EventDispatcherTrait 35 | */ 36 | use EventDispatcherTrait; 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @param \Cake\Event\EventManager $eventManager Event Manager that panels should bind to. 42 | * Typically this is the global manager. 43 | */ 44 | public function __construct(EventManager $eventManager) 45 | { 46 | $this->setEventManager($eventManager); 47 | } 48 | 49 | /** 50 | * Resolve a panel class name. 51 | * 52 | * Part of the template method for Cake\Utility\ObjectRegistry::load() 53 | * 54 | * @param string $class Partial class name to resolve. 55 | * @return string|null Either the correct class name, null if the class is not found. 56 | */ 57 | protected function _resolveClassName(string $class): ?string 58 | { 59 | return App::className($class, 'Panel', 'Panel'); 60 | } 61 | 62 | /** 63 | * Throws an exception when a component is missing. 64 | * 65 | * Part of the template method for Cake\Utility\ObjectRegistry::load() 66 | * 67 | * @param string $class The classname that is missing. 68 | * @param string $plugin The plugin the component is missing in. 69 | * @return void 70 | * @throws \RuntimeException 71 | */ 72 | protected function _throwMissingClassError(string $class, ?string $plugin): void 73 | { 74 | throw new RuntimeException(sprintf("Unable to find '%s' panel.", $class)); 75 | } 76 | 77 | /** 78 | * Create the panels instance. 79 | * 80 | * Part of the template method for Cake\Utility\ObjectRegistry::load() 81 | * 82 | * @param \DebugKit\DebugPanel|class-string<\DebugKit\DebugPanel> $class The classname to create. 83 | * @param string $alias The alias of the panel. 84 | * @param array $config An array of config to use for the panel. 85 | * @return \DebugKit\DebugPanel The constructed panel class. 86 | */ 87 | protected function _create(object|string $class, string $alias, array $config): DebugPanel 88 | { 89 | if (is_string($class)) { 90 | $instance = new $class(); 91 | } else { 92 | $instance = $class; 93 | } 94 | 95 | $this->getEventManager()->on($instance); 96 | 97 | return $instance; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Panel/PluginsPanel.php: -------------------------------------------------------------------------------- 1 | _data['hasEmptyAppConfig'] = empty($config); 35 | $plugins = []; 36 | 37 | foreach ($config as $pluginName => $options) { 38 | $plugins[$pluginName] = [ 39 | 'isLoaded' => $loadedPluginsCollection->has($pluginName), 40 | 'onlyDebug' => $options['onlyDebug'] ?? false, 41 | 'onlyCli' => $options['onlyCli'] ?? false, 42 | 'optional' => $options['optional'] ?? false, 43 | ]; 44 | } 45 | 46 | $this->_data['plugins'] = $plugins; 47 | } 48 | 49 | /** 50 | * Get summary data for the plugins panel. 51 | * 52 | * @return string 53 | */ 54 | public function summary(): string 55 | { 56 | if (!isset($this->_data['plugins'])) { 57 | return '0'; 58 | } 59 | 60 | return (string)count($this->_data['plugins']); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Panel/RequestPanel.php: -------------------------------------------------------------------------------- 1 | getSubject(); 38 | $request = $controller->getRequest(); 39 | $maxDepth = Configure::read('DebugKit.maxDepth', 5); 40 | 41 | $attributes = []; 42 | foreach ($request->getAttributes() as $attr => $value) { 43 | try { 44 | serialize($value); 45 | } catch (Exception $e) { 46 | $value = "Could not serialize `{$attr}`. It failed with {$e->getMessage()}"; 47 | } 48 | $attributes[$attr] = Debugger::exportVarAsNodes($value, $maxDepth); 49 | } 50 | 51 | $this->_data = [ 52 | 'params' => $request->getAttribute('params'), 53 | 'attributes' => $attributes, 54 | 'query' => Debugger::exportVarAsNodes($request->getQueryParams(), $maxDepth), 55 | 'data' => Debugger::exportVarAsNodes($request->getData(), $maxDepth), 56 | 'cookie' => Debugger::exportVarAsNodes($request->getCookieParams(), $maxDepth), 57 | 'get' => Debugger::exportVarAsNodes($_GET, $maxDepth), 58 | 'session' => Debugger::exportVarAsNodes($request->getSession()->read(), $maxDepth), 59 | 'matchedRoute' => $request->getParam('_matchedRoute'), 60 | 'headers' => [ 61 | 'response' => headers_sent($file, $line), 62 | 'file' => $file, 63 | 'line' => $line, 64 | ], 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Panel/RoutesPanel.php: -------------------------------------------------------------------------------- 1 | defaults['plugin']) || $route->defaults['plugin'] !== 'DebugKit'; 35 | }); 36 | 37 | return (string)count($routes); 38 | } 39 | 40 | /** 41 | * Data collection callback. 42 | * 43 | * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event The shutdown event. 44 | * @return void 45 | */ 46 | public function shutdown(EventInterface $event): void 47 | { 48 | $controller = $event->getSubject(); 49 | $this->_data = [ 50 | 'matchedRoute' => $controller->getRequest()->getParam('_matchedRoute'), 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Panel/SessionPanel.php: -------------------------------------------------------------------------------- 1 | getSubject(); 42 | $request = $controller->getRequest(); 43 | 44 | $maxDepth = Configure::read('DebugKit.maxDepth', 5); 45 | $content = Debugger::exportVarAsNodes($request->getSession()->read(), $maxDepth); 46 | $this->_data = compact('content'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Panel/SqlLogPanel.php: -------------------------------------------------------------------------------- 1 | configName() === 'debug_kit') { 68 | return; 69 | } 70 | $driver = $connection->getDriver(); 71 | 72 | if (!method_exists($driver, 'setLogger')) { 73 | return; 74 | } 75 | 76 | $logger = null; 77 | if ($driver instanceof Driver) { 78 | $logger = $driver->getLogger(); 79 | } elseif (method_exists($connection, 'getLogger')) { 80 | // ElasticSearch connection holds the logger, not the Elastica Driver 81 | $logger = $connection->getLogger(); 82 | } 83 | 84 | if ($logger instanceof DebugLog) { 85 | $logger->setIncludeSchema($includeSchemaReflection); 86 | static::$_loggers[] = $logger; 87 | 88 | return; 89 | } 90 | $logger = new DebugLog($logger, $name, $includeSchemaReflection); 91 | 92 | /** @var \Cake\Database\Driver $driver */ 93 | $driver->setLogger($logger); 94 | 95 | static::$_loggers[] = $logger; 96 | } 97 | 98 | /** 99 | * Get the data this panel wants to store. 100 | * 101 | * @return array 102 | */ 103 | public function data(): array 104 | { 105 | return [ 106 | 'tables' => array_map(function (Table $table) { 107 | return $table->getAlias(); 108 | }, $this->getTableLocator()->genericInstances()), 109 | 'loggers' => static::$_loggers, 110 | ]; 111 | } 112 | 113 | /** 114 | * Get summary data from the queries run. 115 | * 116 | * @return string 117 | */ 118 | public function summary(): string 119 | { 120 | $count = $time = 0; 121 | foreach (static::$_loggers as $logger) { 122 | $count += count($logger->queries()); 123 | $time += $logger->totalTime(); 124 | } 125 | if (!$count) { 126 | return '0'; 127 | } 128 | 129 | return "$count / $time ms"; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Panel/TimerPanel.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public function implementedEvents(): array 33 | { 34 | $before = function ($name) { 35 | return function () use ($name): void { 36 | DebugTimer::start($name); 37 | }; 38 | }; 39 | $after = function ($name) { 40 | return function () use ($name): void { 41 | DebugTimer::stop($name); 42 | }; 43 | }; 44 | $both = function ($name) use ($before, $after) { 45 | return [ 46 | ['priority' => 0, 'callable' => $before('Event: ' . $name)], 47 | ['priority' => 999, 'callable' => $after('Event: ' . $name)], 48 | ]; 49 | }; 50 | 51 | return [ 52 | 'Controller.initialize' => [ 53 | ['priority' => 0, 'callable' => function (): void { 54 | DebugMemory::record('Controller initialization'); 55 | }], 56 | ['priority' => 0, 'callable' => $before('Event: Controller.initialize')], 57 | ['priority' => 999, 'callable' => $after('Event: Controller.initialize')], 58 | ], 59 | 'Controller.startup' => [ 60 | ['priority' => 0, 'callable' => $before('Event: Controller.startup')], 61 | ['priority' => 999, 'callable' => $after('Event: Controller.startup')], 62 | ['priority' => 999, 'callable' => function (): void { 63 | DebugMemory::record('Controller action start'); 64 | DebugTimer::start('Controller: action'); 65 | }], 66 | ], 67 | 'Controller.beforeRender' => [ 68 | ['priority' => 0, 'callable' => function (): void { 69 | DebugTimer::stop('Controller: action'); 70 | }], 71 | ['priority' => 0, 'callable' => $before('Event: Controller.beforeRender')], 72 | ['priority' => 999, 'callable' => $after('Event: Controller.beforeRender')], 73 | ['priority' => 999, 'callable' => function (): void { 74 | DebugMemory::record('View Render start'); 75 | DebugTimer::start('View: Render'); 76 | }], 77 | ], 78 | 'View.beforeRender' => $both('View.beforeRender'), 79 | 'View.afterRender' => $both('View.afterRender'), 80 | 'View.beforeLayout' => $both('View.beforeLayout'), 81 | 'View.afterLayout' => $both('View.afterLayout'), 82 | 'View.beforeRenderFile' => [ 83 | ['priority' => 0, 'callable' => function ($event, $filename): void { 84 | DebugTimer::start('Render File: ' . $filename); 85 | }], 86 | ], 87 | 'View.afterRenderFile' => [ 88 | ['priority' => 0, 'callable' => function ($event, $filename): void { 89 | DebugTimer::stop('Render File: ' . $filename); 90 | }], 91 | ], 92 | 'Controller.shutdown' => [ 93 | ['priority' => 0, 'callable' => $before('Event: Controller.shutdown')], 94 | ['priority' => 0, 'callable' => function (): void { 95 | DebugTimer::stop('View: Render'); 96 | DebugMemory::record('Controller shutdown'); 97 | }], 98 | ['priority' => 999, 'callable' => $after('Event: Controller.shutdown')], 99 | ], 100 | ]; 101 | } 102 | 103 | /** 104 | * Get the data for the panel. 105 | * 106 | * @return array 107 | */ 108 | public function data(): array 109 | { 110 | return [ 111 | 'requestTime' => DebugTimer::requestTime(), 112 | 'timers' => DebugTimer::getAll(), 113 | 'memory' => DebugMemory::getAll(), 114 | 'peakMemory' => DebugMemory::getPeak(), 115 | ]; 116 | } 117 | 118 | /** 119 | * Get the summary for the panel. 120 | * 121 | * @return string 122 | */ 123 | public function summary(): string 124 | { 125 | $time = Number::precision(DebugTimer::requestTime(), 2) . ' s'; 126 | $memory = Number::toReadableSize(DebugMemory::getPeak()); 127 | 128 | return "$time / $memory"; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Panel/VariablesPanel.php: -------------------------------------------------------------------------------- 1 | getErrors(); 40 | 41 | foreach ($entity->getVisible() as $property) { 42 | $v = $entity[$property]; 43 | if ($v instanceof EntityInterface) { 44 | $errors[$property] = $this->_getErrors($v); 45 | } elseif (is_array($v)) { 46 | foreach ($v as $key => $varValue) { 47 | if ($varValue instanceof EntityInterface) { 48 | $errors[$property][$key] = $this->_getErrors($varValue); 49 | } 50 | } 51 | } 52 | } 53 | 54 | return Hash::filter($errors); 55 | } 56 | 57 | /** 58 | * Safely retrieves debug information from an object 59 | * and applies a callback. 60 | * 61 | * @param callable $walker The walker to apply on the debug info array. 62 | * @param object $item The item whose debug info to retrieve. 63 | * @return array|string 64 | */ 65 | protected function _walkDebugInfo(callable $walker, object $item): array|string 66 | { 67 | try { 68 | /** @phpstan-ignore method.notFound */ 69 | $info = $item->__debugInfo(); 70 | } catch (Exception $exception) { 71 | return sprintf( 72 | 'Could not retrieve debug info - %s. Error: %s in %s, line %d', 73 | get_class($item), 74 | $exception->getMessage(), 75 | $exception->getFile(), 76 | $exception->getLine(), 77 | ); 78 | } 79 | 80 | return array_map($walker, $info); 81 | } 82 | 83 | /** 84 | * Shutdown event 85 | * 86 | * @param \Cake\Event\EventInterface $event The event 87 | * @return void 88 | */ 89 | public function shutdown(EventInterface $event): void 90 | { 91 | /** @var \Cake\Controller\Controller $controller */ 92 | $controller = $event->getSubject(); 93 | $errors = []; 94 | $content = []; 95 | $vars = $controller->viewBuilder()->getVars(); 96 | $varsMaxDepth = (int)Configure::read('DebugKit.variablesPanelMaxDepth', 5); 97 | 98 | foreach ($vars as $k => $v) { 99 | // Get the validation errors for Entity 100 | if ($v instanceof EntityInterface) { 101 | $errors[$k] = Debugger::exportVarAsNodes($this->_getErrors($v), $varsMaxDepth); 102 | } elseif ($v instanceof Form) { 103 | $formErrors = $v->getErrors(); 104 | if ($formErrors) { 105 | $errors[$k] = Debugger::exportVarAsNodes($formErrors, $varsMaxDepth); 106 | } 107 | } 108 | $content[$k] = Debugger::exportVarAsNodes($v, $varsMaxDepth); 109 | } 110 | 111 | $this->_data = [ 112 | 'variables' => $content, 113 | 'errors' => $errors, 114 | 'varsMaxDepth' => $varsMaxDepth, 115 | ]; 116 | } 117 | 118 | /** 119 | * Get summary data for the variables panel. 120 | * 121 | * @return string 122 | */ 123 | public function summary(): string 124 | { 125 | if (!isset($this->_data['variables'])) { 126 | return '0'; 127 | } 128 | 129 | return (string)count($this->_data['variables']); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/View/AjaxView.php: -------------------------------------------------------------------------------- 1 | response = $this->response->withType('ajax'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/View/Helper/CredentialsHelper.php: -------------------------------------------------------------------------------- 1 | mysql://******@localhost/my_db 42 | * 43 | * @param mixed $in variable to filter 44 | * @return mixed 45 | */ 46 | public function filter(mixed $in): mixed 47 | { 48 | $regexp = '/^([^:;]+:\/\/)([^:;]+:?.*?)@(.*)$/i'; 49 | if (!is_string($in) || empty($in)) { 50 | return $in; 51 | } 52 | preg_match_all($regexp, $in, $tokens); 53 | if ($tokens[0] === []) { 54 | return h($in); 55 | } 56 | $protocol = Hash::get($tokens, '1.0'); 57 | $credentials = Hash::get($tokens, '2.0'); 58 | $tail = Hash::get($tokens, '3.0'); 59 | $link = $this->Html->tag('a', '******', [ 60 | 'class' => 'filtered-credentials', 61 | 'title' => h($credentials), 62 | 'onclick' => 'this.innerHTML = this.title', 63 | ]); 64 | 65 | return h($protocol) . $link . '@' . h($tail); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/View/Helper/SimpleGraphHelper.php: -------------------------------------------------------------------------------- 1 | (int) Maximum value in the graphs 35 | * - width => (int) 36 | * - valueType => string (value, percentage) 37 | * - style => array 38 | * 39 | * @var array 40 | */ 41 | protected array $_defaultSettings = [ 42 | 'max' => 100, 43 | 'width' => 350, 44 | 'valueType' => 'value', 45 | ]; 46 | 47 | /** 48 | * bar method 49 | * 50 | * @param float|int $value Value to be graphed 51 | * @param float|int $offset how much indentation 52 | * @param array $options Graph options 53 | * @return string Html graph 54 | */ 55 | public function bar(float|int $value, float|int $offset, array $options = []): string 56 | { 57 | $settings = array_merge($this->_defaultSettings, $options); 58 | $max = $settings['max']; 59 | $width = $settings['width']; 60 | $valueType = $settings['valueType']; 61 | 62 | $graphValue = $value / $max * $width; 63 | $graphValue = max(round($graphValue), 1); 64 | 65 | if ($valueType === 'percentage') { 66 | $graphOffset = 0; 67 | } else { 68 | $graphOffset = $offset / $max * $width; 69 | $graphOffset = round($graphOffset); 70 | } 71 | 72 | return sprintf( 73 | '
', 74 | "width: {$width}px", 75 | "margin-left: {$graphOffset}px; width: {$graphValue}px", 76 | "Starting {$offset}ms into the request, taking {$value}ms", 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/View/Helper/ToolbarHelper.php: -------------------------------------------------------------------------------- 1 | sort = $sort; 58 | } 59 | 60 | /** 61 | * Dump an array of nodes 62 | * 63 | * @param array<\Cake\Error\Debug\NodeInterface> $nodes An array of dumped variables. 64 | * Variables should be keyed by the name they had in the view. 65 | * @return string Formatted HTML 66 | */ 67 | public function dumpNodes(array $nodes): string 68 | { 69 | $formatter = new HtmlFormatter(); 70 | if ($this->sort) { 71 | ksort($nodes); 72 | } 73 | $items = []; 74 | foreach ($nodes as $key => $value) { 75 | $items[] = new ArrayItemNode(new ScalarNode('string', $key), $value); 76 | } 77 | $root = new ArrayNode($items); 78 | 79 | return implode([ 80 | '
', 81 | $formatter->dump($root), 82 | '
', 83 | ]); 84 | } 85 | 86 | /** 87 | * Dump an error node 88 | * 89 | * @param \Cake\Error\Debug\NodeInterface $node A error node containing dumped variables. 90 | * @return string Formatted HTML 91 | */ 92 | public function dumpNode(NodeInterface $node): string 93 | { 94 | $formatter = new HtmlFormatter(); 95 | 96 | return implode([ 97 | '
', 98 | $formatter->dump($node), 99 | '
', 100 | ]); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/schema.php: -------------------------------------------------------------------------------- 1 | 'requests', 17 | 'columns' => [ 18 | 'id' => ['type' => 'uuid', 'null' => false], 19 | 'url' => ['type' => 'text', 'null' => false], 20 | 'content_type' => ['type' => 'string'], 21 | 'status_code' => ['type' => 'integer'], 22 | 'method' => ['type' => 'string'], 23 | 'requested_at' => ['type' => 'datetime', 'null' => false], 24 | ], 25 | 'constraints' => [ 26 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 27 | ], 28 | ], 29 | [ 30 | 'table' => 'panels', 31 | 'columns' => [ 32 | 'id' => ['type' => 'uuid'], 33 | 'request_id' => ['type' => 'uuid', 'null' => false], 34 | 'panel' => ['type' => 'string'], 35 | 'title' => ['type' => 'string'], 36 | 'element' => ['type' => 'string'], 37 | 'summary' => ['type' => 'string'], 38 | 'content' => ['type' => 'binary', 'length' => TableSchema::LENGTH_LONG], 39 | ], 40 | 'constraints' => [ 41 | 'primary' => ['type' => 'primary', 'columns' => ['id']], 42 | 'unique_panel' => ['type' => 'unique', 'columns' => ['request_id', 'panel']], 43 | 'request_id_fk' => [ 44 | 'type' => 'foreign', 45 | 'columns' => ['request_id'], 46 | 'references' => ['requests', 'id'], 47 | ], 48 | ], 49 | ], 50 | ]; 51 | -------------------------------------------------------------------------------- /templates/Dashboard/index.php: -------------------------------------------------------------------------------- 1 | 8 |

Debug Kit Dashboard

9 | 10 |

Database

11 |
    12 |
  • Driver:
  • 13 | 14 |
  • Requests: Number->format($connection['rows']) ?>
  • 15 | 16 |
17 | 18 | Form->postLink( 19 | 'Reset database', 20 | ['_method' => 'POST', 'action' => 'reset'], 21 | ['confirm' => 'Are you sure?'] 22 | ); ?> 23 | 24 | 25 |

Actions

26 |
    27 |
  • Html->link('Mail Preview', ['controller' => 'MailPreview']); ?>
  • 28 |
29 | -------------------------------------------------------------------------------- /templates/MailPreview/email.php: -------------------------------------------------------------------------------- 1 | 4 | 19 | 20 | request->getQuery('part')) : ?> 21 | element('preview_header'); ?> 22 | 23 | 24 | 25 | 26 | 27 |

You are trying to preview an email that does not have any content.

28 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /templates/MailPreview/index.php: -------------------------------------------------------------------------------- 1 | 4 | $previews) : ?> 5 |

6 | 7 | 8 |

name()) ?>

9 | 10 | 11 | getEmails() as $email) : ?> 12 | 13 | 24 | 25 | 26 | 27 |
14 | Html->link($email, [ 16 | 'controller' => 'MailPreview', 17 | 'action' => 'email', 18 | '?' => ['plugin' => $plugin], 19 | $mailPreview->name(), 20 | $email, 21 | ]); 22 | ?> 23 |
28 | 29 | 30 | 31 |
32 |

How to use this feature?

33 |

Testing emails can be very time consuming ⌛

34 |

Specially when you need to click a bunch of times on an interface to trigger them.

35 |

Wouldn't it be better to just change the templates and refresh the browser to see the result?

36 |

Just the way you work on the web interface! 🏃

37 | 38 |

Example

39 |

MailPreview integrates with CakePHP’s Mailer class. Here's an example of such a mailer:

40 | 41 |
42 |     setTo($user->email)
54 |                     ->setSubject(sprintf("Welcome %s", $user->name))
55 |                     ->setViewVars(["user" => $user]);
56 |                 $mailer->viewBuilder()
57 |                     ->setTemplate("welcome_mail") // By default template with same name as method name is used.
58 |                     ->setLayout("custom");
59 |                 return $mailer;
60 |             }
61 |         }';
62 |         highlight_string($code);
63 |     ?>
64 |     
65 |

Now you create a MailPreview class where you can pass some dummy values.

66 | 67 |
68 |     loadModel("Users");
81 |                 $user = $this->Users->find()->first();
82 | 
83 |                 return $this->getMailer("User")
84 |                     ->welcome($user)
85 |                     ->setViewVars(["activationToken" => "dummy-token"]);
86 |             }
87 |         }';
88 |         highlight_string($code);
89 |     ?>
90 |     
91 | 92 |

Note that the function MUST return the UserMailer object at the end.

93 |

Since Mailers have a fluent interface, you just need to return the result of the chain of calls.

94 |

That's it, now refresh this page! 🙃

95 |
96 | -------------------------------------------------------------------------------- /templates/Panels/view.php: -------------------------------------------------------------------------------- 1 | 8 |

title) ?>

9 |
10 | element($panel->element) ?> 11 |
12 | -------------------------------------------------------------------------------- /templates/Requests/view.php: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
    21 |
  • 22 | 23 | 〈 24 | 25 |
  • 26 |
  • 27 | 28 | 〉 29 | 30 |
  • 31 |
  • 32 |
      33 | panels as $panel) : ?> 34 | 46 | 47 |
    48 |
  • 49 |
  • 50 | Html->image('DebugKit./img/cake.icon.png', [ 51 | 'alt' => 'Debug Kit', 'title' => 'CakePHP ' . Configure::version() . ' Debug Kit' 52 | ]) ?> 53 |
  • 54 |
55 | Html->script('DebugKit./js/jquery', [ 57 | 'block' => true, 58 | ]); 59 | $this->Html->script('DebugKit./js/main', [ 60 | 'type' => 'module', 61 | 'block' => true, 62 | 'id' => '__debug_kit_app', 63 | 'data-id' => $toolbar->id, 64 | 'data-url' => Router::url('/', true), 65 | 'data-webroot' => $this->getRequest()->getAttribute('webroot'), 66 | ]); 67 | ?> 68 | -------------------------------------------------------------------------------- /templates/element/cache_panel.php: -------------------------------------------------------------------------------- 1 | 22 |
23 | 24 |

There were no cache operations in this request.

25 | 26 |

Cache Utilities

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $values) : ?> 36 | 37 | 38 | 53 | 54 | 55 | 56 |
Engine
39 | 52 |
57 |
58 | 59 |

Cache Usage Overview

60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | $counters) : ?> 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
Engineget hitget misssetdelete
82 | 83 |

Cache Logs

84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
Log
98 | 99 |
100 | -------------------------------------------------------------------------------- /templates/element/deprecations_panel.php: -------------------------------------------------------------------------------- 1 | 30 |

31 |
    32 | 33 |
  • 34 | : 35 |
    36 | 37 |
  • 38 | 39 |
40 | 43 |
44 | 45 |

No deprecations

46 | $pluginData) : 55 | $printer($plugin, $pluginData); 56 | endforeach; 57 | endif; 58 | 59 | if (count($cake)) : 60 | $printer('cake', $cake); 61 | endif; 62 | 63 | if (count($vendor)) : 64 | foreach ($vendor as $vendorSection => $vendorData) : 65 | $printer($vendorSection, $vendorData); 66 | endforeach; 67 | endif; 68 | 69 | if (count($other)) : 70 | $printer('other', $other); 71 | endif;?> 72 |
73 | -------------------------------------------------------------------------------- /templates/element/environment_panel.php: -------------------------------------------------------------------------------- 1 | Toolbar 31 | * @var \DebugKit\View\Helper\CredentialsHelper $this->Credentials 32 | */ 33 | ?> 34 |
35 |

CakePHP Version:

36 | 37 | 38 |
39 | You should set zend.assertions to 1 40 | in your php.ini for your development environment. 41 |
42 | 43 | 44 |

Application Constants

45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $val) : ?> 56 | 57 | 58 | 59 | 60 | 61 | 62 |
ConstantValue
63 | 64 |
65 | No application environment available. 66 |
67 | 68 | 69 |

CakePHP Constants

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | $val) : ?> 81 | 82 | 83 | 84 | 85 | 86 | 87 |
ConstantValue
88 | 89 |
90 | CakePHP environment unavailable. 91 |
92 | 93 | 94 |

INI Environment

95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | $val) : ?> 106 | 107 | 108 | 109 | 110 | 111 | 112 |
INI VariableValue
Credentials->filter($val) ?>
113 | 114 |
115 | ini environment unavailable. 116 |
117 | 118 | 119 |

PHP Environment

120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | $val) : ?> 131 | 132 | 133 | 134 | 135 | 136 | 137 |
Environment VariableValue
Credentials->filter($val) ?>
138 | 139 |
140 | PHP environment unavailable. 141 |
142 | 143 | 144 |

Included Files

145 | 146 |

Include Paths

147 | Toolbar->dumpNodes($includePaths) ?> 148 | 149 |

Included Files

150 | Toolbar->dumpNodes($includedFiles) ?> 151 |
152 | -------------------------------------------------------------------------------- /templates/element/include_panel.php: -------------------------------------------------------------------------------- 1 | $paths 18 | * @var array<\Cake\Error\Debug\NodeInterface> $app 19 | * @var array<\Cake\Error\Debug\NodeInterface> $cake 20 | * @var array<\Cake\Error\Debug\NodeInterface> $plugins 21 | * @var array<\Cake\Error\Debug\NodeInterface> $vendor 22 | * @var array<\Cake\Error\Debug\NodeInterface> $other 23 | */ 24 | 25 | // Backwards compat for old DebugKit data. 26 | if (!isset($cake) && isset($core)) { 27 | $cake = $core; 28 | } 29 | ?> 30 |
31 |

Include Paths

32 | Toolbar->dumpNodes(compact('paths')) ?> 33 | 34 |

Included Files

35 | Toolbar->dumpNodes(compact('app', 'cake', 'plugins', 'vendor', 'other')) ?> 36 |
37 | -------------------------------------------------------------------------------- /templates/element/log_panel.php: -------------------------------------------------------------------------------- 1 | 22 |
23 | noLogs()) : ?> 24 |

There were no log entries made this request

25 | 26 | all() as $logName => $logs) : ?> 27 |

28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
TimeMessage
44 | 45 | 46 |
47 | -------------------------------------------------------------------------------- /templates/element/mail_panel.php: -------------------------------------------------------------------------------- 1 | 8 |
9 |

10 | Html->link( 13 | 'Email previews page', 14 | ['controller' => 'MailPreview', 'action' => 'index'], 15 | ['target' => '_blank'] 16 | ) 17 | ) ?> 18 |

19 | No emails were sent during this request

'; 22 | 23 | return; 24 | } 25 | $url = $this->Url->build([ 26 | 'controller' => 'MailPreview', 27 | 'action' => 'sent', 28 | 'panel' => $panel->id, 29 | 'id' => 0, 30 | ]); 31 | ?> 32 |
33 |
34 | 35 | 36 | 37 | 38 | $email) : ?> 39 | 41 | 48 | 49 | 50 |
Subject
42 | 43 | Text->truncate($email['headers']['Subject'])) : 45 | '(No Subject)' 46 | ?> 47 |
51 |
52 | 57 |
58 |
59 | -------------------------------------------------------------------------------- /templates/element/packages_panel.php: -------------------------------------------------------------------------------- 1 | 21 |
29 | 30 |
31 | 'composer.lock' not found 32 |
33 | 34 |
35 | 36 | 37 |
38 |
39 |
40 | Html->image('DebugKit./img/cake.icon.png', ['class' => 'indicator']) ?> 42 |
43 |
44 |
45 | 46 |
47 |

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 67 | 70 | 71 | 72 | 73 |
NameVersion
60 | 64 | 65 | 66 | 68 | 69 |
74 |
75 | 76 | 77 |
78 |

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 98 | 101 | 102 | 103 | 104 |
NameVersion
91 | 95 | 96 | 97 | 99 | 100 |
105 |
106 | 107 |
108 | 109 |
110 | -------------------------------------------------------------------------------- /templates/element/plugins_panel.php: -------------------------------------------------------------------------------- 1 | 21 |
22 | config/plugins.php
'; 25 | printf('

%s

', $msg); 26 | ?> 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | $pluginConfig) : ?> 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
PluginIs LoadedOnly DebugOnly CLIOptional
Html->image('DebugKit./img/cake-red.svg') : '' ?>Html->image('DebugKit./img/cake-red.svg') : '' ?>Html->image('DebugKit./img/cake-red.svg') : '' ?>Html->image('DebugKit./img/cake-red.svg') : '' ?>
50 |
51 |
52 | -------------------------------------------------------------------------------- /templates/element/preview_header.php: -------------------------------------------------------------------------------- 1 | 7 |
8 | 9 | 10 | 11 | 22 | 23 | getHeaders() as $name => $header) :?> 24 | 25 | 26 | 27 | 28 | 29 |
View As 12 | getParts()) ?> 13 | Form->control('part', [ 14 | 'type' => 'select', 15 | 'label' => false, 16 | 'value' => $this->request->getQuery('part') ?: 'html', 17 | 'onChange' => 'formatChanged(this);', 18 | 'options' => array_combine($partNames, $partNames), 19 | ]); 20 | ?> 21 |
30 |
31 | -------------------------------------------------------------------------------- /templates/element/request_panel.php: -------------------------------------------------------------------------------- 1 | 33 |
34 | 35 |

Warning

36 |

37 | 42 |

43 | 44 | 45 |

Route path

46 | 55 |
56 | 57 |
58 |

59 | Route path grammar: [Plugin].[Prefix]/[Controller]::[action] 60 |

61 | 62 |

Attributes

63 | No attributes data.

'; 66 | else : 67 | echo $this->Toolbar->dumpNodes($attributes); 68 | endif; 69 | ?> 70 | 71 |

Post data

72 | No post data.

'; 75 | else : 76 | echo $this->Toolbar->dumpNode($data); 77 | endif; 78 | ?> 79 | 80 |

Query string

81 | No querystring data.

'; 84 | else : 85 | echo $this->Toolbar->dumpNode($query); 86 | endif; 87 | ?> 88 | 89 |

Cookie

90 | 91 | Toolbar->dumpNode($cookie) ?> 92 | 93 |

No Cookie data.

94 | 95 | 96 |

Session

97 | 98 | Toolbar->dumpNode($session) ?> 99 | 100 |

No Session data.

101 | 102 | 103 | 104 |

Matched Route

105 |

Toolbar->dumpNode(Debugger::exportVarAsNodes(['template' => $matchedRoute])) ?>

106 | 107 |
108 | -------------------------------------------------------------------------------- /templates/element/routes_panel.php: -------------------------------------------------------------------------------- 1 | defaults['plugin'] ?? 'app'; 18 | if (!array_key_exists($group, $amountOfRoutesPerGroup)) { 19 | $amountOfRoutesPerGroup[$group] = 0; 20 | } 21 | $amountOfRoutesPerGroup[$group]++; 22 | 23 | if (!array_key_exists($route->template, $duplicateRoutes)) { 24 | $duplicateRoutes[$route->template] = 0; 25 | } 26 | $duplicateRoutes[$route->template]++; 27 | } 28 | 29 | $pluginNames = []; 30 | foreach (CorePlugin::loaded() as $pluginName) { 31 | if (!empty($amountOfRoutesPerGroup[$pluginName])) { 32 | $name = sprintf('%s (%s)', $pluginName, $amountOfRoutesPerGroup[$pluginName]); 33 | $pluginNames[$name] = Text::slug($pluginName); 34 | } 35 | } 36 | 37 | ?> 38 |
39 |
40 | 44 | $parsedName) : ?> 45 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | defaults['plugin'])) : 65 | $class = 'c-routes-panel__route-entry c-routes-panel__route-entry--app'; 66 | else : 67 | $class = 'c-routes-panel__route-entry ' . 68 | 'c-routes-panel__route-entry--plugin c-routes-panel__route-entry--plugin-' . 69 | Text::slug($route->defaults['plugin']); 70 | 71 | // Hide DebugKit internal routes by default 72 | if ($route->defaults['plugin'] === 'DebugKit') { 73 | $class .= ' is-hidden'; 74 | } 75 | endif; 76 | 77 | // Highlight current route 78 | if ($matchedRoute === $route->template) { 79 | $class .= ' highlighted'; 80 | } 81 | 82 | // Mark duplicate routes 83 | if ($duplicateRoutes[$route->template] > 1) { 84 | $class .= ' duplicate-route'; 85 | } 86 | 87 | ?> 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
Route nameURI templateDefaults
options, '_name', $route->getName())) ?>template) ?>
defaults, JSON_PRETTY_PRINT) ?>
96 |
97 | -------------------------------------------------------------------------------- /templates/element/session_panel.php: -------------------------------------------------------------------------------- 1 | 20 |
21 | Toolbar->dumpNode($content) ?> 22 |
23 | -------------------------------------------------------------------------------- /templates/element/sql_log_panel.php: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | 34 |

Generated Models

35 |

36 | The following Table objects used Cake\ORM\Table instead of a concrete class: 37 |

38 |
    39 | 40 |
  • 41 | 42 |
43 |
44 | 45 | 46 | 47 | queries(); 49 | if (empty($queries)) : 50 | continue; 51 | endif; 52 | 53 | $noOutput = false; 54 | ?> 55 |
56 |

name()) ?>

57 |
58 | totalTime(), 61 | count($queries) 62 | ); 63 | ?> 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | > 78 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | 101 | 102 | 103 | 104 |
QueryRowsTook (ms)Role
79 | 'style="color: #004d40;"', 83 | HtmlHighlighter::HIGHLIGHT_BACKTICK_QUOTE => 'style="color: #26a69a;"', 84 | HtmlHighlighter::HIGHLIGHT_NUMBER => 'style="color: #ec407a;"', 85 | HtmlHighlighter::HIGHLIGHT_WORD => 'style="color: #9c27b0;"', 86 | HtmlHighlighter::HIGHLIGHT_PRE => 'style="color: #222; background-color: transparent;"', 87 | ]) 88 | )) 89 | ->format($query['query']) 90 | ?> 91 |
99 |
105 |
106 | 107 | 108 | 109 | 110 |
No active database connections
111 | 112 |
113 | -------------------------------------------------------------------------------- /templates/element/timer_panel.php: -------------------------------------------------------------------------------- 1 | 23 |
24 |
25 |

Memory

26 |
27 | Peak Memory Use: 28 | Number->toReadableSize($peakMemory) ?> 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | $value) : ?> 40 | 41 | 42 | 43 | 44 | 45 | 46 |
MessageMemory Use
Number->toReadableSize($value) ?>
47 |
48 | 49 |
50 |

Timers

51 |
52 | Total Request Time: 53 | Number->precision($requestTime * 1000, 0) ?> ms 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $timeInfo) : 74 | $indent = 0; 75 | for ($j = 0; $j < $i; $j++) : 76 | if (($values[$j]['end'] > $timeInfo['start']) && ($values[$j]['end']) > $timeInfo['end']) : 77 | $indent++; 78 | endif; 79 | endfor; 80 | $indent = str_repeat("\xC2\xA0\xC2\xA0", $indent); 81 | ?> 82 | 83 | 86 | 87 | 97 | 101 | 102 |
EventTime in msTimeline
84 | 85 | Number->precision($timeInfo['time'] * 1000, 2) ?> 88 | SimpleGraph->bar( 89 | $timeInfo['time'] * 1000, 90 | $timeInfo['start'] * 1000, 91 | [ 92 | 'max' => $maxTime * 1000, 93 | 'requestTime' => $requestTime * 1000, 94 | ] 95 | ) ?> 96 |
103 |
104 |
105 | -------------------------------------------------------------------------------- /templates/element/variables_panel.php: -------------------------------------------------------------------------------- 1 | 22 |
23 | %s

', $error); 26 | endif; 27 | 28 | if (isset($varsMaxDepth)) { 29 | $msg = sprintf('%s levels of nested data shown.', $varsMaxDepth); 30 | $msg .= ' You can overwrite this via the config key'; 31 | $msg .= ' DebugKit.variablesPanelMaxDepth
'; 32 | $msg .= 'Increasing the depth value can lead to an out of memory error.'; 33 | printf('

%s

', $msg); 34 | } 35 | 36 | // New node based data. 37 | if (!empty($variables)) :?> 38 |
39 | 46 |
47 | Toolbar->setSort($sort ?? false); 49 | echo $this->Toolbar->dumpNodes($variables); 50 | endif; 51 | 52 | if (!empty($errors)) : 53 | echo '

Validation errors

'; 54 | echo $this->Toolbar->dumpNodes($errors); 55 | endif; 56 | ?> 57 |
58 | -------------------------------------------------------------------------------- /templates/layout/dashboard.php: -------------------------------------------------------------------------------- 1 | extend('toolbar') ?> 2 | 3 |
4 | 5 |

6 | 7 |

8 | 9 | 10 |
11 | fetch('content'); ?> 12 |
13 |
14 | -------------------------------------------------------------------------------- /templates/layout/mailer.php: -------------------------------------------------------------------------------- 1 | extend('toolbar'); 4 | ?> 5 | 6 |
7 | 8 |

9 | 10 |

11 | 12 | 13 |
14 | fetch('content'); ?> 15 |
16 |
17 | -------------------------------------------------------------------------------- /templates/layout/toolbar.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | <?= isset($title) ? h($title) : "Debug Kit Toolbar" ?> 11 | Html->css('DebugKit./css/reset') ?> 12 | Html->css('DebugKit./css/style') ?> 13 | 14 | 15 | fetch('content') ?> 16 |
17 | Html->image('DebugKit./img/cake.icon.png', ['class' => 'o-loader__indicator'])?> 18 |
19 | 20 | fetch('script') ?> 21 | 22 | -------------------------------------------------------------------------------- /tests/Fixture/PanelsFixture.php: -------------------------------------------------------------------------------- 1 | 'articles', 12 | 'columns' => [ 13 | 'id' => ['type' => 'integer'], 14 | 'author_id' => ['type' => 'integer', 'null' => true], 15 | 'title' => ['type' => 'string', 'null' => true], 16 | 'body' => 'text', 17 | 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'], 18 | ], 19 | 'constraints' => [ 20 | 'primary' => [ 21 | 'type' => 'primary', 22 | 'columns' => ['id'], 23 | ], 24 | ], 25 | ], 26 | ]; 27 | 28 | return array_merge($tables, $testTables); 29 | -------------------------------------------------------------------------------- /webroot/css/raleway-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/webroot/css/raleway-regular.eot -------------------------------------------------------------------------------- /webroot/css/raleway-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/webroot/css/raleway-regular.ttf -------------------------------------------------------------------------------- /webroot/css/raleway-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/webroot/css/raleway-regular.woff -------------------------------------------------------------------------------- /webroot/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /webroot/img/cake-red.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webroot/img/cake.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cakephp/debug_kit/291e9c9098bb4fa1afea2a259f1133f7236da7dd/webroot/img/cake.icon.png -------------------------------------------------------------------------------- /webroot/js/inject-iframe.js: -------------------------------------------------------------------------------- 1 | let elem = document.getElementById('__debug_kit_script'); 2 | if (elem) { 3 | window.debugKitId = elem.getAttribute('data-id'); 4 | window.debugKitBaseUrl = elem.getAttribute('data-url'); 5 | elem = null; 6 | } 7 | 8 | ((win, doc) => { 9 | let iframe; 10 | let bodyOverflow; 11 | 12 | const onMessage = (event) => { 13 | if (event.data === 'collapse') { 14 | iframe.height = 40; 15 | iframe.width = 40; 16 | doc.body.style.overflow = bodyOverflow; 17 | return; 18 | } 19 | if (event.data === 'toolbar') { 20 | iframe.height = 40; 21 | iframe.width = '100%'; 22 | doc.body.style.overflow = bodyOverflow; 23 | return; 24 | } 25 | if (event.data === 'expand') { 26 | iframe.width = '100%'; 27 | iframe.height = '100%'; 28 | doc.body.style.overflow = 'hidden'; 29 | } 30 | if (event.data === 'error') { 31 | iframe.width = '40%'; 32 | iframe.height = '40%'; 33 | doc.body.style.overflow = bodyOverflow; 34 | } 35 | }; 36 | 37 | const onReady = () => { 38 | if (!win.debugKitId) { 39 | return; 40 | } 41 | const { body } = doc; 42 | 43 | // Cannot use css text, because of CSP compatibility. 44 | iframe = doc.createElement('iframe'); 45 | iframe.style.position = 'fixed'; 46 | iframe.style.bottom = 0; 47 | iframe.style.right = 0; 48 | iframe.style.border = 0; 49 | iframe.style.outline = 0; 50 | iframe.style.overflow = 'hidden'; 51 | iframe.style.zIndex = 99999; 52 | iframe.height = 40; 53 | iframe.width = 40; 54 | iframe.src = `${window.debugKitBaseUrl}debug-kit/toolbar/${window.debugKitId}`; 55 | 56 | body.appendChild(iframe); 57 | bodyOverflow = body.style.overflow; 58 | 59 | window.addEventListener('message', onMessage, false); 60 | }; 61 | 62 | const logAjaxRequest = (original) => function ajaxRequest() { 63 | if (this.readyState === 4 && this.getResponseHeader('X-DEBUGKIT-ID')) { 64 | const params = { 65 | requestId: this.getResponseHeader('X-DEBUGKIT-ID'), 66 | status: this.status, 67 | date: new Date(), 68 | method: this._arguments && this._arguments[0], 69 | url: this._arguments && this._arguments[1], 70 | type: this.getResponseHeader('Content-Type'), 71 | }; 72 | iframe.contentWindow.postMessage(`ajax-completed$$${JSON.stringify(params)}`, window.location.origin); 73 | } 74 | if (original) { 75 | return original.apply(this, [].slice.call(arguments)); 76 | } 77 | return false; 78 | }; 79 | 80 | const proxyAjaxOpen = () => { 81 | const proxied = window.XMLHttpRequest.prototype.open; 82 | window.XMLHttpRequest.prototype.open = function ajaxCall(...args) { 83 | this._arguments = args; 84 | return proxied.apply(this, [].slice.call(args)); 85 | }; 86 | }; 87 | 88 | const proxyAjaxSend = () => { 89 | const proxied = window.XMLHttpRequest.prototype.send; 90 | window.XMLHttpRequest.prototype.send = function ajaxCall(...args) { 91 | this.onreadystatechange = logAjaxRequest(this.onreadystatechange); 92 | return proxied.apply(this, [].slice.call(args)); 93 | }; 94 | }; 95 | 96 | // Bind on ready callbacks to DOMContentLoaded (native js) 97 | // Since the body is already loaded (DOMContentLoaded), the event is not triggered. 98 | if (doc.addEventListener) { 99 | // This ensures that all event listeners get applied only once. 100 | if (!win.debugKitListenersApplied) { 101 | // Add support for turbo DOMContentLoaded alternative 102 | // see https://turbo.hotwired.dev/reference/events#turbo%3Aload 103 | const loadedEvent = typeof Turbo !== 'undefined' && Turbo !== null ? 'turbo:load' : 'DOMContentLoaded'; 104 | doc.addEventListener(loadedEvent, onReady, false); 105 | doc.addEventListener(loadedEvent, proxyAjaxOpen, false); 106 | doc.addEventListener(loadedEvent, proxyAjaxSend, false); 107 | win.debugKitListenersApplied = true; 108 | } 109 | } else { 110 | throw new Error('Unable to add event listener for DebugKit. Please use a browser' 111 | + ' that supports addEventListener().'); 112 | } 113 | })(window, document); 114 | -------------------------------------------------------------------------------- /webroot/js/main.js: -------------------------------------------------------------------------------- 1 | import Start from './modules/Start.js'; 2 | import Keyboard from './modules/Keyboard.js'; 3 | import CachePanel from './modules/Panels/CachePanel.js'; 4 | import HistoryPanel from './modules/Panels/HistoryPanel.js'; 5 | import RoutesPanel from './modules/Panels/RoutesPanel.js'; 6 | import VariablesPanel from './modules/Panels/VariablesPanel.js'; 7 | import PackagesPanel from './modules/Panels/PackagesPanel.js'; 8 | import MailPanel from './modules/Panels/MailPanel.js'; 9 | 10 | document.addEventListener('DOMContentLoaded', () => { 11 | const toolbar = Start.init(); 12 | toolbar.initialize(); 13 | Keyboard.init(toolbar); 14 | 15 | // Init Panels 16 | CachePanel.onEvent(); 17 | RoutesPanel.onEvent(); 18 | PackagesPanel.onEvent(); 19 | MailPanel.onEvent(); 20 | 21 | // Init Panels with a reference to the toolbar 22 | HistoryPanel.onEvent(toolbar); 23 | VariablesPanel.onEvent(toolbar); 24 | }); 25 | -------------------------------------------------------------------------------- /webroot/js/modules/Helper.js: -------------------------------------------------------------------------------- 1 | export default (() => { 2 | const isLocalStorageAvailable = () => { 3 | if (!window.localStorage) { 4 | return false; 5 | } 6 | try { 7 | window.localStorage.setItem('testKey', '1'); 8 | window.localStorage.removeItem('testKey'); 9 | return true; 10 | } catch (error) { 11 | return false; 12 | } 13 | }; 14 | 15 | return { 16 | isLocalStorageAvailable, 17 | }; 18 | })(); 19 | -------------------------------------------------------------------------------- /webroot/js/modules/Keyboard.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | const init = (toolbar) => { 3 | $(document).on('keydown', (event) => { 4 | if (event.key === 'Escape') { 5 | // Close active panel 6 | if (toolbar.isExpanded()) { 7 | return toolbar.hide(); 8 | } 9 | // Collapse the toolbar 10 | if (toolbar.state() === 'toolbar') { 11 | return toolbar.toggle(); 12 | } 13 | } 14 | if (event.key === 'ArrowLeft' && toolbar.isExpanded()) { 15 | toolbar.$panelButtons.removeClass('is-active'); 16 | const prevPanel = toolbar.currentPanelButton().prev(); 17 | if (prevPanel.hasClass('c-panel')) { 18 | prevPanel.addClass('is-active'); 19 | const id = prevPanel.attr('data-id'); 20 | const panelType = prevPanel.attr('data-panel-type'); 21 | return toolbar.loadPanel(id, panelType); 22 | } 23 | } 24 | if (event.key === 'ArrowRight' && toolbar.isExpanded()) { 25 | toolbar.$panelButtons.removeClass('is-active'); 26 | const nextPanel = toolbar.currentPanelButton().next(); 27 | if (nextPanel.hasClass('c-panel')) { 28 | nextPanel.addClass('is-active'); 29 | const id = nextPanel.attr('data-id'); 30 | const panelType = nextPanel.attr('data-panel-type'); 31 | return toolbar.loadPanel(id, panelType); 32 | } 33 | } 34 | return; 35 | }); 36 | }; 37 | 38 | return { 39 | init, 40 | }; 41 | })(jQuery); 42 | -------------------------------------------------------------------------------- /webroot/js/modules/Panels/CachePanel.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | const addMessage = (text) => { 3 | $(`

${text}

`) 4 | .appendTo('.c-cache-panel__messages') 5 | .fadeOut(2000); 6 | }; 7 | 8 | const init = () => { 9 | $('.js-clear-cache').on('click', function triggerCacheClear(e) { 10 | const el = $(this); 11 | const name = el.attr('data-name'); 12 | const baseUrl = el.attr('data-url'); 13 | const csrf = el.attr('data-csrf'); 14 | 15 | $.ajax({ 16 | headers: { 'X-CSRF-TOKEN': csrf }, 17 | url: baseUrl, 18 | data: { name }, 19 | dataType: 'json', 20 | type: 'POST', 21 | success(data) { 22 | addMessage(data.message); 23 | }, 24 | error(jqXHR, textStatus, errorThrown) { 25 | addMessage(errorThrown); 26 | }, 27 | }); 28 | e.preventDefault(); 29 | }); 30 | }; 31 | 32 | const onEvent = () => { 33 | document.addEventListener('initPanel', (e) => { 34 | if (e.detail === 'panelcache') { 35 | init(); 36 | } 37 | }); 38 | }; 39 | 40 | return { 41 | onEvent, 42 | }; 43 | })(jQuery); 44 | -------------------------------------------------------------------------------- /webroot/js/modules/Panels/HistoryPanel.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | const init = (toolbar) => { 3 | const $historyPanel = $('.c-history-panel'); 4 | const thisPanel = $historyPanel.attr('data-panel-id'); 5 | 6 | if (!$('.c-history-panel > ul').length) { 7 | $historyPanel.html($('#list-template').html()); 8 | } 9 | 10 | const listItem = $('#list-item-template').html(); 11 | 12 | for (let i = 0; i < toolbar.ajaxRequests.length; i++) { 13 | const element = toolbar.ajaxRequests[i]; 14 | const params = { 15 | id: element.requestId, 16 | time: (new Date(element.date)).toLocaleString(), 17 | method: element.method, 18 | status: element.status, 19 | url: element.url, 20 | type: element.type, 21 | }; 22 | const content = listItem.replace(/{([^{}]*)}/g, (a, b) => { 23 | const r = params[b]; 24 | return typeof r === 'string' || typeof r === 'number' ? r : a; 25 | }); 26 | $('.c-history-panel__list li:first').after(content); 27 | } 28 | 29 | const links = $('.c-history-panel__link'); 30 | // Highlight the active request. 31 | links.filter(`[data-request=${toolbar.currentRequest}]`).addClass('is-active'); 32 | 33 | links.on('click', function historyLinkClick(e) { 34 | const el = $(this); 35 | e.preventDefault(); 36 | links.removeClass('is-active'); 37 | el.addClass('is-active'); 38 | 39 | toolbar.currentRequest = el.attr('data-request'); 40 | 41 | $.getJSON(el.attr('href'), (response) => { 42 | if (response.panels[0].request_id === toolbar.originalRequest) { 43 | $('body').removeClass('is-history-mode'); 44 | } else { 45 | $('body').addClass('is-history-mode'); 46 | } 47 | 48 | for (let i = 0, len = response.panels.length; i < len; i++) { 49 | const panel = response.panels[i]; 50 | const button = toolbar.$panelButtons.eq(i); 51 | const summary = button.find('.c-panel__summary'); 52 | 53 | // Don't overwrite the history panel. 54 | if (button.data('id') !== thisPanel) { 55 | button.attr('data-id', panel.id); 56 | summary.text(panel.summary); 57 | } 58 | } 59 | }); 60 | }); 61 | }; 62 | 63 | const onEvent = (toolbar) => { 64 | document.addEventListener('initPanel', (e) => { 65 | if (e.detail === 'panelhistory') { 66 | init(toolbar); 67 | } 68 | }); 69 | }; 70 | 71 | return { 72 | onEvent, 73 | }; 74 | })(jQuery); 75 | -------------------------------------------------------------------------------- /webroot/js/modules/Panels/MailPanel.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | function init() { 3 | $('.js-debugkit-load-sent-email').on('click', function loadSentEmail() { 4 | const $elem = $(this); 5 | const idx = $elem.attr('data-mail-idx'); 6 | const iframe = $('.c-mail-panel__iframe'); 7 | const current = iframe[0].contentWindow.location.href; 8 | const newLocation = current.replace(/\/\d+$/, `/${idx}`); 9 | iframe[0].contentWindow.location.href = newLocation; 10 | 11 | $elem.siblings().removeClass('highlighted'); 12 | $elem.addClass('highlighted'); 13 | }); 14 | } 15 | 16 | const onEvent = () => { 17 | document.addEventListener('initPanel', (e) => { 18 | if (e.detail === 'panelmail') { 19 | init(); 20 | } 21 | }); 22 | }; 23 | 24 | return { 25 | onEvent, 26 | }; 27 | })(jQuery); 28 | -------------------------------------------------------------------------------- /webroot/js/modules/Panels/PackagesPanel.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | const buildSuccessfulMessage = (response) => { 3 | let html = ''; 4 | if (response.packages.bcBreaks === undefined && response.packages.semverCompatible === undefined) { 5 | return '
All dependencies are up to date
'; 6 | } 7 | if (response.packages.bcBreaks !== undefined) { 8 | html += '

Update with potential BC break

'; 9 | html += `
${response.packages.bcBreaks}
`; 10 | } 11 | if (response.packages.semverCompatible !== undefined) { 12 | html += '

Update semver compatible

'; 13 | html += `
${response.packages.semverCompatible}
`; 14 | } 15 | return html; 16 | }; 17 | 18 | const showMessage = (el, html) => { 19 | el.show().html(html); 20 | $('.o-loader').removeClass('is-loading'); 21 | }; 22 | 23 | const buildErrorMessage = (response) => `
${JSON.parse(response.responseText).message}
`; 24 | 25 | const init = () => { 26 | const $panel = $('.c-packages-panel'); 27 | const baseUrl = $panel.attr('data-base-url'); 28 | const csrfToken = $panel.attr('data-csrf-token'); 29 | const $terminal = $('.c-packages-panel__terminal'); 30 | 31 | $('.c-packages-panel__check-update button').on('click', (e) => { 32 | $('.o-loader').addClass('is-loading'); 33 | 34 | const direct = $('.c-packages-panel__check-update input')[0].checked; 35 | $.ajax({ 36 | headers: { 'X-CSRF-TOKEN': csrfToken }, 37 | url: baseUrl, 38 | data: { direct }, 39 | dataType: 'json', 40 | type: 'POST', 41 | success(data) { 42 | showMessage($terminal, buildSuccessfulMessage(data)); 43 | }, 44 | error(jqXHR, textStatus) { 45 | showMessage($terminal, buildErrorMessage(textStatus)); 46 | }, 47 | }); 48 | e.preventDefault(); 49 | }); 50 | }; 51 | 52 | const onEvent = () => { 53 | document.addEventListener('initPanel', (e) => { 54 | if (e.detail === 'panelpackages') { 55 | init(); 56 | } 57 | }); 58 | }; 59 | 60 | return { 61 | onEvent, 62 | }; 63 | })(jQuery); 64 | -------------------------------------------------------------------------------- /webroot/js/modules/Panels/RoutesPanel.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | const init = () => { 3 | $('.js-toggle-plugin-route').on('click', function togglePluginRoute() { 4 | const $this = $(this); 5 | const plugin = $this.attr('data-plugin'); 6 | 7 | if ($this.hasClass('is-active')) { 8 | $this.removeClass('is-active'); 9 | $(`.c-routes-panel__route-entry${plugin}`).removeClass('is-hidden'); 10 | } else { 11 | $this.addClass('is-active'); 12 | $(`.c-routes-panel__route-entry${plugin}`).addClass('is-hidden'); 13 | } 14 | }); 15 | }; 16 | 17 | const onEvent = () => { 18 | document.addEventListener('initPanel', (e) => { 19 | if (e.detail === 'panelroutes') { 20 | init(); 21 | } 22 | }); 23 | }; 24 | 25 | return { 26 | onEvent, 27 | }; 28 | })(jQuery); 29 | -------------------------------------------------------------------------------- /webroot/js/modules/Panels/VariablesPanel.js: -------------------------------------------------------------------------------- 1 | export default (($) => { 2 | function init(toolbar) { 3 | $('.js-debugkit-sort-variables').on('change', function sortVariables() { 4 | if (!$(this).prop('checked')) { 5 | document.cookie = `debugKit_sort=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=${window.debugKitWebroot}`; 6 | } else { 7 | document.cookie = `debugKit_sort=1; path=${window.debugKitWebroot}`; 8 | } 9 | toolbar.loadPanel(toolbar.currentPanel(), 'variables'); 10 | }); 11 | } 12 | 13 | const onEvent = (toolbar) => { 14 | document.addEventListener('initPanel', (e) => { 15 | if (e.detail === 'panelvariables') { 16 | init(toolbar); 17 | } 18 | }); 19 | }; 20 | 21 | return { 22 | onEvent, 23 | }; 24 | })(jQuery); 25 | -------------------------------------------------------------------------------- /webroot/js/modules/Start.js: -------------------------------------------------------------------------------- 1 | import Toolbar from './Toolbar.js'; 2 | import Helper from './Helper.js'; 3 | 4 | export default (($) => { 5 | const init = () => { 6 | const elem = document.getElementById('__debug_kit_app'); 7 | if (elem) { 8 | window.debugKitId = elem.getAttribute('data-id'); 9 | window.debugKitBaseUrl = elem.getAttribute('data-url'); 10 | window.debugKitWebroot = elem.getAttribute('data-webroot'); 11 | } 12 | 13 | return new Toolbar({ 14 | body: $('body'), 15 | container: $('.js-panel-content-container'), 16 | toggleBtn: $('.js-toolbar-toggle'), 17 | panelButtons: $('.js-panel-button'), 18 | currentRequest: window.debugKitId, 19 | originalRequest: window.debugKitId, 20 | baseUrl: window.debugKitBaseUrl, 21 | isLocalStorageAvailable: Helper.isLocalStorageAvailable(), 22 | }); 23 | }; 24 | 25 | return { 26 | init, 27 | }; 28 | })(jQuery); 29 | --------------------------------------------------------------------------------