diff --git a/.gitignore b/.gitignore index 5a9167ba..c14fa910 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,7 @@ system/devices/** !system/devices/MikrotikPppoe.php !system/devices/MikrotikHotspot.php /.vs -docker-compose.yml \ No newline at end of file +docker-compose.yml +docs/** +!docs/*.html +!docs/*.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c484c8c7..20662636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # CHANGELOG +## 2024.7.15 + +- Radius Rest API +- Getting Started Documentation +- Only Show new update just once + ## 2024.6.21 - Add filter result in voucher and internet plan diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..777780f8 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,3298 @@ + + + + + + + + + + + + + + + + + + + + +PHPNuxBill — a Documentation + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + diff --git a/index.php b/index.php index 2e483a60..f3b2dadb 100644 --- a/index.php +++ b/index.php @@ -18,6 +18,14 @@ if(isset($_GET['nux-router']) && !empty($_GET['nux-router'])){ $_SESSION['nux-router'] = $_GET['nux-router']; } +//get chap id and chap challenge +if(isset($_GET['nux-key']) && !empty($_GET['nux-key'])){ + $_SESSION['nux-key'] = $_GET['nux-key']; +} +//get mikrotik hostname +if(isset($_GET['nux-hostname']) && !empty($_GET['nux-hostname'])){ + $_SESSION['nux-hostname'] = $_GET['nux-hostname']; +} require_once 'system/vendor/autoload.php'; require_once 'system/boot.php'; App::_run(); diff --git a/install/phpnuxbill.sql b/install/phpnuxbill.sql index dcc48863..d3074192 100644 --- a/install/phpnuxbill.sql +++ b/install/phpnuxbill.sql @@ -205,6 +205,34 @@ CREATE TABLE `tbl_voucher` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +DROP TABLE IF EXISTS `rad_acct`; +CREATE TABLE `rad_acct` ( + `id` bigint NOT NULL, + `acctsessionid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `realm` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `nasid` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `nasipaddress` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `nasportid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `nasporttype` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `framedipaddress` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `acctstatustype` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `macaddr` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `dateAdded` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +ALTER TABLE `rad_acct` + ADD PRIMARY KEY (`id`), + ADD KEY `username` (`username`), + ADD KEY `framedipaddress` (`framedipaddress`), + ADD KEY `acctsessionid` (`acctsessionid`), + ADD KEY `nasipaddress` (`nasipaddress`); + + +ALTER TABLE `rad_acct` + MODIFY `id` bigint NOT NULL AUTO_INCREMENT; + ALTER TABLE `tbl_appconfig` ADD PRIMARY KEY (`id`); diff --git a/radius.php b/radius.php new file mode 100644 index 00000000..e3662b56 --- /dev/null +++ b/radius.php @@ -0,0 +1,262 @@ + $_SERVER, +// 'get' => $_GET, +// 'post' => $_POST, +// 'time' => time() +// ])); +// } + +try { + switch ($action) { + case 'authenticate': + $username = _req('username'); + $password = _req('password'); + if (empty($username) || empty($password)) { + show_radius_result([ + "control:Auth-Type" => "Reject", + "reply:Reply-Message" => 'Login invalid' + ], 401); + } + if ($username == $password) { + $d = ORM::for_table('tbl_voucher')->where('code', $username)->find_one(); + } else { + $d = ORM::for_table('tbl_customers')->where('username', $username)->find_one(); + if ($d['password'] != $password) { + if ($d['pppoe_password'] != $password) { + unset($d); + } + } + } + if ($d) { + header("HTTP/1.1 204 No Content"); + die(); + } else { + show_radius_result([ + "control:Auth-Type" => "Reject", + "reply:Reply-Message" => 'Login invalid......' + ], 401); + } + break; + case 'authorize': + $username = _req('username'); + $password = _req('password'); + $isVoucher = ($username == $password); + if (empty($username) || empty($password)) { + show_radius_result([ + "control:Auth-Type" => "Reject", + "reply:Reply-Message" => 'Login invalid......' + ], 401); + } + $tur = ORM::for_table('tbl_user_recharges')->where('username', $username)->find_one(); + if ($tur) { + if (!$isVoucher) { + $d = ORM::for_table('tbl_customers')->select('password')->where('username', $username)->find_one(); + if ($d['password'] != $password) { + if ($d['pppoe_password'] != $password) { + show_radius_result(['Reply-Message' => 'Username or Password is wrong'], 401); + } + } + } + process_radiust_rest($tur, $code); + } else { + if ($isVoucher) { + $v = ORM::for_table('tbl_voucher')->where('code', $username)->where('routers', 'radius')->find_one(); + if ($v) { + if ($v['status'] == 0) { + if (Package::rechargeUser(0, $v['routers'], $v['id_plan'], "Voucher", $username)) { + $v->status = "1"; + $v->save(); + $tur = ORM::for_table('tbl_user_recharges')->where('username', $username)->find_one(); + if ($tur) { + process_radiust_rest($tur, $code); + } else { + show_radius_result(['Reply-Message' => 'Voucher activation failed'], 401); + } + } else { + show_radius_result(['Reply-Message' => 'Voucher activation failed.'], 401); + } + } else { + show_radius_result(['Reply-Message' => 'Voucher Expired...'], 401); + } + } else { + show_radius_result(['Reply-Message' => 'Voucher Expired..'], 401); + } + } else { + show_radius_result(['Reply-Message' => 'Internet Plan Expired..'], 401); + } + } + break; + case 'accounting': + $username = _req('username'); + if (empty($username)) { + show_radius_result([ + "control:Auth-Type" => "Reject", + "reply:Reply-Message" => 'Username empty' + ], 200); + die(); + } + header("HTTP/1.1 200 ok"); + $d = ORM::for_table('rad_acct') + ->where('username', $username) + ->where('macaddr', _post('macAddr')) + ->where('acctstatustype', _post('acctStatusType')) + ->findOne(); + if (!$d) { + $d = ORM::for_table('rad_acct')->create(); + } + $d->acctsessionid = _post('acctSessionId'); + $d->username = $username; + $d->realm = _post('realm'); + $d->nasipaddress = _post('nasip'); + $d->nasid = _post('nasid'); + $d->nasportid = _post('nasPortId'); + $d->nasporttype = _post('nasPortType'); + $d->framedipaddress = _post('framedIPAddress'); + $d->acctstatustype = _post('acctStatusType'); + $d->macaddr = _post('macAddr'); + $d->dateAdded = date('Y-m-d H:i:s'); + $d->save(); + show_radius_result([ + "control:Auth-Type" => "Accept", + "reply:Reply-Message" => 'Saved' + ], 200); + break; + } + die(); +} catch (Throwable $e) { + Message::sendTelegram( + "Sistem Error.\n" . + $e->getMessage() . "\n" . + $e->getTraceAsString() + ); + show_radius_result(['Reply-Message' => 'Command Failed : ' . $action], 401); +} catch (Exception $e) { + Message::sendTelegram( + "Sistem Error.\n" . + $e->getMessage() . "\n" . + $e->getTraceAsString() + ); + show_radius_result(['Reply-Message' => 'Command Failed : ' . $action], 401); +} +show_radius_result(['Reply-Message' => 'Invalid Command : ' . $action], 401); + +function process_radiust_rest($tur, $code) +{ + global $config; + $plan = ORM::for_table('tbl_plans')->where('id', $tur['plan_id'])->find_one(); + $bw = ORM::for_table("tbl_bandwidth")->find_one($plan['id_bw']); + if ($bw['rate_down_unit'] == 'Kbps') { + $unitdown = 'K'; + } else { + $unitdown = 'M'; + } + if ($bw['rate_up_unit'] == 'Kbps') { + $unitup = 'K'; + } else { + $unitup = 'M'; + } + $rate = $bw['rate_up'] . $unitup . "/" . $bw['rate_down'] . $unitdown; + $rates = explode('/', $rate); + + if (!empty(trim($bw['burst']))) { + $ratos = $rate . ' ' . $bw['burst']; + } else { + $ratos = $rates[0] . '/' . $rates[1]; + } + + $attrs = []; + $timeexp = strtotime($tur['expiration'] . ' ' . $tur['time']); + $attrs['reply:Reply-Message'] = 'success'; + $attrs['Simultaneous-Use'] = $plan['shared_users']; + $attrs['reply:Mikrotik-Wireless-Comment'] = $plan['name_plan'] . ' | ' . $tur['expiration'] . ' ' . $tur['time']; + + $attrs['reply:Ascend-Data-Rate'] = str_replace('M', '000000', str_replace('K', '000', $rates[1])); + $attrs['reply:Ascend-Xmit-Rate'] = str_replace('M', '000000', str_replace('K', '000', $rates[0])); + $attrs['reply:Mikrotik-Rate-Limit'] = $ratos; + $attrs['reply:WISPr-Bandwidth-Max-Up'] = str_replace('M', '000000', str_replace('K', '000', $rates[0])); + $attrs['reply:WISPr-Bandwidth-Max-Down'] = str_replace('M', '000000', str_replace('K', '000', $rates[1])); + $attrs['reply:expiration'] = date('d M Y H:i:s', $timeexp); + $attrs['reply:WISPr-Session-Terminate-Time'] = date('Y-m-d', $timeexp) . 'T' . date('H:i:sP', $timeexp); + + if ($plan['type'] == 'PPPOE') { + $attrs['reply:Framed-Pool'] = $plan['pool']; + } + + if ($plan['typebp'] == "Limited") { + if ($plan['limit_type'] == "Time_Limit") { + if ($plan['time_unit'] == 'Hrs') + $timelimit = $plan['time_limit'] * 60 * 60; + else + $timelimit = $plan['time_limit'] * 60; + $attrs['reply:Max-All-Session'] = $timelimit; + $attrs['reply:Expire-After'] = $timelimit; + } else if ($plan['limit_type'] == "Data_Limit") { + if ($plan['data_unit'] == 'GB') + $datalimit = $plan['data_limit'] . "000000000"; + else + $datalimit = $plan['data_limit'] . "000000"; + $attrs['reply:Max-Data'] = $datalimit; + $attrs['reply:Mikrotik-Recv-Limit-Gigawords'] = $datalimit; + $attrs['reply:Mikrotik-Xmit-Limit-Gigawords'] = $datalimit; + } else if ($plan['limit_type'] == "Both_Limit") { + if ($plan['time_unit'] == 'Hrs') + $timelimit = $plan['time_limit'] * 60 * 60; + else + $timelimit = $plan['time_limit'] * 60; + if ($plan['data_unit'] == 'GB') + $datalimit = $plan['data_limit'] . "000000000"; + else + $datalimit = $plan['data_limit'] . "000000"; + $attrs['reply:Max-All-Session'] = $timelimit; + $attrs['reply:Max-Data'] = $datalimit; + $attrs['reply:Mikrotik-Recv-Limit-Gigawords'] = $datalimit; + $attrs['reply:Mikrotik-Xmit-Limit-Gigawords'] = $datalimit; + } + } + $result = array_merge([ + "control:Auth-Type" => "Accept", + "reply" => ["Reply-Message" => ['value' => 'success']] + ], $attrs); + show_radius_result($result, $code); +} + +function show_radius_result($array, $code = 200) +{ + if ($code == 401) { + header("HTTP/1.1 401 Unauthorized"); + } else if ($code == 200) { + header("HTTP/1.1 200 OK"); + } else if ($code == 204) { + header("HTTP/1.1 204 No Content"); + die(); + } + die(json_encode($array)); + die(); +} diff --git a/system/autoload/Package.php b/system/autoload/Package.php index a8acdf1f..a04d5e6d 100644 --- a/system/autoload/Package.php +++ b/system/autoload/Package.php @@ -26,16 +26,26 @@ class Package $time_only = date("H:i:s"); $time = date("H:i:s"); $inv = ""; + $isVoucher = false; + $c = []; if ($id_customer == '' or $router_name == '' or $plan_id == '') { return false; } + if(trim($gateway) == 'Voucher' && $id_customer == 0){ + $isVoucher = true; + } - $c = ORM::for_table('tbl_customers')->where('id', $id_customer)->find_one(); $p = ORM::for_table('tbl_plans')->where('id', $plan_id)->find_one(); - if ($c['status'] != 'Active') { - _alert(Lang::T('This account status') . ' : ' . Lang::T($c['status']), 'danger', ""); + if(!$isVoucher){ + $c = ORM::for_table('tbl_customers')->where('id', $id_customer)->find_one(); + if ($c['status'] != 'Active') { + _alert(Lang::T('This account status') . ' : ' . Lang::T($c['status']), 'danger', ""); + } + }else{ + $c['username'] = $channel; + $c['fullname'] = $gateway; } $add_cost = 0; diff --git a/system/autoload/Parsedown.php b/system/autoload/Parsedown.php new file mode 100644 index 00000000..ae0cbdec --- /dev/null +++ b/system/autoload/Parsedown.php @@ -0,0 +1,1994 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'tel:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) + { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') + { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; + } + else + { + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (strpos($Line['text'], '') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) + { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) + { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') + { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') + { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) + { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) + { + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['elements'] as &$li) + { + if (end($li['handler']['argument']) !== '') + { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') + { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) + { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) + { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) + { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['handler']['argument']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) + { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) + { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) + { + $markup .= $this->elements($Element['elements']); + } + elseif (isset($Element['element'])) + { + $markup .= $this->element($Element['element']); + } + else + { + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } + elseif ($hasName) + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) + { + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/system/controllers/home.php b/system/controllers/home.php index f7b8983d..87641d26 100644 --- a/system/controllers/home.php +++ b/system/controllers/home.php @@ -210,7 +210,7 @@ if (isset($_GET['recharge']) && !empty($_GET['recharge'])) { } } -if (!empty($_SESSION['nux-mac']) && !empty($_SESSION['nux-ip'])) { +if (!empty($_SESSION['nux-mac']) && !empty($_SESSION['nux-ip'] && $_c['hs_auth_method'] != 'hchap')) { $ui->assign('nux_mac', $_SESSION['nux-mac']); $ui->assign('nux_ip', $_SESSION['nux-ip']); $bill = ORM::for_table('tbl_user_recharges')->where('id', $_GET['id'])->where('username', $user['username'])->findOne(); @@ -232,6 +232,60 @@ if (!empty($_SESSION['nux-mac']) && !empty($_SESSION['nux-ip'])) { } } +if (!empty($_SESSION['nux-mac']) && !empty($_SESSION['nux-ip'] && !empty($_SESSION['nux-hostname']) && $_c['hs_auth_method'] == 'hchap')) { + $apkurl = (((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'onoff')|| $_SERVER['SERVER_PORT'] == 443)?'https':'http').'://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $ui->assign('nux_mac', $_SESSION['nux-mac']); + $ui->assign('nux_ip', $_SESSION['nux-ip']); + $keys = explode('-', $_SESSION['nux-key']); + $ui->assign('hostname', $_SESSION['nux-hostname']); + $ui->assign('apkurl', $apkurl); + $ui->assign('key1', $keys[0]); + $ui->assign('key2', $keys[1]); + $ui->assign('hchap', $_GET['hchap']); + $ui->assign('logged', $_GET['logged']); + if ($_app_stage != 'demo') { + if ($_GET['mikrotik'] == 'login') { + r2(U . 'home&hchap=true', 's', Lang::T('Login Request successfully')); + } + $getmsg = $_GET['msg']; + ///get auth notification from mikrotik + if($getmsg == 'Connected') { + $msg .= Lang::T($getmsg); + r2(U . 'home&logged=1', 's', $msg); + } else if($getmsg){ + $msg .= Lang::T($getmsg); + r2(U . 'home', 's', $msg); + } + } + } + +if (!empty($_SESSION['nux-mac']) && !empty($_SESSION['nux-ip'] && !empty($_SESSION['nux-hostname']) && $_c['hs_auth_method'] == 'hchap')) { + $apkurl = (((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'onoff')|| $_SERVER['SERVER_PORT'] == 443)?'https':'http').'://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $ui->assign('nux_mac', $_SESSION['nux-mac']); + $ui->assign('nux_ip', $_SESSION['nux-ip']); + $keys = explode('-', $_SESSION['nux-key']); + $ui->assign('hostname', $_SESSION['nux-hostname']); + $ui->assign('apkurl', $apkurl); + $ui->assign('key1', $keys[0]); + $ui->assign('key2', $keys[1]); + $ui->assign('hchap', $_GET['hchap']); + $ui->assign('logged', $_GET['logged']); + if ($_app_stage != 'demo') { + if ($_GET['mikrotik'] == 'login') { + r2(U . 'home&hchap=true', 's', Lang::T('Login Request successfully')); + } + $getmsg = $_GET['msg']; + ///get auth notification from mikrotik + if($getmsg == 'Connected') { + $msg .= Lang::T($getmsg); + r2(U . 'home&logged=1', 's', $msg); + } else if($getmsg){ + $msg .= Lang::T($getmsg); + r2(U . 'home', 's', $msg); + } + } + } + $ui->assign('unpaid', ORM::for_table('tbl_payment_gateway') ->where('username', $user['username']) ->where('status', 1) diff --git a/system/controllers/settings.php b/system/controllers/settings.php index 4096a8d7..70456892 100644 --- a/system/controllers/settings.php +++ b/system/controllers/settings.php @@ -12,6 +12,19 @@ $action = $routes['1']; $ui->assign('_admin', $admin); switch ($action) { + case 'docs': + $d = ORM::for_table('tbl_appconfig')->where('setting', 'docs_clicked')->find_one(); + if ($d) { + $d->value = 'yes'; + $d->save(); + } else { + $d = ORM::for_table('tbl_appconfig')->create(); + $d->setting = 'docs_clicked'; + $d->value = 'yes'; + $d->save(); + } + r2('./docs'); + break; case 'devices': $files = scandir($DEVICE_PATH); $devices = []; @@ -111,9 +124,12 @@ switch ($action) { _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard"); } $company = _post('CompanyName'); + $custom_tax_rate = filter_var(_post('custom_tax_rate'), FILTER_SANITIZE_SPECIAL_CHARS); + if (preg_match('/[^0-9.]/', $custom_tax_rate)) { + r2(U . 'settings/app', 'e', 'Special characters are not allowed in tax rate'); + die(); + } run_hook('save_settings'); #HOOK - - if (!empty($_FILES['logo']['name'])) { if (function_exists('imagecreatetruecolor')) { if (file_exists($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png')) unlink($UPLOAD_PATH . DIRECTORY_SEPARATOR . 'logo.png'); @@ -141,6 +157,9 @@ switch ($action) { } // Save all settings including tax system foreach ($_POST as $key => $value) { + $key = filter_var($key, FILTER_SANITIZE_SPECIAL_CHARS); + $value = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS); + $d = ORM::for_table('tbl_appconfig')->where('setting', $key)->find_one(); if ($d) { $d->value = $value; diff --git a/system/cron_reminder.php b/system/cron_reminder.php index 85b1a076..437f77f5 100644 --- a/system/cron_reminder.php +++ b/system/cron_reminder.php @@ -15,7 +15,7 @@ if (php_sapi_name() !== 'cli') { echo "
";
 }
 
-$d = ORM::for_table('tbl_user_recharges')->where('status', 'on')->find_many();
+$d = ORM::for_table('tbl_user_recharges')->where('status', 'on')->whereNotEqual('customer_id', '0')->find_many();
 
 run_hook('cronjob_reminder'); #HOOK
 
diff --git a/system/devices/Dummy.php b/system/devices/Dummy.php
index e2c76b88..e4db1519 100644
--- a/system/devices/Dummy.php
+++ b/system/devices/Dummy.php
@@ -11,7 +11,7 @@ class Dummy {
             'author' => 'ibnu maksum',
             'url' => [
                 'Github' => 'https://github.com/hotspotbilling/phpnuxbill/',
-                'Telegram' => 'https://t.me/ibnux',
+                'Telegram' => 'https://t.me/phpnuxbill',
                 'Donate' => 'https://paypal.me/ibnux'
             ]
         ];
diff --git a/system/devices/MikrotikHotspot.php b/system/devices/MikrotikHotspot.php
index 8ebb79b4..b4b44b35 100644
--- a/system/devices/MikrotikHotspot.php
+++ b/system/devices/MikrotikHotspot.php
@@ -22,7 +22,7 @@ class MikrotikHotspot
             'author' => 'ibnux',
             'url' => [
                 'Github' => 'https://github.com/hotspotbilling/phpnuxbill/',
-                'Telegram' => 'https://t.me/ibnux',
+                'Telegram' => 'https://t.me/phpnuxbill',
                 'Donate' => 'https://paypal.me/ibnux'
             ]
         ];
diff --git a/system/devices/MikrotikPppoe.php b/system/devices/MikrotikPppoe.php
index 6c311b3d..9424a5c2 100644
--- a/system/devices/MikrotikPppoe.php
+++ b/system/devices/MikrotikPppoe.php
@@ -21,7 +21,7 @@ class MikrotikPppoe
             'author' => 'ibnux',
             'url' => [
                 'Github' => 'https://github.com/hotspotbilling/phpnuxbill/',
-                'Telegram' => 'https://t.me/ibnux',
+                'Telegram' => 'https://t.me/phpnuxbill',
                 'Donate' => 'https://paypal.me/ibnux'
             ]
         ];
@@ -31,9 +31,23 @@ class MikrotikPppoe
     {
         $mikrotik = $this->info($plan['routers']);
         $client = $this->getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']);
-        $this->removePpoeUser($client, $customer['username']);
-        $this->removePpoeActive($client, $customer['username']);
-        $this->addPpoeUser($client, $plan, $customer);
+        //check if customer exists
+        $printRequest = new RouterOS\Request('/ppp/secret/print');
+        $printRequest->setQuery(RouterOS\Query::where('name', $customer['username']));
+        $cid = $client->sendSync($printRequest)->getProperty('.id');
+        if (empty($cid)) {
+            //customer not exists, add it
+            $this->addPpoeUser($client, $plan, $customer);
+        }else{
+            $setRequest = new RouterOS\Request('/ppp/secret/set');
+            $setRequest->setArgument('numbers', $cid);
+            $setRequest->setArgument('profile', $plan['name_plan']);
+            $setRequest->setArgument('comment', $customer['fullname'] . ' | ' . $customer['email']);
+            $setRequest->setArgument('password', $customer['password']);
+            $client->sendSync($setRequest);
+            //disconnect then
+            $this->removePpoeActive($client, $customer['username']);
+        }
     }
 
     function remove_customer($customer, $plan)
@@ -212,15 +226,6 @@ class MikrotikPppoe
         return $client->sendSync($printRequest)->getProperty('.id');
     }
 
-
-    function connect_customer($customer, $ip, $mac_address, $router_name)
-    {
-    }
-
-    function disconnect_customer($customer, $router_name)
-    {
-    }
-
     function info($name)
     {
         return ORM::for_table('tbl_routers')->where('name', $name)->find_one();
diff --git a/system/devices/Radius.php b/system/devices/Radius.php
index af09b6fa..60e4d080 100644
--- a/system/devices/Radius.php
+++ b/system/devices/Radius.php
@@ -20,7 +20,7 @@ class Radius
             'author' => 'ibnux',
             'url' => [
                 'Github' => 'https://github.com/hotspotbilling/phpnuxbill/',
-                'Telegram' => 'https://t.me/ibnux',
+                'Telegram' => 'https://t.me/phpnuxbill',
                 'Donate' => 'https://paypal.me/ibnux'
             ]
         ];
diff --git a/system/devices/RadiusRest.php b/system/devices/RadiusRest.php
new file mode 100644
index 00000000..0dc34eae
--- /dev/null
+++ b/system/devices/RadiusRest.php
@@ -0,0 +1,60 @@
+ 'Radius Rest API',
+            'description' => 'This devices will handle Radius Connection using Rest API',
+            'author' => 'ibnu maksum',
+            'url' => [
+                'Wiki Tutorial' => 'https://github.com/hotspotbilling/phpnuxbill/wiki/FreeRadius-Rest',
+                'Telegram' => 'https://t.me/phpnuxbill',
+                'Donate' => 'https://paypal.me/ibnux'
+            ]
+        ];
+    }
+
+    // Add Customer to Mikrotik/Device
+    function add_customer($customer, $plan)
+    {
+    }
+
+    // Remove Customer to Mikrotik/Device
+    function remove_customer($customer, $plan)
+    {
+    }
+
+    // Add Plan to Mikrotik/Device
+    function add_plan($plan)
+    {
+    }
+
+    // Update Plan to Mikrotik/Device
+    function update_plan($old_name, $plan)
+    {
+    }
+
+    // Remove Plan from Mikrotik/Device
+    function remove_plan($plan)
+    {
+    }
+
+    // check if customer is online
+    function online_customer($customer, $router_name)
+    {
+    }
+
+    // make customer online
+    function connect_customer($customer, $ip, $mac_address, $router_name)
+    {
+    }
+
+    // make customer disconnect
+    function disconnect_customer($customer, $router_name)
+    {
+    }
+
+}
\ No newline at end of file
diff --git a/system/lan/english.json b/system/lan/english.json
index 0a759219..7859fa03 100644
--- a/system/lan/english.json
+++ b/system/lan/english.json
@@ -1,219 +1,655 @@
 {
-    "Recharge_Account": "Recharge Account",
-    "Refill_Account": "Refill Account",
-    "SuperAdmin": "SuperAdmin",
-    "Change_Password": "Change Password",
-    "My_Account": "My Account",
-    "Logout": "Logout",
-    "Dashboard": "Dashboard",
-    "Customer": "Customer",
-    "Lists": "Lists",
-    "Location": "Location",
-    "Services": "Services",
-    "Active_Users": "Active Users",
-    "Vouchers": "Vouchers",
-    "Refill_Customer": "Refill Customer",
-    "Recharge_Customer": "Recharge Customer",
-    "Refill_Balance": "Refill Balance",
-    "Internet_Plan": "Internet Plan",
-    "Bandwidth": "Bandwidth",
-    "Customer_Balance": "Customer Balance",
-    "Reports": "Reports",
-    "Daily_Reports": "Daily Reports",
-    "Period_Reports": "Period Reports",
-    "Activation_History": "Activation History",
-    "Send_Message": "Send Message",
-    "Single_Customer": "Single Customer",
-    "Bulk_Customers": "Bulk Customers",
-    "Network": "Network",
-    "Routers": "Routers",
-    "IP_Pool": "IP Pool",
-    "Radius": "Radius",
-    "Radius_NAS": "Radius NAS",
-    "Static_Pages": "Static Pages",
-    "Order_Voucher": "Order Voucher",
-    "Voucher": "Voucher",
+    "Log_in": "Login",
+    "Register": "Register",
     "Announcement": "Announcement",
-    "Customer_Announcement": "Customer Announcement",
     "Registration_Info": "Registration Info",
-    "Privacy_Policy": "Privacy Policy",
-    "Terms_and_Conditions": "Terms and Conditions",
-    "Settings": "Settings",
-    "General_Settings": "General Settings",
-    "Localisation": "Localisation",
-    "Maintenance_Mode": "Maintenance Mode",
-    "User_Notification": "User Notification",
-    "Administrator_Users": "Administrator Users",
-    "Backup_Restore": "Backup\/Restore",
-    "Payment_Gateway": "Payment Gateway",
-    "Plugin_Manager": "Plugin Manager",
-    "Logs": "Logs",
-    "Community": "Community",
-    "Select_Account": "Select Account",
-    "Select_a_customer": "Select a customer",
-    "Code_Voucher": "Code Voucher",
-    "Enter_voucher_code_here": "Enter voucher code here",
-    "Recharge": "Recharge",
-    "Cancel": "Cancel",
-    "Hotspot_Plans": "Hotspot Plans",
-    "Search_by_Name": "Search by Name",
-    "Search": "Search",
-    "New_Service_Plan": "New Service Plan",
-    "Expired": "Expired",
-    "Name": "Name",
-    "Type": "Type",
-    "Category": "Category",
-    "Price": "Price",
-    "Validity": "Validity",
-    "Time": "Time",
-    "Data": "Data",
-    "Device": "Device",
-    "Date": "Date",
-    "ID": "ID",
+    "Voucher_not_found__please_buy_voucher_befor_register": "Voucher not found, please buy voucher befor register",
+    "Register_Success__You_can_login_now": "Register Success! You can login now",
+    "Log_in_to_Member_Panel": "Log in to Member Panel",
+    "Register_as_Member": "Register as Member",
+    "Enter_Admin_Area": "Enter Admin Area",
+    "PHPNuxBill": "PHPNuxBill",
+    "Username": "Username",
+    "Password": "Password",
+    "Passwords_does_not_match": "Passwords does not match",
+    "Account_already_axist": "Account already axist",
     "Manage": "Manage",
+    "Submit": "Submit",
+    "Save_Changes": "Save Changes",
+    "Cancel": "Cancel",
     "Edit": "Edit",
     "Delete": "Delete",
-    "Prev": "Prev",
-    "Next": "Next",
+    "Welcome": "Welcome",
+    "Data_Created_Successfully": "Data Created Successfully",
+    "Data_Updated_Successfully": "Data Updated Successfully",
+    "Data_Deleted_Successfully": "Data Deleted Successfully",
+    "Static_Pages": "Static Pages",
+    "Failed_to_save_page__make_sure_i_can_write_to_folder_pages___i_chmod_664_pages___html_i_": "Failed to save page, make sure i can write to folder pages, chmod 664 pages\/*.html",
+    "Saving_page_success": "Saving page success",
+    "Sometimes_you_need_to_refresh_3_times_until_content_change": "Sometimes you need to refresh 3 times until content change",
+    "Dashboard": "Dashboard",
+    "Search_Customers___": "Search Customers...",
+    "My_Account": "My Account",
+    "My_Profile": "My Profile",
+    "Settings": "Settings",
+    "Edit_Profile": "Edit Profile",
+    "Change_Password": "Change Password",
+    "Logout": "Logout",
+    "Services": "Services",
+    "Bandwidth_Plans": "Bandwidth Plans",
+    "Bandwidth_Name": "Bandwidth Name",
+    "New_Bandwidth": "New Bandwidth",
+    "Edit_Bandwidth": "Edit Bandwidth",
+    "Add_New_Bandwidth": "Add New Bandwidth",
+    "Rate_Download": "Rate Download",
+    "Rate_Upload": "Rate Upload",
+    "Name_Bandwidth_Already_Exist": "Name Bandwidth Already Exist",
+    "Hotspot_Plans": "Hotspot Plans",
     "PPPOE_Plans": "PPPOE Plans",
     "Plan_Name": "Plan Name",
-    "Plan_Type": "Plan Type",
-    "Bandwidth_Plans": "Bandwidth Plans",
-    "Plan_Price": "Plan Price",
-    "Plan_Validity": "Plan Validity",
-    "Status": "Status",
-    "Business": "Business",
-    "Personal": "Personal",
-    "Hap_Lite": "Hap Lite",
-    "": "",
+    "New_Service_Plan": "New Service Plan",
     "Add_Service_Plan": "Add Service Plan",
-    "Cannot_be_change_after_saved": "Cannot be change after saved",
+    "Edit_Service_Plan": "Edit Service Plan",
+    "Name_Plan_Already_Exist": "Name Plan Already Exist",
+    "Plan_Type": "Plan Type",
+    "Plan_Price": "Plan Price",
+    "Limit_Type": "Limit Type",
     "Unlimited": "Unlimited",
     "Limited": "Limited",
-    "Limit_Type": "Limit Type",
     "Time_Limit": "Time Limit",
     "Data_Limit": "Data Limit",
     "Both_Limit": "Both Limit",
-    "Hrs": "Hrs",
-    "Mins": "Mins",
-    "Bandwidth_Name": "Bandwidth Name",
+    "Plan_Validity": "Plan Validity",
     "Select_Bandwidth": "Select Bandwidth",
     "Shared_Users": "Shared Users",
-    "1_Period___1_Month__Expires_the_20th_of_each_month": "1 Period = 1 Month, Expires the 20th of each month",
-    "Expired_Date": "Expired Date",
-    "Router_Name": "Router Name",
-    "Select_Routers": "Select Routers",
-    "Save_Changes": "Save Changes",
-    "Days": "Days",
-    "Months": "Months",
-    "Period": "Period",
-    "New_Bandwidth": "New Bandwidth",
-    "Rate": "Rate",
-    "Burst": "Burst",
-    "Balance_Plans": "Balance Plans",
-    "New_Router": "New Router",
-    "IP_Address": "IP Address",
-    "Username": "Username",
-    "Description": "Description",
-    "Edit_Service_Plan": "Edit Service Plan",
-    "Expired_Action": "Expired Action",
-    "Optional": "Optional",
-    "Expired_Internet_Plan": "Expired Internet Plan",
-    "When_Expired__customer_will_be_move_to_selected_internet_plan": "When Expired, customer will be move to selected internet plan",
-    "Data_Updated_Successfully": "Data Updated Successfully",
-    "Plugin_Installer": "Plugin Installer",
-    "Plugin": "Plugin",
-    "Search_by_Username": "Search by Username",
+    "Choose_User_Type_Sales_to_disable_access_to_Settings": "Choose User Type Sales to disable access to Settings",
+    "Current_Password": "Current Password",
+    "New_Password": "New Password",
+    "Administrator": "Administrator",
+    "Sales": "Sales",
+    "Member": "Member",
+    "Confirm_New_Password": "Confirm New Password",
+    "Confirm_Password": "Confirm Password",
+    "Full_Name": "Full Name",
+    "User_Type": "User Type",
+    "Address": "Address",
     "Created_On": "Created On",
     "Expires_On": "Expires On",
+    "Phone_Number": "Phone Number",
+    "User_deleted_Successfully": "User deleted Successfully",
+    "Full_Administrator": "Full Administrator",
+    "Keep_Blank_to_do_not_change_Password": "Keep Blank to do not change Password",
+    "Keep_it_blank_if_you_do_not_want_to_show_currency_code": "Keep it blank if you do not want to show currency code",
+    "Theme_Style": "Theme Style",
+    "Theme_Color": "Theme Color",
+    "Default_Language": "Default Language",
+    "Network": "Network",
+    "Routers": "Routers",
+    "IP_Pool": "IP Pool",
+    "New_Router": "New Router",
+    "Add_Router": "Add Router",
+    "Edit_Router": "Edit Router",
+    "Router_Name": "Router Name",
+    "IP_Address": "IP Address",
+    "Router_Secret": "Router Secret",
+    "Description": "Description",
+    "IP_Router_Already_Exist": "IP Router Already Exist",
+    "Name_Pool": "Name Pool",
+    "Range_IP": "Range IP",
+    "New_Pool": "New Pool",
+    "Add_Pool": "Add Pool",
+    "Edit_Pool": "Edit Pool",
+    "Pool_Name_Already_Exist": "Pool Name Already Exist",
+    "Refill_Account": "Refill Account",
+    "Recharge_Account": "Recharge Account",
+    "Select_Account": "Select Account",
+    "Service_Plan": "Service Plan",
+    "Recharge": "Recharge",
     "Method": "Method",
-    "Extend": "Extend",
+    "Account_Created_Successfully": "Account Created Successfully",
+    "Database_Status": "Database Status",
+    "Total_Database_Size": "Total Database Size",
+    "Download_Database_Backup": "Download Database Backup",
+    "Table_Name": "Table Name",
+    "Rows": "Rows",
+    "Size": "Size",
+    "Customer": "Customer",
+    "Add_New_Contact": "Add New Contact",
+    "Edit_Contact": "Edit Contact",
+    "List_Contact": "List Contact",
+    "Manage_Contact": "Manage Contact",
+    "Reports": "Reports",
+    "Daily_Reports": "Daily Reports",
+    "Period_Reports": "Period Reports",
+    "All_Transactions": "All Transactions",
+    "Total_Income": "Total Income",
+    "All_Transactions_at_Date": "All Transactions at Date",
+    "Export_for_Print": "Export for Print",
+    "Print": "Print",
+    "Export_to_PDF": "Export to PDF",
+    "Click_Here_to_Print": "Click Here to Print",
+    "You_can_use_html_tag": "You can use html tag",
+    "Date_Format": "Date Format",
+    "Income_Today": "Income Today",
+    "Income_This_Month": "Income This Month",
+    "Users_Active": "Users Active",
+    "Total_Users": "Total Users",
+    "Users": "Users",
+    "Edit_User": "Edit User",
+    "Last_Login": "Last Login",
+    "Administrator_Users": "Administrator Users",
+    "Manage_Administrator": "Manage Administrator",
+    "Add_New_Administrator": "Add New Administrator",
+    "Localisation": "Localisation",
+    "Backup_Restore": "Backup\/Restore",
+    "General_Settings": "General Settings",
+    "Date": "Date",
+    "Login_Successful": "Login Successful",
+    "Failed_Login": "Failed Login",
+    "Settings_Saved_Successfully": "Settings Saved Successfully",
+    "User_Updated_Successfully": "User Updated Successfully",
+    "User_Expired__Today": "User Expired, Today",
+    "Activity_Log": "Activity Log",
+    "View_Reports": "View Reports",
+    "View_All": "View All",
+    "Number_of_Vouchers": "Number of Vouchers",
+    "Length_Code": "Length Code",
+    "Code_Voucher": "Code Voucher",
+    "Voucher": "Voucher",
+    "Hotspot_Voucher": "Hotspot Voucher",
     "Status_Voucher": "Status Voucher",
-    "Generated_By": "Generated By",
-    "View": "View",
-    "Buy_Balance": "Buy Balance",
-    "Buy_Package": "Buy Package",
-    "Order_History": "Order History",
-    "List_Activated_Voucher": "List Activated Voucher",
-    "Invoice": "Invoice",
-    "Select_Plans": "Select Plans",
+    "Add_Vouchers": "Add Vouchers",
+    "Create_Vouchers_Successfully": "Create Vouchers Successfully",
+    "Generate": "Generate",
+    "Print_side_by_side__it_will_easy_to_cut": "Print side by side, it will easy to cut",
+    "From_Date": "From Date",
+    "To_Date": "To Date",
+    "New_Service": "New Service",
+    "Type": "Type",
+    "Finish": "Finish",
     "Application_Name__Company_Name": "Application Name\/ Company Name",
     "This_Name_will_be_shown_on_the_Title": "This Name will be shown on the Title",
-    "Company_Logo": "Company Logo",
+    "Next": "Next",
+    "Last": "Last",
+    "Timezone": "Timezone",
+    "Decimal_Point": "Decimal Point",
+    "Thousands_Separator": "Thousands Separator",
+    "Currency_Code": "Currency Code",
+    "Order_Voucher": "Order Voucher",
+    "Voucher_Activation": "Voucher Activation",
+    "List_Activated_Voucher": "List Activated Voucher",
+    "Enter_voucher_code_here": "Enter voucher code here",
+    "Private_Message": "Private Message",
+    "Inbox": "Inbox",
+    "Outbox": "Outbox",
+    "Compose": "Compose",
+    "Send_to": "Send to",
+    "Title": "Title",
+    "Message": "Message",
+    "Your_Account_Information": "Your Account Information",
+    "Welcome_to_the_Panel_Members_page__on_this_page_you_can_": "Welcome to the Panel Members page, on this page you can:",
+    "Invalid_Username_or_Password": "Invalid Username or Password",
+    "You_do_not_have_permission_to_access_this_page": "You do not have permission to access this page",
+    "Incorrect_Current_Password": "Incorrect Current Password",
+    "Password_changed_successfully__Please_login_again": "Password changed successfully, Please login again",
+    "All_field_is_required": "All field is required",
+    "Voucher_Not_Valid": "Voucher Not Valid",
+    "Activation_Vouchers_Successfully": "Activation Vouchers Successfully",
+    "Data_Not_Found": "Data Not Found",
+    "Search_by_Username": "Search by Username",
+    "Search_by_Name": "Search by Name",
+    "Search_by_Code_Voucher": "Search by Code Voucher",
+    "Search": "Search",
+    "Select_a_customer": "Select a customer",
+    "Select_Routers": "Select Routers",
+    "Select_Plans": "Select Plans",
+    "Select_Pool": "Select Pool",
+    "Hrs": "Hrs",
+    "Mins": "Mins",
+    "Days": "Days",
+    "Months": "Months",
+    "Add_Language": "Add Language",
+    "Language_Name": "Language Name",
+    "Folder_Name": "Folder Name",
+    "Translator": "Translator",
+    "Language_Name_Already_Exist": "Language Name Already Exist",
+    "Payment_Gateway": "Payment Gateway",
+    "Community": "Community",
+    "1_user_can_be_used_for_many_devices_": "1 user can be used for many devices?",
+    "Cannot_be_change_after_saved": "Cannot be change after saved",
+    "Explain_Coverage_of_router": "Explain Coverage of router",
+    "Name_of_Area_that_router_operated": "Name of Area that router operated",
+    "Payment_Notification_URL__Recurring_Notification_URL__Pay_Account_Notification_URL": "Payment Notification URL, Recurring Notification URL, Pay Account Notification URL",
+    "Finish_Redirect_URL__Unfinish_Redirect_URL__Error_Redirect_URL": "Finish Redirect URL, Unfinish Redirect URL, Error Redirect URL",
+    "Status": "Status",
+    "Plan_Not_found": "Plan Not found",
+    "Failed_to_create_transaction_": "Failed to create transaction.",
+    "Seller_has_not_yet_setup_Xendit_payment_gateway": "Seller has not yet setup Xendit payment gateway",
+    "Admin_has_not_yet_setup_Xendit_payment_gateway__please_tell_admin": "Admin has not yet setup Xendit payment gateway, please tell admin",
+    "You_already_have_unpaid_transaction__cancel_it_or_pay_it_": "You already have unpaid transaction, cancel it or pay it.",
+    "Transaction_Not_found": "Transaction Not found",
+    "Cancel_it_": "Cancel it?",
+    "expired": "expired",
+    "Check_for_Payment": "Check for Payment",
+    "Transaction_still_unpaid_": "Transaction still unpaid.",
+    "Paid_Date": "Paid Date",
+    "Transaction_has_been_paid_": "Transaction has been paid.",
+    "PAID": "PAID",
+    "CANCELED": "CANCELED",
+    "UNPAID": "UNPAID",
+    "PAY_NOW": "PAY NOW",
+    "Buy_Hotspot_Plan": "Buy Hotspot Plan",
+    "Buy_PPOE_Plan": "Buy PPOE Plan",
+    "Package": "Package",
+    "Order_Internet_Package": "Order Internet Package",
+    "Unknown_Command_": "Unknown Command.",
+    "Checking_payment": "Checking payment",
+    "Create_Transaction_Success": "Create Transaction Success",
+    "You_have_unpaid_transaction": "You have unpaid transaction",
+    "TripayPayment_Channel": "TripayPayment Channel",
+    "Payment_Channel": "Payment Channel",
+    "Payment_check_failed_": "Payment check failed.",
+    "Order_Package": "Order Package",
+    "Transactions": "Transactions",
+    "Payments": "Payments",
+    "History": "History",
+    "Order_History": "Order History",
+    "Gateway": "Gateway",
+    "Date_Done": "Date Done",
+    "Unpaid_Order": "Unpaid Order",
+    "Payment_Gateway_Not_Found": "Payment Gateway Not Found",
+    "Payment_Gateway_saved_successfully": "Payment Gateway saved successfully",
+    "ORDER": "ORDER",
+    "Package_History": "Package History",
+    "Buy_History": "Buy History",
+    "Activation_History": "Activation History",
+    "Buy_Package": "Buy Package",
+    "Email": "Email",
     "Company_Footer": "Company Footer",
     "Will_show_below_user_pages": "Will show below user pages",
-    "Address": "Address",
-    "You_can_use_html_tag": "You can use html tag",
-    "Phone_Number": "Phone Number",
-    "Invoice_Footer": "Invoice Footer",
-    "Recharge_Using": "Recharge Using",
-    "Cash": "Cash",
-    "Bank_Transfer": "Bank Transfer",
-    "Income_reset_date": "Income reset date",
-    "Monthly_Registered_Customers": "Monthly Registered Customers",
-    "Total_Monthly_Sales": "Total Monthly Sales",
-    "All_Users_Insights": "All Users Insights",
-    "Activity_Log": "Activity Log",
-    "User_Expired__Today": "User Expired, Today",
+    "Request_OTP": "Request OTP",
+    "Verification_Code": "Verification Code",
+    "SMS_Verification_Code": "SMS Verification Code",
+    "Please_enter_your_email_address": "Please enter your email address",
+    "Failed_to_create_Paypal_transaction_": "Failed to create Paypal transaction.",
+    "Plugin": "Plugin",
+    "Plugin_Manager": "Plugin Manager",
+    "User_Notification": "User Notification",
+    "Expired_Notification": "Expired Notification",
+    "User_will_get_notification_when_package_expired": "User will get notification when package expired",
+    "Expired_Notification_Message": "Expired Notification Message",
+    "Payment_Notification": "Payment Notification",
+    "User_will_get_invoice_notification_when_buy_package_or_package_refilled": "User will get invoice notification when buy package or package refilled",
+    "Current_IP": "Current IP",
+    "Current_MAC": "Current MAC",
+    "Login_Status": "Login Status",
+    "Login_Request_successfully": "Login Request successfully",
+    "Logout_Request_successfully": "Logout Request successfully",
+    "Disconnect_Internet_": "Disconnect Internet?",
+    "Not_Online__Login_now_": "Not Online, Login now?",
+    "You_are_Online__Logout_": "You are Online, Logout?",
+    "Connect_to_Internet_": "Connect to Internet?",
+    "Your_account_not_connected_to_internet": "Your account not connected to internet",
+    "Failed_to_create_transaction__": "Failed to create transaction. ",
+    "Failed_to_check_status_transaction__": "Failed to check status transaction. ",
     "Disable_Voucher": "Disable Voucher",
-    "Voucher_activation_menu_will_be_hidden": "Voucher activation menu will be hidden",
-    "Voucher_Format": "Voucher Format",
-    "Disable_Registration": "Disable Registration",
-    "Customer_just_Login_with_Phone_number_and_Voucher_Code__Voucher_will_be_password": "Customer just Login with Phone number and Voucher Code, Voucher will be password",
-    "After_Customer_activate_voucher_or_login__customer_will_be_redirected_to_this_url": "After Customer activate voucher or login, customer will be redirected to this url",
-    "Extend_Postpaid_Expiration": "Extend Postpaid Expiration",
-    "Allow_Extend": "Allow Extend",
-    "Extend_Days": "Extend Days",
-    "Confirmation_Message": "Confirmation Message",
+    "Balance": "Balance",
     "Balance_System": "Balance System",
     "Enable_System": "Enable System",
-    "Customer_can_deposit_money_to_buy_voucher": "Customer can deposit money to buy voucher",
     "Allow_Transfer": "Allow Transfer",
-    "Allow_balance_transfer_between_customers": "Allow balance transfer between customers",
-    "Minimum_Balance_Transfer": "Minimum Balance Transfer",
     "Telegram_Notification": "Telegram Notification",
     "SMS_OTP_Registration": "SMS OTP Registration",
     "Whatsapp_Notification": "Whatsapp Notification",
-    "Email_Notification": "Email Notification",
-    "Expired_Notification": "Expired Notification",
-    "User_will_get_notification_when_package_expired": "User will get notification when package expired",
-    "Payment_Notification": "Payment Notification",
-    "User_will_get_invoice_notification_when_buy_package_or_package_refilled": "User will get invoice notification when buy package or package refilled",
-    "Reminder_Notification": "Reminder Notification",
     "Tawk_to_Chat_Widget": "Tawk.to Chat Widget",
-    "This_Token_will_act_as_SuperAdmin_Admin": "This Token will act as SuperAdmin\/Admin",
+    "Invoice": "Invoice",
+    "Country_Code_Phone": "Country Code Phone",
+    "Voucher_activation_menu_will_be_hidden": "Voucher activation menu will be hidden",
+    "Customer_can_deposit_money_to_buy_voucher": "Customer can deposit money to buy voucher",
+    "Allow_balance_transfer_between_customers": "Allow balance transfer between customers",
+    "Reminder_Notification": "Reminder Notification",
+    "Reminder_Notification_Message": "Reminder Notification Message",
+    "Reminder_7_days": "Reminder 7 days",
+    "Reminder_3_days": "Reminder 3 days",
+    "Reminder_1_day": "Reminder 1 day",
+    "PPPOE_Password": "PPPOE Password",
+    "User_Cannot_change_this__only_admin__if_it_Empty_it_will_use_user_password": "User Cannot change this, only admin. if it Empty it will use user password",
+    "Invoice_Balance_Message": "Invoice Balance Message",
+    "Invoice_Notification_Payment": "Invoice Notification Payment",
+    "Balance_Notification_Payment": "Balance Notification Payment",
+    "Balance_Plans": "Balance Plans",
+    "Buy_Balance": "Buy Balance",
+    "Price": "Price",
+    "Validity": "Validity",
+    "Disable_auto_renewal_": "Disable auto renewal?",
+    "Auto_Renewal_On": "Auto Renewal On",
+    "Enable_auto_renewal_": "Enable auto renewal?",
+    "Auto_Renewal_Off": "Auto Renewal Off",
+    "Refill_Balance": "Refill Balance",
+    "Invoice_Footer": "Invoice Footer",
+    "Pay_With_Balance": "Pay With Balance",
+    "Pay_this_with_Balance__your_active_package_will_be_overwrite": "Pay this with Balance? your active package will be overwrite",
+    "Success_to_buy_package": "Success to buy package",
+    "Auto_Renewal": "Auto Renewal",
+    "View": "View",
+    "Back": "Back",
+    "Active": "Active",
+    "Transfer_Balance": "Transfer Balance",
+    "Send_your_balance_": "Send your balance?",
+    "Send": "Send",
+    "Cannot_send_to_yourself": "Cannot send to yourself",
+    "Sending_balance_success": "Sending balance success",
+    "From": "From",
+    "To": "To",
+    "insufficient_balance": "insufficient balance",
+    "Send_Balance": "Send Balance",
+    "Received_Balance": "Received Balance",
+    "Minimum_Balance_Transfer": "Minimum Balance Transfer",
+    "Minimum_Transfer": "Minimum Transfer",
+    "Company_Logo": "Company Logo",
+    "Expired_IP_Pool": "Expired IP Pool",
     "Proxy": "Proxy",
     "Proxy_Server": "Proxy Server",
     "Proxy_Server_Login": "Proxy Server Login",
+    "Hotspot_Plan": "Hotspot Plan",
+    "PPPOE_Plan": "PPPOE Plan",
+    "UNKNOWN": "UNKNOWN",
+    "Are_You_Sure_": "Are You Sure?",
+    "Success_to_send_package": "Success to send package",
+    "Target_has_active_plan__different_with_current_plant_": "Target has active plan, different with current plant.",
+    "Recharge_a_friend": "Recharge a friend",
+    "Buy_for_friend": "Buy for friend",
+    "Buy_this_for_friend_account_": "Buy this for friend account?",
+    "Review_package_before_recharge": "Review package before recharge",
+    "Activate": "Activate",
+    "Deactivate": "Deactivate",
+    "Sync": "Sync",
+    "Failed_to_create_PaymeTrust_transaction_": "Failed to create PaymeTrust transaction.",
+    "Location": "Location",
+    "Radius_Plans": "Radius Plans",
+    "Change_title_in_user_Plan_order": "Change title in user Plan order",
+    "Logs": "Logs",
+    "Voucher_Format": "Voucher Format",
+    "Resend_To_Customer": "Resend To Customer",
+    "Your_friend_do_not_have_active_package": "Your friend do not have active package",
+    "Service_Type": "Service Type",
+    "Others": "Others",
+    "PPPoE": "PPPoE",
+    "Hotspot": "Hotspot",
+    "Disable_Registration": "Disable Registration",
+    "Customer_just_Login_with_Phone_number_and_Voucher_Code__Voucher_will_be_password": "Customer just Login with Phone number and Voucher Code, Voucher will be password",
+    "Login___Activate_Voucher": "Login \/ Activate Voucher",
+    "After_Customer_activate_voucher_or_login__customer_will_be_redirected_to_this_url": "After Customer activate voucher or login, customer will be redirected to this url",
+    "Voucher_Prefix": "Voucher Prefix",
+    "Voucher_activation_success__now_you_can_login": "Voucher activation success, now you can login",
+    "Buy_this__your_active_package_will_be_overwritten": "Buy this? your active package will be overwritten",
+    "Pay_this_with_Balance__your_active_package_will_be_overwritten": "Pay this with Balance? your active package will be overwritten",
+    "Buy_this__your_active_package_will_be_overwrite": "Buy this? your active package will be overwrite",
+    "Monthly_Registered_Customers": "Monthly Registered Customers",
+    "Total_Monthly_Sales": "Total Monthly Sales",
+    "Active_Users": "Active Users",
+    "All_Users_Insights": "All Users Insights",
+    "SuperAdmin": "Super Admin",
+    "Radius": "Radius",
+    "Radius_NAS": "Radius NAS",
+    "Translation": "Translation",
+    "Translation_saved_Successfully": "Translation saved Successfully",
+    "Language_Editor": "Language Editor",
+    "year": "year",
+    "month": "month",
+    "week": "week",
+    "day": "day",
+    "hour": "hour",
+    "minute": "minute",
+    "second": "second",
+    "Attributes": "Attributes",
+    "Profile": "Profile",
+    "Phone": "Phone",
+    "City": "City",
+    "Sub_District": "Sub District",
+    "Ward": "Ward",
+    "Credentials": "Credentials",
+    "Agent": "Agent",
+    "This_Token_will_act_as_SuperAdmin_Admin": "This Token will act as SuperAdmin\/Admin",
+    "Login": "Login",
+    "Expired_Action": "Expired Action",
+    "Expired_Address_List_Name": "Expired Address List Name",
+    "Address_List": "Address List",
+    "Optional": "Optional",
+    "Generated_By": "Generated By",
+    "Admin": "Admin",
+    "Password_should_be_minimum_6_characters": "Password should be minimum 6 characters",
+    "Add_User": "Add User",
+    "Send_Notification": "Send Notification",
+    "Code": "Code",
+    "Send_To_Customer": "Send To Customer",
+    "Prev": "Prev",
+    "Voucher_Not_Found": "Voucher Not Found",
     "Miscellaneous": "Miscellaneous",
     "OTP_Required": "OTP Required",
+    "Change": "Change",
+    "Change_Phone_Number": "Change Phone Number",
+    "Current_Number": "Current Number",
+    "New_Number": "New Number",
+    "Input_your_phone_number": "Input your phone number",
+    "OTP": "OTP",
+    "Enter_OTP_that_was_sent_to_your_phone": "Enter OTP that was sent to your phone",
+    "Update": "Update",
     "OTP_is_required_when_user_want_to_change_phone_number": "OTP is required when user want to change phone number",
+    "Rate": "Rate",
+    "Burst": "Burst",
+    "Editing_Bandwidth_will_not_automatically_update_the_plan__you_need_to_edit_the_plan_then_save_again": "Editing Bandwidth will not automatically update the plan, you need to edit the plan then save again",
     "OTP_Method": "OTP Method",
     "SMS": "SMS",
     "WhatsApp": "WhatsApp",
     "SMS_and_WhatsApp": "SMS and WhatsApp",
     "The_method_which_OTP_will_be_sent_to_user": "The method which OTP will be sent to user",
+    "Report_Viewer": "Report Viewer",
+    "Super_Administrator": "Super Administrator",
+    "Send_To": "Send To",
+    "Resend": "Resend",
+    "Alert": "Alert",
+    "success": "success",
+    "Click_Here": "Click Here",
+    "danger": "danger",
+    "Logout_Successful": "Logout Successful",
+    "warning": "warning",
+    "Users_Announcement": "Users Announcement",
+    "Customer_Announcement": "Customer Announcement",
+    "1_Period___1_Month__Expires_the_20th_of_each_month": "1 Period = 1 Month, Expires the 20th of each month",
+    "Period": "Period",
+    "Add": "Add",
+    "Select_Payment_Gateway": "Select Payment Gateway",
+    "Available_Payment_Gateway": "Available Payment Gateway",
+    "Pay_Now": "Pay Now",
+    "Please_select_Payment_Gateway": "Please select Payment Gateway",
+    "Payment_Gateway_Deleted": "Payment Gateway Deleted",
+    "Payment_Gateway_not_set__please_set_it_in_Settings": "Payment Gateway not set, please set it in Settings",
+    "Failed_to_create_Transaction__": "Failed to create Transaction..",
+    "Show_To_Customer": "Type",
+    "Using": "Using",
+    "Default": "Default",
+    "Customer_Balance": "Customer Balance",
+    "Vouchers": "Vouchers",
+    "Refill_Customer": "Refill Customer",
+    "Recharge_Customer": "Recharge Customer",
+    "Plans": "Plans",
+    "PPPOE": "PPPOE",
+    "Bandwidth": "Bandwidth",
+    "Customers": "Customers",
+    "Actives": "Actives",
+    "Name": "Name",
+    "Confirm": "Confirm",
+    "Plan": "Plan",
+    "Total": "Total",
+    "Current_Cycle": "Current Cycle",
+    "Additional_Cost": "Additional Cost",
+    "Remaining": "Remaining",
+    "Not_Found": "Not Found",
+    "Cash": "Cash",
+    "Payment_not_found": "Payment not found",
+    "If_your_friend_have_Additional_Cost__you_will_pay_for_that_too": "If your friend have Additional Cost, you will pay for that too",
+    "Cache_cleared_successfully_": "Cache cleared successfully!",
+    "Paid": "Paid",
+    "Send_Message": "Send Message",
+    "Send_Personal_Message": "Send Personal Message",
+    "Send_Via": "Send Via",
+    "Compose_your_message___": "Compose your message...",
+    "Use_placeholders_": "Use placeholders:",
+    "Customer_Name": "Customer Name",
+    "Customer_Username": "Customer Username",
+    "Customer_Phone": "Customer Phone",
+    "Your_Company_Name": "Your Company Name",
+    "Message_Sent_Successfully": "Message Sent Successfully",
+    "Send_Bulk_Message": "Send Bulk Message",
+    "Group": "Group",
+    "All_Customers": "All Customers",
+    "New_Customers": "New Customers",
+    "Expired_Customers": "Expired Customers",
+    "Active_Customers": "Active Customers",
+    "Map": "Map",
+    "Customer_Location": "Customer Location",
+    "Account_Type": "Account Type",
+    "Coordinates": "Coordinates",
+    "Latitude_and_Longitude_coordinates_for_map_must_be_separate_with_comma____": "Latitude and Longitude coordinates for map must be separate with comma ","",
+    "Customer_Geo_Location_Information": "Customer Geo Location Information",
+    "List": "List",
+    "Lists": "Lists",
+    "Single_Customer": "Single Customer",
+    "Bulk_Customers": "Bulk Customers",
+    "Message_per_time": "Message per time",
+    "5_Messages": "5 Messages",
+    "10_Messages": "10 Messages",
+    "15_Messages": "15 Messages",
+    "20_Messages": "20 Messages",
+    "30_Messages": "30 Messages",
+    "40_Messages": "40 Messages",
+    "50_Messages": "50 Messages",
+    "60_Messages": "60 Messages",
+    "Use_20_and_above_if_you_are_sending_to_all_customers_to_avoid_server_time_out": "Use 20 and above if you are sending to all customers to avoid server time out",
+    "Delay": "Delay",
+    "No_Delay": "No Delay",
+    "5_Seconds": "5 Seconds",
+    "10_Seconds": "10 Seconds",
+    "15_Seconds": "15 Seconds",
+    "20_Seconds": "20 Seconds",
+    "Use_at_least_5_secs_if_you_are_sending_to_all_customers_to_avoid_being_banned_by_your_message_provider": "Use at least 5 secs if you are sending to all customers to avoid being banned by your message provider",
+    "Testing__if_checked_no_real_message_is_sent_": "Testing [if checked no real message is sent]",
+    "All_fields_are_required": "All fields are required",
+    "Personal": "Personal",
+    "Email_Notification": "Email Notification",
+    "Router_Name___Location": "Router Name \/ Location",
+    "Plan_Category": "Plan Category",
+    "ID": "ID",
+    "Internet_Plan": "Internet Plan",
+    "Privacy_Policy": "Privacy Policy",
+    "Terms_and_Conditions": "Terms and Conditions",
+    "Contact": "Contact",
+    "will_be_replaced_with_Customer_Name": "will be replaced with Customer Name",
+    "will_be_replaced_with_Customer_username": "will be replaced with Customer username",
+    "will_be_replaced_with_Package_name": "will be replaced with Package name",
+    "will_be_replaced_with_Package_price": "will be replaced with Package price",
+    "additional_bills_for_customers": "additional bills for customers",
+    "will_be_replaced_with_Expiration_date": "will be replaced with Expiration date",
+    "Your_Company_Name_at_Settings": "Your Company Name at Settings",
+    "Your_Company_Address_at_Settings": "Your Company Address at Settings",
+    "Your_Company_Phone_at_Settings": "Your Company Phone at Settings",
+    "Invoice_number": "Invoice number",
+    "Date_invoice_created": "Date invoice created",
+    "Payment_gateway_user_paid_from": "Payment gateway user paid from",
+    "Payment_channel_user_paid_from": "Payment channel user paid from",
+    "is_Hotspot_or_PPPOE": "is Hotspot or PPPOE",
+    "Internet_Package": "Internet Package",
+    "Internet_Package_Prices": "Internet Package Prices",
+    "Receiver_name": "Receiver name",
+    "Username_internet": "Username internet",
+    "User_password": "User password",
+    "Expired_datetime": "Expired datetime",
+    "For_Notes_by_admin": "For Notes by admin",
+    "Transaction_datetime": "Transaction datetime",
+    "Balance_Before": "Balance Before",
+    "Balance_After": "Balance After",
+    "how_much_balance_have_been_send": "how much balance have been send",
+    "Current_Balance": "Current Balance",
+    "Sender_name": "Sender name",
+    "how_much_balance_have_been_received": "how much balance have been received",
+    "Extend_Postpaid_Expiration": "Extend Postpaid Expiration",
+    "Allow_Extend": "Allow Extend",
+    "Extend_Days": "Extend Days",
+    "Confirmation_Message": "Confirmation Message",
+    "You_are_already_logged_in": "You are already logged in",
+    "Extend": "Extend",
+    "Created___Expired": "Created \/ Expired",
+    "Bank_Transfer": "Bank Transfer",
+    "Recharge_Using": "Recharge Using",
+    "ago": "ago",
+    "Disabled": "Disabled",
+    "Banned": "Banned",
+    "Customer_cannot_login_again": "Customer cannot login again",
+    "Customer_can_login_but_cannot_buy_internet_plan__Admin_cannot_recharge_customer": "Customer can login but cannot buy internet plan, Admin cannot recharge customer",
+    "Don_t_forget_to_deactivate_all_active_plan_too": "Don't forget to deactivate all active plan too",
+    "Ascending": "Ascending",
+    "Descending": "Descending",
+    "Created_Date": "Created Date",
+    "Inactive": "Inactive",
+    "Suspended": "Suspended",
+    "Query": "Query",
+    "Notes": "Notes",
+    "This_account_status": "This account status",
+    "Maintenance_Mode": "Maintenance Mode",
+    "Maintenance_Mode_Settings": "Maintenance Mode Settings",
+    "Status_": "Status:",
+    "End_Date_": "End Date:",
+    "Save": "Save",
+    "Site_is_temporarily_unavailable_": "Site is temporarily unavailable.",
+    "Scheduled_maintenance_is_currently_in_progress__Please_check_back_soon_": "Scheduled maintenance is currently in progress. Please check back soon.",
+    "We_apologize_for_any_inconvenience_": "We apologize for any inconvenience.",
+    "The": "The",
+    "Team": "Team",
     "Extend_Package_Expiry": "Extend Package Expiry",
-    "Yes": "Yes",
     "No": "No",
+    "Yes": "Yes",
     "If_user_buy_same_internet_plan__expiry_date_will_extend": "If user buy same internet plan, expiry date will extend",
     "Tax_System": "Tax System",
     "Enable_Tax_System": "Enable Tax System",
     "Tax_will_be_calculated_in_Internet_Plan_Price": "Tax will be calculated in Internet Plan Price",
     "Tax_Rate": "Tax Rate",
+    "0_5_": "0.5%",
+    "1_": "1%",
+    "1_5_": "1.5%",
+    "2_": "2%",
+    "5_": "5%",
+    "10_": "10%",
     "Custom": "Custom",
     "Tax_Rates_in_percentage": "Tax Rates in percentage",
     "Custom_Tax_Rate": "Custom Tax Rate",
     "Enter_Custom_Tax_Rate": "Enter Custom Tax Rate",
     "Enter_the_custom_tax_rate__e_g___3_75_for_3_75__": "Enter the custom tax rate (e.g., 3.75 for 3.75%)",
-    "Timezone": "Timezone",
-    "Date_Format": "Date Format",
-    "Default_Language": "Default Language",
-    "Language_Editor": "Language Editor",
-    "Decimal_Point": "Decimal Point",
-    "Thousands_Separator": "Thousands Separator",
-    "Currency_Code": "Currency Code",
-    "Keep_it_blank_if_you_do_not_want_to_show_currency_code": "Keep it blank if you do not want to show currency code",
-    "Country_Code_Phone": "Country Code Phone",
-    "Change_title_in_user_Plan_order": "Change title in user Plan order"
+    "Additional_Information": "Additional Information",
+    "City_of_Resident": "City of Resident",
+    "District": "District",
+    "State": "State",
+    "State_of_Resident": "State of Resident",
+    "Zip": "Zip",
+    "Zip_Code": "Zip Code",
+    "Local_IP": "Local IP",
+    "Device": "Device",
+    "Expired_Internet_Plan": "Expired Internet Plan",
+    "When_Expired__customer_will_be_move_to_selected_internet_plan": "When Expired, customer will be move to selected internet plan",
+    "Plugin_Installer": "Plugin Installer",
+    "Expired_Date": "Expired Date",
+    "Expired": "Expired",
+    "Time": "Time",
+    "Data": "Data",
+    "Category": "Category",
+    "later": "later",
+    "Package_Details": "Package Details",
+    "Summary": "Summary",
+    "Devices_Not_Found": "Devices Not Found",
+    "Income_reset_date": "Income reset date",
+    "Devices": "Devices",
+    "Documentation": "Documentation",
+    "Hotspot_Auth_Method": "Hotspot Auth Method",
+    "Api": "Api",
+    "Http_Chap": "Http-Chap",
+    "Hotspot_Authentication_Method__Make_sure_you_have_changed_your_hotspot_login_page_": "Hotspot Authentication Method. Make sure you have changed your hotspot login page."
 }
\ No newline at end of file
diff --git a/system/lan/indonesia.json b/system/lan/indonesia.json
index 61a5fa43..962c844a 100644
--- a/system/lan/indonesia.json
+++ b/system/lan/indonesia.json
@@ -561,5 +561,11 @@
     "Please_wait_1015_seconds_before_sending_another_SMS": "Harap tunggu 1015 detik sebelum mengirim SMS lainnya",
     "Phone_number_updated_successfully": "Nomor telepon berhasil diperbarui",
     "You_cannot_use_your_current_phone_number": "Anda tidak dapat menggunakan nomor telepon Anda saat ini",
-    "Devices": "Perangkat"
+    "Devices": "Perangkat",
+    "Voucher_Prefix": "Awalan Voucher",
+    "This_account_status": "Status akun ini",
+    "Hotspot_Auth_Method": "Metode Otentikasi Hotspot",
+    "Api": "Api",
+    "Http_Chap": "Http-Bab",
+    "Hotspot_Authentication_Method__Make_sure_you_have_changed_your_hotspot_login_page_": "Metode Otentikasi Hotspot. Pastikan Anda telah mengubah halaman login hotspot Anda."
 }
\ No newline at end of file
diff --git a/system/updates.json b/system/updates.json
index a0aadee5..0ed14652 100644
--- a/system/updates.json
+++ b/system/updates.json
@@ -120,5 +120,10 @@
     "2024.6.21" : [
         "ALTER TABLE `tbl_plans` ADD `on_login` TEXT NULL DEFAULT NULL AFTER `device`;",
         "ALTER TABLE `tbl_plans` ADD `on_logout` TEXT NULL DEFAULT NULL AFTER `on_login`;"
+    ],
+    "2024.7.6" : [
+        "CREATE TABLE IF NOT EXISTS `rad_acct` ( `id` bigint NOT NULL, `acctsessionid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `realm` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `nasid` varchar(32) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `nasipaddress` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `nasportid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `nasporttype` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `framedipaddress` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',`acctstatustype` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, `macaddr` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `dateAdded` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;",
+        "ALTER TABLE `rad_acct` ADD PRIMARY KEY (`id`), ADD KEY `username` (`username`), ADD KEY `framedipaddress` (`framedipaddress`), ADD KEY `acctsessionid` (`acctsessionid`), ADD KEY `nasipaddress` (`nasipaddress`);",
+        "ALTER TABLE `rad_acct` MODIFY `id` bigint NOT NULL AUTO_INCREMENT;"
     ]
 }
\ No newline at end of file
diff --git a/ui/ui/app-settings.tpl b/ui/ui/app-settings.tpl
index c3d215a1..11486ca7 100644
--- a/ui/ui/app-settings.tpl
+++ b/ui/ui/app-settings.tpl
@@ -621,6 +621,22 @@
                         

{Lang::T('If user buy same internet plan, expiry date will extend')}

+
+ +
+ +
+

+ {Lang::T('Hotspot Authentication Method. Make sure you have changed your hotspot login page.')}
Download phpnuxbill-login-hotspot +

+
diff --git a/ui/ui/hotspot-edit.tpl b/ui/ui/hotspot-edit.tpl index d5332f1d..75de4635 100644 --- a/ui/ui/hotspot-edit.tpl +++ b/ui/ui/hotspot-edit.tpl @@ -248,20 +248,24 @@
-
-
-
on-login / on-up
-
- + {if !$d['is_radius']} +
+
+
on-login / on-up
+
+ +
+
+
+
on-logout / on-down
+
+ +
-
-
on-logout / on-down
-
- -
-
-
+ {/if}
@@ -306,11 +310,17 @@ {/literal} {/if} - - + + - - + + + + + +{/if} {include file="sections/user-footer.tpl"} \ No newline at end of file diff --git a/version.json b/version.json index d8a2a3b9..a2aa74b9 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2024.6.25" + "version": "2024.7.15" } \ No newline at end of file