<?php if ( !defined('ENVIRONMENT') ) exit('Only STG can run script!' . "\n");

    class Executer {
        
        // Executer's vars:
        private $connected;
        private $socket;
        private $error_no;
        private $error_str;
        
        // Recived data from `ubilling.cls`:
        private $log;
        private $config;
        private $database;
        private $parameters;
        private $options;
        
        // Constructor:
        public function __construct($data) {
            // Put all recived data to specified vars:
            foreach ($data as $key => $value) {
                switch ($key) {
                    case 'parameters':
                        $this->options = $value['options'];
                        unset($value['options']);
                    default:
                        $this->$key = $value;
                        break;
                }
            }
            // Write log message, that class is loaded:
            $this->log->message(__CLASS__, "MikroTik driver loaded", "debug");
            
            // Run:
            if ( !empty($this->parameters['ip']) ) {
                if ( $this->connect() ) {
                    $this->firewall();
                    $this->queue();
					$this->queue_tree();
                    $this->dhcp();
                    $this->arp();
                    $this->ppp();
                }
            } else $this->log->message(__CLASS__, "Cannot put data to MikroTik, no IP address!", 'error');
        }

        /* MikroTik API << */

        private function encode_length($length) {
            if ( $length < 0x80 ) {
                $length = chr($length);
            } else if ( $length < 0x4000 ) {
                $length |= 0x8000;
                $length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
            } else if ( $length < 0x200000 ) {
                $length |= 0xC00000;
                $length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
            } else if ( $length < 0x10000000 ) {
                $length |= 0xE0000000;
                $length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
            } else if ( $length >= 0x10000000 )
                $length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
            return $length;
        }

        private function connect() {
            for ($ATTEMPT = 1; $ATTEMPT <= $this->config['attempts']; $ATTEMPT++) {
                $this->connected = FALSE;
                $portAPI = (empty($this->options['apiport'])) ? $this->config['api_port'] : $this->options['apiport'];

                $this->log->message(__CLASS__, 'Connection attempt #' . $ATTEMPT . ' to ' . $this->parameters['ip'] . ':' . $portAPI . '...', "debug");
                if ( $this->socket = @fsockopen($this->parameters['ip'], $portAPI, $this->error_no, $this->error_str, $this->config['timeout']) ) {
                    socket_set_timeout($this->socket, $this->config['timeout']);

                    if ( isset($this->options['use_new_conn_mode']) && $this->options['use_new_conn_mode'] ) {
                        $this->write('/login', FALSE);
                        $this->write('=name=' . $this->options['username'], FALSE);
                        $this->write('=password=' . $this->options['password']);

                        $RESPONSE = $this->read(FALSE);
                        if ( isset($RESPONSE[0]) && $RESPONSE[0] == '!done' ) {
                            $this->connected = TRUE;
                            break;
                        }
                    } else {
                        $this->write('/login');
                        $RESPONSE = $this->read(FALSE);
                        if ( isset($RESPONSE[0]) && $RESPONSE[0] == '!done' ) {
                            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=' . $this->options['username'], FALSE);
                                    $this->write('=response=00' . md5(chr(0) . $this->options['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->config['delay']);
            }

            // Write log message about connection attempt result:
            if ( $this->connected ) $this->log->message(__CLASS__, "Connection with MikroTik is established", 'debug');
            else                    $this->log->message(__CLASS__, "Cannot establish connection with MikroTik!", 'error');

            // Return connection state:
            return $this->connected;
        }

        private function parse_response($response) {
            if ( is_array($response) ) {
                $PARSED = array();
                $CURRENT = NULL;
                $singlevalue = NULL;
                $count = 0;
                foreach ($response as $x) {
                    if ( in_array($x, array('!fatal', '!re', '!trap')) ) {
                        if ( $x == '!re' ) {
                            $CURRENT = & $PARSED[];
                        } else $CURRENT = & $PARSED[$x][];
                    } else if ( $x != '!done' ) {
                        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();
        }
        
        private function read($parse = TRUE) {
            $RESPONSE = array();
            while (TRUE) {
                $BYTE = ord(fread($this->socket, 1));
                $LENGTH = 0;
                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 ( $LENGTH > 0 ) {
                    $_ = '';
                    $retlen = 0;
                    while ($retlen < $LENGTH) {
                        $toread = $LENGTH - $retlen;
                        $_ .= fread($this->socket, $toread);
                        $retlen = strlen($_);
                    }
                    $RESPONSE[] = $_;
                    $this->log->message(__CLASS__, '>>> [' . $retlen . '/' . $LENGTH . '] bytes read.', "api");
                }
                if ( $_ == '!done' ) $receiveddone = TRUE;
                $STATUS = socket_get_status($this->socket);
                if ( $LENGTH > 0 ) $this->log->message(__CLASS__, '>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_, "api");
                if ( (!$this->connected && !$STATUS['unread_bytes'] ) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) {
                    break;
                }
            }
            if ( $parse ) $RESPONSE = $this->parse_response($RESPONSE);
            return $RESPONSE;
        }

        private function write($command, $param2 = TRUE) {
            if ( $command ) {
                $data = explode("\n", $command);
                foreach ($data as $com) {
                    $com = trim($com);
                    fwrite($this->socket, $this->encode_length(strlen($command)) . $command);
                    $this->log->message(__CLASS__, '<<< [' . strlen($command) . '] ' . $command, "api");
                }
                if ( gettype($param2) == 'integer' ) {
                    fwrite($this->socket, $this->encode_length(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0));
                    $this->log->message(__CLASS__, '<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2, "api");
                } elseif ( gettype($param2) == 'boolean' )
                    fwrite($this->socket, ($param2 ? chr(0) : ''));
                return TRUE;
            } else return FALSE;
        }

        private function command($command, $arr = array()) {
            $count = count($arr);
            $this->write($command, !$arr);
            $i = 0;
            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();
        }

        /* >> MikroTik API */
        
        private function logReply($reply, $function) {
            $search  = array('firewall', 'arp', 'queue', 'dhcp');
            $replace = array('Firewall', 'ARP', 'Queue', 'DHCP');
            if ( empty($reply) ) {
                $doneAction = ( ENVIRONMENT == 'OnConnect' OR  ENVIRONMENT == 'OnDisconnect' ) ? 'updated/created' : 'removed';
                $this->log->message(__CLASS__, str_replace($search, $replace, $function) . " entry was " . $doneAction, 'success');
            } elseif ( is_string($reply) ) {
                $this->log->message(__CLASS__, str_replace($search, $replace, $function) . " entry " . $reply . " was added", 'success');
            } else {
                foreach ($reply['!trap'] as $error) {
                    if ( !empty($error['message']) ) {
                        $this->log->message(__CLASS__, str_replace($search, $replace, $function) . " entry can't be updated, " . $error['message'], 'error');
                    }
                }
            }
        }

        private function firewall() {
            if ( !empty($this->options['manage_firewall']) ) {
                $reply = NULL;
                $template = NULL;
                switch ( ENVIRONMENT ) {
                    case 'OnConnect':
                    case 'OnDisconnect':
                        // Load template:
                        if ( $this->config['use2addrlists'] ) {
                            $firewall_ini_path = ( ENVIRONMENT == 'OnConnect' ) ? 'firewall.ini' : 'firewall_off.ini';
                            $template = parse_ini_file(NASPATH . 'tpls/' . $firewall_ini_path);
                            $template['disabled'] = 'no';
                        } else {
                            $template = parse_ini_file(NASPATH . 'tpls/firewall.ini');
                            $template['disabled'] = ( ENVIRONMENT != 'OnConnect' ) ? 'yes' : 'no';
                        }

                    case 'OnChange':
                    case 'OnUserDel':
                    // Find entry on NAS:
                        $entries = $this->command('/ip/firewall/address-list/print', array(
                            '.proplist' => '.id',
                            '?comment'  => LOGIN
                        ));
                        // a. If entry is present:
                        if ( !empty($entries) ) {
                            foreach ($entries as $entry) {
                                // Update entry:
                                $template['.id'] = $entry['.id'];
                                $action = ( ENVIRONMENT != 'OnChange' && ENVIRONMENT != 'OnUserDel') ? 'set' : 'remove';
                                $reply = $this->command('/ip/firewall/address-list/' . $action, $template);
                            }
                        // b. If entry is absent:
                        } else {
                            if ( ENVIRONMENT == 'OnConnect' || ENVIRONMENT == 'OnDisconnect') {
                                $reply = $this->command('/ip/firewall/address-list/add', $template);
                            } else $this->log->message(__CLASS__, __FUNCTION__ . ": can't remove entry, it's already absent!", 'error');
                        }
                        break;
                }
                $this->logReply($reply, __FUNCTION__);
            } else $this->log->message(__CLASS__, "MikroTik firewall managing isn't enabled", 'debug');
        }

        private function arp() {
            if ( !empty($this->options['manage_arp']) ) {
                $reply = NULL;
                $template = NULL;
                switch ( ENVIRONMENT ) {
                    case 'OnConnect':
                    case 'OnDisconnect':
                        // Load template:
                        $template = parse_ini_file(NASPATH . 'tpls/arp.ini');
                        $template['mac-address'] = $this->database->get_user_mac();
                        $template['interface']   = $this->options['users_interface'];
                    case 'OnChange':
                    case 'OnUserDel':
                    // Find entry on NAS:
                        $entries = $this->command('/ip/arp/print', array(
                            '.proplist' => '.id',
                            '?comment'  => LOGIN
                        ));
                        // a. If entry is present:
                        if ( !empty($entries) ) {
                            foreach ($entries as $entry) {
                                // Update entry:
                                $template['.id'] = $entry['.id'];
                                $action = ( ENVIRONMENT != 'OnChange' && ENVIRONMENT != 'OnUserDel') ? 'set' : 'remove';
                                $reply = $this->command('/ip/arp/' . $action, $template);
                            }
                        // b. If entry is absent:
                        } else {
                            if ( ENVIRONMENT == 'OnConnect' || ENVIRONMENT == 'OnDisconnect') {
                                $reply = $this->command('/ip/arp/add', $template);
                            } else $this->log->message(__CLASS__, __FUNCTION__ . ": can't remove entry, it's already absent!", 'error');
                        }
                        break;
                }
                $this->logReply($reply, __FUNCTION__);
            } else $this->log->message(__CLASS__, "MikroTik ARP managing isn't enabled", 'debug');
        }

        private function queue() {
            if ( !empty($this->options['manage_queue']) ) {
                $reply = NULL;
                $template = NULL;
                switch ( ENVIRONMENT ) {
                    case 'OnConnect':
                    case 'OnDisconnect':
                        // Load template:
                        $template = parse_ini_file(NASPATH . 'tpls/queue/ros_v' . $this->options['version'] . '.ini');
                        $rate = $this->database->get_user_rate();
                        $template['limit-at']  = $rate['tx'] . $this->config['rate_val'] . '/' . $rate['rx'] . $this->config['rate_val'];
                        $template['max-limit'] = $rate['tx'] . $this->config['rate_val'] . '/' . $rate['rx'] . $this->config['rate_val'];
                        $template['burst-limit'] = $rate['bt_tx'] . $this->config['rate_val'] . '/' . $rate['bt_rx'] . $this->config['rate_val'];
                        $template['burst-time'] = $rate['bt_tx_t'] . '/' . $rate['bt_rx_t'];
                        $template['burst-threshold'] =  $rate['tx']*0.8 . $this->config['rate_val'] . '/' . $rate['rx']*0.8 . $this->config['rate_val'];
                        if ($this->options['version'] == 4 || $this->options['version'] == 5) {
                            $template['interface'] = $this->options['users_interface'];
                        }
                    case 'OnChange':
                    case 'OnUserDel':
                    // Find entry on NAS:
                        $entries = $this->command('/queue/simple/print', array(
                            '.proplist' => '.id',
                            '?name'     => LOGIN
                        ));
                        // a. If entry is present:
                        if ( !empty($entries) ) {
                            foreach ($entries as $entry) {
                                // Update entry:
                                $template['.id'] = $entry['.id'];
                                $action = ( ENVIRONMENT != 'OnChange' && ENVIRONMENT != 'OnUserDel') ? 'set' : 'remove';
                                $reply = $this->command('/queue/simple/' . $action, $template);
                            }
                        // b. If entry is absent:
                        } else {
                            if ( ENVIRONMENT == 'OnConnect' || ENVIRONMENT == 'OnDisconnect') {
                                $reply = $this->command('/queue/simple/add', $template);
                            } else $this->log->message(__CLASS__, __FUNCTION__ . ": can't remove entry, it's already absent!", 'error');
                        }
                        break;
                }
                $this->logReply($reply, __FUNCTION__);
            } else $this->log->message(__CLASS__, "MikroTik queue managing isn't enabled", 'debug');
        }
		
		private function queue_tree() {
			if ( !empty($this->options['manage_queue_tree']) ) {
                $reply    = null;
                $template = null;
                switch ( ENVIRONMENT ) {
                    case 'OnConnect':
                    case 'OnDisconnect':
                        // Load template:
                        $template            = parse_ini_file( NASPATH . 'tpls/queue_tree.ini' );
                        $template['list']    = $this->database->get_user_tariff();
						$template['comment'] = LOGIN . ':QT';
                    case 'OnChange':
                    case 'OnUserDel':
                        // Find entry on NAS:
                        $entries = $this->command('/ip/firewall/address-list/print', array(
                            '.proplist' => '.id',
                            '?comment'  => LOGIN . ':QT'
                        ));
                        // a. If entry is present:
                        if ( !empty($entries) ) {
                            foreach ($entries as $entry) {
                                // Update entry:
                                $template['.id'] = $entry['.id'];
                                $action = ( ENVIRONMENT != 'OnChange' && ENVIRONMENT != 'OnUserDel') ? 'set' : 'remove';
                                $reply = $this->command('/ip/firewall/address-list/' . $action, $template);
                            }
                        // b. If entry is absent:
                        } else {
                            if ( in_array( ENVIRONMENT, array('OnConnect', 'OnDisconnect') ) ) {
                                $reply = $this->command('/ip/firewall/address-list/add', $template);
                            } else $this->log->message(__CLASS__, __FUNCTION__ . ": can't remove entry, it's already absent!", 'error');
                        }
                        break;
                }
                $this->logReply($reply, __FUNCTION__);
            } else $this->log->message(__CLASS__, "MikroTik QT is not enabled", 'debug');
		}

        private function dhcp() {
            if ( !empty($this->options['manage_dhcp']) ) {
                if ( !empty($this->options['users_interface']) ) {
                    // Find DHCP-server `name` value:
                    $server = $this->command("/ip/dhcp-server/print", array(
                        '.proplist' => 'name',
                        '?interface' => $this->options['users_interface']
                    ));
                    if ( !empty($server) ) {
                        $reply = NULL;
                        $template = NULL;
                        switch ( ENVIRONMENT ) {
                            case 'OnConnect':
                            case 'OnDisconnect':
                                // Load template:
                                $template = parse_ini_file(NASPATH . 'tpls/dhcp.ini');
                                $template['mac-address'] = $this->database->get_user_mac();
                                $template['server'] = $server[0]['name'];
                            case 'OnChange':
                            case 'OnUserDel':
                            // Find entry on NAS:
                                $entries = $this->command('/ip/dhcp-server/lease/print', array(
                                    '.proplist' => '.id',
                                    '?comment'  => LOGIN
                                ));
                                // a. If entry is present:
                                if ( !empty($entries) ) {
                                    foreach ($entries as $entry) {
                                        // Update entry:
                                        $template['.id'] = $entry['.id'];
                                        $action = ( ENVIRONMENT != 'OnChange' && ENVIRONMENT != 'OnUserDel') ? 'set' : 'remove';
                                        $reply = $this->command('/ip/dhcp-server/lease/' . $action, $template);
                                    }
                                // b. If entry is absent:
                                } else {
                                    if ( ENVIRONMENT == 'OnConnect' || ENVIRONMENT == 'OnDisconnect') {
                                        $reply = $this->command('/ip/dhcp-server/lease/add', $template);
                                    } else $this->log->message(__CLASS__, __FUNCTION__ . ": can't remove entry, it's already absent!", 'error');
                                }
                                break;
                        }
                        $this->logReply($reply, __FUNCTION__);
                    } else $this->log->message(__CLASS__, "No configurated DHCP-server found for this network!", 'error');
                } else $this->log->message(__CLASS__, "There is no `user_interface` was not defined in database!", 'error');
            } else $this->log->message(__CLASS__, "MikroTik DHCP-leases managing is not enabled", 'debug');
        }
        
        private function ppp() {
            if ( !empty($this->options['manage_ppp']) ) {
                if ( !empty($this->options['users_interface']) ) {
                    $reply = NULL;
                    $template = NULL;
                    // Check custom profile exists&
                    $profile = $this->command('/ppp/profile/print', array(
                        '.proplist' => 'name',
                        '?name' => $this->options['users_interface']
                    ));
                    switch ( ENVIRONMENT ) {
                        case 'OnConnect':
                        case 'OnDisconnect':
                            // Load template:
                            $template = parse_ini_file(NASPATH . 'tpls/ppp.ini');
                            $template['password']  = $this->database->get_user_password();
                            $template['profile']   = !empty( $profile ) ? $profile[0]['name'] : $template['profile'];
                            $template['caller-id'] = ( isset( $template['caller-id'] ) ) ? strtoupper($this->database->get_user_mac()) : null;
                            $template['disabled']  = ( isset( $template['disabled'] ) && ENVIRONMENT != 'OnConnect' ) ? 'yes' : 'no';
                        case 'OnChange':
                        case 'OnUserDel':
                        // Find entry on NAS:
                            $entries = $this->command('/ppp/secret/print', array(
                                '.proplist' => '.id',
                                '?name'  => LOGIN
                            ));
                            // a. If entry is present:
                            if ( !empty($entries) ) {
                                foreach ($entries as $entry) {
                                    // Update entry:
                                    $template['.id'] = $entry['.id'];
                                    $action = ( ENVIRONMENT != 'OnChange' && ENVIRONMENT != 'OnUserDel') ? 'set' : 'remove';
                                    $reply = $this->command('/ppp/secret/' . $action, $template);
                                }
                            // b. If entry is absent:
                            } else {
                                if ( ENVIRONMENT == 'OnConnect' || ENVIRONMENT == 'OnDisconnect') {
                                    $reply = $this->command('/ppp/secret/add', $template);
                               } else $this->log->message(__CLASS__, __FUNCTION__ . ": can't remove entry, it's already absent!", 'error');
                            }
                            break;
                    }
                    $this->logReply($reply, __FUNCTION__);
		} else $this->log->message(__CLASS__, "There is no `user_interface` was not defined in database!", 'error');
            } else $this->log->message(__CLASS__, "MikroTik PPP-secrets managing is not enabled", 'debug');
        }
    }
?>