Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
Comment on lines +1 to +8
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependabot is configured for the npm ecosystem, but this repository has no package.json / npm dependencies. With the new composer.json, it would be more accurate to switch this entry to the composer ecosystem (and keep github-actions if desired) to avoid dependabot configuration errors/no-op updates.

Copilot uses AI. Check for mistakes.
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
18 changes: 18 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "cacti/plugin_flowview",
"description": "plugin_flowview plugin for Cacti",
"license": "GPL-2.0-or-later",
"require-dev": {
"pestphp/pest": "^1.23"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"autoload-dev": {
"files": [
"tests/bootstrap.php"
]
}
}
14 changes: 14 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Pest configuration file.
*/

require_once __DIR__ . '/bootstrap.php';
103 changes: 103 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify plugin source files do not use PHP 8.0+ syntax.
* Cacti 1.2.x plugins must remain compatible with PHP 7.4.
*/

describe('PHP 7.4 compatibility in flowview', function () {
$files = array(
'flowview.php',
'flowview_devices.php',
'flowview_filters.php',
'flowview_schedules.php',
'setup.php',
);

it('does not use str_contains (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}

expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_contains() which requires PHP 8.0"
);
}
});

it('does not use str_starts_with (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}

expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_starts_with() which requires PHP 8.0"
);
}
});

it('does not use str_ends_with (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}

expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_ends_with() which requires PHP 8.0"
);
}
});

it('does not use nullsafe operator (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}

Comment on lines +24 to +97
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each loop continues when a target file is missing/unreadable, which can produce false-green results (compatibility constraints aren’t actually enforced for that file). Consider asserting that realpath() and file_get_contents() succeed for every expected plugin file so the test fails if it can’t validate the source.

Suggested change
it('does not use str_contains (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) {
continue;
}
$contents = file_get_contents($path);
if ($contents === false) {
continue;
}
expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_contains() which requires PHP 8.0"
);
}
});
it('does not use str_starts_with (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) {
continue;
}
$contents = file_get_contents($path);
if ($contents === false) {
continue;
}
expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_starts_with() which requires PHP 8.0"
);
}
});
it('does not use str_ends_with (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) {
continue;
}
$contents = file_get_contents($path);
if ($contents === false) {
continue;
}
expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_ends_with() which requires PHP 8.0"
);
}
});
it('does not use nullsafe operator (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) {
continue;
}
$contents = file_get_contents($path);
if ($contents === false) {
continue;
}
$readPluginFile = function (string $relativeFile): string {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) {
throw new \RuntimeException("Failed to resolve expected plugin file: {$relativeFile}");
}
$contents = file_get_contents($path);
if ($contents === false) {
throw new \RuntimeException("Failed to read expected plugin file: {$relativeFile}");
}
return $contents;
};
it('does not use str_contains (PHP 8.0)', function () use ($files, $readPluginFile) {
foreach ($files as $relativeFile) {
$contents = $readPluginFile($relativeFile);
expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_contains() which requires PHP 8.0"
);
}
});
it('does not use str_starts_with (PHP 8.0)', function () use ($files, $readPluginFile) {
foreach ($files as $relativeFile) {
$contents = $readPluginFile($relativeFile);
expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_starts_with() which requires PHP 8.0"
);
}
});
it('does not use str_ends_with (PHP 8.0)', function () use ($files, $readPluginFile) {
foreach ($files as $relativeFile) {
$contents = $readPluginFile($relativeFile);
expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_ends_with() which requires PHP 8.0"
);
}
});
it('does not use nullsafe operator (PHP 8.0)', function () use ($files, $readPluginFile) {
foreach ($files as $relativeFile) {
$contents = $readPluginFile($relativeFile);

Copilot uses AI. Check for mistakes.
expect(preg_match('/\?->/', $contents))->toBe(0,
"{$relativeFile} uses nullsafe operator which requires PHP 8.0"
);
}
});
});
61 changes: 61 additions & 0 deletions tests/Security/PreparedStatementConsistencyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify migrated files use prepared DB helpers exclusively.
* Catches regressions where raw db_execute/db_fetch_* calls creep back in.
*/

