mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
feat(cli): add unified command frontend and normalize legacy failures (#7903)
* feat(cli): add unified command frontend and normalize legacy failures Add the Symfony-backed bin/cacti entry point while preserving legacy argument, terminal, signal, and exit-status behavior. Fold in the focused legacy CLI correctness fixes from #7906 without adding its unused parser abstraction. Keep path containment and the PHP 8.3 floor out of this review; the latter remains in #7939. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(cli): enforce PHP floor and deny web dispatch --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
1 parent
49b10f8046
commit
1ae479d4f5
31 files changed
+1556
-362
No files matched your search
@@ -0,0 +1,78 @@
|
||||
name: CLI Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/cli-coverage.yml'
|
||||
- '.github/actions/setup-php-composer/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'bin/cacti'
|
||||
- 'bin/.htaccess'
|
||||
- 'include/cli_check.php'
|
||||
- 'include/cli_only.php'
|
||||
- 'lib/Console/**'
|
||||
- 'phpunit-cli-coverage.xml'
|
||||
- 'tests/fixtures/Console/**'
|
||||
- 'tests/.htaccess'
|
||||
- 'tests/index.php'
|
||||
- 'tests/Unit/Core/Cli/CactiApplicationTest.php'
|
||||
- 'tests/Unit/Core/Cli/CactiEntrypointSecurityTest.php'
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/cli-coverage.yml'
|
||||
- '.github/actions/setup-php-composer/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'bin/cacti'
|
||||
- 'bin/.htaccess'
|
||||
- 'include/cli_check.php'
|
||||
- 'include/cli_only.php'
|
||||
- 'lib/Console/**'
|
||||
- 'phpunit-cli-coverage.xml'
|
||||
- 'tests/fixtures/Console/**'
|
||||
- 'tests/.htaccess'
|
||||
- 'tests/index.php'
|
||||
- 'tests/Unit/Core/Cli/CactiApplicationTest.php'
|
||||
- 'tests/Unit/Core/Cli/CactiEntrypointSecurityTest.php'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: cli-coverage-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ALLOW_SUPERUSER: 1
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: CLI 100% coverage
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup locked PHP dependencies
|
||||
uses: ./.github/actions/setup-php-composer
|
||||
with:
|
||||
php-version: '8.2'
|
||||
coverage: xdebug
|
||||
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring
|
||||
dependency-mode: locked-cli-coverage
|
||||
install-args: --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Enforce line coverage
|
||||
env:
|
||||
XDEBUG_MODE: coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
composer test:cli-coverage
|
||||
@@ -0,0 +1,10 @@
|
||||
# Apache 2.4
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
|
||||
# Apache 2.2
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use Cacti\Console\CactiApplication;
|
||||
use Cacti\Console\Input\RawArgvInput;
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$autoload = $root . '/include/vendor/autoload.php';
|
||||
|
||||
require_once $root . '/include/cli_only.php';
|
||||
|
||||
if (!is_file($autoload)) {
|
||||
fwrite(STDERR, "Cacti dependencies are not installed. Run composer install.\n");
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require $autoload;
|
||||
|
||||
$application = new CactiApplication($root);
|
||||
|
||||
exit($application->run(new RawArgvInput()));
|
||||
@@ -0,0 +1 @@
|
||||
Add a unified bin/cacti command line entry point over the legacy cli scripts and activate the shared CLI-only constants for CLI entrypoints
|
||||
+2
-2
@@ -32,7 +32,7 @@ include_once(CACTI_PATH_LIBRARY . '/api_tree.php');
|
||||
$parms = $_SERVER['argv'];
|
||||
array_shift($parms);
|
||||
|
||||
if (sizeof($parms)) {
|
||||
if (cacti_sizeof($parms)) {
|
||||
// setup defaults
|
||||
$siteName = ''; // Site Name
|
||||
$siteAddr1 = ''; // Site Address 1
|
||||
@@ -380,7 +380,7 @@ function mapDevices(int $siteId, bool $doMap) : void {
|
||||
}
|
||||
}
|
||||
|
||||
$numMatched = sizeof($matchedDevices);
|
||||
$numMatched = cacti_sizeof($matchedDevices);
|
||||
|
||||
if ($numMatched) {
|
||||
echoQuiet(PHP_EOL);
|
||||
|
||||
+2
-2
@@ -231,13 +231,13 @@ if ((file_exists($keypair_path . '/package.pem') || file_exists($keypair_path .
|
||||
if ($privKey === false) {
|
||||
print "FATAL: Unable to open your private key $privkey" . PHP_EOL;
|
||||
|
||||
exit(0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($pubKey === false) {
|
||||
print "FATAL: Unable to open your public key $pubkey" . PHP_EOL;
|
||||
|
||||
exit(0);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
print 'NOTE: Generating custom public/private key pair.' . PHP_EOL;
|
||||
|
||||
@@ -52,6 +52,7 @@ if (cacti_sizeof($parms)) {
|
||||
|
||||
switch ($arg) {
|
||||
case '-id':
|
||||
case '--id':
|
||||
case '--host-id':
|
||||
$host_id = $value;
|
||||
|
||||
|
||||
@@ -184,6 +184,10 @@ if (cacti_sizeof($parms)) {
|
||||
|
||||
default:
|
||||
print "ERROR: Invalid Argument: ($arg)" . PHP_EOL . PHP_EOL;
|
||||
|
||||
display_help();
|
||||
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -225,7 +225,7 @@ if (!$result) {
|
||||
print 'ERROR: Remove Spikes experienced errors' . PHP_EOL;
|
||||
print $spiker->get_errors();
|
||||
|
||||
exit(-1);
|
||||
exit(1);
|
||||
} else {
|
||||
print $spiker->get_output();
|
||||
|
||||
|
||||
+8
-8
@@ -36,7 +36,7 @@ require_once('./include/cli_check.php');
|
||||
$parms = $_SERVER['argv'];
|
||||
array_shift($parms);
|
||||
|
||||
if (sizeof($parms)) {
|
||||
if (cacti_sizeof($parms)) {
|
||||
$data_template_id = false;
|
||||
$displayDataTemplates = false;
|
||||
$debug_mode = false;
|
||||
@@ -164,7 +164,7 @@ if ($displayDataTemplates) {
|
||||
|
||||
print 'Known Data Templates: (id, name)' . PHP_EOL;
|
||||
|
||||
if (sizeof($data_templates)) {
|
||||
if (cacti_sizeof($data_templates)) {
|
||||
foreach ($data_templates as $data_template) {
|
||||
print $data_template['id'] . "\t" . $data_template['name'] . PHP_EOL;
|
||||
}
|
||||
@@ -179,13 +179,13 @@ if ($displayDataTemplates) {
|
||||
if (!$data_template_id) {
|
||||
print 'ERROR: You must supply a valid data template id to run this script!' . PHP_EOL;
|
||||
|
||||
exit(0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!$backup_folder) {
|
||||
print 'ERROR: You must define a backup folder!' . PHP_EOL;
|
||||
|
||||
exit(0);
|
||||
exit(1);
|
||||
} else {
|
||||
$tmp_xml_file = $tmp_backup_folder . 'tmp_rrdresize.xml';
|
||||
$tmp_rrd_file = $tmp_backup_folder . 'tmp_rrdfile.rrd';
|
||||
@@ -240,7 +240,7 @@ if ($scanned_directory['folders']) {
|
||||
if (!mkdir($tmp_backup_folder)) {
|
||||
print 'ERROR: Unable to create a backup folder!' . PHP_EOL;
|
||||
|
||||
exit(0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// create log file
|
||||
@@ -261,7 +261,7 @@ if ($scanned_directory['folders']) {
|
||||
|
||||
$dt_hosts = [];
|
||||
|
||||
if (sizeof($db_hosts) > 0) {
|
||||
if (cacti_sizeof($db_hosts) > 0) {
|
||||
foreach ($db_hosts as $key => $value) {
|
||||
$dt_hosts[] = $value['host_id'];
|
||||
}
|
||||
@@ -280,7 +280,7 @@ if ($scanned_directory['folders']) {
|
||||
|
||||
$dt_data_sources = [];
|
||||
|
||||
if (sizeof($db_data_sources) > 0) {
|
||||
if (cacti_sizeof($db_data_sources) > 0) {
|
||||
foreach ($db_data_sources as $key => $value) {
|
||||
$dt_data_sources[] = $value['local_data_id'];
|
||||
}
|
||||
@@ -361,7 +361,7 @@ if ($scanned_directory['folders']) {
|
||||
[$local_data['data_template_id']]
|
||||
);
|
||||
|
||||
$data_template_ds_counter = sizeof($data_template_ds);
|
||||
$data_template_ds_counter = cacti_sizeof($data_template_ds);
|
||||
$data_template_data_settings = db_fetch_assoc_prepared('SELECT *
|
||||
FROM data_template_data_rra
|
||||
LEFT JOIN rra
|
||||
|
||||
+1
-1
@@ -278,7 +278,7 @@ if (strlen($response)) {
|
||||
} else {
|
||||
print 'FATAL: RRDTool not found in configuration or path.' . PHP_EOL . 'Please insure RRDTool can be found using one of these methods!' . PHP_EOL;
|
||||
|
||||
exit(-1);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* The dump files and the backups were previously named from the RRD basename
|
||||
|
||||
@@ -56,12 +56,14 @@
|
||||
"slim/slim": "^4.14",
|
||||
"stevenmaguire/oauth2-keycloak": "^6.1.0",
|
||||
"stevenmaguire/oauth2-microsoft": "^2.2",
|
||||
"symfony/console": "^7.4",
|
||||
"symfony/filesystem": "^7.4",
|
||||
"symfony/finder": "^6.4 || ^7.4",
|
||||
"symfony/http-foundation": "^7.4",
|
||||
"symfony/lock": "^7.4",
|
||||
"symfony/mailer": "^7.4",
|
||||
"symfony/notifier": "^7.4",
|
||||
"symfony/process": "^7.4",
|
||||
"symfony/validator": "^7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -81,6 +83,7 @@
|
||||
"hardening-patterns": "php tests/tools/check_hardening_patterns.php",
|
||||
"vendor-policy": "php tests/tools/check_vendor_policy.php",
|
||||
"php-cs-fixit": "php-cs-fixer fix ",
|
||||
"test:cli-coverage": "@php include/vendor/bin/pest --configuration=phpunit-cli-coverage.xml --coverage --coverage-text --min=100",
|
||||
"install-hooks": "bash -c 'install -m 755 tests/tools/pre-commit \"$(git rev-parse --git-path hooks/pre-commit)\"'",
|
||||
"test": "include/vendor/bin/pest --display-warnings",
|
||||
"sink-guard": "bash tests/security/verify_sink_inventory.sh",
|
||||
@@ -114,6 +117,11 @@
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Cacti\\Console\\": "lib/Console/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"vendor-dir": "./include/vendor",
|
||||
|
||||
Generated
+345
-345
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "22cda0241ac22aff9c2fe23ef7abb3c6",
|
||||
"content-hash": "33ddeb7d701bf817126ad36feb9a92e0",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
@@ -1935,6 +1935,104 @@
|
||||
},
|
||||
"time": "2017-06-07T13:42:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0",
|
||||
"reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/string": "^7.2|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/dotenv": "<6.4",
|
||||
"symfony/event-dispatcher": "<6.4",
|
||||
"symfony/lock": "<6.4",
|
||||
"symfony/process": "<6.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0|2.0|3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/lock": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Eases the creation of beautiful and testable command line interfaces",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
"console",
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.18"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-08-25T14:18:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"version": "v3.7.1",
|
||||
@@ -2814,6 +2912,88 @@
|
||||
],
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.41.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
|
||||
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's grapheme_* functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"grapheme",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-07-28T08:25:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.42.0",
|
||||
@@ -3316,17 +3496,82 @@
|
||||
"time": "2026-07-01T12:47:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v3.7.1",
|
||||
"name": "symfony/process",
|
||||
"version": "v7.4.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0"
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "058d17fc284cce14efb2385783b55014a461b176"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0",
|
||||
"reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176",
|
||||
"reference": "058d17fc284cce14efb2385783b55014a461b176",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Process\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.18"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
"version": "v3.7.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/service-contracts.git",
|
||||
"reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
|
||||
"reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3380,7 +3625,7 @@
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.7.1"
|
||||
"source": "https://github.com/symfony/service-contracts/tree/v3.7.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -3400,7 +3645,98 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-16T09:55:08+00:00"
|
||||
"time": "2026-07-27T15:39:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.4.15",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "e394af32256bf9e7bf80849d95e589167c10097b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
|
||||
"reference": "e394af32256bf9e7bf80849d95e589167c10097b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-intl-grapheme": "~1.33",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/translation-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/emoji": "^7.1|^8.0",
|
||||
"symfony/http-client": "^6.4|^7.0|^8.0",
|
||||
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation-contracts": "^2.5|^3.0",
|
||||
"symfony/var-exporter": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\String\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"grapheme",
|
||||
"i18n",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.4.15"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-07-28T07:33:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/translation-contracts",
|
||||
@@ -7734,104 +8070,6 @@
|
||||
],
|
||||
"time": "2026-06-05T06:23:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/962e18f09ebe68a49039b4c82fc0ea4871824fca",
|
||||
"reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/service-contracts": "^2.5|^3",
|
||||
"symfony/string": "^7.2|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/dependency-injection": "<6.4",
|
||||
"symfony/dotenv": "<6.4",
|
||||
"symfony/event-dispatcher": "<6.4",
|
||||
"symfony/lock": "<6.4",
|
||||
"symfony/process": "<6.4"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0|2.0|3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/config": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/lock": "^6.4|^7.0|^8.0",
|
||||
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||
"symfony/process": "^6.4|^7.0|^8.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Console\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Eases the creation of beautiful and testable command line interfaces",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
"console",
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-08-21T12:09:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
"version": "v7.4.8",
|
||||
@@ -7903,88 +8141,6 @@
|
||||
],
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.41.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
|
||||
"reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill for intl's grapheme_* functions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"grapheme",
|
||||
"intl",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-07-28T08:25:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.38.1",
|
||||
@@ -8145,71 +8301,6 @@
|
||||
],
|
||||
"time": "2026-05-26T12:51:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v7.4.17",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "058d17fc284cce14efb2385783b55014a461b176"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176",
|
||||
"reference": "058d17fc284cce14efb2385783b55014a461b176",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Process\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v7.4.17"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-08-21T17:40:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v7.4.8",
|
||||
@@ -8276,97 +8367,6 @@
|
||||
],
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v7.4.15",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "e394af32256bf9e7bf80849d95e589167c10097b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b",
|
||||
"reference": "e394af32256bf9e7bf80849d95e589167c10097b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-intl-grapheme": "~1.33",
|
||||
"symfony/polyfill-intl-normalizer": "~1.0",
|
||||
"symfony/polyfill-mbstring": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/translation-contracts": "<2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/emoji": "^7.1|^8.0",
|
||||
"symfony/http-client": "^6.4|^7.0|^8.0",
|
||||
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation-contracts": "^2.5|^3.0",
|
||||
"symfony/var-exporter": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"Resources/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\String\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"grapheme",
|
||||
"i18n",
|
||||
"string",
|
||||
"unicode",
|
||||
"utf-8",
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v7.4.15"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"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": "2026-07-28T07:33:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/var-exporter",
|
||||
"version": "v6.4.42",
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
*
|
||||
* Any source wishing to check whether the system is in
|
||||
* CLI or WEB mode should use the CACTI_WEB and
|
||||
* CACTI_CLI constants
|
||||
define('CACTI_CLI_ONLY', true);
|
||||
* CACTI_CLI constants.
|
||||
*/
|
||||
require_once(__DIR__ . '/cli_only.php');
|
||||
|
||||
/* We are not talking to the browser */
|
||||
// We are not talking to the browser
|
||||
$no_http_headers = true;
|
||||
|
||||
// Make sure CLI's are have minimum settings
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
/* Keep this guard independent of global.php so lightweight launchers can
|
||||
* reject web requests without opening the database merely to show --help or
|
||||
* --version. Full legacy scripts continue through cli_check.php afterwards. */
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(403);
|
||||
print 'FATAL: This file can only be called from the command line.' . PHP_EOL;
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!defined('CACTI_CLI_ONLY')) {
|
||||
define('CACTI_CLI_ONLY', true);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Cacti\Console;
|
||||
|
||||
use Cacti\Console\Command\LegacyScriptCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class CactiApplication extends Application {
|
||||
public function __construct(private readonly string $root) {
|
||||
parent::__construct('Cacti', $this->readVersion());
|
||||
|
||||
foreach (LegacyCommandMap::commands() as $script => $command) {
|
||||
$this->add(new LegacyScriptCommand($command, $script, $this->root));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a legacy command before Symfony can claim its flags.
|
||||
*
|
||||
* Application::doRun() answers --version/-V itself and rewrites --help/-h
|
||||
* into the help command, and it reads both straight off the token vector,
|
||||
* so dropping them from the input definition changes nothing. Forty-five
|
||||
* scripts under cli/ carry their own --version and --help handling, so a
|
||||
* legacy command owns its whole flag namespace and Symfony must not read
|
||||
* any of it.
|
||||
*/
|
||||
#[\Override]
|
||||
public function doRun(InputInterface $input, OutputInterface $output): int {
|
||||
$name = $this->getCommandName($input);
|
||||
|
||||
if ($name !== null && $name !== '') {
|
||||
try {
|
||||
$command = $this->find($name);
|
||||
} catch (\Throwable) {
|
||||
// Unknown or ambiguous; let Symfony report it as it always has.
|
||||
$command = null;
|
||||
}
|
||||
|
||||
if ($command instanceof LegacyScriptCommand) {
|
||||
/* -q belongs to the legacy script too. Symfony has already applied
|
||||
* it to this output, which would swallow the child's stream while
|
||||
* the flag is also forwarded to the child. */
|
||||
$output->setVerbosity(OutputInterface::VERBOSITY_NORMAL);
|
||||
|
||||
return $command->run($input, $output);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::doRun($input, $output);
|
||||
}
|
||||
|
||||
private function readVersion(): string {
|
||||
$version = @file_get_contents($this->root . '/include/cacti_version');
|
||||
|
||||
return $version === false ? 'unknown' : trim($version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Cacti\Console\Command;
|
||||
|
||||
use Cacti\Console\Input\RawArgvInput;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
use Symfony\Component\Process\Exception\LogicException as ProcessLogicException;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
final class LegacyScriptCommand extends Command {
|
||||
/** @var \Closure(array<int, string>): Process */
|
||||
private readonly \Closure $process_factory;
|
||||
/** @var \Closure(): bool */
|
||||
private readonly \Closure $tty_probe;
|
||||
/** @var (\Closure(int, callable): void)|null */
|
||||
private readonly ?\Closure $signal_registrar;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
private readonly string $script,
|
||||
private readonly string $root,
|
||||
?callable $process_factory = null,
|
||||
?callable $tty_probe = null,
|
||||
callable|false|null $signal_registrar = null,
|
||||
) {
|
||||
parent::__construct($name);
|
||||
$this->process_factory = $process_factory === null
|
||||
? static fn (array $command): Process => new Process($command, null, null, STDIN, null)
|
||||
: \Closure::fromCallable($process_factory);
|
||||
$this->tty_probe = $tty_probe === null
|
||||
? static fn (): bool => defined('STDIN') && defined('STDOUT') && defined('STDERR') && Process::isTtySupported() && stream_isatty(STDIN) && stream_isatty(STDOUT) && stream_isatty(STDERR)
|
||||
: \Closure::fromCallable($tty_probe);
|
||||
$signals_available = function_exists('pcntl_async_signals') && function_exists('pcntl_signal');
|
||||
|
||||
if ($signal_registrar === false || ($signal_registrar === null && !$signals_available)) {
|
||||
$this->signal_registrar = null;
|
||||
} elseif ($signal_registrar !== null) {
|
||||
$this->signal_registrar = \Closure::fromCallable($signal_registrar);
|
||||
} else {
|
||||
$this->signal_registrar = static function (int $signal, callable $handler): void {
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal($signal, $handler);
|
||||
};
|
||||
}
|
||||
$this->setAliases([$script]);
|
||||
$this->setDescription(sprintf('Runs the legacy cli/%s.php command.', $script));
|
||||
$this->setHelp(sprintf(
|
||||
'This compatibility command forwards arguments, input, output, and the exit status to cli/%s.php.',
|
||||
$script
|
||||
));
|
||||
$this->ignoreValidationErrors();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
if (!$input instanceof RawArgvInput) {
|
||||
throw new \LogicException('Legacy commands require RawArgvInput.');
|
||||
}
|
||||
|
||||
$arguments = $input->argumentsAfterCommand(array_values(array_merge([$this->getName()], $this->getAliases())));
|
||||
/* Inherit the caller's directory. The script path is absolute, and the
|
||||
* legacy scripts resolve relative path arguments against the cwd with no
|
||||
* normalization. Anchoring here would send a relative output into the
|
||||
* web-served root instead of the directory the operator ran it from. */
|
||||
$process = ($this->process_factory)(
|
||||
array_merge([PHP_BINARY, $this->root . '/cli/' . $this->script . '.php'], $arguments)
|
||||
);
|
||||
$process->setTimeout(null);
|
||||
|
||||
$this->forwardSignals($process);
|
||||
|
||||
try {
|
||||
/* Several scripts under cli/ prompt with fgets(STDIN). Behind a pipe
|
||||
* the prompt sits in a buffer while the child blocks on the read, so
|
||||
* hand the terminal over whenever there is one. */
|
||||
if ($this->canUseTty()) {
|
||||
$process->setTty(true);
|
||||
|
||||
return $process->run();
|
||||
}
|
||||
|
||||
/* Process::buildCallback() appends every byte to a php://temp stream
|
||||
* as well as calling this callback, and nothing ever reads it back.
|
||||
* With no timeout, a long poller run spooled its whole output to disk
|
||||
* for no reader. */
|
||||
$process->disableOutput();
|
||||
|
||||
return $process->run(function (string $type, string $buffer) use ($output): void {
|
||||
$target = $type === Process::ERR && $output instanceof ConsoleOutputInterface
|
||||
? $output->getErrorOutput()
|
||||
: $output;
|
||||
$target->write($buffer, false, OutputInterface::OUTPUT_RAW);
|
||||
});
|
||||
} catch (ProcessSignaledException $exception) {
|
||||
/* A child the kernel killed has no exit code of its own, and Process
|
||||
* throws rather than returning one. Report it the way a shell does so
|
||||
* cron and systemd see 137 or 143, not a rendered stack trace. */
|
||||
return 128 + $exception->getSignal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this invocation owns a terminal it can hand to the child.
|
||||
*/
|
||||
private function canUseTty(): bool {
|
||||
return ($this->tty_probe)();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass a termination signal on to the child.
|
||||
*
|
||||
* Without this a SIGTERM to bin/cacti leaves the legacy script running,
|
||||
* possibly mid-write to the database, with nothing left to reap it.
|
||||
*/
|
||||
private function forwardSignals(Process $process): void {
|
||||
if ($this->signal_registrar === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ([SIGTERM, SIGINT, SIGHUP] as $signal) {
|
||||
($this->signal_registrar)($signal, static function (int $received) use ($process): void {
|
||||
try {
|
||||
$process->signal($received);
|
||||
} catch (ProcessLogicException) {
|
||||
/* The child can exit between signal delivery and Process::signal().
|
||||
* Its real exit status remains authoritative in that race. */
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Cacti\Console\Input;
|
||||
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
|
||||
final class RawArgvInput extends ArgvInput {
|
||||
/** @var list<string> */
|
||||
private readonly array $rawTokens;
|
||||
|
||||
/** @param list<string>|null $argv */
|
||||
public function __construct(?array $argv = null, ?InputDefinition $definition = null) {
|
||||
$argv ??= $_SERVER['argv'] ?? [];
|
||||
$this->rawTokens = array_values(array_slice($argv, 1));
|
||||
|
||||
parent::__construct($argv, $definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string|null> $names
|
||||
* @return list<string>
|
||||
*/
|
||||
public function argumentsAfterCommand(array $names): array {
|
||||
/* Symfony resolves abbreviated command names, so the typed token is
|
||||
* often neither the registered name nor an alias. Without the token it
|
||||
* actually consumed, an abbreviation matches nothing and the legacy
|
||||
* script is spawned with an empty argv, silently taking its
|
||||
* no-argument path. */
|
||||
$names[] = $this->getFirstArgument();
|
||||
|
||||
foreach ($this->rawTokens as $offset => $token) {
|
||||
if (in_array($token, $names, true)) {
|
||||
/* Drop the command token, keep everything else. getFirstArgument()
|
||||
* skips leading options, so 'bin/cacti --help poller:rebuild-cache'
|
||||
* dispatches with the flag ahead of the name; slicing only forward
|
||||
* handed the script an empty argv and it rebuilt every cache. */
|
||||
return array_merge(
|
||||
array_slice($this->rawTokens, 0, $offset),
|
||||
array_slice($this->rawTokens, $offset + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new \LogicException('The resolved command token is missing from the raw argument vector.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Cacti\Console;
|
||||
|
||||
final class LegacyCommandMap {
|
||||
/** @var array<string, string> Legacy script basename to command name. */
|
||||
private const COMMANDS = [
|
||||
'add_data_query' => 'data-query:add',
|
||||
'add_datasource' => 'data-source:add',
|
||||
'add_device' => 'device:add',
|
||||
'add_graph_template' => 'graph-template:add',
|
||||
'add_graphs' => 'graph:add',
|
||||
'add_group' => 'group:add',
|
||||
'add_group_perms' => 'group:permissions:add',
|
||||
'add_perms' => 'permissions:add',
|
||||
'add_site' => 'site:add',
|
||||
'add_tree' => 'tree:add',
|
||||
'analyze_database' => 'database:analyze',
|
||||
'apply_automation_rules' => 'automation:apply-rules',
|
||||
'audit_database' => 'database:audit',
|
||||
'audit_graph_template_inputs' => 'graph-template:audit-inputs',
|
||||
'batchgapfix' => 'rrd:batch-gap-fix',
|
||||
'change_device' => 'device:change',
|
||||
'clone_device_template' => 'device-template:clone',
|
||||
'convert_tables' => 'database:convert-tables',
|
||||
'copy_user' => 'user:copy',
|
||||
'fetch_plugins' => 'plugin:fetch',
|
||||
'fix_mediumint' => 'database:fix-mediumint',
|
||||
'float_rrdfiles' => 'rrd:float-files',
|
||||
'genkey' => 'security:generate-key',
|
||||
'genmanifest' => 'package:generate-manifest',
|
||||
'host_update_template' => 'device:update-template',
|
||||
'import_package' => 'package:import',
|
||||
'import_template' => 'template:import',
|
||||
'input_whitelist' => 'security:input-whitelist',
|
||||
'install_cacti' => 'system:install',
|
||||
'md5sum' => 'integrity:md5',
|
||||
'migrate_poller' => 'poller:migrate',
|
||||
'plugin_manage' => 'plugin:manage',
|
||||
'poller_data_sources_reapply_names' => 'poller:data-sources:reapply-names',
|
||||
'poller_graphs_reapply_names' => 'poller:graphs:reapply-names',
|
||||
'poller_output_empty' => 'poller:clear-empty-output',
|
||||
'poller_reindex_hosts' => 'poller:reindex',
|
||||
'poller_replicate' => 'poller:replicate',
|
||||
'push_out_hosts' => 'poller:push-hosts',
|
||||
'rebuild_poller_cache' => 'poller:rebuild-cache',
|
||||
'refresh_csrf' => 'security:refresh-csrf',
|
||||
'remove_broken_graphs' => 'graph:remove-broken',
|
||||
'remove_device' => 'device:remove',
|
||||
'remove_graphs' => 'graph:remove',
|
||||
'removespikes' => 'rrd:remove-spikes',
|
||||
'reorder_data_query' => 'data-query:reorder',
|
||||
'repair_database' => 'database:repair',
|
||||
'repair_graphs' => 'graph:repair',
|
||||
'repair_templates' => 'template:repair',
|
||||
'rrdresize' => 'rrd:resize',
|
||||
'show_perms' => 'permissions:show',
|
||||
'splice_rrd' => 'rrd:splice',
|
||||
'sqltable_to_php' => 'developer:sql-table-to-php',
|
||||
'structure_rra_paths' => 'rrd:structure-paths',
|
||||
'update_heartbeat' => 'rrd:update-heartbeat',
|
||||
'upgrade_database' => 'database:upgrade',
|
||||
'version' => 'system:version',
|
||||
];
|
||||
|
||||
/** @return array<string, string> Legacy script basename to command name. */
|
||||
public static function commands(): array {
|
||||
return self::COMMANDS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<phpunit bootstrap="tests/bootstrap-unit.php" beStrictAboutOutputDuringTests="false">
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<env name="CACTI_TEST_BOOTSTRAP" value="1"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">lib/Console</directory>
|
||||
</include>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuite name="CLI coverage">
|
||||
<file>tests/Unit/Core/Cli/CactiApplicationTest.php</file>
|
||||
<file>tests/Unit/Core/Cli/CactiEntrypointSecurityTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,10 @@
|
||||
# Apache 2.4
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
|
||||
# Apache 2.2
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -0,0 +1,438 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| of the License, or (at your option) any later version. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use Cacti\Console\CactiApplication;
|
||||
use Cacti\Console\Command\LegacyScriptCommand;
|
||||
use Cacti\Console\Input\RawArgvInput;
|
||||
use Cacti\Console\LegacyCommandMap;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Tester\ApplicationTester;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\Exception\LogicException as ProcessLogicException;
|
||||
use Symfony\Component\Process\Exception\ProcessSignaledException;
|
||||
|
||||
final class CactiControllableLegacyProcess extends Process {
|
||||
public bool $tty_enabled = false;
|
||||
public bool $output_disabled = false;
|
||||
public bool $running = false;
|
||||
public int|null $received_signal = null;
|
||||
|
||||
public function __construct(private readonly int $result = 0, private readonly int|null $term_signal = null) {
|
||||
parent::__construct(['true']);
|
||||
}
|
||||
|
||||
public function setTimeout(float|null $timeout): static {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTty(bool $tty): static {
|
||||
$this->tty_enabled = $tty;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function disableOutput(): static {
|
||||
$this->output_disabled = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(callable|null $callback = null, array $env = []): int {
|
||||
if ($this->term_signal !== null) {
|
||||
throw new ProcessSignaledException($this);
|
||||
}
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function getTermSignal(): int {
|
||||
return $this->term_signal ?? 0;
|
||||
}
|
||||
|
||||
public function isRunning(): bool {
|
||||
return $this->running;
|
||||
}
|
||||
|
||||
public function signal(int $signal): static {
|
||||
if (!$this->running) {
|
||||
throw new ProcessLogicException('Cannot send signal on a non running process.');
|
||||
}
|
||||
$this->received_signal = $signal;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
it('registers every executable legacy CLI script exactly once', function (): void {
|
||||
$root = dirname(__DIR__, 4);
|
||||
$non_commands = ['cacti', 'index'];
|
||||
$scripts = array_map(
|
||||
static fn (string $path): string => basename($path, '.php'),
|
||||
glob($root . '/cli/*.php') ?: []
|
||||
);
|
||||
|
||||
$mapped = array_keys(LegacyCommandMap::commands());
|
||||
$missing = array_values(array_diff($mapped, $scripts));
|
||||
$unexpected = array_values(array_diff($scripts, $mapped, $non_commands));
|
||||
|
||||
expect($missing)->toBe([], 'Every mapped command must name an existing cli/*.php script.')
|
||||
->and($unexpected)->toBe([], 'Classify new non-command files in $non_commands, or add commands to LegacyCommandMap.')
|
||||
->and(array_unique(array_values(LegacyCommandMap::commands())))
|
||||
->toHaveCount(count($mapped));
|
||||
});
|
||||
|
||||
it('lists commands without bootstrapping the database', function (): void {
|
||||
$root = dirname(__DIR__, 4);
|
||||
$application = new CactiApplication($root);
|
||||
$application->setAutoExit(false);
|
||||
$tester = new ApplicationTester($application);
|
||||
|
||||
$status = $tester->run(['command' => 'list', '--format' => 'json']);
|
||||
$commands = json_decode($tester->getDisplay(), true, flags: JSON_THROW_ON_ERROR)['commands'];
|
||||
$names = array_column($commands, 'name');
|
||||
|
||||
expect($status)->toBe(0)
|
||||
->and($names)->toContain('device:add', 'database:audit', 'poller:reindex', 'rrd:resize');
|
||||
});
|
||||
|
||||
it('reports an unknown version when the version file is unavailable', function (): void {
|
||||
$application = new CactiApplication('/path/that/does/not/exist');
|
||||
|
||||
expect($application->getVersion())->toBe('unknown');
|
||||
});
|
||||
|
||||
it('leaves unknown commands to Symfony after legacy lookup fails', function (): void {
|
||||
$application = new CactiApplication(dirname(__DIR__, 4));
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
|
||||
expect(fn (): int => $application->run(new RawArgvInput(['bin/cacti', 'not:a:command']), new BufferedOutput()))
|
||||
->toThrow(Symfony\Component\Console\Exception\CommandNotFoundException::class);
|
||||
});
|
||||
|
||||
it('hands a terminal directly to a legacy process', function (): void {
|
||||
$process = new CactiControllableLegacyProcess(17);
|
||||
$command = new LegacyScriptCommand(
|
||||
'probe:run',
|
||||
'probe',
|
||||
dirname(__DIR__, 3) . '/fixtures/Console',
|
||||
static fn (array $command): Process => $process,
|
||||
static fn (): bool => true,
|
||||
false
|
||||
);
|
||||
|
||||
expect($command->run(new RawArgvInput(['bin/cacti', 'probe:run']), new BufferedOutput()))->toBe(17)
|
||||
->and($process->tty_enabled)->toBeTrue()
|
||||
->and($process->output_disabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('maps a signaled legacy process to the shell exit status', function (): void {
|
||||
$process = new CactiControllableLegacyProcess(term_signal: SIGKILL);
|
||||
$command = new LegacyScriptCommand(
|
||||
'probe:run',
|
||||
'probe',
|
||||
dirname(__DIR__, 3) . '/fixtures/Console',
|
||||
static fn (array $command): Process => $process,
|
||||
static fn (): bool => false,
|
||||
false
|
||||
);
|
||||
|
||||
expect($command->run(new RawArgvInput(['bin/cacti', 'probe:run']), new BufferedOutput()))->toBe(128 + SIGKILL);
|
||||
});
|
||||
|
||||
it('relays registered termination signals only while the child is running', function (): void {
|
||||
$process = new CactiControllableLegacyProcess();
|
||||
$handlers = [];
|
||||
$command = new LegacyScriptCommand(
|
||||
'probe:run',
|
||||
'probe',
|
||||
dirname(__DIR__, 3) . '/fixtures/Console',
|
||||
static fn (array $command): Process => $process,
|
||||
static fn (): bool => false,
|
||||
static function (int $signal, callable $handler) use (&$handlers): void {
|
||||
$handlers[$signal] = $handler;
|
||||
}
|
||||
);
|
||||
|
||||
$command->run(new RawArgvInput(['bin/cacti', 'probe:run']), new BufferedOutput());
|
||||
$process->running = true;
|
||||
$handlers[SIGTERM](SIGTERM);
|
||||
|
||||
expect($process->received_signal)->toBe(SIGTERM)
|
||||
->and($handlers)->toHaveKeys([SIGTERM, SIGINT, SIGHUP]);
|
||||
|
||||
$process->running = false;
|
||||
$process->received_signal = null;
|
||||
$handlers[SIGINT](SIGINT);
|
||||
|
||||
expect($process->received_signal)->toBeNull();
|
||||
});
|
||||
|
||||
it('preserves the child status when a signal arrives before process startup', function (): void {
|
||||
$process = new CactiControllableLegacyProcess(17);
|
||||
$command = new LegacyScriptCommand(
|
||||
'probe:run',
|
||||
'probe',
|
||||
dirname(__DIR__, 3) . '/fixtures/Console',
|
||||
static fn (array $command): Process => $process,
|
||||
static fn (): bool => false,
|
||||
static function (int $signal, callable $handler): void {
|
||||
if ($signal === SIGTERM) {
|
||||
$handler($signal);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect($command->run(new RawArgvInput(['bin/cacti', 'probe:run']), new BufferedOutput()))->toBe(17)
|
||||
->and($process->received_signal)->toBeNull();
|
||||
});
|
||||
|
||||
it('forwards raw legacy arguments and preserves the exit status', function (): void {
|
||||
$root = dirname(__DIR__, 3) . '/fixtures/Console';
|
||||
$application = new Application('test');
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', $root));
|
||||
$input = new RawArgvInput(['bin/cacti', 'probe:run', '--arbitrary=value', '-x', 'plain']);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$status = $application->run($input, $output);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($output->fetch())->toBe('["--arbitrary=value","-x","plain"]probe-error');
|
||||
});
|
||||
|
||||
it('routes child stderr separately for console outputs and accepts aliases', function (): void {
|
||||
$root = dirname(__DIR__, 3) . '/fixtures/Console';
|
||||
$application = new Application('test');
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', $root));
|
||||
$output = new ConsoleOutput(decorated: false);
|
||||
$errorOutput = new BufferedOutput();
|
||||
$output->setErrorOutput($errorOutput);
|
||||
|
||||
$status = $application->run(
|
||||
new RawArgvInput(['bin/cacti', 'probe', 'alias-value']),
|
||||
$output
|
||||
);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($errorOutput->fetch())->toBe('probe-error');
|
||||
});
|
||||
|
||||
it('forwards arguments when the command name is abbreviated', function (): void {
|
||||
$root = dirname(__DIR__, 3) . '/fixtures/Console';
|
||||
$application = new Application('test');
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', $root));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$status = $application->run(
|
||||
new RawArgvInput(['bin/cacti', 'pro:r', '--arbitrary=value', 'plain']),
|
||||
$output
|
||||
);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($output->fetch())->toBe('["--arbitrary=value","plain"]probe-error');
|
||||
});
|
||||
|
||||
it('leaves --help, -h, --version and -V to the legacy script', function (string $flag): void {
|
||||
$application = new CactiApplication(dirname(__DIR__, 4));
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', dirname(__DIR__, 3) . '/fixtures/Console'));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$status = $application->run(new RawArgvInput(['bin/cacti', 'probe:run', $flag]), $output);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($output->fetch())->toBe(sprintf('["%s"]probe-error', $flag));
|
||||
})->with(['--help', '-h', '--version', '-V']);
|
||||
|
||||
it('forwards a flag written before the command name', function (string $flag): void {
|
||||
$application = new CactiApplication(dirname(__DIR__, 4));
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', dirname(__DIR__, 3) . '/fixtures/Console'));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
// getFirstArgument() skips leading options, so the command dispatches with
|
||||
// the flag ahead of its name. Slicing only forward handed the script an
|
||||
// empty argv, and a script whose flags are all optional then ran its
|
||||
// default path.
|
||||
$status = $application->run(new RawArgvInput(['bin/cacti', $flag, 'probe:run']), $output);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($output->fetch())->toBe(sprintf('["%s"]probe-error', $flag));
|
||||
})->with(['--help', '-h', '--version', '-V']);
|
||||
|
||||
it('preserves the order of flags on both sides of the command name', function (): void {
|
||||
$application = new CactiApplication(dirname(__DIR__, 4));
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', dirname(__DIR__, 3) . '/fixtures/Console'));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$status = $application->run(
|
||||
new RawArgvInput(['bin/cacti', '-v', 'probe:run', '--arbitrary=value', 'plain']),
|
||||
$output
|
||||
);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($output->fetch())->toBe('["-v","--arbitrary=value","plain"]probe-error');
|
||||
});
|
||||
|
||||
it('still answers --version and --help for the application itself', function (): void {
|
||||
$application = new CactiApplication(dirname(__DIR__, 4));
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$output = new BufferedOutput();
|
||||
|
||||
expect($application->run(new RawArgvInput(['bin/cacti', '--version']), $output))->toBe(0)
|
||||
->and($output->fetch())->toStartWith('Cacti');
|
||||
});
|
||||
|
||||
it('does not swallow legacy output when -q is passed through', function (): void {
|
||||
$application = new CactiApplication(dirname(__DIR__, 4));
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('probe:run', 'probe', dirname(__DIR__, 3) . '/fixtures/Console'));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$status = $application->run(new RawArgvInput(['bin/cacti', 'probe:run', '-q', 'plain']), $output);
|
||||
|
||||
expect($status)->toBe(23)
|
||||
->and($output->fetch())->toBe('["-q","plain"]probe-error');
|
||||
});
|
||||
|
||||
it('streams large legacy output without retaining it in the parent', function (): void {
|
||||
$root = dirname(__DIR__, 3) . '/fixtures/Console';
|
||||
$application = new Application('test');
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('bulk:run', 'bulk', $root));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$status = $application->run(new RawArgvInput(['bin/cacti', 'bulk:run', '20000']), $output);
|
||||
|
||||
// 20000 lines is past the 1MB php://temp threshold Process spools at.
|
||||
expect($status)->toBe(0)
|
||||
->and(strlen($output->fetch()))->toBe(20000 * 100);
|
||||
});
|
||||
|
||||
it('reports a killed legacy script as the shell does', function (): void {
|
||||
if (!function_exists('pcntl_fork') || !function_exists('posix_kill') || !defined('SIGKILL')) {
|
||||
$this->markTestSkipped('pcntl and posix signal support are required.');
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__, 3) . '/fixtures/Console';
|
||||
$runner = dirname(__DIR__, 3) . '/fixtures/Console/kill_runner.php';
|
||||
|
||||
$descriptor = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||
$process = proc_open([PHP_BINARY, $runner, $root], $descriptor, $pipes);
|
||||
|
||||
expect($process)->toBeResource();
|
||||
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_close($process);
|
||||
|
||||
// 128 + SIGKILL, the status a shell reports for the same death.
|
||||
expect(trim($stdout))->toBe('137')
|
||||
->and($stderr)->not->toContain('ProcessSignaledException');
|
||||
});
|
||||
|
||||
it('runs the legacy script in the directory the caller was in', function (): void {
|
||||
$root = dirname(__DIR__, 3) . '/fixtures/Console';
|
||||
$original = getcwd();
|
||||
$elsewhere = sys_get_temp_dir();
|
||||
|
||||
try {
|
||||
chdir($elsewhere);
|
||||
|
||||
$application = new Application('test');
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('pwd:run', 'pwd', $root));
|
||||
$output = new BufferedOutput();
|
||||
|
||||
$application->run(new RawArgvInput(['bin/cacti', 'pwd:run']), $output);
|
||||
|
||||
// A relative path argument has to mean what it would have meant to
|
||||
// `php cli/<script>.php` from the same shell.
|
||||
expect(realpath(trim($output->fetch())))->toBe(realpath($elsewhere));
|
||||
} finally {
|
||||
chdir((string) $original);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects parsed inputs that cannot preserve the raw argument vector', function (): void {
|
||||
$command = new LegacyScriptCommand('probe:run', 'probe', dirname(__DIR__, 3) . '/fixtures/Console');
|
||||
|
||||
expect(fn () => $command->run(new ArrayInput([]), new BufferedOutput()))
|
||||
->toThrow(LogicException::class, 'Legacy commands require RawArgvInput.');
|
||||
});
|
||||
|
||||
it('fails closed when no command token is present', function (): void {
|
||||
$input = new RawArgvInput(['bin/cacti', '--version']);
|
||||
|
||||
expect(fn (): array => $input->argumentsAfterCommand(['probe:run', 'probe']))
|
||||
->toThrow(LogicException::class, 'resolved command token is missing');
|
||||
});
|
||||
|
||||
it('uses the server argument vector when one is not supplied', function (): void {
|
||||
$original = $_SERVER['argv'] ?? null;
|
||||
$_SERVER['argv'] = ['bin/cacti', 'probe:run', 'server-value'];
|
||||
|
||||
try {
|
||||
$input = new RawArgvInput();
|
||||
expect($input->argumentsAfterCommand(['probe:run']))->toBe(['server-value']);
|
||||
} finally {
|
||||
if ($original === null) {
|
||||
unset($_SERVER['argv']);
|
||||
} else {
|
||||
$_SERVER['argv'] = $original;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('runs the installed CLI entry point without application bootstrap', function (): void {
|
||||
$root = dirname(__DIR__, 4);
|
||||
$process = new Process([PHP_BINARY, $root . '/bin/cacti', 'list', '--raw']);
|
||||
|
||||
expect($process->run())->toBe(0)
|
||||
->and($process->getOutput())->toContain('device:add')
|
||||
->and($process->getOutput())->toContain('system:version');
|
||||
});
|
||||
|
||||
it('fails clearly when the CLI entry point has no installed dependencies', function (): void {
|
||||
$fixture = sys_get_temp_dir() . '/cacti-cli-no-deps-' . bin2hex(random_bytes(8));
|
||||
mkdir($fixture . '/bin', 0777, true);
|
||||
mkdir($fixture . '/include', 0777, true);
|
||||
copy(dirname(__DIR__, 4) . '/bin/cacti', $fixture . '/bin/cacti');
|
||||
copy(dirname(__DIR__, 4) . '/include/cli_only.php', $fixture . '/include/cli_only.php');
|
||||
$process = new Process([PHP_BINARY, $fixture . '/bin/cacti']);
|
||||
|
||||
expect($process->run())->toBe(1)
|
||||
->and($process->getErrorOutput())->toContain('Cacti dependencies are not installed');
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
test('the unified CLI entrypoint loads the CLI-only guard before autoloading', function () : void {
|
||||
$source = file_get_contents(dirname(__DIR__, 4) . '/bin/cacti');
|
||||
|
||||
expect($source)->not->toBeFalse()
|
||||
->and($source)->toContain("require_once \$root . '/include/cli_only.php';");
|
||||
|
||||
if ($source === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(strpos($source, 'include/cli_only.php'))->toBeLessThan(strpos($source, 'require $autoload'));
|
||||
});
|
||||
|
||||
test('the CLI-only guard rejects non-CLI SAPIs before application bootstrap', function () : void {
|
||||
$source = file_get_contents(dirname(__DIR__, 4) . '/include/cli_only.php');
|
||||
|
||||
expect($source)->not->toBeFalse()
|
||||
->and($source)->toContain("PHP_SAPI !== 'cli'")
|
||||
->and($source)->toContain('http_response_code(403)')
|
||||
->and($source)->toContain('exit(1)');
|
||||
});
|
||||
|
||||
test('the bin directory denies web access under supported Apache versions', function () : void {
|
||||
$source = file_get_contents(dirname(__DIR__, 4) . '/bin/.htaccess');
|
||||
|
||||
expect($source)->not->toBeFalse()
|
||||
->and($source)->toContain('Require all denied')
|
||||
->and($source)->toContain('Order Allow,Deny')
|
||||
->and($source)->toContain('Deny from all');
|
||||
});
|
||||
|
||||
test('every executable Console fixture loads the CLI-only guard', function () : void {
|
||||
$fixtures = glob(dirname(__DIR__, 3) . '/fixtures/Console/cli/*.php');
|
||||
|
||||
expect($fixtures)->not->toBeFalse()->not->toBeEmpty();
|
||||
|
||||
foreach ($fixtures ?: [] as $fixture) {
|
||||
expect(file_get_contents($fixture))->toContain("require_once dirname(__DIR__, 4) . '/include/cli_only.php';");
|
||||
}
|
||||
});
|
||||
|
||||
test('the tests directory denies browsing and direct Apache access', function () : void {
|
||||
$root = dirname(__DIR__, 4);
|
||||
$deny = file_get_contents($root . '/tests/.htaccess');
|
||||
$redirect = file_get_contents($root . '/tests/index.php');
|
||||
|
||||
expect($deny)->not->toBeFalse()
|
||||
->and($deny)->toContain('Require all denied')
|
||||
->and($deny)->toContain('Deny from all')
|
||||
->and($redirect)->not->toBeFalse()
|
||||
->and($redirect)->toContain("header('Location:../index.php')");
|
||||
});
|
||||
|
||||
test('TTY handoff requires stderr to remain attached to the terminal', function () : void {
|
||||
$source = file_get_contents(dirname(__DIR__, 4) . '/lib/Console/Command/LegacyScriptCommand.php');
|
||||
|
||||
expect($source)->not->toBeFalse()
|
||||
->and($source)->toContain("stream_isatty(STDERR)");
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
test('rrdresize reports a missing required data template as failure', function () : void {
|
||||
$source = file_get_contents(dirname(__DIR__, 4) . '/cli/rrdresize.php');
|
||||
expect($source)->not->toBeFalse();
|
||||
|
||||
if ($source === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$start = strpos($source, 'if (!$data_template_id)');
|
||||
expect($start)->not->toBeFalse();
|
||||
|
||||
if ($start === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = substr($source, $start, 220);
|
||||
|
||||
expect($body)->toContain('exit(1);')
|
||||
->not->toContain('exit(0);');
|
||||
});
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 4) . '/include/cli_only.php';
|
||||
|
||||
$lines = (int) ($argv[1] ?? 1);
|
||||
|
||||
for ($i = 0; $i < $lines; $i++) {
|
||||
fwrite(STDOUT, str_repeat('x', 99) . "\n");
|
||||
}
|
||||
|
||||
exit(0);
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 4) . '/include/cli_only.php';
|
||||
|
||||
fwrite(STDOUT, json_encode(array_slice($argv, 1), JSON_THROW_ON_ERROR));
|
||||
fwrite(STDERR, 'probe-error');
|
||||
|
||||
exit(23);
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 4) . '/include/cli_only.php';
|
||||
|
||||
fwrite(STDOUT, (string) getcwd());
|
||||
|
||||
exit(0);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
require_once dirname(__DIR__, 4) . '/include/cli_only.php';
|
||||
|
||||
/* Publish the pid so the runner can signal exactly this process, then wait
|
||||
* long enough to be killed but not long enough to stall a suite if it is not. */
|
||||
if (isset($argv[1])) {
|
||||
file_put_contents($argv[1], (string) getmypid());
|
||||
}
|
||||
|
||||
fwrite(STDOUT, "ready\n");
|
||||
sleep(5);
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/*
|
||||
* Runs a legacy command whose child is killed mid-flight, and prints the status
|
||||
* LegacyScriptCommand reports. Its own process because the kill has to land on
|
||||
* a child this script owns.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/include/vendor/autoload.php';
|
||||
|
||||
use Cacti\Console\Command\LegacyScriptCommand;
|
||||
use Cacti\Console\Input\RawArgvInput;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
$root = $argv[1];
|
||||
$pidfile = tempnam(sys_get_temp_dir(), 'cacti-sleeper-');
|
||||
|
||||
$application = new Application('test');
|
||||
$application->setAutoExit(false);
|
||||
$application->setCatchExceptions(false);
|
||||
$application->add(new LegacyScriptCommand('sleeper:run', 'sleeper', $root));
|
||||
|
||||
if (function_exists('pcntl_fork') && function_exists('posix_kill')) {
|
||||
$pid = pcntl_fork();
|
||||
|
||||
if ($pid === 0) {
|
||||
/* Signal the pid the child published, never a pattern match: pgrep
|
||||
* would reach every matching process on the host, so two suite runs on
|
||||
* one runner would kill each other's children. */
|
||||
$deadline = microtime(true) + 5;
|
||||
$target = 0;
|
||||
|
||||
while ($target === 0 && microtime(true) < $deadline) {
|
||||
$published = is_file($pidfile) ? trim((string) file_get_contents($pidfile)) : '';
|
||||
|
||||
if ($published !== '') {
|
||||
$target = (int) $published;
|
||||
} else {
|
||||
usleep(20000);
|
||||
}
|
||||
}
|
||||
|
||||
if ($target > 0) {
|
||||
posix_kill($target, SIGKILL);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
$status = $application->run(new RawArgvInput(['bin/cacti', 'sleeper:run', $pidfile]), new BufferedOutput());
|
||||
|
||||
@unlink($pidfile);
|
||||
|
||||
print $status . "\n";
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
header('Location:../index.php');
|
||||
@@ -604,6 +604,7 @@ header_redirect ./support.php header('Location: support.php?tab=summary');
|
||||
header_redirect ./templates_export.php header('Location: templates_export.php');
|
||||
header_redirect ./templates_import.php header('Location: templates_import.php');
|
||||
header_redirect ./templates_import.php header('Location: templates_import.php');
|
||||
header_redirect ./tests/index.php header('Location:../index.php');
|
||||
header_redirect ./tree.php header('Location: tree.php');
|
||||
header_redirect ./tree.php header('Location: tree.php');
|
||||
header_redirect ./tree.php header('Location: tree.php');
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 22 and column 251.
|
Reference in new issue
Block a user