diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b14cfa0
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml
index 1780696..02869ae 100644
--- a/.github/workflows/plugin-ci-workflow.yml
+++ b/.github/workflows/plugin-ci-workflow.yml
@@ -86,7 +86,7 @@ jobs:
run: sudo apt-get update
- name: Install System Dependencies
- run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping libapache2-mod-php${{ matrix.php }}
+ run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping
- name: Start SNMPD Agent and Test
run: |
@@ -195,7 +195,9 @@ jobs:
working-directory: ${{ github.workspace }}/cacti
- name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues
- run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/maint
+ run: |
+ find ${{ github.workspace }}/cacti/plugins/maint -type f -name '*.php' ! -path '*/tests/*' -print0 |
+ xargs -0 ./include/vendor/bin/phpstan analyze --level 6
working-directory: ${{ github.workspace }}/cacti
- name: Run Cacti Poller
diff --git a/.gitignore b/.gitignore
index eb71606..32791f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@
# +-------------------------------------------------------------------------+
locales/po/*.mo
+.omc/
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..54c451d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "cacti/plugin_maint",
+ "description": "plugin_maint 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"
+ ]
+ },
+ "scripts": {
+ "test": "vendor/bin/pest"
+ }
+}
diff --git a/maint.php b/maint.php
index af10520..a855465 100644
--- a/maint.php
+++ b/maint.php
@@ -924,9 +924,9 @@ function changemaintType () {
function schedules(): void {
global $actions, $maint_types, $maint_intervals, $yesno;
- $schedules = db_fetch_assoc('SELECT *
+ $schedules = db_fetch_assoc_prepared('SELECT *
FROM plugin_maint_schedules
- ORDER BY name');
+ ORDER BY name', []);
form_start('maint.php', 'chk');
@@ -1134,10 +1134,10 @@ function clearFilter() {
";
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..675e214
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,12 @@
+toBe(0,
+ "{$relativeFile} uses str_contains() which requires PHP 8.0"
+ );
+ }
+ });
+
+ it('does not use str_starts_with (PHP 8.0)', function () use ($files, $readRequiredFile) {
+ foreach ($files as $relativeFile) {
+ $contents = $readRequiredFile($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, $readRequiredFile) {
+ foreach ($files as $relativeFile) {
+ $contents = $readRequiredFile($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, $readRequiredFile) {
+ foreach ($files as $relativeFile) {
+ $contents = $readRequiredFile($relativeFile);
+
+ expect(preg_match('/\?->/', $contents))->toBe(0,
+ "{$relativeFile} uses nullsafe operator which requires PHP 8.0"
+ );
+ }
+ });
+});
diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php
new file mode 100644
index 0000000..a5659a5
--- /dev/null
+++ b/tests/Security/PreparedStatementConsistencyTest.php
@@ -0,0 +1,59 @@
+toBe(0,
+ "File {$relativeFile} contains raw (unprepared) DB calls"
+ );
+ }
+ });
+});
diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php
new file mode 100644
index 0000000..c8a6c0b
--- /dev/null
+++ b/tests/Security/SetupStructureTest.php
@@ -0,0 +1,44 @@
+not->toBeFalse();
+
+ $source = file_get_contents($setupPath);
+ expect($source)->not->toBeFalse();
+
+ $infoPath = realpath(__DIR__ . '/../../INFO');
+ expect($infoPath)->not->toBeFalse();
+
+ $info = parse_ini_file($infoPath, true);
+ expect($info)->not->toBeFalse();
+
+ it('defines plugin_maint_install function', function () use ($source) {
+ expect($source)->toContain('function plugin_maint_install');
+ });
+
+ it('defines plugin_maint_version function', function () use ($source) {
+ expect($source)->toContain('function plugin_maint_version');
+ });
+
+ it('defines plugin_maint_uninstall function', function () use ($source) {
+ expect($source)->toContain('function plugin_maint_uninstall');
+ });
+
+ it('declares a name in INFO', function () use ($info) {
+ expect($info['info'])->toHaveKey('name');
+ });
+
+ it('declares a version in INFO', function () use ($info) {
+ expect($info['info'])->toHaveKey('version');
+ });
+});
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..bb20bc6
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,202 @@
+ 'db_execute', 'sql' => $sql, 'params' => []];
+
+ return true;
+ }
+}
+
+if (!function_exists('db_execute_prepared')) {
+ function db_execute_prepared($sql, $params = []) {
+ $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params];
+
+ return true;
+ }
+}
+
+if (!function_exists('db_fetch_assoc')) {
+ function db_fetch_assoc($sql) {
+ return [];
+ }
+}
+
+if (!function_exists('db_fetch_assoc_prepared')) {
+ function db_fetch_assoc_prepared($sql, $params = []) {
+ return [];
+ }
+}
+
+if (!function_exists('db_fetch_row')) {
+ function db_fetch_row($sql) {
+ return [];
+ }
+}
+
+if (!function_exists('db_fetch_row_prepared')) {
+ function db_fetch_row_prepared($sql, $params = []) {
+ return [];
+ }
+}
+
+if (!function_exists('db_fetch_cell')) {
+ function db_fetch_cell($sql) {
+ return '';
+ }
+}
+
+if (!function_exists('db_fetch_cell_prepared')) {
+ function db_fetch_cell_prepared($sql, $params = []) {
+ return '';
+ }
+}
+
+if (!function_exists('db_index_exists')) {
+ function db_index_exists($table, $index) {
+ return false;
+ }
+}
+
+if (!function_exists('db_column_exists')) {
+ function db_column_exists($table, $column) {
+ return false;
+ }
+}
+
+if (!function_exists('api_plugin_db_add_column')) {
+ function api_plugin_db_add_column($plugin, $table, $data) {
+ return true;
+ }
+}
+
+if (!function_exists('api_plugin_db_table_create')) {
+ function api_plugin_db_table_create($plugin, $table, $data) {
+ return true;
+ }
+}
+
+if (!function_exists('read_config_option')) {
+ function read_config_option($name, $force = false) {
+ return '';
+ }
+}
+
+if (!function_exists('set_config_option')) {
+ function set_config_option($name, $value) {
+ }
+}
+
+if (!function_exists('html_escape')) {
+ function html_escape($string) {
+ return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ }
+}
+
+if (!function_exists('__')) {
+ function __($text, $domain = '') {
+ return $text;
+ }
+}
+
+if (!function_exists('__esc')) {
+ function __esc($text, $domain = '') {
+ return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ }
+}
+
+if (!function_exists('cacti_log')) {
+ function cacti_log($message, $also_print = false, $log_type = '', $level = 0) {
+ }
+}
+
+if (!function_exists('cacti_sizeof')) {
+ function cacti_sizeof($array) {
+ return is_array($array) ? count($array) : 0;
+ }
+}
+
+if (!function_exists('is_realm_allowed')) {
+ function is_realm_allowed($realm) {
+ return true;
+ }
+}
+
+if (!function_exists('raise_message')) {
+ function raise_message($id, $text = '', $level = 0) {
+ }
+}
+
+if (!function_exists('get_request_var')) {
+ function get_request_var($name) {
+ return '';
+ }
+}
+
+if (!function_exists('get_nfilter_request_var')) {
+ function get_nfilter_request_var($name) {
+ return '';
+ }
+}
+
+if (!function_exists('get_filter_request_var')) {
+ function get_filter_request_var($name) {
+ return '';
+ }
+}
+
+if (!function_exists('form_input_validate')) {
+ function form_input_validate($value, $name, $regex, $optional, $error) {
+ return $value;
+ }
+}
+
+if (!function_exists('is_error_message')) {
+ function is_error_message() {
+ return false;
+ }
+}
+
+if (!function_exists('sql_save')) {
+ function sql_save($array, $table, $key = 'id') {
+ return isset($array['id']) ? $array['id'] : 1;
+ }
+}
+
+if (!defined('CACTI_PATH_BASE')) {
+ define('CACTI_PATH_BASE', '/var/www/html/cacti');
+}
+
+if (!defined('POLLER_VERBOSITY_LOW')) {
+ define('POLLER_VERBOSITY_LOW', 2);
+}
+
+if (!defined('POLLER_VERBOSITY_MEDIUM')) {
+ define('POLLER_VERBOSITY_MEDIUM', 3);
+}
+
+if (!defined('POLLER_VERBOSITY_DEBUG')) {
+ define('POLLER_VERBOSITY_DEBUG', 5);
+}
+
+if (!defined('POLLER_VERBOSITY_NONE')) {
+ define('POLLER_VERBOSITY_NONE', 6);
+}
+
+if (!defined('MESSAGE_LEVEL_ERROR')) {
+ define('MESSAGE_LEVEL_ERROR', 1);
+}