diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..54a69ad
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,23 @@
+# Cacti mikrotik Plugin AI Instructions
+
+## Project Overview
+This is a Cacti plugin. It integrates with the Cacti monitoring platform via the plugin hook architecture.
+
+## Technology Stack
+- PHP 7.4+ (targeting Cacti 1.2.x compatibility)
+- MySQL/MariaDB via Cacti's DB abstraction layer
+- PSR-12 coding standards
+
+## Key Rules
+- Use prepared statements (db_execute_prepared, db_fetch_row_prepared, etc.) for ALL queries with variables
+- Use get_request_var() / get_filter_request_var() for ALL user input, never raw $_REQUEST/$_GET/$_POST
+- Use html_escape() / htmlspecialchars() for ALL output of DB/user values in HTML context
+- Use cacti_escapeshellarg() for ALL shell command arguments
+- No PHP 8.0+ features (str_contains, match, union types, named args) - target PHP 7.4
+- Use ?? and ??= operators (PHP 7.4) instead of isset() ternary patterns
+- All unserialize() calls must use allowed_classes => false
+
+## Testing
+- Tests in tests/ directory
+- Use Pest PHP or PHPUnit
+- php -l lint check required before commit
diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml
new file mode 100644
index 0000000..1fb3b67
--- /dev/null
+++ b/.github/workflows/plugin-ci-workflow.yml
@@ -0,0 +1,225 @@
+# +-------------------------------------------------------------------------+
+# | 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
+
+jobs:
+ integration-test:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['8.1', '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
+ path: cacti
+
+ - name: Checkout mikrotik Plugin
+ uses: actions/checkout@v4
+ with:
+ path: cacti/plugins/mikrotik
+
+ - 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
+ 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 apache2 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
+ run: |
+ cd ${{ github.workspace }}/cacti
+ if [ -f composer.json ]; then
+ sudo composer install --prefer-dist --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 mikrotik Plugin
+ run: |
+ cd ${{ github.workspace }}/cacti
+ sudo php cli/plugin_manage.php --plugin=mikrotik --install --enable
+
+# - name: import mikrotik Plugin Sample Data
+# run: |
+# cd ${{ github.workspace }}/cacti/plugins/mikrotik
+# sudo php cli_import.php --filename=.github/workflows/mikrotik_sample_data.xml
+# if [ $? -ne 0 ]; then
+# echo "Failed to import Thold sample data"
+# exit 1
+# fi
+
+ - name: Check PHP Syntax for Plugin
+ run: |
+ cd ${{ github.workspace }}/cacti/plugins/mikrotik
+ if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then
+ echo "Syntax errors found!"
+ exit 1
+ fi
+
+ - name: Remove the plugins directory exclusion from the .phpstan.neon
+ run: sed '/plugins/d' -i .phpstan.neon
+ working-directory: ${{ github.workspace }}/cacti
+
+ - name: Mark composer scripts executable
+ run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/*
+
+ - name: Run Linter on base code
+ run: composer run-script lint ${{ github.workspace }}/cacti/plugins/mikrotik
+ working-directory: ${{ github.workspace }}/cacti
+
+ - name: Checking coding standards on base code
+ run: composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/mikrotik
+ 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/mikrotik
+# 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/RouterOS/examples/example1.php b/RouterOS/examples/example1.php
index 6ec2bd0..2cf86be 100644
--- a/RouterOS/examples/example1.php
+++ b/RouterOS/examples/example1.php
@@ -7,15 +7,12 @@
$API->debug = true;
if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) {
+ $API->write('/interface/getall');
- $API->write('/interface/getall');
+ $READ = $API->read(false);
+ $ARRAY = $API->parseResponse($READ);
- $READ = $API->read(false);
- $ARRAY = $API->parseResponse($READ);
-
- print_r($ARRAY);
-
- $API->disconnect();
+ print_r($ARRAY);
+ $API->disconnect();
}
-
diff --git a/RouterOS/examples/example2.php b/RouterOS/examples/example2.php
index 1e204d2..4aa3f31 100644
--- a/RouterOS/examples/example2.php
+++ b/RouterOS/examples/example2.php
@@ -7,16 +7,13 @@
$API->debug = true;
if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) {
+ $API->write('/interface/wireless/registration-table/print',false);
+ $API->write('=stats=');
- $API->write('/interface/wireless/registration-table/print',false);
- $API->write('=stats=');
+ $READ = $API->read(false);
+ $ARRAY = $API->parseResponse($READ);
- $READ = $API->read(false);
- $ARRAY = $API->parseResponse($READ);
-
- print_r($ARRAY);
-
- $API->disconnect();
+ print_r($ARRAY);
+ $API->disconnect();
}
-
diff --git a/RouterOS/examples/example3.php b/RouterOS/examples/example3.php
index b4e911e..aefa03a 100644
--- a/RouterOS/examples/example3.php
+++ b/RouterOS/examples/example3.php
@@ -1,6 +1,6 @@
debug = true;
if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) {
-
- $API->comm('/ppp/secret/add', array(
- 'name' => 'user',
- 'password' => 'pass',
- 'remote-address' => '172.16.1.10',
- 'comment' => '{new VPN user}',
- 'service' => 'pptp',
- ));
-
- $API->disconnect();
-
+ $API->comm('/ppp/secret/add', [
+ 'name' => 'user',
+ 'password' => 'pass',
+ 'remote-address' => '172.16.1.10',
+ 'comment' => '{new VPN user}',
+ 'service' => 'pptp',
+ ]);
+
+ $API->disconnect();
}
-
diff --git a/RouterOS/examples/example4.php b/RouterOS/examples/example4.php
index 05c1f07..88f6e46 100644
--- a/RouterOS/examples/example4.php
+++ b/RouterOS/examples/example4.php
@@ -1,6 +1,6 @@
debug = true;
if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) {
+ $ARRAY = $API->comm('/interface/wireless/registration-table/print', [
+ '.proplist' => '.id',
+ '?mac-address' => '00:0E:BB:DD:FF:FF',
+ ]);
- $ARRAY = $API->comm("/interface/wireless/registration-table/print", array(
- ".proplist"=> ".id",
- "?mac-address" => "00:0E:BB:DD:FF:FF",
- ));
-
- print_r($ARRAY);
-
- $API->disconnect();
+ print_r($ARRAY);
+ $API->disconnect();
}
-
diff --git a/RouterOS/examples/example5.php b/RouterOS/examples/example5.php
index 70a12ec..0105ef3 100644
--- a/RouterOS/examples/example5.php
+++ b/RouterOS/examples/example5.php
@@ -1,6 +1,6 @@
debug = true;
if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) {
+ $ARRAY = $API->comm('/ip/dhcp-server/lease/print', [
+ 'count-only' => '',
+ '~active-address' => '1.1.',
+ ]);
- $ARRAY = $API->comm("/ip/dhcp-server/lease/print", array(
- "count-only"=> "",
- "~active-address" => "1.1.",
- ));
-
- print_r($ARRAY);
-
- $API->disconnect();
+ print_r($ARRAY);
+ $API->disconnect();
}
-
diff --git a/RouterOS/examples/example6.php b/RouterOS/examples/example6.php
index a106f30..c608ef9 100644
--- a/RouterOS/examples/example6.php
+++ b/RouterOS/examples/example6.php
@@ -10,26 +10,26 @@
$API = new RouterosAPI();
if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) {
- # Get all current hosts
- $API->write('/ip/dns/static/print');
- $ips = $API->read();
+ // Get all current hosts
+ $API->write('/ip/dns/static/print');
+ $ips = $API->read();
- # delete them all !
- foreach($ips as $num => $ip_data) {
- $API->write('/ip/dns/static/remove', false);
- $API->write('=.id=' . $ip_data['.id'], true);
- }
+ // delete them all !
+ foreach ($ips as $num => $ip_data) {
+ $API->write('/ip/dns/static/remove', false);
+ $API->write('=.id=' . $ip_data['.id'], true);
+ }
- #add some new
- $API->comm('/ip/dns/static/add', array(
- 'name' => 'jefkeklak',
- 'address' => '1.2.3.4',
- 'ttl' => '1m'
- ));
+ // add some new
+ $API->comm('/ip/dns/static/add', [
+ 'name' => 'jefkeklak',
+ 'address' => '1.2.3.4',
+ 'ttl' => '1m'
+ ]);
- #show me what you got
- $API->write('/ip/dns/static/print');
- $ips = $API->read();
- var_dump($ips);
- $API->disconnect();
+ // show me what you got
+ $API->write('/ip/dns/static/print');
+ $ips = $API->read();
+ var_dump($ips);
+ $API->disconnect();
}
diff --git a/RouterOS/routeros_api.class.php b/RouterOS/routeros_api.class.php
index 28e242d..2707add 100644
--- a/RouterOS/routeros_api.class.php
+++ b/RouterOS/routeros_api.class.php
@@ -1,5 +1,5 @@
debug) {
- echo $text . "\n";
- }
- }
-
- /**
- *
- *
- * @param string $length
- *
- * @return void
- */
- public function encodeLength($length) {
- if ($length < 0x80) {
- $length = chr($length);
- } elseif ($length < 0x4000) {
- $length |= 0x8000;
- $length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
- } elseif ($length < 0x200000) {
- $length |= 0xC00000;
- $length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
- } elseif ($length < 0x10000000) {
- $length |= 0xE0000000;
- $length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
- } elseif ($length >= 0x10000000) {
- $length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
- }
-
- return $length;
- }
-
- /**
- * Login to RouterOS
- *
- * @param string $ip Hostname (IP or domain) of the RouterOS server
- * @param string $login The RouterOS username
- * @param string $password The RouterOS password
- *
- * @return boolean If we are connected or not
- */
- public function connect($ip, $login, $password) {
- for ($ATTEMPT = 1; $ATTEMPT <= $this->attempts; $ATTEMPT++) {
- $this->connected = false;
- $PROTOCOL = ($this->ssl ? 'ssl://' : '' );
- $this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...');
- $this->socket = @fsockopen($PROTOCOL . $ip, $this->port, $this->error_no, $this->error_str, $this->timeout);
- if ($this->socket) {
- socket_set_timeout($this->socket, $this->timeout);
-
- // Try new post-v6.43 API first
- $this->write('/login', false);
- $this->write('=name=' . $login, false);
- $this->write('=password=' . $password);
- $RESPONSE = $this->read(false);
- if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
- $this->connected = true;
- break;
- }
-
- // Try legacy pre-v6.43 API next
- $this->write('/login');
- $RESPONSE = $this->read(false);
- if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
- $MATCHES = array();
- if (preg_match_all('/[^=]+/i', $RESPONSE[1], $MATCHES)) {
- if ($MATCHES[0][0] == 'ret' && strlen($MATCHES[0][1]) == 32) {
- $this->write('/login', false);
- $this->write('=name=' . $login, false);
- $this->write('=response=00' . md5(chr(0) . $password . pack('H*', $MATCHES[0][1])));
- $RESPONSE = $this->read(false);
- if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
- $this->connected = true;
- break;
- }
- }
- }
- }
- fclose($this->socket);
- }
- sleep($this->delay);
- }
-
- if ($this->connected) {
- $this->debug('Connected...');
- } else {
- $this->debug('Error...');
- }
- return $this->connected;
- }
-
- /**
- * Disconnect from RouterOS
- *
- * @return void
- */
- public function disconnect() {
- // let's make sure this socket is still valid. it may have been closed by something else
- if( is_resource($this->socket) ) {
- fclose($this->socket);
- }
- $this->connected = false;
- $this->debug('Disconnected...');
- }
-
- /**
- * Parse response from Router OS
- *
- * @param array $response Response data
- *
- * @return array Array with parsed data
- */
- public function parseResponse($response) {
- if (is_array($response)) {
- $PARSED = array();
- $CURRENT = null;
- $singlevalue = null;
- foreach ($response as $x) {
- if (in_array($x, array('!fatal','!re','!trap'))) {
- if ($x == '!re') {
- $CURRENT =& $PARSED[];
- } else {
- $CURRENT =& $PARSED[$x][];
- }
- } elseif ($x != '!done') {
- $MATCHES = array();
- if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
- if ($MATCHES[0][0] == 'ret') {
- $singlevalue = $MATCHES[0][1];
- }
- $CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : '');
- }
- }
- }
-
- if (empty($PARSED) && !is_null($singlevalue)) {
- $PARSED = $singlevalue;
- }
-
- return $PARSED;
- } else {
- return array();
- }
- }
-
- /**
- * Parse response from Router OS
- *
- * @param array $response Response data
- *
- * @return array Array with parsed data
- */
- public function parseResponse4Smarty($response) {
- if (is_array($response)) {
- $PARSED = array();
- $CURRENT = null;
- $singlevalue = null;
- foreach ($response as $x) {
- if (in_array($x, array('!fatal','!re','!trap'))) {
- if ($x == '!re') {
- $CURRENT =& $PARSED[];
- } else {
- $CURRENT =& $PARSED[$x][];
- }
- } elseif ($x != '!done') {
- $MATCHES = array();
- if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
- if ($MATCHES[0][0] == 'ret') {
- $singlevalue = $MATCHES[0][1];
- }
- $CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : '');
- }
- }
- }
- foreach ($PARSED as $key => $value) {
- $PARSED[$key] = $this->arrayChangeKeyName($value);
- }
- return $PARSED;
- if (empty($PARSED) && !is_null($singlevalue)) {
- $PARSED = $singlevalue;
- }
- } else {
- return array();
- }
- }
-
- /**
- * Change "-" and "/" from array key to "_"
- *
- * @param array $array Input array
- *
- * @return array Array with changed key names
- */
- public function arrayChangeKeyName(&$array) {
- if (is_array($array)) {
- foreach ($array as $k => $v) {
- $tmp = str_replace("-", "_", $k);
- $tmp = str_replace("/", "_", $tmp);
- if ($tmp) {
- $array_new[$tmp] = $v;
- } else {
- $array_new[$k] = $v;
- }
- }
- return $array_new;
- } else {
- return $array;
- }
- }
-
- /**
- * Read data from Router OS
- *
- * @param boolean $parse Parse the data? default: true
- *
- * @return array Array with parsed or unparsed data
- */
- public function read($parse = true) {
- $RESPONSE = array();
- $receiveddone = false;
- while (true) {
- // Read the first byte of input which gives us some or all of the length
- // of the remaining reply.
- $BYTE = ord(fread($this->socket, 1));
- $LENGTH = 0;
- // If the first bit is set then we need to remove the first four bits, shift left 8
- // and then read another byte in.
- // We repeat this for the second and third bits.
- // If the fourth bit is set, we need to remove anything left in the first byte
- // and then read in yet another byte.
- if ($BYTE & 128) {
- if (($BYTE & 192) == 128) {
- $LENGTH = (($BYTE & 63) << 8) + ord(fread($this->socket, 1));
- } else {
- if (($BYTE & 224) == 192) {
- $LENGTH = (($BYTE & 31) << 8) + ord(fread($this->socket, 1));
- $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
- } else {
- if (($BYTE & 240) == 224) {
- $LENGTH = (($BYTE & 15) << 8) + ord(fread($this->socket, 1));
- $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
- $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
- } else {
- $LENGTH = ord(fread($this->socket, 1));
- $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
- $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
- $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
- }
- }
- }
- } else {
- $LENGTH = $BYTE;
- }
-
- $_ = "";
-
- // If we have got more characters to read, read them in.
- if ($LENGTH > 0) {
- $_ = "";
- $retlen = 0;
- while ($retlen < $LENGTH) {
- $toread = $LENGTH - $retlen;
- $_ .= fread($this->socket, $toread);
- $retlen = strlen($_);
- }
- $RESPONSE[] = $_;
- $this->debug('>>> [' . $retlen . '/' . $LENGTH . '] bytes read.');
- }
-
- // If we get a !done, make a note of it.
- if ($_ == "!done") {
- $receiveddone = true;
- }
-
- $STATUS = socket_get_status($this->socket);
- if ($LENGTH > 0) {
- $this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_);
- }
-
- if ((!$this->connected && !$STATUS['unread_bytes']) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) {
- break;
- }
- }
-
- if ($parse) {
- $RESPONSE = $this->parseResponse($RESPONSE);
- }
-
- return $RESPONSE;
- }
-
- /**
- * Write (send) data to Router OS
- *
- * @param string $command A string with the command to send
- * @param mixed $param2 If we set an integer, the command will send this data as a "tag"
- * If we set it to boolean true, the funcion will send the comand and finish
- * If we set it to boolean false, the funcion will send the comand and wait for next command
- * Default: true
- *
- * @return boolean Return false if no command especified
- */
- public function write($command, $param2 = true) {
- if ($command) {
- $data = explode("\n", $command);
- foreach ($data as $com) {
- $com = trim($com);
- fwrite($this->socket, $this->encodeLength(strlen($com)) . $com);
- $this->debug('<<< [' . strlen($com) . '] ' . $com);
- }
-
- if (gettype($param2) == 'integer') {
- fwrite($this->socket, $this->encodeLength(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0));
- $this->debug('<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2);
- } elseif (gettype($param2) == 'boolean') {
- fwrite($this->socket, ($param2 ? chr(0) : ''));
- }
-
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Write (send) data to Router OS
- *
- * @param string $com A string with the command to send
- * @param array $arr An array with arguments or queries
- *
- * @return array Array with parsed
- */
- public function comm($com, $arr = array()) {
- $count = count($arr);
- $this->write($com, !$arr);
- $i = 0;
- if ($this->isIterable($arr)) {
- foreach ($arr as $k => $v) {
- switch ($k[0]) {
- case "?":
- $el = "$k=$v";
- break;
- case "~":
- $el = "$k~$v";
- break;
- default:
- $el = "=$k=$v";
- break;
- }
-
- $last = ($i++ == $count - 1);
- $this->write($el, $last);
- }
- }
-
- return $this->read();
- }
-
- /**
- * Standard destructor
- *
- * @return void
- */
- public function __destruct() {
- $this->disconnect();
- }
+ var $debug = false; // Show debug information
+ var $connected = false; // Connection state
+ var $port = 8728; // Port to connect to (default 8729 for ssl)
+ var $ssl = false; // Connect using SSL (must enable api-ssl in IP/Services)
+ var $timeout = 3; // Connection attempt timeout and data read timeout
+ var $attempts = 5; // Connection attempt count
+ var $delay = 3; // Delay between connection attempts in seconds
+
+ var $socket; // Variable for storing socket resource
+ var $error_no; // Variable for storing connection error number, if any
+ var $error_str; // Variable for storing connection error text, if any
+
+ // Check, can be var used in foreach
+ public function isIterable($var) {
+ return $var !== null
+ && (is_array($var)
+ || $var instanceof Traversable
+ || $var instanceof Iterator
+ || $var instanceof IteratorAggregate
+ );
+ }
+
+ /**
+ * Print text for debug purposes
+ *
+ * @param string $text Text to print
+ *
+ * @return void
+ */
+ public function debug($text) {
+ if ($this->debug) {
+ print $text . "\n";
+ }
+ }
+
+ /**
+ *
+ *
+ * @param string $length
+ *
+ * @return void
+ */
+ public function encodeLength($length) {
+ if ($length < 0x80) {
+ $length = chr($length);
+ } elseif ($length < 0x4000) {
+ $length |= 0x8000;
+ $length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
+ } elseif ($length < 0x200000) {
+ $length |= 0xC00000;
+ $length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
+ } elseif ($length < 0x10000000) {
+ $length |= 0xE0000000;
+ $length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
+ } elseif ($length >= 0x10000000) {
+ $length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
+ }
+
+ return $length;
+ }
+
+ /**
+ * Login to RouterOS
+ *
+ * @param string $ip Hostname (IP or domain) of the RouterOS server
+ * @param string $login The RouterOS username
+ * @param string $password The RouterOS password
+ *
+ * @return boolean If we are connected or not
+ */
+ public function connect($ip, $login, $password) {
+ for ($ATTEMPT = 1; $ATTEMPT <= $this->attempts; $ATTEMPT++) {
+ $this->connected = false;
+ $PROTOCOL = ($this->ssl ? 'ssl://' : '');
+ $this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...');
+ $this->socket = @fsockopen($PROTOCOL . $ip, $this->port, $this->error_no, $this->error_str, $this->timeout);
+
+ if ($this->socket) {
+ socket_set_timeout($this->socket, $this->timeout);
+
+ // Try new post-v6.43 API first
+ $this->write('/login', false);
+ $this->write('=name=' . $login, false);
+ $this->write('=password=' . $password);
+ $RESPONSE = $this->read(false);
+
+ if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
+ $this->connected = true;
+
+ break;
+ }
+
+ // Try legacy pre-v6.43 API next
+ $this->write('/login');
+ $RESPONSE = $this->read(false);
+
+ if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
+ $MATCHES = [];
+
+ if (preg_match_all('/[^=]+/i', $RESPONSE[1], $MATCHES)) {
+ if ($MATCHES[0][0] == 'ret' && strlen($MATCHES[0][1]) == 32) {
+ $this->write('/login', false);
+ $this->write('=name=' . $login, false);
+ $this->write('=response=00' . md5(chr(0) . $password . pack('H*', $MATCHES[0][1])));
+ $RESPONSE = $this->read(false);
+
+ if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
+ $this->connected = true;
+
+ break;
+ }
+ }
+ }
+ }
+ fclose($this->socket);
+ }
+ sleep($this->delay);
+ }
+
+ if ($this->connected) {
+ $this->debug('Connected...');
+ } else {
+ $this->debug('Error...');
+ }
+
+ return $this->connected;
+ }
+
+ /**
+ * Disconnect from RouterOS
+ *
+ * @return void
+ */
+ public function disconnect() {
+ // let's make sure this socket is still valid. it may have been closed by something else
+ if (is_resource($this->socket)) {
+ fclose($this->socket);
+ }
+ $this->connected = false;
+ $this->debug('Disconnected...');
+ }
+
+ /**
+ * Parse response from Router OS
+ *
+ * @param array $response Response data
+ *
+ * @return array Array with parsed data
+ */
+ public function parseResponse($response) {
+ if (is_array($response)) {
+ $PARSED = [];
+ $CURRENT = null;
+ $singlevalue = null;
+
+ foreach ($response as $x) {
+ if (in_array($x, ['!fatal', '!re', '!trap'], true)) {
+ if ($x == '!re') {
+ $CURRENT = & $PARSED[];
+ } else {
+ $CURRENT = & $PARSED[$x][];
+ }
+ } elseif ($x != '!done') {
+ $MATCHES = [];
+
+ if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
+ if ($MATCHES[0][0] == 'ret') {
+ $singlevalue = $MATCHES[0][1];
+ }
+ $CURRENT[$MATCHES[0][0]] = ($MATCHES[0][1] ?? '');
+ }
+ }
+ }
+
+ if (empty($PARSED) && !is_null($singlevalue)) {
+ $PARSED = $singlevalue;
+ }
+
+ return $PARSED;
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Parse response from Router OS
+ *
+ * @param array $response Response data
+ *
+ * @return array Array with parsed data
+ */
+ public function parseResponse4Smarty($response) {
+ if (is_array($response)) {
+ $PARSED = [];
+ $CURRENT = null;
+ $singlevalue = null;
+
+ foreach ($response as $x) {
+ if (in_array($x, ['!fatal', '!re', '!trap'], true)) {
+ if ($x == '!re') {
+ $CURRENT = & $PARSED[];
+ } else {
+ $CURRENT = & $PARSED[$x][];
+ }
+ } elseif ($x != '!done') {
+ $MATCHES = [];
+
+ if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
+ if ($MATCHES[0][0] == 'ret') {
+ $singlevalue = $MATCHES[0][1];
+ }
+ $CURRENT[$MATCHES[0][0]] = ($MATCHES[0][1] ?? '');
+ }
+ }
+ }
+
+ foreach ($PARSED as $key => $value) {
+ $PARSED[$key] = $this->arrayChangeKeyName($value);
+ }
+
+ return $PARSED;
+
+ if (empty($PARSED) && !is_null($singlevalue)) {
+ $PARSED = $singlevalue;
+ }
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Change "-" and "/" from array key to "_"
+ *
+ * @param array $array Input array
+ *
+ * @return array Array with changed key names
+ */
+ public function arrayChangeKeyName(&$array) {
+ if (is_array($array)) {
+ foreach ($array as $k => $v) {
+ $tmp = str_replace('-', '_', $k);
+ $tmp = str_replace('/', '_', $tmp);
+
+ if ($tmp) {
+ $array_new[$tmp] = $v;
+ } else {
+ $array_new[$k] = $v;
+ }
+ }
+
+ return $array_new;
+ } else {
+ return $array;
+ }
+ }
+
+ /**
+ * Read data from Router OS
+ *
+ * @param boolean $parse Parse the data? default: true
+ *
+ * @return array Array with parsed or unparsed data
+ */
+ public function read($parse = true) {
+ $RESPONSE = [];
+ $receiveddone = false;
+
+ while (true) {
+ // Read the first byte of input which gives us some or all of the length
+ // of the remaining reply.
+ $BYTE = ord(fread($this->socket, 1));
+ $LENGTH = 0;
+
+ // If the first bit is set then we need to remove the first four bits, shift left 8
+ // and then read another byte in.
+ // We repeat this for the second and third bits.
+ // If the fourth bit is set, we need to remove anything left in the first byte
+ // and then read in yet another byte.
+ if ($BYTE & 128) {
+ if (($BYTE & 192) == 128) {
+ $LENGTH = (($BYTE & 63) << 8) + ord(fread($this->socket, 1));
+ } else {
+ if (($BYTE & 224) == 192) {
+ $LENGTH = (($BYTE & 31) << 8) + ord(fread($this->socket, 1));
+ $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
+ } else {
+ if (($BYTE & 240) == 224) {
+ $LENGTH = (($BYTE & 15) << 8) + ord(fread($this->socket, 1));
+ $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
+ $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
+ } else {
+ $LENGTH = ord(fread($this->socket, 1));
+ $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
+ $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
+ $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
+ }
+ }
+ }
+ } else {
+ $LENGTH = $BYTE;
+ }
+
+ $_ = '';
+
+ // If we have got more characters to read, read them in.
+ if ($LENGTH > 0) {
+ $_ = '';
+ $retlen = 0;
+
+ while ($retlen < $LENGTH) {
+ $toread = $LENGTH - $retlen;
+ $_ .= fread($this->socket, $toread);
+ $retlen = strlen($_);
+ }
+ $RESPONSE[] = $_;
+ $this->debug('>>> [' . $retlen . '/' . $LENGTH . '] bytes read.');
+ }
+
+ // If we get a !done, make a note of it.
+ if ($_ == '!done') {
+ $receiveddone = true;
+ }
+
+ $STATUS = socket_get_status($this->socket);
+
+ if ($LENGTH > 0) {
+ $this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_);
+ }
+
+ if ((!$this->connected && !$STATUS['unread_bytes']) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) {
+ break;
+ }
+ }
+
+ if ($parse) {
+ $RESPONSE = $this->parseResponse($RESPONSE);
+ }
+
+ return $RESPONSE;
+ }
+
+ /**
+ * Write (send) data to Router OS
+ *
+ * @param string $command A string with the command to send
+ * @param mixed $param2 If we set an integer, the command will send this data as a "tag"
+ * If we set it to boolean true, the funcion will send the comand and finish
+ * If we set it to boolean false, the funcion will send the comand and wait for next command
+ * Default: true
+ *
+ * @return boolean Return false if no command especified
+ */
+ public function write($command, $param2 = true) {
+ if ($command) {
+ $data = explode("\n", $command);
+
+ foreach ($data as $com) {
+ $com = trim($com);
+ fwrite($this->socket, $this->encodeLength(strlen($com)) . $com);
+ $this->debug('<<< [' . strlen($com) . '] ' . $com);
+ }
+
+ if (gettype($param2) == 'integer') {
+ fwrite($this->socket, $this->encodeLength(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0));
+ $this->debug('<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2);
+ } elseif (gettype($param2) == 'boolean') {
+ fwrite($this->socket, ($param2 ? chr(0) : ''));
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Write (send) data to Router OS
+ *
+ * @param string $com A string with the command to send
+ * @param array $arr An array with arguments or queries
+ *
+ * @return array Array with parsed
+ */
+ public function comm($com, $arr = []) {
+ $count = count($arr);
+ $this->write($com, !$arr);
+ $i = 0;
+
+ if ($this->isIterable($arr)) {
+ foreach ($arr as $k => $v) {
+ switch ($k[0]) {
+ case '?':
+ $el = "$k=$v";
+
+ break;
+ case '~':
+ $el = "$k~$v";
+
+ break;
+ default:
+ $el = "=$k=$v";
+
+ break;
+ }
+
+ $last = ($i++ == $count - 1);
+ $this->write($el, $last);
+ }
+ }
+
+ return $this->read();
+ }
+
+ /**
+ * Standard destructor
+ *
+ * @return void
+ */
+ public function __destruct() {
+ $this->disconnect();
+ }
}
-
diff --git a/locales/index.php b/locales/index.php
index 5c8c08f..d503d49 100644
--- a/locales/index.php
+++ b/locales/index.php
@@ -1,3 +1,2 @@
__('Present', 'mikrotik'),
1 => __('Unknown', 'mikrotik'),
2 => __('Running', 'mikrotik'),
3 => __('Warning', 'mikrotik'),
4 => __('Testing', 'mikrotik'),
5 => __('Down', 'mikrotik')
-);
+];
switch(get_request_var('action')) {
-case 'devices':
- mikrotik_devices();
- break;
-case 'trees':
- mikrotik_trees();
- break;
-case 'queues':
- mikrotik_queues();
- break;
-case 'interfaces':
- mikrotik_interfaces();
- break;
-case 'dhcp':
- mikrotik_dhcp();
- break;
-case 'dns':
- mikrotik_dns(false);
- break;
-case 'dnsexport':
- mikrotik_dns(true);
- break;
-case 'list':
- mikrotik_list(false);
- break;
-case 'listexport':
- mikrotik_list(true);
- break;
-case 'wireless_aps':
- mikrotik_wireless_aps();
- break;
-case 'wireless_regs':
- mikrotik_wireless_regs();
- break;
-case 'users':
- mikrotik_users();
- break;
-case 'graphs':
- mikrotik_view_graphs();
- break;
+ case 'devices':
+ mikrotik_devices();
+
+ break;
+ case 'trees':
+ mikrotik_trees();
+
+ break;
+ case 'queues':
+ mikrotik_queues();
+
+ break;
+ case 'interfaces':
+ mikrotik_interfaces();
+
+ break;
+ case 'dhcp':
+ mikrotik_dhcp();
+
+ break;
+ case 'dns':
+ mikrotik_dns(false);
+
+ break;
+ case 'dnsexport':
+ mikrotik_dns(true);
+
+ break;
+ case 'list':
+ mikrotik_list(false);
+
+ break;
+ case 'listexport':
+ mikrotik_list(true);
+
+ break;
+ case 'wireless_aps':
+ mikrotik_wireless_aps();
+
+ break;
+ case 'wireless_regs':
+ mikrotik_wireless_regs();
+
+ break;
+ case 'users':
+ mikrotik_users();
+
+ break;
+ case 'graphs':
+ mikrotik_view_graphs();
+
+ break;
}
if (strpos(get_request_var('action'), 'export') === false) {
@@ -95,8 +108,9 @@
function mikrotik_get_network($mask) {
$octets = explode('.', $mask);
$output = '';
+
if (cacti_sizeof($octets)) {
- foreach($octets as $octet) {
+ foreach ($octets as $octet) {
$output .= decbin($octet);
}
@@ -107,45 +121,45 @@ function mikrotik_get_network($mask) {
}
function mikrotik_users_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_users");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_users');
}
function mikrotik_queues_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_queues");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_queues');
}
function mikrotik_queue_trees_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_trees");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_trees');
}
function mikrotik_interfaces_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_interfaces");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_interfaces');
}
function mikrotik_lists_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_lists");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_lists');
}
function mikrotik_dns_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_dns");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_dns');
}
function mikrotik_dhcp_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_dhcp");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_dhcp');
}
function mikrotik_wireless_aps_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_wireless_aps");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_wireless_aps');
}
function mikrotik_wregs_exist() {
- return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_wireless_registrations");
+ return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_wireless_registrations');
}
function mikrotik_tabs() {
global $config;
- /* present a tabbed interface */
+ // present a tabbed interface
$tabs['devices'] = __('Devices', 'mikrotik');
if (mikrotik_users_exist()) {
@@ -186,17 +200,17 @@ function mikrotik_tabs() {
$tabs['graphs'] = __('Graphs', 'mikrotik');
- /* set the default tab */
+ // set the default tab
$current_tab = get_request_var('action');
print "