diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fd6a2e2 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,391 @@ +# GitHub Copilot Instructions + +## Priority Guidelines + +When generating code for this repository: + +1. **Version Compatibility**: This is a Cacti plugin (`evidence`, version 0.3) targeting Cacti 1.2.x +2. **Context Files**: Prioritize patterns and standards defined in this file (`.github/copilot-instructions.md`) +3. **Codebase Patterns**: When context files don't provide specific guidance, scan the codebase for established patterns +4. **Architectural Consistency**: Maintain plugin-based architecture extending Cacti core +5. **Code Quality**: Prioritize security, maintainability, and compatibility in all generated code + +## Technology Stack + +### Core Technologies +- **PHP**: Minimum PHP 8.1 +- **Platform**: Cacti Plugin Architecture (Cacti 1.2.x) +- **Database**: MySQL/MariaDB with InnoDB engine +- **SNMP**: Cacti's SNMP library (`lib/snmp.php`, `cacti_snmp_get()`/`cacti_snmp_walk()`) for device polling + +### Key Dependencies +- Cacti core framework (functions like `api_plugin_*`, `db_*`, `cacti_snmp_*`) +- PHP extensions: `snmp`, `mysqli` +- Optional: `gettext` for internationalization + +## Project Structure + +``` +evidence/ # Repository root (install to plugins/evidence/ in Cacti) +├── include/ +│ ├── functions.php # Core polling, display and hook logic +│ ├── database.php # Table creation and upgrade logic +│ ├── settings.php # Plugin config_settings hook +│ ├── arrays.php # Configuration arrays (entities, datatypes) +│ └── index.php # Access protection +├── data/ # SQL seed data (enterprise-numbers.sql) and prep scripts +├── images/ # Tab icons and UI images +├── evidence.php # Main standalone/console page +├── evidence_tab.php # Device tab integration page +├── evidence.js # Client-side JS for device edit page +├── poller_evidence.php # Background poller entry point (CLI) +├── setup.php # Plugin install/uninstall/upgrade hooks +├── INFO # Plugin metadata (name, version, compat) +├── README.md # Feature overview, installation and usage +└── CHANGELOG.md # Version history +``` + +## Naming Conventions + +### Function Names + +#### Plugin Hook Functions +Functions that integrate with Cacti's plugin system MUST be prefixed with `plugin_evidence_`: + +```php +function plugin_evidence_install() { } +function plugin_evidence_poller_bottom() { } +function plugin_evidence_config_settings() { } +function plugin_evidence_device_remove($device_id) { } +``` + +#### Internal/Display Functions +Other functions MUST be prefixed with `evidence_`: + +```php +function evidence_show_tab() { } +function evidence_show_host_info($data, $host_id) { } +``` + +**IMPORTANT**: Match the existing prefix used by the function you are editing; do not introduce a third naming scheme. + +### Database Tables +All database tables MUST be prefixed with `plugin_evidence_`: + +``` +plugin_evidence_organization +plugin_evidence_snmp_info +plugin_evidence_specific_query +plugin_evidence_entity +plugin_evidence_mac +plugin_evidence_ip +plugin_evidence_vendor_specific +``` + +### Variables and Constants +- Use snake_case for variables: `$host_id`, `$evidence_records`, `$snmp_info` +- Global configuration arrays use descriptive names: `$entities`, `$datatypes` + +## Code Style + +### Indentation and Formatting +- **Tabs**: Use tabs (not spaces) for indentation throughout all PHP files +- **Braces**: Opening brace on same line for functions and control structures +- **Spacing**: Space after control structure keywords (`if`, `foreach`, `while`) + +```php +function evidence_example($param) { + if ($param > 0) { + foreach ($items as $item) { + // code here + } + } +} +``` + +### File Headers +ALL PHP files MUST include the standard GPL v2 license header used throughout this repository: + +```php + 'host_id', 'type' => 'int(11)', 'NULL' => false); +$data['columns'][] = array('name' => 'sysdescr', 'type' => 'varchar(255)', 'default' => null); +$data['type'] = 'InnoDB'; +$data['comment'] = 'evidence snmp info'; + +api_plugin_db_table_create('evidence', 'plugin_evidence_snmp_info', $data); +``` + +### Upgrade Handling +Version-gate schema changes in `plugin_evidence_upgrade_database()` (`include/database.php`) using `cacti_version_compare()`, and always update the stored version at the end: + +```php +function plugin_evidence_upgrade_database() { + global $config; + + $info = parse_ini_file($config['base_path'] . '/plugins/evidence/INFO', true); + $info = $info['info']; + $current = $info['version']; + $oldv = db_fetch_cell('SELECT version FROM plugin_config WHERE directory = "evidence"'); + + if (!cacti_version_compare($oldv, $current, '=')) { + if (cacti_version_compare($oldv, '0.3', '<')) { + // create/alter tables here + } + + db_execute_prepared("UPDATE plugin_config + SET version = ?, author = ?, webpage = ? + WHERE directory = 'evidence'", + array($info['version'], $info['author'], $info['homepage'])); + } +} +``` + +## Internationalization + +### Translation Wrapping +ALL user-facing strings MUST use the `__()` function with the `'evidence'` text domain: + +```php +// CORRECT +print __('Disabled/down device. No actual data', 'evidence') . '
'; +$datatypes = array( + 'info' => __('SNMP info', 'evidence'), + 'entity' => __('Entity MIB', 'evidence'), +); + +// WRONG - Never use plain strings for user-facing text +print 'Disabled/down device'; // Missing translation +``` + +## Plugin Architecture + +### Plugin Hooks +Register all plugin hooks in `plugin_evidence_install()` (`setup.php`): + +```php +function plugin_evidence_install() { + api_plugin_register_hook('evidence', 'device_edit_top_links', 'plugin_evidence_device_edit_top_links', 'include/functions.php'); + api_plugin_register_hook('evidence', 'top_header_tabs', 'evidence_show_tab', 'include/functions.php'); + api_plugin_register_hook('evidence', 'top_graph_header_tabs', 'evidence_show_tab', 'include/functions.php'); + api_plugin_register_hook('evidence', 'host_device_remove', 'plugin_evidence_device_remove', 'include/functions.php'); + api_plugin_register_hook('evidence', 'config_settings', 'plugin_evidence_config_settings', 'include/settings.php'); + api_plugin_register_hook('evidence', 'poller_bottom', 'plugin_evidence_poller_bottom', 'include/functions.php'); + api_plugin_register_hook('evidence', 'host_edit_bottom', 'plugin_evidence_host_edit_bottom', 'include/functions.php'); + + api_plugin_register_realm('evidence', 'evidence.php,evidence_tab.php,', 'Plugin evidence - view', 1); + + plugin_evidence_setup_database(); +} +``` + +### Poller Integration +Background collection runs via `poller_evidence.php`, invoked from `plugin_evidence_poller_bottom()` using `exec_background()` and gated by `plugin_evidence_time_to_run()` (which honors the `evidence_frequency` and `evidence_base_time` settings): + +```php +function plugin_evidence_poller_bottom() { + global $config; + + if (plugin_evidence_time_to_run()) { + include_once($config['library_path'] . '/poller.php'); + $command_string = trim(read_config_option('path_php_binary')); + + if (trim($command_string) == '') { + $command_string = 'php'; + } + + $extra_args = ' -q ' . $config['base_path'] . '/plugins/evidence/poller_evidence.php --id=all'; + + exec_background($command_string, $extra_args); + } +} +``` + +## Configuration Arrays + +Define configuration in `include/arrays.php` and settings in `include/settings.php`: + +```php +// include/arrays.php +$datatypes = array( + 'info' => __('SNMP info', 'evidence'), + 'entity' => __('Entity MIB', 'evidence'), + 'mac' => __('Mac addresses', 'evidence'), + 'ip' => __('IP addresses', 'evidence'), +); + +// include/settings.php - registered via the config_settings hook +$settings['evidence'] = array( + 'evidence_frequency' => array( + 'friendly_name' => 'How often gather data', + 'method' => 'drop_array', + 'array' => array('0' => 'Disabled', '6' => 'Every 6 hours', '24' => 'Every day', '168' => 'Every week'), + 'default' => '24', + ), +); +``` + +## Best Practices + +### 1. Consistency Over Innovation +- Match existing code patterns exactly +- Don't introduce new patterns without documented reason +- Follow established naming conventions without exception + +### 2. Security First +- Always use prepared statements for SQL +- Validate all user input using Cacti's validation functions (`get_filter_request_var()`, etc.) +- Verify device access via `plugin_evidence_get_allowed_devices()` before displaying host data +- Never trust user input in file operations + +### 3. Cacti Integration +- Use Cacti's API functions (`api_plugin_*`, `db_*`, `cacti_snmp_*`) +- Follow Cacti's plugin architecture requirements +- Respect Cacti's configuration options and settings + +### 4. Internationalization +- Wrap ALL user-facing strings with `__('text', 'evidence')` +- Never use plain strings for labels, messages, or UI text +- Keep text domain consistent (`evidence`) + +### 5. SNMP Resilience +- Suppress and check SNMP calls (`@cacti_snmp_get()`/`@cacti_snmp_walk()`) since not all devices support every OID +- Treat missing/failed SNMP data as "not available" rather than a hard error + +### 6. Performance +- Limit history retention based on the `evidence_records` setting +- Avoid unnecessary polling; respect `evidence_frequency`/`evidence_base_time` + +## Common Pitfalls to Avoid + +### ❌ NEVER Do This +```php +// Don't concatenate SQL queries +$sql = "SELECT * FROM host WHERE id = $id"; // WRONG + +// Don't use hardcoded strings for UI +print 'Permission issue'; // WRONG + +// Don't use spaces for indentation + if ($condition) { // WRONG (spaces used) + +// Don't skip input validation +$id = $_GET['host_id']; // WRONG +``` + +### ✅ ALWAYS Do This +```php +// Use prepared statements +$host = db_fetch_row_prepared('SELECT * FROM host WHERE id = ?', array($id)); // CORRECT + +// Translate all user-facing strings +print __('Permission issue', 'evidence'); // CORRECT + +// Use tabs for indentation + if ($condition) { // CORRECT (tabs used) + +// Always validate input +$id = get_filter_request_var('host_id'); // CORRECT +``` + +## Version Control + +### Changelog Maintenance +Document all changes in `CHANGELOG.md`: + +```markdown +--- 0.3 --- +* Add generic snmp info +``` + +### Commit Messages +Follow the established pattern from git history: +- Use descriptive commit messages +- Reference issue numbers when applicable +- Group related changes logically + +## References + +- Cacti Plugin Development Guide +- Cacti API Documentation +- Project README.md for feature descriptions +- CHANGELOG.md for version history diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml new file mode 100644 index 0000000..225e41c --- /dev/null +++ b/.github/workflows/plugin-ci-workflow.yml @@ -0,0 +1,251 @@ +# +-------------------------------------------------------------------------+ +# | 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. | +# | | +# | This program is distributed in the hope that it will be useful, | +# | but WITHOUT ANY WARRANTY; without even the implied warranty of | +# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +# | GNU General Public License for more details. | +# +-------------------------------------------------------------------------+ +# | Cacti: The Complete RRDtool-based Graphing Solution | +# +-------------------------------------------------------------------------+ +# | This code is designed, written, and maintained by the Cacti Group. See | +# | about.php and/or the AUTHORS file for specific developer information. | +# +-------------------------------------------------------------------------+ +# | http://www.cacti.net/ | +# +-------------------------------------------------------------------------+ + +name: Plugin Integration Tests + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +env: + CACTI: 1.2.x + +jobs: + integration-test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4'] + os: [ubuntu-latest] + + services: + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: cactiroot + MYSQL_DATABASE: cacti + MYSQL_USER: cactiuser + MYSQL_PASSWORD: cactiuser + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + name: PHP ${{ matrix.php }} Integration Test on ${{ matrix.os }} + + steps: + - name: Checkout Cacti + uses: actions/checkout@v4 + with: + repository: Cacti/cacti + ref: ${{ env.CACTI }} + path: cacti + + - name: Checkout evidence Plugin + uses: actions/checkout@v4 + with: + path: cacti/plugins/evidence + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring, snmp + ini-values: "post_max_size=256M, max_execution_time=60, date.timezone=America/New_York" + + - name: Check PHP version + run: php -v + + - name: Run apt-get update + run: sudo apt-get update + + - name: Install System Dependencies + run: sudo apt-get install -y snmp snmpd rrdtool fping + + - name: Start SNMPD Agent and Test + run: | + sudo systemctl start snmpd + sudo snmpwalk -c public -v2c -On localhost .1.3.6.1.2.1.1 + + - name: Setup Permissions + run: | + sudo chown -R www-data:runner ${{ github.workspace }}/cacti + sudo find ${{ github.workspace }}/cacti -type d -exec chmod 775 {} \; + sudo find ${{ github.workspace }}/cacti -type f -exec chmod 664 {} \; + sudo chmod +x ${{ github.workspace }}/cacti/cmd.php + sudo chmod +x ${{ github.workspace }}/cacti/poller.php + + - name: Create MySQL Config + run: | + echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf + cat ~/.my.cnf + + - name: Initialize Cacti Database + env: + MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf' + run: | + mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;' + mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" + mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;" + mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql + mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti + + - name: Validate composer files + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + composer validate --strict || true + fi + + - name: Install Composer Dependencies + env: + COMPOSER_ROOT_VERSION: 1.3.0-dev + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + sudo composer config --no-plugins allow-plugins.pestphp/pest-plugin true + sudo composer require --dev --no-progress --no-interaction "pestphp/pest: ^3" "pestphp/pest-plugin-drift: ^3.0" + sudo rm -f composer.lock + sudo composer install --dev --no-progress + fi + + - name: Create Cacti config.php + run: | + cat ${{ github.workspace }}/cacti/include/config.php.dist | \ + sed -r "s/localhost/127.0.0.1/g" | \ + sed -r "s/'cacti'/'cacti'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" > ${{ github.workspace }}/cacti/include/config.php + sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php + + - name: Configure Apache + run: | + cat << 'EOF' | sed 's#GITHUB_WORKSPACE#${{ github.workspace }}#g' > /tmp/cacti.conf + + ServerAdmin webmaster@localhost + DocumentRoot GITHUB_WORKSPACE/cacti + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + EOF + sudo cp /tmp/cacti.conf /etc/apache2/sites-available/000-default.conf + sudo systemctl restart apache2 + + - name: Install Cacti via CLI + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/install_cacti.php --accept-eula --install --force + + - name: Install evidence Plugin + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/plugin_manage.php --plugin=evidence --install --enable + + - name: Check PHP Syntax for Plugin + run: | + cd ${{ github.workspace }}/cacti/plugins/evidence + if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then + echo "Syntax errors found!" + exit 1 + fi + + - name: Set expected Cacti version for unit tests + run: echo -n "${{ env.CACTI }}" | sudo tee ${{ github.workspace }}/cacti/plugins/evidence/tests/.cacti-version > /dev/null + + - name: Run Pest Unit Tests + env: + COMPOSER_ROOT_VERSION: 1.3.0-dev + run: | + cd ${{ github.workspace }}/cacti + include/vendor/bin/pest --configuration=plugins/evidence/phpunit.xml \ + --coverage-clover=plugins/evidence/coverage/clover.xml \ + plugins/evidence/tests/Security + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-php${{ matrix.php }} + path: ${{ github.workspace }}/cacti/plugins/evidence/coverage/ + if-no-files-found: warn + + - name: Remove the plugins directory exclusion from the .phpstan.neon + if: ${{ env.CACTI != '1.2.x' }} + run: sed '/plugins/d' -i .phpstan.neon + working-directory: ${{ github.workspace }}/cacti + + - name: Mark composer scripts executable + if: ${{ env.CACTI != '1.2.x' }} + run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* + + - name: Run Linter on base code + if: ${{ env.CACTI != '1.2.x' }} + run: composer run-script lint ${{ github.workspace }}/cacti/plugins/evidence + working-directory: ${{ github.workspace }}/cacti + + - name: Checking coding standards on base code + if: ${{ env.CACTI != '1.2.x' }} + run: composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/evidence + working-directory: ${{ github.workspace }}/cacti + + - name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues + if: ${{ env.CACTI != '1.2.x' }} + run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/evidence + working-directory: ${{ github.workspace }}/cacti + + - name: Run Cacti Poller + run: | + cd ${{ github.workspace }}/cacti + sudo php poller.php --poller=1 --force --debug + + if ! grep -q "SYSTEM STATS" log/cacti.log; then + echo "Cacti poller did not finish successfully" + cat log/cacti.log + exit 1 + fi + + - name: View Cacti Logs + if: always() + run: | + if [ -f ${{ github.workspace }}/cacti/log/cacti.log ]; then + echo "=== Cacti Log ===" + sudo cat ${{ github.workspace }}/cacti/log/cacti.log + fi diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..5ccf98d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,34 @@ + + + + + + + + + tests/Security + + + + + + + setup.php + include/database.php + include/functions.php + + + + diff --git a/tests/.cacti-version b/tests/.cacti-version new file mode 100644 index 0000000..585c5bc --- /dev/null +++ b/tests/.cacti-version @@ -0,0 +1 @@ +1.2.31 diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..c639e23 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,14 @@ +getExtension() !== 'php') { + continue; + } + + $relativeFile = ltrim(str_replace($pluginRoot, '', $file->getPathname()), DIRECTORY_SEPARATOR); + $relativeFile = str_replace(DIRECTORY_SEPARATOR, '/', $relativeFile); + + if (strpos($relativeFile, 'tests/') === 0) { + continue; + } + + $files[] = $relativeFile; + } + + sort($files); + + it('does not use str_contains (PHP 8.0)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + 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) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + 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) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + 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) { + throw new RuntimeException("Unable to resolve required plugin source"); + } + + $contents = file_get_contents($path); + + if ($contents === false) { + throw new RuntimeException("Unable to read required plugin source"); + } + + expect(preg_match('/\?->/', $contents))->toBe(0, + "{$relativeFile} uses nullsafe operator which requires PHP 8.0" + ); + } + }); diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php new file mode 100644 index 0000000..6b0acf6 --- /dev/null +++ b/tests/Security/SetupStructureTest.php @@ -0,0 +1,40 @@ +toContain('function plugin_evidence_install'); +}); + +it('defines plugin_evidence_version function', function () use ($source) { + expect($source)->toContain('function plugin_evidence_version'); +}); + +it('defines plugin_evidence_uninstall function', function () use ($source) { + expect($source)->toContain('function plugin_evidence_uninstall'); +}); + +it('declares a plugin name in INFO', function () use ($info) { + expect($info)->toHaveKey('name'); +}); + +it('declares a plugin version in INFO', function () use ($info) { + expect($info)->toHaveKey('version'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..81addc5 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,44 @@ + $cacti_root, + 'url_path' => '/cacti/', + 'cacti_version' => $cacti_version, + 'cacti_server_os' => 'unix', +); + +$GLOBALS['__test_db_calls'] = array(); + +if (!function_exists('db_execute')) { + function db_execute($sql) { + $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute', 'sql' => $sql, 'params' => array()); + return true; + } +} + +if (!function_exists('db_execute_prepared')) { + function db_execute_prepared($sql, $params = array()) { + $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params); + return true; + } +} + +if (!function_exists('db_fetch_assoc')) { + function db_fetch_assoc($sql) { + return array(); + } +} + +if (!function_exists('db_fetch_assoc_prepared')) { + function db_fetch_assoc_prepared($sql, $params = array()) { + return array(); + } +} + +if (!function_exists('db_fetch_row')) { + function db_fetch_row($sql) { + return array(); + } +} + +if (!function_exists('db_fetch_row_prepared')) { + function db_fetch_row_prepared($sql, $params = array()) { + return array(); + } +} + +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 = array()) { + 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', $GLOBALS['config']['base_path']); +} + +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); +} + +if (!function_exists('plugin_test_read_source')) { + function plugin_test_read_source($relative_file) { + $path = realpath(__DIR__ . '/../' . $relative_file); + if ($path === false) { + throw new RuntimeException("Unable to resolve required file: {$relative_file}"); + } + + $contents = file_get_contents($path); + if ($contents === false) { + throw new RuntimeException("Unable to read required file: {$relative_file}"); + } + + return $contents; + } +} + +/** + * Load a plugin source file at global scope. + * + * Some plugin files define data as file-scope variables that the rest of + * the plugin reads as globals, and they read $config while doing so. + * Requiring them from inside a method would make both halves of that + * method-local, so the require happens here and any variable the file + * introduced is published to $GLOBALS. + * + * @param string $path Absolute path to the file. + * + * @return void + */ +function evidence_test_load($path) { + global $config; + + $__before = get_defined_vars(); + + require_once $path; + + foreach (get_defined_vars() as $__name => $__value) { + if (!array_key_exists($__name, $__before) && strncmp($__name, '__', 2) !== 0) { + $GLOBALS[$__name] = $__value; + } + } +}