Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
# +-------------------------------------------------------------------------+

locales/po/*.mo
.omc/
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_thold",
"description": "plugin_thold 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';
107 changes: 107 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?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.
*/
Comment on lines +10 to +13
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.

This test hard-codes a requirement that the plugin must remain compatible with PHP 7.4, but the repo docs/changelog indicate ongoing PHP 8.1+ support (e.g., .github/copilot-instructions.md:10 and CHANGELOG entries about PHP 8.1.2). If the supported minimum PHP version is 8.1+, this test will block legitimate use of PHP 8 features; please align the minimum-version assertions and the file/comment naming with the project's actual support policy.

Copilot uses AI. Check for mistakes.

describe('PHP 7.4 compatibility in thold', function () {
$files = array(
'includes/database.php',
'includes/polling.php',
'includes/settings.php',
'notify_lists.php',
'notify_queue.php',
'poller_thold.php',
'setup.php',
'thold.php',
'thold_graph.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;
}

expect(preg_match('/\?->/', $contents))->toBe(0,
"{$relativeFile} uses nullsafe operator which requires PHP 8.0"
);
}
});
});
65 changes: 65 additions & 0 deletions tests/Security/PreparedStatementConsistencyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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 thold', function () {
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = array(
'includes/database.php',
'includes/polling.php',
'includes/settings.php',
'notify_lists.php',
'notify_queue.php',
'poller_thold.php',
'setup.php',
'thold.php',
'thold_graph.php',
);
Comment on lines +16 to +27
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.

This test will currently fail because several files in $targetFiles contain non-*_prepared DB helper calls (e.g., notify_lists.php contains db_fetch_assoc(...) at line ~1078). Either narrow $targetFiles to only the migrated/hardened files, or change the assertion to enforce prepared statements only for queries that include dynamic/user-supplied values (instead of banning all db_fetch_* calls).

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 +35 to +43
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 test silently continues when a file path can't be resolved or read. That can make the suite pass while skipping checks (e.g., if a file gets renamed/removed). Consider failing the test when realpath() or file_get_contents() returns false so missing/unreadable target files are surfaced as failures.

Suggested change
if ($path === false) {
continue;
}
$contents = file_get_contents($path);
if ($contents === false) {
continue;
}
expect($path)->not->toBeFalse("Failed to resolve target file path: {$relativeFile}");
$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Failed to read target 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++;
}
}

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('thold setup.php structure', function () {
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));

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

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

it('defines plugin_thold_uninstall function', function () use ($source) {
expect($source)->toContain('function plugin_thold_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) {
Comment on lines +15 to +33
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.

$source = file_get_contents(realpath(...)) will throw a TypeError on PHP 8+ if realpath() returns false (e.g., unexpected directory layout), causing a hard error instead of a clean test failure. Consider asserting that the path resolves and the file contents are readable before running the toContain/toMatch assertions.

Suggested change
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));
it('defines plugin_thold_install function', function () use ($source) {
expect($source)->toContain('function plugin_thold_install');
});
it('defines plugin_thold_version function', function () use ($source) {
expect($source)->toContain('function plugin_thold_version');
});
it('defines plugin_thold_uninstall function', function () use ($source) {
expect($source)->toContain('function plugin_thold_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) {
$read_setup_source = function (): string {
$setup_path = realpath(__DIR__ . '/../../setup.php');
expect($setup_path)->not->toBeFalse();
if ($setup_path === false) {
return '';
}
expect(is_readable($setup_path))->toBeTrue();
$source = file_get_contents($setup_path);
expect($source)->not->toBeFalse();
return ($source === false) ? '' : $source;
};
it('defines plugin_thold_install function', function () use ($read_setup_source) {
$source = $read_setup_source();
expect($source)->toContain('function plugin_thold_install');
});
it('defines plugin_thold_version function', function () use ($read_setup_source) {
$source = $read_setup_source();
expect($source)->toContain('function plugin_thold_version');
});
it('defines plugin_thold_uninstall function', function () use ($read_setup_source) {
$source = $read_setup_source();
expect($source)->toContain('function plugin_thold_uninstall');
});
it('returns version array with name key', function () use ($read_setup_source) {
$source = $read_setup_source();
expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/');
});
it('returns version array with version key', function () use ($read_setup_source) {
$source = $read_setup_source();

Copilot uses AI. Check for mistakes.
expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/');
});
});
Loading
Loading