diff --git a/.changeset/forty-fans-behave.md b/.changeset/forty-fans-behave.md new file mode 100644 index 00000000..b3df060d --- /dev/null +++ b/.changeset/forty-fans-behave.md @@ -0,0 +1,5 @@ +--- +"@wpengine/wpgraphql-logging-wordpress-plugin": patch +--- + +chore: Added admin view, filters and CSV downloads. diff --git a/plugins/wpgraphql-logging/composer.json b/plugins/wpgraphql-logging/composer.json index 11e22b28..c3e1970a 100644 --- a/plugins/wpgraphql-logging/composer.json +++ b/plugins/wpgraphql-logging/composer.json @@ -26,7 +26,8 @@ "monolog" ], "require": { - "php": "^8.1", + "php": ">=8.1.2", + "league/csv": "^9.9", "monolog/monolog": "^3.9" }, "minimum-stability": "dev", @@ -63,7 +64,7 @@ }, "optimize-autoloader": true, "platform": { - "php": "8.1" + "php": "8.1.2" }, "preferred-install": "dist", "sort-packages": true @@ -143,8 +144,8 @@ "phpstan": [ "vendor/bin/phpstan analyze --ansi --memory-limit=1G" ], - "php:psalm": "psalm --output-format=text --no-progress", - "php:psalm:fix": "psalm --alter --output-format=text --no-progress", + "php:psalm": "psalm --no-progress", + "php:psalm:fix": "psalm --alter --no-progress", "qa": "sh bin/local/run-qa.sh", "test": [ "sh bin/local/run-unit-tests.sh coverage", diff --git a/plugins/wpgraphql-logging/composer.lock b/plugins/wpgraphql-logging/composer.lock index d943791d..72fc3786 100644 --- a/plugins/wpgraphql-logging/composer.lock +++ b/plugins/wpgraphql-logging/composer.lock @@ -4,8 +4,99 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "964e2ef59c5c3fb7f03c0439c6c94ae3", + "content-hash": "39c90e7e58d79f6b447c2e916f498bdc", "packages": [ + { + "name": "league/csv", + "version": "9.24.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/csv.git", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1.2" + }, + "require-dev": { + "ext-dom": "*", + "ext-xdebug": "*", + "friendsofphp/php-cs-fixer": "^3.75.0", + "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^1.12.27", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.2", + "phpunit/phpunit": "^10.5.16 || ^11.5.22", + "symfony/var-dumper": "^6.4.8 || ^7.3.0" + }, + "suggest": { + "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", + "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", + "ext-mysqli": "Requiered to use the package with the MySQLi extension", + "ext-pdo": "Required to use the package with the PDO extension", + "ext-pgsql": "Requiered to use the package with the PgSQL extension", + "ext-sqlite3": "Required to use the package with the SQLite3 extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "League\\Csv\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://github.com/nyamsprod/", + "role": "Developer" + } + ], + "description": "CSV data manipulation made easy in PHP", + "homepage": "https://csv.thephpleague.com", + "keywords": [ + "convert", + "csv", + "export", + "filter", + "import", + "read", + "transform", + "write" + ], + "support": { + "docs": "https://csv.thephpleague.com", + "issues": "https://github.com/thephpleague/csv/issues", + "rss": "https://github.com/thephpleague/csv/releases.atom", + "source": "https://github.com/thephpleague/csv" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2025-06-25T14:53:51+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -163,16 +254,16 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.6.4", + "version": "v2.6.5", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" + "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "url": "https://api.github.com/repos/amphp/amp/zipball/d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", + "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207", "shasum": "" }, "require": { @@ -188,11 +279,6 @@ "vimeo/psalm": "^3.12" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "files": [ "lib/functions.php", @@ -240,7 +326,7 @@ "support": { "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.4" + "source": "https://github.com/amphp/amp/tree/v2.6.5" }, "funding": [ { @@ -248,7 +334,7 @@ "type": "github" } ], - "time": "2024-03-21T18:52:26+00:00" + "time": "2025-09-03T19:41:28+00:00" }, { "name": "amphp/byte-stream", @@ -1052,16 +1138,16 @@ }, { "name": "codeception/module-phpbrowser", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/Codeception/module-phpbrowser.git", - "reference": "a972411f60cd00d00d5e5e3b35496ba4a23bcffc" + "reference": "460e392c77370f7836012b16e06071eb1607876a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/a972411f60cd00d00d5e5e3b35496ba4a23bcffc", - "reference": "a972411f60cd00d00d5e5e3b35496ba4a23bcffc", + "url": "https://api.github.com/repos/Codeception/module-phpbrowser/zipball/460e392c77370f7836012b16e06071eb1607876a", + "reference": "460e392c77370f7836012b16e06071eb1607876a", "shasum": "" }, "require": { @@ -1069,8 +1155,8 @@ "codeception/lib-innerbrowser": "*@dev", "ext-json": "*", "guzzlehttp/guzzle": "^7.4", - "php": "^8.0", - "symfony/browser-kit": "^5.4 || ^6.0 || ^7.0" + "php": "^8.1", + "symfony/browser-kit": "^5.4 | ^6.0 | ^7.0" }, "conflict": { "codeception/codeception": "<5.0", @@ -1078,8 +1164,10 @@ }, "require-dev": { "aws/aws-sdk-php": "^3.199", - "codeception/module-rest": "^2.0 || *@dev", - "ext-curl": "*" + "codeception/module-rest": "^2.0 | *@dev", + "ext-curl": "*", + "phpstan/phpstan": "^1.10", + "squizlabs/php_codesniffer": "^3.10" }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" @@ -1111,9 +1199,9 @@ ], "support": { "issues": "https://github.com/Codeception/module-phpbrowser/issues", - "source": "https://github.com/Codeception/module-phpbrowser/tree/3.0.1" + "source": "https://github.com/Codeception/module-phpbrowser/tree/3.0.2" }, - "time": "2023-12-08T19:41:28+00:00" + "time": "2025-09-04T10:45:58+00:00" }, { "name": "codeception/module-rest", @@ -1238,20 +1326,20 @@ }, { "name": "codeception/stub", - "version": "4.1.4", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "6ce453073a0c220b254dd7f4383645615e4071c3" + "reference": "19014cec368cefc0579499779c451551cd288557" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/6ce453073a0c220b254dd7f4383645615e4071c3", - "reference": "6ce453073a0c220b254dd7f4383645615e4071c3", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/19014cec368cefc0579499779c451551cd288557", + "reference": "19014cec368cefc0579499779c451551cd288557", "shasum": "" }, "require": { - "php": "^7.4 | ^8.0", + "php": "^8.1", "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11 | ^12" }, "conflict": { @@ -1273,9 +1361,9 @@ "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", "support": { "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/4.1.4" + "source": "https://github.com/Codeception/Stub/tree/4.2.0" }, - "time": "2025-02-14T06:56:33+00:00" + "time": "2025-08-01T08:15:29+00:00" }, { "name": "codeception/util-universalframework", @@ -1316,16 +1404,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.5.7", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "d665d22c417056996c59019579f1967dfe5c1e82" + "reference": "719026bb30813accb68271fee7e39552a58e9f65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", - "reference": "d665d22c417056996c59019579f1967dfe5c1e82", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/719026bb30813accb68271fee7e39552a58e9f65", + "reference": "719026bb30813accb68271fee7e39552a58e9f65", "shasum": "" }, "require": { @@ -1372,7 +1460,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.7" + "source": "https://github.com/composer/ca-bundle/tree/1.5.8" }, "funding": [ { @@ -1382,26 +1470,22 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-05-26T15:08:54+00:00" + "time": "2025-08-20T18:49:47+00:00" }, { "name": "composer/class-map-generator", - "version": "1.6.1", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34" + "reference": "ba9f089655d4cdd64e762a6044f411ccdaec0076" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/134b705ddb0025d397d8318a75825fe3c9d1da34", - "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/ba9f089655d4cdd64e762a6044f411ccdaec0076", + "reference": "ba9f089655d4cdd64e762a6044f411ccdaec0076", "shasum": "" }, "require": { @@ -1445,7 +1529,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.6.1" + "source": "https://github.com/composer/class-map-generator/tree/1.6.2" }, "funding": [ { @@ -1455,26 +1539,22 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-03-24T13:50:44+00:00" + "time": "2025-08-20T18:52:43+00:00" }, { "name": "composer/composer", - "version": "2.8.10", + "version": "2.8.11", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "53834f587d7ab2527eb237459d7b94d1fb9d4c5a" + "reference": "00e1a3396eea67033775c4a49c772376f45acd73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/53834f587d7ab2527eb237459d7b94d1fb9d4c5a", - "reference": "53834f587d7ab2527eb237459d7b94d1fb9d4c5a", + "url": "https://api.github.com/repos/composer/composer/zipball/00e1a3396eea67033775c4a49c772376f45acd73", + "reference": "00e1a3396eea67033775c4a49c772376f45acd73", "shasum": "" }, "require": { @@ -1488,7 +1568,7 @@ "justinrainbow/json-schema": "^6.3.1", "php": "^7.2.5 || ^8.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "react/promise": "^2.11 || ^3.2", + "react/promise": "^2.11 || ^3.3", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.2", "seld/signal-handler": "^2.0", @@ -1559,7 +1639,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.8.10" + "source": "https://github.com/composer/composer/tree/2.8.11" }, "funding": [ { @@ -1569,13 +1649,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2025-07-10T17:08:33+00:00" + "time": "2025-08-21T09:29:39+00:00" }, { "name": "composer/metadata-minifier", @@ -1727,16 +1803,16 @@ }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -1788,7 +1864,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -1798,13 +1874,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/spdx-licenses", @@ -2358,16 +2430,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -2377,10 +2449,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -2407,7 +2479,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -2415,7 +2487,7 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "gettext/gettext", @@ -2637,22 +2709,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -2743,7 +2815,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -2759,20 +2831,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -2780,7 +2852,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -2826,7 +2898,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -2842,20 +2914,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -2871,7 +2943,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -2942,7 +3014,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -2958,7 +3030,7 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -3167,16 +3239,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.4.2", + "version": "6.5.1", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" + "reference": "b5ab21e431594897e5bb86343c01f140ba862c26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", - "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/b5ab21e431594897e5bb86343c01f140ba862c26", + "reference": "b5ab21e431594897e5bb86343c01f140ba862c26", "shasum": "" }, "require": { @@ -3186,7 +3258,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "1.2.0", + "json-schema/json-schema-test-suite": "^23.2", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -3236,22 +3308,22 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.1" }, - "time": "2025-06-03T18:27:04+00:00" + "time": "2025-08-29T10:58:11+00:00" }, { "name": "lucatume/wp-browser", - "version": "4.5.4", + "version": "4.5.5", "source": { "type": "git", "url": "https://github.com/lucatume/wp-browser.git", - "reference": "82e713954afcea9b1515ba0c472fe5fab1f8bb91" + "reference": "5cf7588f6c23166e3ee53e2d21257bf25338323f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lucatume/wp-browser/zipball/82e713954afcea9b1515ba0c472fe5fab1f8bb91", - "reference": "82e713954afcea9b1515ba0c472fe5fab1f8bb91", + "url": "https://api.github.com/repos/lucatume/wp-browser/zipball/5cf7588f6c23166e3ee53e2d21257bf25338323f", + "reference": "5cf7588f6c23166e3ee53e2d21257bf25338323f", "shasum": "" }, "require": { @@ -3330,7 +3402,7 @@ ], "support": { "issues": "https://github.com/lucatume/wp-browser/issues", - "source": "https://github.com/lucatume/wp-browser/tree/4.5.4" + "source": "https://github.com/lucatume/wp-browser/tree/4.5.5" }, "funding": [ { @@ -3338,7 +3410,7 @@ "type": "github" } ], - "time": "2025-06-17T21:01:53+00:00" + "time": "2025-08-15T15:15:45+00:00" }, { "name": "marc-mabe/php-enum", @@ -3415,16 +3487,16 @@ }, { "name": "masterminds/html5", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + "reference": "fcf91eb64359852f00d921887b219479b4f21251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", "shasum": "" }, "require": { @@ -3476,9 +3548,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" }, - "time": "2024-03-31T07:05:07+00:00" + "time": "2025-07-25T09:04:22+00:00" }, { "name": "mck89/peast", @@ -3612,18 +3684,71 @@ }, "time": "2024-05-16T03:13:13+00:00" }, + { + "name": "mustache/mustache", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4", + "reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.19.3", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mustache\\": "src/" + }, + "classmap": [ + "src/compat.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "support": { + "issues": "https://github.com/bobthecow/mustache.php/issues", + "source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0" + }, + "time": "2025-06-28T18:28:20+00:00" + }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -3662,7 +3787,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -3670,7 +3795,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nb/oxymel", @@ -4208,18 +4333,18 @@ "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "5e207bcca923a6afd5a8084427cb43a37bf306d3" + "reference": "ec975efc6752a2a41c2d0823e9c5e464c3c2dbb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/5e207bcca923a6afd5a8084427cb43a37bf306d3", - "reference": "5e207bcca923a6afd5a8084427cb43a37bf306d3", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/ec975efc6752a2a41c2d0823e9c5e464c3c2dbb6", + "reference": "ec975efc6752a2a41c2d0823e9c5e464c3c2dbb6", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.1.0", - "squizlabs/php_codesniffer": "^3.13.0" + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.3" }, "replace": { "wimg/php-compatibility": "*" @@ -4227,10 +4352,10 @@ "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.1.3", - "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.1.0", - "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + "phpcsstandards/phpcsdevcs": "^1.1.6", + "phpcsstandards/phpcsdevtools": "^1.2.3", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.5.32 || ^11.3.3", + "yoast/phpunit-polyfills": "^1.1.5 || ^2.0.5 || ^3.1.0" }, "suggest": { "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." @@ -4294,7 +4419,7 @@ "type": "thanks_dev" } ], - "time": "2025-07-06T15:59:37+00:00" + "time": "2025-09-05T15:02:16+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", @@ -4445,22 +4570,22 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" + "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", - "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/882b8c947ada27eb002870fe77fee9ce0a454cdb", + "reference": "882b8c947ada27eb002870fe77fee9ce0a454cdb", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.1.0", - "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", @@ -4523,26 +4648,26 @@ "type": "thanks_dev" } ], - "time": "2025-06-14T07:40:39+00:00" + "time": "2025-09-05T06:54:52+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.1.0", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad" + "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad", - "reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", + "reference": "b22b59e3d9ec8fe4953e42c7d59117c6eae70eae", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" }, "require-dev": { "ext-filter": "*", @@ -4616,7 +4741,7 @@ "type": "thanks_dev" } ], - "time": "2025-06-12T04:32:33+00:00" + "time": "2025-09-05T00:00:03+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4673,16 +4798,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.2", + "version": "5.6.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", - "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", "shasum": "" }, "require": { @@ -4731,9 +4856,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" }, - "time": "2025-04-13T19:20:35+00:00" + "time": "2025-08-01T19:43:32+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4795,16 +4920,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "shasum": "" }, "require": { @@ -4812,7 +4937,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -4854,7 +4979,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" }, "funding": [ { @@ -4866,20 +4991,20 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-08-21T11:53:16+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", "shasum": "" }, "require": { @@ -4911,22 +5036,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "time": "2025-07-13T07:04:09+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.19", + "version": "2.1.22", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "473a8c30e450d87099f76313edcbb90852f9afdf" + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/473a8c30e450d87099f76313edcbb90852f9afdf", - "reference": "473a8c30e450d87099f76313edcbb90852f9afdf", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4", + "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4", "shasum": "" }, "require": { @@ -4971,7 +5096,7 @@ "type": "github" } ], - "time": "2025-07-21T19:58:24+00:00" + "time": "2025-08-04T19:17:37+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -5344,16 +5469,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.48", + "version": "10.5.53", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541" + "reference": "32768472ebfb6969e6c7399f1c7b09009723f653" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e0a2bc39f6fae7617989d690d76c48e6d2eb541", - "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653", + "reference": "32768472ebfb6969e6c7399f1c7b09009723f653", "shasum": "" }, "require": { @@ -5363,7 +5488,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.3", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -5380,7 +5505,7 @@ "sebastian/exporter": "^5.1.2", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", - "sebastian/recursion-context": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", "sebastian/type": "^4.0.0", "sebastian/version": "^4.0.1" }, @@ -5425,7 +5550,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.48" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53" }, "funding": [ { @@ -5449,7 +5574,7 @@ "type": "tidelift" } ], - "time": "2025-07-11T04:07:17+00:00" + "time": "2025-08-20T14:40:06+00:00" }, { "name": "psr/container", @@ -5716,16 +5841,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.9", + "version": "v0.12.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "1b801844becfe648985372cb4b12ad6840245ace" + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", - "reference": "1b801844becfe648985372cb4b12ad6840245ace", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", "shasum": "" }, "require": { @@ -5775,12 +5900,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -5789,9 +5913,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" }, - "time": "2025-06-23T02:35:06+00:00" + "time": "2025-08-04T12:39:37+00:00" }, { "name": "ralouphie/getallheaders", @@ -5839,23 +5963,23 @@ }, { "name": "react/promise", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpstan/phpstan": "1.12.28 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", @@ -5900,7 +6024,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { @@ -5908,7 +6032,7 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "sebastian/cli-parser", @@ -6080,16 +6204,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.3", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { @@ -6145,15 +6269,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", @@ -6656,23 +6792,23 @@ }, { "name": "sebastian/recursion-context", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", "shasum": "" }, "require": { "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -6707,15 +6843,28 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2025-08-10T07:50:56+00:00" }, { "name": "sebastian/type", @@ -7058,32 +7207,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.19.1", + "version": "8.22.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" + "reference": "a4cef983bad2e70125612d22b2f6e2bd1333d5c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/a4cef983bad2e70125612d22b2f6e2bd1333d5c2", + "reference": "a4cef983bad2e70125612d22b2f6e2bd1333d5c2", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.1.2", "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.13.0" + "phpstan/phpdoc-parser": "^2.3.0", + "squizlabs/php_codesniffer": "^3.13.4" }, "require-dev": { - "phing/phing": "3.0.1", + "phing/phing": "3.0.1|3.1.0", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.17", + "phpstan/phpstan": "2.1.22", "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.6", - "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "2.0.6", + "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.3.8" }, "type": "phpcodesniffer-standard", "extra": { @@ -7107,7 +7256,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.22.0" }, "funding": [ { @@ -7119,7 +7268,7 @@ "type": "tidelift" } ], - "time": "2025-06-09T17:53:57+00:00" + "time": "2025-09-06T09:14:48+00:00" }, { "name": "softcreatr/jsonpath", @@ -7260,16 +7409,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "version": "3.13.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", "shasum": "" }, "require": { @@ -7340,20 +7489,20 @@ "type": "thanks_dev" } ], - "time": "2025-06-17T22:17:01+00:00" + "time": "2025-09-05T05:47:09+00:00" }, { "name": "symfony/browser-kit", - "version": "v6.4.19", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "ce95f3e3239159e7fa3be7690c6ce95a4714637f" + "reference": "3537d17782f8c20795b194acb6859071b60c6fac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/ce95f3e3239159e7fa3be7690c6ce95a4714637f", - "reference": "ce95f3e3239159e7fa3be7690c6ce95a4714637f", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3537d17782f8c20795b194acb6859071b60c6fac", + "reference": "3537d17782f8c20795b194acb6859071b60c6fac", "shasum": "" }, "require": { @@ -7392,7 +7541,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v6.4.19" + "source": "https://github.com/symfony/browser-kit/tree/v6.4.24" }, "funding": [ { @@ -7403,25 +7552,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-14T11:23:16+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/console", - "version": "v6.4.23", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93" + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93", - "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93", + "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", "shasum": "" }, "require": { @@ -7486,7 +7639,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.23" + "source": "https://github.com/symfony/console/tree/v6.4.25" }, "funding": [ { @@ -7497,25 +7650,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:37:22+00:00" + "time": "2025-08-22T10:21:53+00:00" }, { "name": "symfony/css-selector", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e" + "reference": "9b784413143701aa3c94ac1869a159a9e53e8761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/cb23e97813c5837a041b73a6d63a9ddff0778f5e", - "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/9b784413143701aa3c94ac1869a159a9e53e8761", + "reference": "9b784413143701aa3c94ac1869a159a9e53e8761", "shasum": "" }, "require": { @@ -7551,7 +7708,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v6.4.13" + "source": "https://github.com/symfony/css-selector/tree/v6.4.24" }, "funding": [ { @@ -7562,12 +7719,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/deprecation-contracts", @@ -7638,16 +7799,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v6.4.23", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "22210aacb35dbadd772325d759d17bce2374a84d" + "reference": "976302990f9f2a6d4c07206836dd4ca77cae9524" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/22210aacb35dbadd772325d759d17bce2374a84d", - "reference": "22210aacb35dbadd772325d759d17bce2374a84d", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/976302990f9f2a6d4c07206836dd4ca77cae9524", + "reference": "976302990f9f2a6d4c07206836dd4ca77cae9524", "shasum": "" }, "require": { @@ -7685,7 +7846,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.23" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.25" }, "funding": [ { @@ -7696,25 +7857,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-13T12:10:00+00:00" + "time": "2025-08-05T18:56:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "b0cf3162020603587363f0551cd3be43958611ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff", + "reference": "b0cf3162020603587363f0551cd3be43958611ff", "shasum": "" }, "require": { @@ -7765,7 +7930,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25" }, "funding": [ { @@ -7776,12 +7941,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7861,16 +8030,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", "shasum": "" }, "require": { @@ -7907,7 +8076,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.24" }, "funding": [ { @@ -7918,25 +8087,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/finder", - "version": "v6.4.17", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + "reference": "73089124388c8510efb8d2d1689285d285937b08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", "shasum": "" }, "require": { @@ -7971,7 +8144,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.17" + "source": "https://github.com/symfony/finder/tree/v6.4.24" }, "funding": [ { @@ -7982,16 +8155,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-29T13:51:37+00:00" + "time": "2025-07-15T12:02:45+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -8050,7 +8227,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -8061,6 +8238,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8070,16 +8251,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -8128,7 +8309,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -8139,16 +8320,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -8209,7 +8394,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -8220,6 +8405,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8229,7 +8418,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -8290,7 +8479,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -8301,6 +8490,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8310,7 +8503,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -8366,7 +8559,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" }, "funding": [ { @@ -8377,6 +8570,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8386,7 +8583,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -8446,7 +8643,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -8457,6 +8654,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8466,7 +8667,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -8522,7 +8723,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -8533,6 +8734,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8625,16 +8830,16 @@ }, { "name": "symfony/string", - "version": "v6.4.21", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6" + "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73e2c6966a5aef1d4892873ed5322245295370c6", - "reference": "73e2c6966a5aef1d4892873ed5322245295370c6", + "url": "https://api.github.com/repos/symfony/string/zipball/7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", + "reference": "7cdec7edfaf2cdd9c18901e35bcf9653d6209ff1", "shasum": "" }, "require": { @@ -8691,7 +8896,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.21" + "source": "https://github.com/symfony/string/tree/v6.4.25" }, "funding": [ { @@ -8702,25 +8907,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-18T15:23:29+00:00" + "time": "2025-08-22T12:33:20+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.23", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600" + "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600", - "reference": "d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6cd92486e9fc32506370822c57bc02353a5a92c", + "reference": "c6cd92486e9fc32506370822c57bc02353a5a92c", "shasum": "" }, "require": { @@ -8732,7 +8941,6 @@ "symfony/console": "<5.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^5.4|^6.0|^7.0", "symfony/error-handler": "^6.3|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0", @@ -8776,7 +8984,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.23" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.25" }, "funding": [ { @@ -8787,25 +8995,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T15:05:27+00:00" + "time": "2025-08-13T09:41:44+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.23", + "version": "v6.4.25", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "93e29e0deb5f1b2e360adfb389a20d25eb81a27b" + "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/93e29e0deb5f1b2e360adfb389a20d25eb81a27b", - "reference": "93e29e0deb5f1b2e360adfb389a20d25eb81a27b", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e54b060bc9c3dc3d4258bf0d165d0064e755f565", + "reference": "e54b060bc9c3dc3d4258bf0d165d0064e755f565", "shasum": "" }, "require": { @@ -8848,7 +9060,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.23" + "source": "https://github.com/symfony/yaml/tree/v6.4.25" }, "funding": [ { @@ -8859,12 +9071,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-03T06:46:12+00:00" + "time": "2025-08-26T16:59:00+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -9518,16 +9734,16 @@ }, { "name": "wp-cli/core-command", - "version": "v2.1.21", + "version": "v2.1.22", "source": { "type": "git", "url": "https://github.com/wp-cli/core-command.git", - "reference": "f157fb37dae1d13fe7318452f932917161e83e53" + "reference": "ac6f8d742808e11e349ce099c7de2fc3c7009b84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/core-command/zipball/f157fb37dae1d13fe7318452f932917161e83e53", - "reference": "f157fb37dae1d13fe7318452f932917161e83e53", + "url": "https://api.github.com/repos/wp-cli/core-command/zipball/ac6f8d742808e11e349ce099c7de2fc3c7009b84", + "reference": "ac6f8d742808e11e349ce099c7de2fc3c7009b84", "shasum": "" }, "require": { @@ -9583,9 +9799,9 @@ "homepage": "https://github.com/wp-cli/core-command", "support": { "issues": "https://github.com/wp-cli/core-command/issues", - "source": "https://github.com/wp-cli/core-command/tree/v2.1.21" + "source": "https://github.com/wp-cli/core-command/tree/v2.1.22" }, - "time": "2025-07-08T17:31:39+00:00" + "time": "2025-09-04T08:14:53+00:00" }, { "name": "wp-cli/cron-command", @@ -10157,28 +10373,28 @@ }, { "name": "wp-cli/extension-command", - "version": "v2.1.24", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/wp-cli/extension-command.git", - "reference": "d21a2f504ac43a86b6b08697669b5b0844748133" + "reference": "cf68e1f3244a0a9557dd8cf4cc9fb03779b14678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/extension-command/zipball/d21a2f504ac43a86b6b08697669b5b0844748133", - "reference": "d21a2f504ac43a86b6b08697669b5b0844748133", + "url": "https://api.github.com/repos/wp-cli/extension-command/zipball/cf68e1f3244a0a9557dd8cf4cc9fb03779b14678", + "reference": "cf68e1f3244a0a9557dd8cf4cc9fb03779b14678", "shasum": "" }, "require": { "composer/semver": "^1.4 || ^2 || ^3", - "wp-cli/wp-cli": "^2.12" + "wp-cli/wp-cli": "^2.13" }, "require-dev": { "wp-cli/cache-command": "^2.0", "wp-cli/entity-command": "^1.3 || ^2", "wp-cli/language-command": "^2.0", "wp-cli/scaffold-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "^4.3.7" + "wp-cli/wp-cli-tests": "^5" }, "type": "wp-cli-package", "extra": { @@ -10249,9 +10465,9 @@ "homepage": "https://github.com/wp-cli/extension-command", "support": { "issues": "https://github.com/wp-cli/extension-command/issues", - "source": "https://github.com/wp-cli/extension-command/tree/v2.1.24" + "source": "https://github.com/wp-cli/extension-command/tree/v2.2.0" }, - "time": "2025-05-06T19:17:53+00:00" + "time": "2025-09-04T12:33:20+00:00" }, { "name": "wp-cli/i18n-command", @@ -10384,16 +10600,16 @@ }, { "name": "wp-cli/language-command", - "version": "v2.0.24", + "version": "v2.0.25", "source": { "type": "git", "url": "https://github.com/wp-cli/language-command.git", - "reference": "1f1ca0ce3ee6cc46edfe06ee093cf3a57a131a18" + "reference": "ad1bbfbf2699eff415436a00bb4195900fa1cfe5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/language-command/zipball/1f1ca0ce3ee6cc46edfe06ee093cf3a57a131a18", - "reference": "1f1ca0ce3ee6cc46edfe06ee093cf3a57a131a18", + "url": "https://api.github.com/repos/wp-cli/language-command/zipball/ad1bbfbf2699eff415436a00bb4195900fa1cfe5", + "reference": "ad1bbfbf2699eff415436a00bb4195900fa1cfe5", "shasum": "" }, "require": { @@ -10458,9 +10674,9 @@ "homepage": "https://github.com/wp-cli/language-command", "support": { "issues": "https://github.com/wp-cli/language-command/issues", - "source": "https://github.com/wp-cli/language-command/tree/v2.0.24" + "source": "https://github.com/wp-cli/language-command/tree/v2.0.25" }, - "time": "2025-07-08T17:50:14+00:00" + "time": "2025-09-04T10:30:12+00:00" }, { "name": "wp-cli/maintenance-mode-command", @@ -10585,58 +10801,6 @@ }, "time": "2025-04-11T09:28:29+00:00" }, - { - "name": "wp-cli/mustache", - "version": "v2.14.99", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/mustache.php.git", - "reference": "ca23b97ac35fbe01c160549eb634396183d04a59" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/mustache.php/zipball/ca23b97ac35fbe01c160549eb634396183d04a59", - "reference": "ca23b97ac35fbe01c160549eb634396183d04a59", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "replace": { - "mustache/mustache": "^2.14.2" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.19.3", - "yoast/phpunit-polyfills": "^2.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mustache": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "A Mustache implementation in PHP.", - "homepage": "https://github.com/bobthecow/mustache.php", - "keywords": [ - "mustache", - "templating" - ], - "support": { - "source": "https://github.com/wp-cli/mustache.php/tree/v2.14.99" - }, - "time": "2025-05-06T16:15:37+00:00" - }, { "name": "wp-cli/mustangostang-spyc", "version": "0.6.3", @@ -10690,16 +10854,16 @@ }, { "name": "wp-cli/package-command", - "version": "v2.6.0", + "version": "v2.6.1", "source": { "type": "git", "url": "https://github.com/wp-cli/package-command.git", - "reference": "682d8c6bb30c782c3b09c015478c7cbe1cc727a9" + "reference": "17ede348446844c20da199683e96f7a3e70c5559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/package-command/zipball/682d8c6bb30c782c3b09c015478c7cbe1cc727a9", - "reference": "682d8c6bb30c782c3b09c015478c7cbe1cc727a9", + "url": "https://api.github.com/repos/wp-cli/package-command/zipball/17ede348446844c20da199683e96f7a3e70c5559", + "reference": "17ede348446844c20da199683e96f7a3e70c5559", "shasum": "" }, "require": { @@ -10709,7 +10873,7 @@ }, "require-dev": { "wp-cli/scaffold-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "^5" }, "type": "wp-cli-package", "extra": { @@ -10749,9 +10913,9 @@ "homepage": "https://github.com/wp-cli/package-command", "support": { "issues": "https://github.com/wp-cli/package-command/issues", - "source": "https://github.com/wp-cli/package-command/tree/v2.6.0" + "source": "https://github.com/wp-cli/package-command/tree/v2.6.1" }, - "time": "2025-04-11T09:28:45+00:00" + "time": "2025-08-25T13:32:31+00:00" }, { "name": "wp-cli/php-cli-tools", @@ -10995,16 +11159,16 @@ }, { "name": "wp-cli/scaffold-command", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/wp-cli/scaffold-command.git", - "reference": "b4238ea12e768b3f15d10339a53a8642f82e1d2b" + "reference": "cd1e49a393b1af4eee4f5ccc3ac562862c65ccdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/scaffold-command/zipball/b4238ea12e768b3f15d10339a53a8642f82e1d2b", - "reference": "b4238ea12e768b3f15d10339a53a8642f82e1d2b", + "url": "https://api.github.com/repos/wp-cli/scaffold-command/zipball/cd1e49a393b1af4eee4f5ccc3ac562862c65ccdf", + "reference": "cd1e49a393b1af4eee4f5ccc3ac562862c65ccdf", "shasum": "" }, "require": { @@ -11012,7 +11176,7 @@ }, "require-dev": { "wp-cli/extension-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "^5" }, "type": "wp-cli-package", "extra": { @@ -11055,9 +11219,9 @@ "homepage": "https://github.com/wp-cli/scaffold-command", "support": { "issues": "https://github.com/wp-cli/scaffold-command/issues", - "source": "https://github.com/wp-cli/scaffold-command/tree/v2.5.0" + "source": "https://github.com/wp-cli/scaffold-command/tree/v2.5.1" }, - "time": "2025-04-11T09:29:34+00:00" + "time": "2025-09-05T04:13:09+00:00" }, { "name": "wp-cli/search-replace-command", @@ -11364,37 +11528,40 @@ }, { "name": "wp-cli/wp-cli", - "version": "v2.12.0", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "03d30d4138d12b4bffd8b507b82e56e129e0523f" + "reference": "4d0741eb1050b4b939808410a44812127d680fce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/03d30d4138d12b4bffd8b507b82e56e129e0523f", - "reference": "03d30d4138d12b4bffd8b507b82e56e129e0523f", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/4d0741eb1050b4b939808410a44812127d680fce", + "reference": "4d0741eb1050b4b939808410a44812127d680fce", "shasum": "" }, "require": { "ext-curl": "*", - "php": "^5.6 || ^7.0 || ^8.0", + "mustache/mustache": "^3.0.0", + "php": ">=7.2.24 || ^8.0", "symfony/finder": ">2.7", - "wp-cli/mustache": "^2.14.99", "wp-cli/mustangostang-spyc": "^0.6.3", "wp-cli/php-cli-tools": "~0.12.4" }, "require-dev": { + "justinrainbow/json-schema": "^6.3", + "roave/security-advisories": "dev-latest", "wp-cli/db-command": "^1.3 || ^2", "wp-cli/entity-command": "^1.2 || ^2", "wp-cli/extension-command": "^1.1 || ^2", "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^4.3.10" + "wp-cli/wp-cli-tests": "^4" }, "suggest": { "ext-readline": "Include for a better --prompt implementation", "ext-zip": "Needed to support extraction of ZIP archives when doing downloads or updates" }, + "default-branch": true, "bin": [ "bin/wp", "bin/wp.bat" @@ -11402,7 +11569,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.12.x-dev" + "dev-main": "2.13.x-dev" } }, "autoload": { @@ -11429,7 +11596,7 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2025-05-07T01:16:12+00:00" + "time": "2025-09-08T07:33:55+00:00" }, { "name": "wp-cli/wp-cli-bundle", @@ -11556,16 +11723,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/d2421de7cec3274ae622c22c744de9a62c7925af", + "reference": "d2421de7cec3274ae622c22c744de9a62c7925af", "shasum": "" }, "require": { @@ -11574,13 +11741,13 @@ "ext-tokenizer": "*", "ext-xmlreader": "*", "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.10", - "squizlabs/php_codesniffer": "^3.9.0" + "phpcsstandards/phpcsextra": "^1.4.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-parallel-lint": "^1.4.0", "phpcompatibility/php-compatibility": "^9.0", "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" @@ -11618,7 +11785,7 @@ "type": "custom" } ], - "time": "2024-03-25T16:39:00+00:00" + "time": "2025-07-24T20:08:31+00:00" }, { "name": "wp-graphql/wp-graphql", @@ -11870,11 +12037,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.1" + "php": ">=8.1.2" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { - "php": "8.1" + "php": "8.1.2" }, "plugin-api-version": "2.6.0" } diff --git a/plugins/wpgraphql-logging/psalm.xml b/plugins/wpgraphql-logging/psalm.xml index 1bfaf14c..9bf8ed5a 100644 --- a/plugins/wpgraphql-logging/psalm.xml +++ b/plugins/wpgraphql-logging/psalm.xml @@ -36,5 +36,17 @@ + + + + + + + + + + + + diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php index 54e433f8..ac69e1f5 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Fields/Tab/Basic_Configuration_Tab.php @@ -38,13 +38,6 @@ class Basic_Configuration_Tab implements Settings_Tab_Interface { */ public const DATA_SAMPLING = 'data_sampling'; - /** - * The field ID for the WPGraphQL query filtering text input. - * - * @var string - */ - public const WPGRAPHQL_FILTERING = 'wpgraphql_filtering'; - /** * The field ID for the user-based logging select. * @@ -52,13 +45,6 @@ class Basic_Configuration_Tab implements Settings_Tab_Interface { */ public const ADMIN_USER_LOGGING = 'admin_user_logging'; - /** - * The field ID for the performance metrics text input. - * - * @var string - */ - public const PERFORMANCE_METRICS = 'performance_metrics'; - /** * The field ID for the log point selection select. * @@ -115,40 +101,22 @@ public function get_fields(): array { __( 'Log only for admin users.', 'wpgraphql-logging' ) ); - $fields[ self::WPGRAPHQL_FILTERING ] = new Text_Input_Field( - self::WPGRAPHQL_FILTERING, - $this->get_name(), - __( 'WPGraphQL Query Filtering', 'wpgraphql-logging' ), - '', - __( 'Comma-separated list of query names or patterns to log. Leave empty to log all queries.', 'wpgraphql-logging' ), - __( 'e.g., GetPost, GetPosts, introspection', 'wpgraphql-logging' ) - ); - $fields[ self::DATA_SAMPLING ] = new Select_Field( self::DATA_SAMPLING, $this->get_name(), __( 'Data Sampling Rate', 'wpgraphql-logging' ), [ - '100' => __( '100% (All requests)', 'wpgraphql-logging' ), - '50' => __( '50% (Every other request)', 'wpgraphql-logging' ), - '25' => __( '25% (Every 4th request)', 'wpgraphql-logging' ), '10' => __( '10% (Every 10th request)', 'wpgraphql-logging' ), + '25' => __( '25% (Every 4th request)', 'wpgraphql-logging' ), + '50' => __( '50% (Every other request)', 'wpgraphql-logging' ), + '75' => __( '75% (Every 3 out of 4 requests)', 'wpgraphql-logging' ), + '100' => __( '100% (All requests)', 'wpgraphql-logging' ), ], '', __( 'Percentage of requests to log for performance optimization.', 'wpgraphql-logging' ), false ); - $fields[ self::PERFORMANCE_METRICS ] = new Text_Input_Field( - self::PERFORMANCE_METRICS, - $this->get_name(), - __( 'Performance Threshold (seconds)', 'wpgraphql-logging' ), - '', - __( 'Only log requests that take longer than this threshold. 0 logs all requests. Calculated in seconds.', 'wpgraphql-logging' ), - __( 'e.g., 1.5', 'wpgraphql-logging' ) - ); - - $fields[ self::EVENT_LOG_SELECTION ] = new Select_Field( self::EVENT_LOG_SELECTION, $this->get_name(), @@ -162,7 +130,7 @@ public function get_fields(): array { Events::RESPONSE_HEADERS_TO_SEND => __( 'Response Headers', 'wpgraphql-logging' ), ], '', - __( 'Select which points in the request lifecycle to log. By default, all points are logged.', 'wpgraphql-logging' ), + __( 'Select which points in the request lifecycle to log. By default, no events are logged.', 'wpgraphql-logging' ), true ); diff --git a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php index 02388e6b..ef74a8dd 100644 --- a/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php +++ b/plugins/wpgraphql-logging/src/Admin/Settings/Templates/admin.php @@ -69,7 +69,6 @@
  • -
  • + + + diff --git a/plugins/wpgraphql-logging/src/Admin/View/.gitkeep b/plugins/wpgraphql-logging/src/Admin/View/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php b/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php new file mode 100644 index 00000000..b16f8ba5 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/Download/Download_Log_Service.php @@ -0,0 +1,84 @@ +get_log( $log_id ); + if ( is_null( $log ) ) { + wp_die( esc_html__( 'Log not found.', 'wpgraphql-logging' ) ); + } + + // Set headers for CSV download. + $filename = apply_filters( 'wpgraphql_logging_csv_filename', 'graphql_log_' . $log_id . '.csv' ); + header( 'Content-Type: text/csv; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Cache-Control: must-revalidate, post-check=0, pre-check=0' ); + header( 'Expires: 0' ); + + // Create CSV. + $output = fopen( 'php://output', 'w' ); + if ( ! is_resource( $output ) ) { + wp_die( esc_html__( 'Failed to create CSV output.', 'wpgraphql-logging' ) ); + } + $writer = Writer::createFromStream( $output ); + + $headers = [ + 'ID', + 'Date', + 'Level', + 'Level Name', + 'Message', + 'Channel', + 'Query', + 'Context', + 'Extra', + ]; + + $content = [ + $log->get_id(), + $log->get_datetime(), + $log->get_level(), + $log->get_level_name(), + $log->get_message(), + $log->get_channel(), + $log->get_query(), + wp_json_encode( $log->get_context() ), + wp_json_encode( $log->get_extra() ), + ]; + + + $headers = apply_filters( 'wpgraphql_logging_csv_headers', $headers, $log_id, $log ); + $content = apply_filters( 'wpgraphql_logging_csv_content', $content, $log_id, $log ); + $writer->insertOne( $headers ); + $writer->insertOne( $content ); + fclose( $output ); + exit; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php new file mode 100644 index 00000000..664206c4 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/List/List_Table.php @@ -0,0 +1,506 @@ + $args Optional. An array of arguments. + */ + public function __construct( + public readonly LogsRepository $repository, + $args = [] + ) { + $args = wp_parse_args( + $args, + [ + 'singular' => __( 'Log', 'wpgraphql-logging' ), + 'plural' => __( 'Logs', 'wpgraphql-logging' ), + 'ajax' => false, + ] + ); + parent::__construct( $args ); + } + + /** + * Prepare items for display. + * + * @phpcs:disable WordPress.Security.NonceVerification.Recommended + * + * @psalm-suppress PossiblyInvalidCast + */ + public function prepare_items(): void { + $this->process_bulk_action(); + $this->_column_headers = + apply_filters( + 'wpgraphql_logging_logs_table_column_headers', + [ + $this->get_columns(), + [], // hidden. + $this->get_sortable_columns(), + 'id', + ] + ); + + $per_page = $this->get_items_per_page( 'logs_per_page', self::DEFAULT_PER_PAGE ); + $current_page = $this->get_pagenum(); + /** @psalm-suppress InvalidArgument */ + $where = $this->process_where( $_REQUEST ); + $total_items = $this->repository->get_log_count( $where ); + + $this->set_pagination_args( + [ + 'total_items' => $total_items, + 'per_page' => $per_page, + ] + ); + + $args = [ + 'number' => $per_page, + 'offset' => ( $current_page - 1 ) * $per_page, + ]; + + if ( array_key_exists( 'orderby', $_REQUEST ) ) { + $args['orderby'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + if ( array_key_exists( 'order', $_REQUEST ) ) { + $args['order'] = sanitize_text_field( wp_unslash( (string) $_REQUEST['order'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + $args['where'] = $where; + + $this->items = $this->repository->get_logs( apply_filters( 'wpgraphql_logging_logs_table_query_args', $args ) ); + } + + /** + * Define bulk actions. + * + * @return array The bulk actions. + */ + public function get_bulk_actions(): array { + return [ + 'delete' => __( 'Delete Selected', 'wpgraphql-logging' ), + 'delete_all' => __( 'Delete All', 'wpgraphql-logging' ), + ]; + } + + /** + * Handle bulk actions. + * + * @phpcs:disable WordPress.Security.NonceVerification.Missing + * @phpcs:disable WordPress.Security.NonceVerification.Recommended + */ + public function process_bulk_action(): void { + $action = $this->current_action(); + + if ( ! in_array( $action, [ 'delete', 'delete_all' ], true ) ) { + return; + } + + $nonce_action = 'bulk-' . $this->_args['plural']; + $nonce_value = $_REQUEST['_wpnonce'] ?? ''; + + $nonce = is_string( $nonce_value ) ? $nonce_value : ''; + + $nonce_result = wp_verify_nonce( $nonce, $nonce_action ); + if ( false === $nonce_result ) { + wp_die( esc_html__( 'Nonce verification failed!', 'wpgraphql-logging' ) ); + } + + $deleted_count = 0; + + // WordPress sometimes sends 'delete' for selected items. + if ( in_array( $action, [ 'delete', 'bulk_delete' ], true ) && ! empty( $_REQUEST['log'] ) ) { + $ids = array_map( 'absint', (array) $_REQUEST['log'] ); + // Remove redundant empty check since array_map always returns array + foreach ( $ids as $id ) { + if ( $id > 0 ) { // Only process valid IDs + $this->repository->delete( $id ); + } + } + $deleted_count = count( array_filter( $ids, static fn( $id ) => $id > 0 ) ); + } + + if ( 'delete_all' === $action ) { + $count_before_delete = $this->repository->get_log_count( [] ); + $this->repository->delete_all(); + $deleted_count = $count_before_delete; + } + + if ( $deleted_count > 0 ) { + $preserved_filters = []; + $filter_keys = [ 'level_filter', 'start_date', 'end_date' ]; + + foreach ( $filter_keys as $key ) { + $value = $_REQUEST[ $key ] ?? null; + if ( ! empty( $value ) && is_string( $value ) ) { + $preserved_filters[ $key ] = sanitize_text_field( wp_unslash( $value ) ); + } + } + + $redirect_url = remove_query_arg( [ 'action', 'action2', 'log', '_wpnonce' ] ); + $redirect_url = add_query_arg( + array_merge( + [ 'deleted_count' => $deleted_count ], + $preserved_filters + ), + $redirect_url + ); + + if ( ! headers_sent() ) { + wp_safe_redirect( esc_url_raw( $redirect_url ) ); + exit; + } else { + echo ''; + printf( + '

    %s %s

    ', + esc_html__( 'Logs deleted successfully.', 'wpgraphql-logging' ), + esc_url( $redirect_url ), + esc_html__( 'Return to Logs', 'wpgraphql-logging' ) + ); + exit; + } + } + } + + /** + * Get the columns for the logs table. + * + * @return array The columns. + */ + public function get_columns(): array { + return apply_filters( + 'wpgraphql_logging_logs_table_column_headers', + [ + 'cb' => '', + 'id' => __( 'ID', 'wpgraphql-logging' ), + 'date' => __( 'Date', 'wpgraphql-logging' ), + 'wpgraphql_query' => __( 'Query', 'wpgraphql-logging' ), + 'level' => __( 'Level', 'wpgraphql-logging' ), + 'level_name' => __( 'Level Name', 'wpgraphql-logging' ), + 'event' => __( 'Event', 'wpgraphql-logging' ), + 'process_id' => __( 'Process ID', 'wpgraphql-logging' ), + 'request_headers' => __( 'Headers', 'wpgraphql-logging' ), + 'memory_usage' => __( 'Memory Usage', 'wpgraphql-logging' ), + ] + ); + } + + /** + * Get the default column value for a log entry. + * + * @param mixed|\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * @param string $column_name The column name. + * + * @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded + * + * @return mixed The default column value or null. + */ + public function column_default( $item, $column_name ): mixed { + if ( ! $item instanceof DatabaseEntity ) { + return null; + } + + $value = ''; + + switch ( $column_name ) { + case 'date': + $value = $item->get_datetime(); + break; + case 'channel': + $value = $item->get_channel(); + break; + case 'level': + $value = $item->get_level(); + break; + case 'level_name': + $value = $item->get_level_name(); + break; + case 'message': + $value = $item->get_message(); + break; + case 'event': + $value = $this->get_event( $item ); + break; + case 'process_id': + $value = $this->get_process_id( $item ); + break; + case 'memory_usage': + $value = $this->get_memory_usage( $item ); + break; + case 'wpgraphql_query': + $value = $this->get_query( $item ); + break; + case 'request_headers': + $value = $this->get_request_headers( $item ); + break; + } + + return apply_filters( 'wpgraphql_logging_logs_table_column_value', $value, $item, $column_name ); + } + + /** + * Renders the checkbox column for a log entry. + * + * @param mixed|\WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return string The rendered checkbox column or null. + */ + public function column_cb( $item ): string { + if ( ! $item instanceof DatabaseEntity ) { + return ''; + } + return sprintf( + '', + $item->get_id() + ); + } + + /** + * Renders the ID column for a log entry. + * + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return string The rendered ID column or null. + */ + public function column_id( DatabaseEntity $item ): string { + $url = \WPGraphQL\Logging\Admin\View_Logs_Page::ADMIN_PAGE_SLUG; + $actions = [ + 'view' => sprintf( + '%s', + esc_attr( $url ), + 'view', + $item->get_id(), + esc_html__( 'View', 'wpgraphql-logging' ) + ), + 'download' => sprintf( + '%s', + esc_attr( $url ), + 'download', + $item->get_id(), + esc_html__( 'Download', 'wpgraphql-logging' ) + ), + ]; + + return sprintf( + '%1$s %2$s', + $item->get_id(), + $this->row_actions( $actions ) + ); + } + + /** + * Renders the query column for a log entry. + * + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return string|null The rendered query column or null. + */ + public function column_query( DatabaseEntity $item ): ?string { + $extra = $item->get_extra(); + return ! empty( $extra['wpgraphql_query'] ) ? esc_html( $extra['wpgraphql_query'] ) : ''; + } + + /** + * Gets the query from extra. + * + * @return string The query + */ + public function get_query(DatabaseEntity $item): string { + $query = $item->get_query(); + if ( ! is_string( $query ) || '' === $query ) { + return ''; + } + return $this->format_code( $query ); + } + + /** + * Gets the event from extra. + * + * @return string The event + */ + public function get_event(DatabaseEntity $item): string { + + $extra = $item->get_extra(); + return ! empty( $extra['wpgraphql_event'] ) ? esc_html( $extra['wpgraphql_event'] ) : $item->get_message(); + } + + /** + * Gets the event from extra. + * + * @param \WPGraphQL\Logging\Logger\Database\DatabaseEntity $item The log entry item. + * + * @return int The event + */ + public function get_process_id(DatabaseEntity $item): int { + $extra = $item->get_extra(); + return ! empty( $extra['process_id'] ) ? (int) $extra['process_id'] : 0; + } + + /** + * Gets the event from extra. + * + * @return string The event + */ + public function get_memory_usage(DatabaseEntity $item): string { + $extra = $item->get_extra(); + return ! empty( $extra['memory_peak_usage'] ) ? esc_html( $extra['memory_peak_usage'] ) : ''; + } + + /** + * Gets the request headers from extra. + * + * @return string The event + */ + public function get_request_headers(DatabaseEntity $item): string { + $extra = $item->get_extra(); + $request_headers = $extra['request_headers'] ?? []; + if ( empty( $request_headers ) || ! is_array( $request_headers ) ) { + return ''; + } + + $formatted_request_headers = wp_json_encode( $request_headers, JSON_PRETTY_PRINT ); + if ( false === $formatted_request_headers ) { + return ''; + } + return $this->format_code( $formatted_request_headers ); + } + + /** + * Format code for display in a table cell. + * + * @param string $code The code to format. + * + * @return string The formatted code. + */ + protected function format_code(string $code): string { + if ( empty( $code ) ) { + return ''; + } + return '
    ' . esc_html( $code ) . '
    '; + } + + /** + * Process the where clauses for filtering. + * + * @param array $request The request data. + * + * @return array The where clauses. + */ + protected function process_where(array $request): array { + $where_clauses = []; + + if ( ! empty( $request['wpgraphql_logging_nonce'] ) && false === wp_verify_nonce( $request['wpgraphql_logging_nonce'], 'wpgraphql_logging_filter' ) ) { + return []; + } + + if ( ! empty( $request['level_filter'] ) ) { + $level = sanitize_text_field( wp_unslash( (string) $request['level_filter'] ) ); + $where_clauses[] = "level_name = '" . $level . "'"; + } + + if ( ! empty( $request['start_date'] ) ) { + $start_date = sanitize_text_field( $request['start_date'] ); + $date = new \DateTime( $start_date ); + $where_clauses[] = "datetime >= '" . $date->format( 'Y-m-d H:i:s' ) . "'"; + } + + if ( ! empty( $request['end_date'] ) ) { + $end_date = sanitize_text_field( $request['end_date'] ); + $date = new \DateTime( $end_date ); + $where_clauses[] = "datetime <= '" . $date->format( 'Y-m-d H:i:s' ) . "'"; + } + + // Allow developers to modify the where clauses. + return apply_filters( 'wpgraphql_logging_logs_table_where_clauses', $where_clauses, $request ); + } + + /** + * Get a list of sortable columns. + * + * @return array The sortable columns. + */ + protected function get_sortable_columns(): array { + return [ + 'id' => [ 'id', false ], + 'date' => [ 'datetime', true ], + 'level' => [ 'level', false ], + 'level_name' => [ 'level_name', false ], + ]; + } + + /** + * Display the table navigation and actions. + * + * @param string $which The location of the nav ('top' or 'bottom'). + */ + protected function display_tablenav( $which ): void { + $which_position = ( 'top' === $which ) ? 'top' : 'bottom'; + ?> +
    + + render_custom_filters(); ?> + _args['plural'] ); ?> + + +
    + bulk_actions( $which_position ); ?> +
    + + extra_tablenav( $which ); + $this->pagination( $which_position ); + ?> + +
    +
    + '; + require $template; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + echo ''; + } +} diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php new file mode 100644 index 00000000..91dc67f1 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-filters.php @@ -0,0 +1,42 @@ + + +
    + + + + + + + 'margin: 0;' ] ); ?> +
    diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php new file mode 100644 index 00000000..398ed9dc --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-list.php @@ -0,0 +1,30 @@ + + +
    +

    +
    + +
    + prepare_items(); + $list_table->display(); + ?> +
    +
    diff --git a/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php new file mode 100644 index 00000000..7a782aac --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View/Templates/wpgraphql-logger-view.php @@ -0,0 +1,78 @@ + +
    +
    +

    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    get_id() ); ?>
    get_datetime() ); ?>
    get_level(); ?>
    get_level_name() ); ?>
    get_message() ); ?>
    get_channel() ); ?>
    get_query() ); ?>
    get_context(), JSON_PRETTY_PRINT ) ); ?>
    get_extra(), JSON_PRETTY_PRINT ) ); ?>
    + +

    + + + +

    +
    diff --git a/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php new file mode 100644 index 00000000..5116eb59 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Admin/View_Logs_Page.php @@ -0,0 +1,281 @@ +setup(); + } + + do_action( 'wpgraphql_logging_view_logs_init', self::$instance ); + + return self::$instance; + } + + /** + * Sets up the view logs page. + */ + public function setup(): void { + add_action( 'admin_menu', [ $this, 'register_settings_page' ], 10, 0 ); + } + + /** + * Registers the settings page for the view logs. + */ + public function register_settings_page(): void { + + // Add submenu under GraphQL menu using the correct parent slug. + $this->page_hook = add_menu_page( + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + 'manage_options', + self::ADMIN_PAGE_SLUG, + [ $this, 'render_admin_page' ], + 'dashicons-list-view', + 25 + ); + add_submenu_page( + 'graphiql-ide', + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + esc_html__( 'GraphQL Logs', 'wpgraphql-logging' ), + 'manage_options', + self::ADMIN_PAGE_SLUG, + [ $this, 'render_admin_page' ] + ); + + // Updates the list table when filters are applied. + add_action( 'load-' . $this->page_hook, [ $this, 'process_page_actions_before_rendering' ], 10, 0 ); + + // Enqueue scripts for the admin page. + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); + } + + /** + * Enqueues scripts and styles for the admin page. + * + * @param string $hook_suffix The current admin page. + */ + public function enqueue_admin_scripts( string $hook_suffix ): void { + if ( $hook_suffix !== $this->page_hook ) { + return; + } + + // Enqueue WordPress's built-in datepicker and slider. + wp_enqueue_script( 'jquery-ui-datepicker' ); + wp_enqueue_script( 'jquery-ui-slider' ); + + // Enqueue the timepicker addon script and styles from a CDN. + wp_enqueue_script( + 'jquery-ui-timepicker-addon', + 'https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-timepicker-addon.min.js', + [ 'jquery-ui-datepicker', 'jquery-ui-slider' ], + '1.6.3', + true + ); + wp_enqueue_style( + 'jquery-ui-timepicker-addon-style', + 'https://cdnjs.cloudflare.com/ajax/libs/jquery-ui-timepicker-addon/1.6.3/jquery-ui-timepicker-addon.min.css', + [], + '1.6.3' + ); + + // Enqueue the base jQuery UI styles. + wp_enqueue_style( 'jquery-ui-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css', [], '1.12.1' ); + + // Add inline script to initialize the datetimepicker. + wp_add_inline_script( + 'jquery-ui-timepicker-addon', + 'jQuery(document).ready(function($){ $(".wpgraphql-logging-datepicker").datetimepicker({ dateFormat: "yy-mm-dd", timeFormat: "HH:mm:ss" }); });' + ); + } + + /** + * Renders the admin page for the logs. + */ + public function render_admin_page(): void { + /** @psalm-suppress PossiblyInvalidArgument */ + $action = sanitize_text_field( $_REQUEST['action'] ?? 'link' ); // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + + switch ( $action ) { + case 'view': + $this->render_view_page(); + break; + case 'download': + // Handled in process_page_actions_before_rendering. + break; + default: + $this->render_list_page(); + break; + } + } + + /** + * Processes actions for the page, such as filtering and downloading logs. + * This runs before any HTML is output. + */ + public function process_page_actions_before_rendering(): void { + // Check for a download request. + if ( isset( $_GET['action'] ) && 'download' === $_GET['action'] ) { // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $this->process_log_download(); + } + + $this->process_filters_redirect(); + } + + /** + * Process filter form submission and redirect to a GET request. + * This runs before any HTML is output. + */ + public function process_filters_redirect(): void { + // Handle POST from filter form and redirect to GET. + $nonce = $this->get_post_value( 'wpgraphql_logging_nonce' ); + if ( ! is_string( $nonce ) ) { + return; + } + + // Verify nonce for security. + if ( false === wp_verify_nonce( $nonce, 'wpgraphql_logging_filter' ) ) { + return; + } + + $redirect_url = menu_page_url( self::ADMIN_PAGE_SLUG, false ); + + $possible_filters = [ + 'start_date', + 'end_date', + 'level_filter', + 'orderby', + 'order', + ]; + $filters = []; + foreach ( $possible_filters as $key ) { + $value = $this->get_post_value( $key ); + if ( null !== $value ) { + $filters[ $key ] = $value; + } + } + + $redirect_url = add_query_arg( array_filter( $filters, static function ( $value ) { + return '' !== $value; + } ), $redirect_url ); + $redirect_url = apply_filters( 'wpgraphql_logging_filter_redirect_url', $redirect_url, $filters ); + + wp_safe_redirect( $redirect_url ); + exit; + } + + /** + * Retrieves and sanitizes a value from the $_POST superglobal. + * + * @param string $key The key to retrieve from $_POST. + * + * @return string|null The sanitized value or null if not set or invalid. + */ + protected function get_post_value(string $key): ?string { + $value = $_POST[ $key ] ?? null; // @phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + if ( ! is_string( $value ) || '' === $value ) { + return null; + } + $value = wp_unslash( $value ); + if ( ! is_string( $value ) ) { + return null; + } + return sanitize_text_field( $value ); + } + + /** + * Renders the list page for log entries. + */ + protected function render_list_page(): void { + // Variable required for list template. + $list_table = new List_Table( new LogsRepository() ); // @phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable + $list_template = apply_filters( + 'wpgraphql_logging_list_template', + __DIR__ . '/View/Templates/wpgraphql-logger-list.php' + ); + require_once $list_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + } + + /** + * Renders the list page for log entries. + */ + protected function process_log_download(): void { + if ( ! current_user_can( 'manage_options' ) || ! is_admin() ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wpgraphql-logging' ) ); + } + + $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + $downloader = new Download_Log_Service(); + $downloader->generate_csv( $log_id ); + } + + /** + * Renders the view page for a single log entry. + */ + protected function render_view_page(): void { + $log_id = isset( $_GET['log'] ) ? absint( $_GET['log'] ) : 0; // @phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( 0 === (int) $log_id ) { + echo '

    ' . esc_html__( 'Invalid log ID.', 'wpgraphql-logging' ) . '

    '; + return; + } + + $repository = new LogsRepository(); + $log = $repository->get_log( $log_id ); + + if ( is_null( $log ) ) { + echo '

    ' . esc_html__( 'Log not found.', 'wpgraphql-logging' ) . '

    '; + return; + } + + $log_template = apply_filters( + 'wpgraphql_logging_view_template', + __DIR__ . '/View/Templates/wpgraphql-logger-view.php' + ); + + require_once $log_template; // @phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable + } +} diff --git a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php index bd9c66fc..e56e9744 100644 --- a/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php +++ b/plugins/wpgraphql-logging/src/Events/QueryActionLogger.php @@ -61,7 +61,7 @@ public function __construct( LoggerService $logger, array $config ) { */ public function log_pre_request( string $query, ?string $operation_name, ?array $variables ): void { try { - if ( ! $this->is_logging_enabled( $this->config ) ) { + if ( ! $this->is_logging_enabled( $this->config, $query ) ) { return; } $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; @@ -96,7 +96,15 @@ public function log_pre_request( string $query, ?string $operation_name, ?array */ public function log_graphql_before_execute( Request $request ): void { try { - if ( ! $this->is_logging_enabled( $this->config ) ) { + /** @var \GraphQL\Server\OperationParams $params */ + $params = $request->params; + $context = [ + 'query' => $params->query, + 'operation_name' => $params->operation, + 'variables' => $params->variables, + 'params' => $params, + ]; + if ( ! $this->is_logging_enabled( $this->config, $params->query ) ) { return; } $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; @@ -106,14 +114,7 @@ public function log_graphql_before_execute( Request $request ): void { if ( ! in_array( Events::BEFORE_GRAPHQL_EXECUTION, $selected_events, true ) ) { return; } - /** @var \GraphQL\Server\OperationParams $params */ - $params = $request->params; - $context = [ - 'query' => $params->query, - 'operation_name' => $params->operation, - 'variables' => $params->variables, - 'params' => $params, - ]; + $payload = EventManager::transform( Events::BEFORE_GRAPHQL_EXECUTION, [ 'context' => $context, 'level' => Level::Info, @@ -150,7 +151,7 @@ public function log_before_response_returned( ?string $query_id ): void { try { - if ( ! $this->is_logging_enabled( $this->config ) ) { + if ( ! $this->is_logging_enabled( $this->config, $query ) ) { return; } $selected_events = $this->config[ Basic_Configuration_Tab::EVENT_LOG_SELECTION ] ?? []; diff --git a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php index 7385bd46..c1ea5a18 100644 --- a/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php +++ b/plugins/wpgraphql-logging/src/Logger/Database/DatabaseEntity.php @@ -107,7 +107,7 @@ public static function create(string $channel, int $level, string $level_name, s * * @return self|null Returns an instance of DatabaseEntity if found, or null if not found. */ - public static function find(int $id): ?self { + public static function find_by_id(int $id): ?self { global $wpdb; $table_name = self::get_table_name(); @@ -121,6 +121,26 @@ public static function find(int $id): ?self { return self::create_from_db_row( $row ); } + /** + * Helper to populate an instance from a database row. + * + * @param array $row The database row to populate from. + * + * @return self The populated instance. + */ + public static function create_from_db_row(array $row): self { + $log = new self(); + $log->id = (int) $row['id']; + $log->channel = $row['channel']; + $log->level = (int) $row['level']; + $log->level_name = $row['level_name']; + $log->message = $row['message']; + $log->context = ( isset( $row['context'] ) && '' !== $row['context'] ) ? json_decode( $row['context'], true ) : []; + $log->extra = ( isset( $row['extra'] ) && '' !== $row['extra'] ) ? json_decode( $row['extra'], true ) : []; + $log->datetime = $row['datetime']; + return $log; + } + /** * Saves a new logging entity to the database. This is an insert-only operation. * @@ -155,8 +175,8 @@ public function save(): int { /** * Gets the ID of the log entry. */ - public function get_id(): ?int { - return $this->id; + public function get_id(): int { + return (int) $this->id; } /** @@ -182,6 +202,8 @@ public function get_level_name(): string { /** * Gets the message of the log entry. + * + * @return string The message of the log entry. */ public function get_message(): string { return $this->message; @@ -214,6 +236,89 @@ public function get_datetime(): string { return $this->datetime; } + /** + * Extracts and returns the GraphQL query from the context, if available. + * + * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh + * + * @return string|null The GraphQL query string, or null if not available. + */ + public function get_query(): ?string { + + $context = $this->get_context(); + if ( empty( $context ) ) { + return null; + } + + $query = $context['query']; + + $request = $context['request'] ?? null; + if ( empty( $request ) || ! is_array( $request ) ) { + return $query; + } + + $params = $request['params'] ?? null; + if ( empty( $params ) || ! is_array( $params ) ) { + return $query; + } + + if ( isset( $params['query'] ) && is_string( $params['query'] ) ) { + return $params['query']; + } + + return $query; + } + + /** + * Finds multiple log entries and returns them as an array. + * + * @param int $limit The maximum number of log entries to return. + * @param int $offset The offset for pagination. + * @param array $where_clauses Optional. Additional WHERE conditions. + * @param string $orderby The column to order by. + * @param string $order The order direction (ASC or DESC). + * + * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> An array of DatabaseEntity instances, or an empty array if none found. + */ + public static function find_logs(int $limit, int $offset, array $where_clauses = [], string $orderby = 'id', string $order = 'DESC'): array { + global $wpdb; + $table_name = self::get_table_name(); + $order = sanitize_text_field( strtoupper( $order ) ); + $orderby = sanitize_text_field( $orderby ); + + $where = ''; + foreach ( $where_clauses as $clause ) { + if ( '' !== $where ) { + $where .= ' AND '; + } + $where .= (string) $clause; + } + if ( '' !== $where ) { + $where = 'WHERE ' . $where; + } + + /** @psalm-suppress PossiblyInvalidCast */ + $query = $wpdb->prepare( + "SELECT * FROM {$table_name} {$where} ORDER BY {$orderby} {$order} LIMIT %d, %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $offset, + $limit + ); + + // We do not want to cache as this is a paginated query. + $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + + if ( empty( $results ) || ! is_array( $results ) ) { + return []; + } + + return array_map( + static function (array $row) { + return DatabaseEntity::create_from_db_row( $row ); + }, + $results + ); + } + /** * Gets the name of the logging table. */ @@ -297,24 +402,4 @@ protected static function sanitize_array_field(array $data): array { } return $data; } - - /** - * Helper to populate an instance from a database row. - * - * @param array $row The database row to populate from. - * - * @return self The populated instance. - */ - private static function create_from_db_row(array $row): self { - $log = new self(); - $log->id = (int) $row['id']; - $log->channel = $row['channel']; - $log->level = (int) $row['level']; - $log->level_name = $row['level_name']; - $log->message = $row['message']; - $log->context = ( isset( $row['context'] ) && '' !== $row['context'] ) ? json_decode( $row['context'], true ) : []; - $log->extra = ( isset( $row['extra'] ) && '' !== $row['extra'] ) ? json_decode( $row['extra'], true ) : []; - $log->datetime = $row['datetime']; - return $log; - } } diff --git a/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php new file mode 100644 index 00000000..83132411 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Database/LogsRepository.php @@ -0,0 +1,120 @@ + $args + * + * @return array<\WPGraphQL\Logging\Logger\Database\DatabaseEntity> + */ + public function get_logs(array $args = []): array { + global $wpdb; + $defaults = [ + 'number' => 100, + 'offset' => 0, + 'orderby' => 'id', + 'order' => 'DESC', + 'where' => [], + ]; + $args = wp_parse_args( $args, $defaults ); + + $orderby = $args['orderby']; + if ( ! is_string( $orderby ) || '' === $orderby ) { + $orderby = $defaults['orderby']; + } + $order = $args['order']; + if ( ! is_string( $order ) || '' === $order ) { + $order = $defaults['order']; + } + $where = $args['where']; + if ( ! is_array( $where ) ) { + $where = $defaults['where']; + } + + $limit = absint( $args['number'] ); + $offset = absint( $args['offset'] ); + + + return DatabaseEntity::find_logs( $limit, $offset, $where, $orderby, $order ); + } + + /** + * Get the total number of log entries. + * + * @param array $where_clauses Array of where clauses to filter the count. + * + * @phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching + * + * @return int The total number of log entries. + */ + public function get_log_count(array $where_clauses): int { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + + if ( empty( $where_clauses ) ) { + return (int) $wpdb->get_var( $wpdb->prepare( // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + 'SELECT COUNT(*) FROM %i', + $table_name + ) ); + } + + $where = ''; + foreach ( $where_clauses as $clause ) { + if ( '' !== $where ) { + $where .= ' AND '; + } + $where .= (string) $clause; + } + + return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name} WHERE {$where}" ); // @phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + /** + * Get a single log entry by ID. + * + * @param int $id The log entry ID. + * + * @return ?\WPGraphQL\Logging\Logger\Database\DatabaseEntity The log entry or null if not found. + */ + public function get_log( int $id ): ?DatabaseEntity { + return DatabaseEntity::find_by_id( $id ); + } + + /** + * Delete a single log entry by ID. + * + * @param int $id + */ + public function delete(int $id): bool { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + + if ( $id <= 0 ) { + return false; + } + + $result = $wpdb->delete( $table_name, [ 'id' => $id ], [ '%d' ] ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return false !== $result; + } + + /** + * Delete all log entries. + */ + public function delete_all(): void { + global $wpdb; + $table_name = DatabaseEntity::get_table_name(); + $wpdb->query( $wpdb->prepare( "DELETE FROM %i", $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery + } +} diff --git a/plugins/wpgraphql-logging/src/Logger/LoggerService.php b/plugins/wpgraphql-logging/src/Logger/LoggerService.php index 459c1e02..bdc7d35d 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggerService.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggerService.php @@ -12,6 +12,7 @@ use Monolog\Processor\ProcessorInterface; use Monolog\Processor\WebProcessor; use WPGraphQL\Logging\Logger\Handlers\WordPressDatabaseHandler; +use WPGraphQL\Logging\Logger\Processors\RequestHeadersProcessor; use WPGraphQL\Logging\Logger\Processors\WPGraphQLQueryProcessor; /** @@ -225,6 +226,7 @@ public static function get_default_processors(): array { new WebProcessor(), // Logs web request data. e.g. IP address, request method, URI, etc. new ProcessIdProcessor(), // Logs the process ID. new WPGraphQLQueryProcessor(), // Custom processor to capture GraphQL request data. + new RequestHeadersProcessor(), // Custom processor to capture request headers. ]; // Filter for users to add their own processors. diff --git a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php index b742f410..eaaa2eee 100644 --- a/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php +++ b/plugins/wpgraphql-logging/src/Logger/LoggingHelper.php @@ -17,9 +17,8 @@ trait LoggingHelper { * * phpcs:disable Generic.Metrics.CyclomaticComplexity, SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh */ - protected function is_logging_enabled( array $config ): bool { + protected function is_logging_enabled( array $config, ?string $query_string = null ): bool { $is_enabled = true; - // Check the main "Enabled" checkbox. if ( ! (bool) ( $config[ Basic_Configuration_Tab::ENABLED ] ?? false ) ) { $is_enabled = false; @@ -50,6 +49,11 @@ protected function is_logging_enabled( array $config ): bool { } } + // Check if the query is an introspection query and skip logging if it is. + if ( $is_enabled && $this->is_introspection_query( $query_string ) ) { + $is_enabled = false; + } + /** * Filter the final decision on whether to log a request. * @@ -58,4 +62,17 @@ protected function is_logging_enabled( array $config ): bool { */ return apply_filters( 'wpgraphql_logging_is_enabled', $is_enabled, $config ); } + + /** + * Checks if a query is an introspection query. + * + * @param string|null $query_string The GraphQL query string. + */ + protected function is_introspection_query( ?string $query_string ): bool { + if ( null === $query_string ) { + return false; + } + + return strpos( $query_string, '__schema' ) !== false; + } } diff --git a/plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php b/plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php new file mode 100644 index 00000000..006107e2 --- /dev/null +++ b/plugins/wpgraphql-logging/src/Logger/Processors/RequestHeadersProcessor.php @@ -0,0 +1,52 @@ + The request headers. + */ + private function get_headers(): array { + $headers = []; + foreach ( $_SERVER as $key => $value ) { + if ( ! is_string( $value ) || empty( $value ) ) { + continue; + } + $header_key = substr( $key, 5 ); + $header_key = str_replace( '_', '-', $header_key ); + $header_key = ucwords( strtolower( sanitize_text_field( (string) $header_key ) ), '-' ); + $headers[ $header_key ] = sanitize_text_field( $value ); + } + + return $headers; + } + + /** + * This method is called for each log record. It adds the captured + * request headers to the record's 'extra' array. + * + * @param \Monolog\LogRecord $record The log record to process. + * + * @return \Monolog\LogRecord The processed log record. + */ + public function __invoke( LogRecord $record ): LogRecord { + $record->extra['request_headers'] = $this->get_headers(); + + return $record; + } +} diff --git a/plugins/wpgraphql-logging/src/Plugin.php b/plugins/wpgraphql-logging/src/Plugin.php index 8c9ad882..61621f8b 100644 --- a/plugins/wpgraphql-logging/src/Plugin.php +++ b/plugins/wpgraphql-logging/src/Plugin.php @@ -5,6 +5,7 @@ namespace WPGraphQL\Logging; use WPGraphQL\Logging\Admin\Settings_Page; +use WPGraphQL\Logging\Admin\View_Logs_Page; use WPGraphQL\Logging\Events\EventManager; use WPGraphQL\Logging\Events\QueryEventLifecycle; use WPGraphQL\Logging\Logger\Database\DatabaseEntity; @@ -54,6 +55,7 @@ public static function init(): self { */ public function setup(): void { Settings_Page::init(); + View_Logs_Page::init(); QueryEventLifecycle::init(); } diff --git a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php index 0be1c628..cc0b1e05 100644 --- a/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php +++ b/plugins/wpgraphql-logging/tests/wpunit/Logger/Database/DatabaseEntityTest.php @@ -82,7 +82,7 @@ public function test_save_method_inserts_log_into_database(): void $this->assertIsInt( $insert_id ); $this->assertGreaterThan(0, $insert_id, 'The save method should return a positive insert ID.'); - $entity = DatabaseEntity::find($insert_id); + $entity = DatabaseEntity::find_by_id($insert_id); $this->assertInstanceOf(DatabaseEntity::class, $entity, 'The find method should return an instance of DatabaseEntity.'); $this->assertEquals($log_data['channel'], $entity->get_channel(), 'The channel should match the saved data.'); $this->assertEquals($log_data['level'], $entity->get_level(), 'The level should match the saved data.'); @@ -98,8 +98,8 @@ public function test_save_method_inserts_log_into_database(): void public function test_find_returns_null_for_nonexistent_id(): void { - $entity = DatabaseEntity::find(999999); - $this->assertNull($entity, 'find() should return null for a non-existent ID.'); + $entity = DatabaseEntity::find_by_id(999999); + $this->assertNull($entity, 'find_by_id() should return null for a non-existent ID.'); } /** diff --git a/plugins/wpgraphql-logging/wpgraphql-logging.php b/plugins/wpgraphql-logging/wpgraphql-logging.php index 91b33a12..35ecbe84 100644 --- a/plugins/wpgraphql-logging/wpgraphql-logging.php +++ b/plugins/wpgraphql-logging/wpgraphql-logging.php @@ -144,7 +144,7 @@ static function (): void { * Display an admin notice if the PHP version is not met. */ function wpgraphql_logging_plugin_admin_notice_min_php_version(): void { - if ( version_compare( PHP_VERSION, '8.1', '>=' ) ) { + if ( version_compare( PHP_VERSION, '8.1.2', '>=' ) ) { return; } @@ -155,7 +155,7 @@ static function (): void {