describe('prepared statement consistency in flowview', function () {
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = array(
'flowview.php',
'flowview_devices.php',
'flowview_filters.php',
'flowview_schedules.php',
'setup.php',
Comment on lines +16 to +22
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup.php is included in $targetFiles, but the current setup.php in this repo contains raw db_fetch_cell( / db_execute( calls (e.g. setup.php:110, setup.php:324, setup.php:1024). This test will fail immediately under vendor/bin/pest even without any regressions. Either (a) migrate those calls to prepared variants / wrappers, or (b) narrow the check (e.g. exclude setup.php and/or ignore DDL-only db_execute() usage) so the new test suite is green on the current codebase.

Suggested change
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = array(
'flowview.php',
'flowview_devices.php',
'flowview_filters.php',
'flowview_schedules.php',
'setup.php',
it('uses prepared DB helpers in all migrated plugin files', function () {
$targetFiles = array(
'flowview.php',
'flowview_devices.php',
'flowview_filters.php',
'flowview_schedules.php',

Copilot uses AI. Check for mistakes.
);

$rawPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)\s*\(/';
$preparedPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)_prepared\s*\(/';

foreach ($targetFiles as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}
Comment on lines +31 to +39
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These continue branches mean the test can pass while silently skipping missing/unreadable files (e.g. if the repo layout changes or a file is deleted), which undermines the purpose of a regression/security check. Prefer failing fast with an assertion when realpath() or file_get_contents() fails so the test suite can’t go green without actually validating the files.

Suggested change
if ($path === false) {
continue;
}
$contents = file_get_contents($path);
if ($contents === false) {
continue;
}
expect($path)->not->toBeFalse(
"Failed to resolve expected plugin file: {$relativeFile}"
);
$contents = file_get_contents($path);
expect($contents)->not->toBeFalse(
"Failed to read expected plugin file: {$relativeFile}"
);

Copilot uses AI. Check for mistakes.

$lines = explode("\n", $contents);
$rawCallsOutsideComments = 0;

foreach ($lines as $line) {
$trimmed = ltrim($line);

if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0 || strpos($trimmed, '#') === 0) {
continue;
}

if (preg_match($rawPattern, $line) && !preg_match($preparedPattern, $line)) {
$rawCallsOutsideComments++;
}
}

Comment on lines +41 to +55
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line-based comment skipping here is not robust: block comments starting with /* (or inline /* ... */) and string literals containing db_execute( will still be scanned and can cause false positives. Consider tokenizing with token_get_all() and ignoring T_COMMENT, T_DOC_COMMENT, and T_CONSTANT_ENCAPSED_STRING tokens before applying the regex, to make this test stable.

Suggested change
$lines = explode("\n", $contents);
$rawCallsOutsideComments = 0;
foreach ($lines as $line) {
$trimmed = ltrim($line);
if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0 || strpos($trimmed, '#') === 0) {
continue;
}
if (preg_match($rawPattern, $line) && !preg_match($preparedPattern, $line)) {
$rawCallsOutsideComments++;
}
}
$tokens = token_get_all($contents);
$sanitizedContents = '';
foreach ($tokens as $token) {
if (is_array($token)) {
$tokenId = $token[0];
$tokenText = $token[1];
if ($tokenId === T_COMMENT || $tokenId === T_DOC_COMMENT || $tokenId === T_CONSTANT_ENCAPSED_STRING) {
$sanitizedContents .= preg_replace('/[^\r\n]/', ' ', $tokenText);
} else {
$sanitizedContents .= $tokenText;
}
} else {
$sanitizedContents .= $token;
}
}
preg_match_all($rawPattern, $sanitizedContents, $matches);
$rawCallsOutsideComments = count($matches[0]);

Copilot uses AI. Check for mistakes.
expect($rawCallsOutsideComments)->toBe(0,
"File {$relativeFile} contains raw (unprepared) DB calls"
);
}
});
});
36 changes: 36 additions & 0 deletions tests/Security/SetupStructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify setup.php defines required plugin hooks and info function.
*/

describe('flowview setup.php structure', function () {
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));

Comment on lines +15 to +16
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realpath() can return false and file_get_contents() can return false, which will emit warnings and then make the subsequent toContain() assertions fail with an unhelpful type/value. Add explicit assertions that the path exists and the file contents were read successfully (fail fast with a clear message) before running the string checks.

Suggested change
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));
$setupPath = realpath(__DIR__ . '/../../setup.php');
\PHPUnit\Framework\Assert::assertNotFalse($setupPath, 'Failed to resolve setup.php path.');
$source = file_get_contents($setupPath);
\PHPUnit\Framework\Assert::assertNotFalse($source, 'Failed to read setup.php contents.');

Copilot uses AI. Check for mistakes.
it('defines plugin_flowview_install function', function () use ($source) {
expect($source)->toContain('function plugin_flowview_install');
});

it('defines plugin_flowview_version function', function () use ($source) {
expect($source)->toContain('function plugin_flowview_version');
});

it('defines plugin_flowview_uninstall function', function () use ($source) {
expect($source)->toContain('function plugin_flowview_uninstall');
});

it('returns version array with name key', function () use ($source) {
expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/');
});

it('returns version array with version key', function () use ($source) {
expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/');
});
});
Loading
Loading