diff --git a/CHANGELOG.md b/CHANGELOG.md index 273041d8..a1d09b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ # CHANGELOG +## 2023.8.24 + +- Balance transfer between Customer +- Optimize Cronjob +- View Customer Info +- Ajax for select customer + +## 2023.8.18 + +- Fix Auto Renewall Cronjob +- Add comment to Mikrotik User + ## 2023.8.16 - Admin Can Add Balance to Customer diff --git a/install/phpnuxbill.sql b/install/phpnuxbill.sql index 602f8364..28903fe6 100644 --- a/install/phpnuxbill.sql +++ b/install/phpnuxbill.sql @@ -50,12 +50,12 @@ CREATE TABLE `id` int(10) NOT NULL, `username` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `password` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, - `pppoe_password` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1', + `pppoe_password` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', `fullname` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `address` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, `phonenumber` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0', `email` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1', - `balance` decimal(15,2) NOT NULL COMMENT 'For Money Deposit', + `balance` decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT 'For Money Deposit', `auto_renewal` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Auto renewal from balance', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_login` datetime DEFAULT NULL diff --git a/system/autoload/Balance.php b/system/autoload/Balance.php index 2b3713d2..f6557a3f 100644 --- a/system/autoload/Balance.php +++ b/system/autoload/Balance.php @@ -18,18 +18,14 @@ class Balance public static function transfer($id_customer, $phoneTarget, $amount) { global $config; - if ($config['allow_balance_transfer'] == 'yes') { - if(Balance::min($id_customer, $amount)){ - if(Balance::plusByPhone($phoneTarget, $amount)){ - return true; - }else{ - Balance::plus($id_customer, $amount); - return false; - } - }else{ + if (Balance::min($id_customer, $amount)) { + if (Balance::plusByPhone($phoneTarget, $amount)) { + return true; + } else { + Balance::plus($id_customer, $amount); return false; } - }else{ + } else { return false; } } @@ -38,7 +34,7 @@ class Balance { $c = ORM::for_table('tbl_customers')->where('id', $id_customer)->find_one(); if ($c && $c['balance'] >= $amount) { - $c->balance = $amount - $c['balance']; + $c->balance = $c['balance'] - $amount; $c->save(); return true; } else { @@ -49,7 +45,7 @@ class Balance public static function plusByPhone($phone_customer, $amount) { $c = ORM::for_table('tbl_customers')->where('username', $phone_customer)->find_one(); - if($c){ + if ($c) { $c->balance = $amount + $c['balance']; $c->save(); return true; @@ -61,7 +57,7 @@ class Balance { $c = ORM::for_table('tbl_customers')->where('username', $phone_customer)->find_one(); if ($c && $c['balance'] >= $amount) { - $c->balance = $amount - $c['balance']; + $c->balance = $c['balance'] - $amount; $c->save(); return true; } else { diff --git a/system/autoload/Lang.php b/system/autoload/Lang.php index 6c790bd4..52abdd0f 100644 --- a/system/autoload/Lang.php +++ b/system/autoload/Lang.php @@ -31,4 +31,31 @@ class Lang return $phone; } } + + public static function dateFormat($date){ + global $config; + return date($config['date_format'], strtotime($date)); + } + + public static function dateTimeFormat($date){ + global $config; + return date($config['date_format']. ' H:i', strtotime($date)); + } + + public static function nl2br($text){ + return nl2br($text); + } + + public static function arrayCount($arr){ + return count($arr); + } + + public static function getNotifText($key){ + global $_notifmsg, $_notifmsg_default; + if(isset($_notifmsg[$key])){ + return $_notifmsg[$key]; + }else{ + return $_notifmsg_default[$key]; + } + } } diff --git a/system/autoload/Message.php b/system/autoload/Message.php index 0373d682..15981d06 100644 --- a/system/autoload/Message.php +++ b/system/autoload/Message.php @@ -56,4 +56,20 @@ class Message return "$via: $msg"; } + public static function sendBalanceNotification($phone, $name, $balance, $message, $via) + { + $msg = str_replace('[[name]]', "*$name*", $message); + $msg = str_replace('[[balance]]', "*" . Lang::moneyFormat($balance) . "*", $msg); + if ( + !empty($phone) && strlen($phone) > 5 + && !empty($message) && in_array($via, ['sms', 'wa']) + ) { + if ($via == 'sms') { + Message::sendSMS($phone, $msg); + } else if ($via == 'wa') { + Message::sendWhatsapp($phone, $msg); + } + } + return "$via: $msg"; + } } diff --git a/system/autoload/Mikrotik.php b/system/autoload/Mikrotik.php index 17b344e0..7b7f82ca 100644 --- a/system/autoload/Mikrotik.php +++ b/system/autoload/Mikrotik.php @@ -4,8 +4,9 @@ use PEAR2\Net\RouterOS; class Mikrotik { - public static function info($name){ - $d = ORM::for_table('tbl_routers')->where('name',$name)->find_one(); + public static function info($name) + { + $d = ORM::for_table('tbl_routers')->where('name', $name)->find_one(); return $d; } @@ -19,7 +20,8 @@ class Mikrotik } } - public static function isUserLogin($client, $username){ + public static function isUserLogin($client, $username) + { $printRequest = new RouterOS\Request( '/ip hotspot active print', RouterOS\Query::where('user', $username) @@ -27,7 +29,8 @@ class Mikrotik return $client->sendSync($printRequest)->getProperty('.id'); } - public static function logMeIn($client, $user, $pass, $ip, $mac){ + public static function logMeIn($client, $user, $pass, $ip, $mac) + { $addRequest = new RouterOS\Request('/ip/hotspot/active/login'); $client->sendSync( $addRequest @@ -38,7 +41,8 @@ class Mikrotik ); } - public static function logMeOut($client, $user){ + public static function logMeOut($client, $user) + { $printRequest = new RouterOS\Request( '/ip hotspot active print', RouterOS\Query::where('user', $user) @@ -51,7 +55,8 @@ class Mikrotik ); } - public static function addHotspotPlan($client, $name, $sharedusers, $rate){ + public static function addHotspotPlan($client, $name, $sharedusers, $rate) + { $addRequest = new RouterOS\Request('/ip/hotspot/user/profile/add'); $client->sendSync( $addRequest @@ -61,7 +66,8 @@ class Mikrotik ); } - public static function setHotspotPlan($client, $name, $sharedusers, $rate){ + public static function setHotspotPlan($client, $name, $sharedusers, $rate) + { $printRequest = new RouterOS\Request( '/ip hotspot user profile print .proplist=name', RouterOS\Query::where('name', $name) @@ -77,7 +83,8 @@ class Mikrotik ); } - public static function removeHotspotPlan($client, $name){ + public static function removeHotspotPlan($client, $name) + { $printRequest = new RouterOS\Request( '/ip hotspot user profile print .proplist=name', RouterOS\Query::where('name', $name) @@ -119,6 +126,8 @@ class Mikrotik ->setArgument('name', $customer['username']) ->setArgument('profile', $plan['name_plan']) ->setArgument('password', $customer['password']) + ->setArgument('comment', $customer['fullname']) + ->setArgument('email', $customer['email']) ->setArgument('limit-uptime', $timelimit) ); } else if ($plan['limit_type'] == "Data_Limit") { @@ -131,6 +140,8 @@ class Mikrotik ->setArgument('name', $customer['username']) ->setArgument('profile', $plan['name_plan']) ->setArgument('password', $customer['password']) + ->setArgument('comment', $customer['fullname']) + ->setArgument('email', $customer['email']) ->setArgument('limit-bytes-total', $datalimit) ); } else if ($plan['limit_type'] == "Both_Limit") { @@ -147,6 +158,8 @@ class Mikrotik ->setArgument('name', $customer['username']) ->setArgument('profile', $plan['name_plan']) ->setArgument('password', $customer['password']) + ->setArgument('comment', $customer['fullname']) + ->setArgument('email', $customer['email']) ->setArgument('limit-uptime', $timelimit) ->setArgument('limit-bytes-total', $datalimit) ); @@ -156,12 +169,15 @@ class Mikrotik $addRequest ->setArgument('name', $customer['username']) ->setArgument('profile', $plan['name_plan']) + ->setArgument('comment', $customer['fullname']) + ->setArgument('email', $customer['email']) ->setArgument('password', $customer['password']) ); } } - public static function setHotspotUser($client, $user, $pass, $nuser= null){ + public static function setHotspotUser($client, $user, $pass, $nuser = null) + { $printRequest = new RouterOS\Request('/ip/hotspot/user/print'); $printRequest->setArgument('.proplist', '.id'); $printRequest->setQuery(RouterOS\Query::where('name', $user)); @@ -216,9 +232,9 @@ class Mikrotik public static function addPpoeUser($client, $plan, $customer) { $addRequest = new RouterOS\Request('/ppp/secret/add'); - if(!empty($customer['pppoe_password'])){ + if (!empty($customer['pppoe_password'])) { $pass = $customer['pppoe_password']; - }else{ + } else { $pass = $customer['password']; } $client->sendSync( @@ -226,11 +242,13 @@ class Mikrotik ->setArgument('name', $customer['username']) ->setArgument('service', 'pppoe') ->setArgument('profile', $plan['name_plan']) + ->setArgument('comment', $customer['fullname'] . ' | ' . $customer['email']) ->setArgument('password', $pass) ); } - public static function setPpoeUser($client, $user, $pass, $nuser= null){ + public static function setPpoeUser($client, $user, $pass, $nuser = null) + { $printRequest = new RouterOS\Request('/ppp/secret/print'); $printRequest->setArgument('.proplist', '.id'); $printRequest->setQuery(RouterOS\Query::where('name', $user)); @@ -266,7 +284,8 @@ class Mikrotik $client->sendSync($removeRequest); } - public static function removePool($client, $name){ + public static function removePool($client, $name) + { $printRequest = new RouterOS\Request( '/ip pool print .proplist=name', RouterOS\Query::where('name', $name) @@ -274,29 +293,33 @@ class Mikrotik $poolName = $client->sendSync($printRequest)->getProperty('name'); $removeRequest = new RouterOS\Request('/ip/pool/remove'); - $client($removeRequest - ->setArgument('numbers', $poolName) + $client( + $removeRequest + ->setArgument('numbers', $poolName) ); } - public static function addPool($client, $name, $ip_address){ + public static function addPool($client, $name, $ip_address) + { $addRequest = new RouterOS\Request('/ip/pool/add'); - $client->sendSync($addRequest - ->setArgument('name', $name) - ->setArgument('ranges', $ip_address) + $client->sendSync( + $addRequest + ->setArgument('name', $name) + ->setArgument('ranges', $ip_address) ); } - public static function setPool($client, $name, $ip_address){ + public static function setPool($client, $name, $ip_address) + { $printRequest = new RouterOS\Request( '/ip pool print .proplist=name', RouterOS\Query::where('name', $name) ); $poolName = $client->sendSync($printRequest)->getProperty('name'); - if(empty($poolName)){ + if (empty($poolName)) { self::addPool($client, $name, $ip_address); - }else{ + } else { $setRequest = new RouterOS\Request('/ip/pool/set'); $client( $setRequest @@ -307,7 +330,8 @@ class Mikrotik } - public static function addPpoePlan($client, $name, $pool, $rate){ + public static function addPpoePlan($client, $name, $pool, $rate) + { $addRequest = new RouterOS\Request('/ppp/profile/add'); $client->sendSync( $addRequest @@ -318,15 +342,16 @@ class Mikrotik ); } - public static function setPpoePlan($client, $name, $pool, $rate){ + public static function setPpoePlan($client, $name, $pool, $rate) + { $printRequest = new RouterOS\Request( '/ppp profile print .proplist=name', RouterOS\Query::where('name', $name) ); $profileName = $client->sendSync($printRequest)->getProperty('name'); - if(empty($profileName)){ + if (empty($profileName)) { self::addPpoePlan($client, $name, $pool, $rate); - }else{ + } else { $setRequest = new RouterOS\Request('/ppp/profile/set'); $client( $setRequest @@ -338,7 +363,8 @@ class Mikrotik } } - public static function removePpoePlan($client, $name){ + public static function removePpoePlan($client, $name) + { $printRequest = new RouterOS\Request( '/ppp profile print .proplist=name', RouterOS\Query::where('name', $name) diff --git a/system/autoload/Package.php b/system/autoload/Package.php index 0a803dfc..97fbda01 100644 --- a/system/autoload/Package.php +++ b/system/autoload/Package.php @@ -33,7 +33,7 @@ class Package if ($router_name == 'balance') { // insert table transactions - $inv = "INV-" . _raid(5); + $inv = "INV-" . Package::_raid(5); $t = ORM::for_table('tbl_transactions')->create(); $t->invoice = $inv; $t->username = $c['username']; @@ -49,7 +49,7 @@ class Package Balance::plus($id_customer, $p['price']); - $textInvoice = $_notifmsg['invoice_balance']; + $textInvoice = Lang::getNotifText('invoice_balance'); $textInvoice = str_replace('[[company_name]]', $_c['CompanyName'], $textInvoice); $textInvoice = str_replace('[[address]]', $_c['address'], $textInvoice); $textInvoice = str_replace('[[phone]]', $_c['phone'], $textInvoice); @@ -96,6 +96,7 @@ class Package if (!$_c['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); Mikrotik::removeHotspotUser($client, $c['username']); + Mikrotik::removePpoeUser($client, $c['username']); Mikrotik::addHotspotUser($client, $p, $c); } @@ -114,7 +115,7 @@ class Package // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -128,6 +129,7 @@ class Package } else { if (!$_c['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); + Mikrotik::removePpoeUser($client, $c['username']); Mikrotik::addHotspotUser($client, $p, $c); } @@ -147,7 +149,7 @@ class Package // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -163,12 +165,13 @@ class Package "\nRouter: " . $router_name . "\nGateway: " . $gateway . "\nChannel: " . $channel . - "\nPrice: " . $p['price']); + "\nPrice: " . Lang::moneyFormat($p['price'])); } else { if ($b) { if (!$_c['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); + Mikrotik::removeHotspotUser($client, $c['username']); Mikrotik::removePpoeUser($client, $c['username']); Mikrotik::addPpoeUser($client, $p, $c); } @@ -188,7 +191,7 @@ class Package // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -202,6 +205,7 @@ class Package } else { if (!$_c['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); + Mikrotik::removeHotspotUser($client, $c['username']); Mikrotik::addPpoeUser($client, $p, $c); } @@ -221,7 +225,7 @@ class Package // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -237,12 +241,12 @@ class Package "\nRouter: " . $router_name . "\nGateway: " . $gateway . "\nChannel: " . $channel . - "\nPrice: " . $p['price']); + "\nPrice: " . Lang::moneyFormat($p['price'])); } $in = ORM::for_table('tbl_transactions')->where('username', $c['username'])->order_by_desc('id')->find_one(); - $textInvoice = $_notifmsg['invoice_paid']; + $textInvoice = Lang::getNotifText('invoice_paid'); $textInvoice = str_replace('[[company_name]]', $_c['CompanyName'], $textInvoice); $textInvoice = str_replace('[[address]]', $_c['address'], $textInvoice); $textInvoice = str_replace('[[phone]]', $_c['phone'], $textInvoice); @@ -301,4 +305,10 @@ class Package } } } + + + public static function _raid($l) + { + return substr(str_shuffle(str_repeat('0123456789', $l)), 0, $l); + } } diff --git a/system/autoload/Paginator.php b/system/autoload/Paginator.php index 97553cd8..cb6e4b92 100644 --- a/system/autoload/Paginator.php +++ b/system/autoload/Paginator.php @@ -1,28 +1,29 @@ where($w1,$c1)->count(); - }elseif($w2 != ''){ - $totalReq = ORM::for_table($table)->where($w1,$c1)->where($w2,$c2)->count(); - }elseif($w3 != ''){ - $totalReq = ORM::for_table($table)->where($w1,$c1)->where($w2,$c2)->where($w3,$c3)->count(); - }elseif($w4 != ''){ - $totalReq = ORM::for_table($table)->where($w1,$c1)->where($w2,$c2)->where($w3,$c3)->where($w4,$c4)->count(); - }else{ + if ($w1 != '') { + $totalReq = ORM::for_table($table)->where($w1, $c1)->count(); + } elseif ($w2 != '') { + $totalReq = ORM::for_table($table)->where($w1, $c1)->where($w2, $c2)->count(); + } elseif ($w3 != '') { + $totalReq = ORM::for_table($table)->where($w1, $c1)->where($w2, $c2)->where($w3, $c3)->count(); + } elseif ($w4 != '') { + $totalReq = ORM::for_table($table)->where($w1, $c1)->where($w2, $c2)->where($w3, $c3)->where($w4, $c4)->count(); + } else { $totalReq = ORM::for_table($table)->count(); } @@ -85,16 +86,103 @@ Class Paginator } if ($page < $counter - 1) { - $pagination .= "
  • ".$_L['Next']."
  • "; - $pagination .= "
  • ".$_L['Last']."
  • "; + $pagination .= "
  • " . $_L['Next'] . "
  • "; + $pagination .= "
  • " . $_L['Last'] . "
  • "; } else { - $pagination .= "
  • ".$_L['Next']."
  • "; - $pagination .= "
  • ".$_L['Last']."
  • "; + $pagination .= "
  • " . $_L['Next'] . "
  • "; + $pagination .= "
  • " . $_L['Last'] . "
  • "; } $pagination .= ""; - + $gen = array("startpoint" => $startpoint, "limit" => $limit, "found" => $totalReq, "page" => $page, "lastpage" => $lastpage, "contents" => $pagination); return $gen; } } -} \ No newline at end of file + + public static function bootstrapRaw($table, $w1 = '', $c1 = [], $per_page = '10') + { + global $routes; + global $_L; + $url = U . $routes['0'] . '/' . $routes['1'] . '/'; + $adjacents = "2"; + $page = (int)(!isset($routes['2']) ? 1 : $routes['2']); + $pagination = ""; + + if ($w1 != '') { + $totalReq = ORM::for_table($table)->where_raw($w1, $c1)->count(); + } else { + $totalReq = ORM::for_table($table)->count(); + } + + $i = 0; + $page = ($page == 0 ? 1 : $page); + $start = ($page - 1) * $per_page; + + $prev = $page - 1; + $next = $page + 1; + $lastpage = ceil($totalReq / $per_page); + + $lpm1 = $lastpage - 1; + $limit = $per_page; + $startpoint = ($page * $limit) - $limit; + + if ($lastpage >= 1) { + $pagination .= '"; + + $gen = array("startpoint" => $startpoint, "limit" => $limit, "found" => $totalReq, "page" => $page, "lastpage" => $lastpage, "contents" => $pagination); + return $gen; + } + } +} diff --git a/system/boot.php b/system/boot.php index 9e458289..dcfe4f11 100644 --- a/system/boot.php +++ b/system/boot.php @@ -131,9 +131,8 @@ include "autoload/Hookers.php"; // notification message if(file_exists("system/uploads/notifications.json")){ $_notifmsg =json_decode(file_get_contents('system/uploads/notifications.json'), true); -}else{ - $_notifmsg = json_decode(file_get_contents('system/uploads/notifications.default.json'), true); } +$_notifmsg_default = json_decode(file_get_contents('system/uploads/notifications.default.json'), true); //register all plugin foreach (glob("system/plugin/*.php") as $filename) { @@ -191,10 +190,6 @@ function _admin($login = true) } } -function _raid($l) -{ - return substr(str_shuffle(str_repeat('0123456789', $l)), 0, $l); -} function _log($description, $type = '', $userid = '0') { diff --git a/system/controllers/autoload.php b/system/controllers/autoload.php index 7d545b4d..e3b782eb 100644 --- a/system/controllers/autoload.php +++ b/system/controllers/autoload.php @@ -1,8 +1,9 @@ assign('_title', $_L['Network']); @@ -14,28 +15,45 @@ $ui->assign('_admin', $admin); switch ($action) { case 'pool': - $routers = _get('routers'); - $d = ORM::for_table('tbl_pool')->where('routers', $routers)->find_many(); - $ui->assign('d',$d); + $routers = _get('routers'); + $d = ORM::for_table('tbl_pool')->where('routers', $routers)->find_many(); + $ui->assign('d', $d); $ui->display('autoload-pool.tpl'); break; case 'server': - $d = ORM::for_table('tbl_routers')->where('enabled', '1')->find_many(); - $ui->assign('d',$d); + $d = ORM::for_table('tbl_routers')->where('enabled', '1')->find_many(); + $ui->assign('d', $d); $ui->display('autoload-server.tpl'); break; case 'plan': - $server = _post('server'); - $jenis = _post('jenis'); - $d = ORM::for_table('tbl_plans')->where('routers', $server)->where('type', $jenis)->where('enabled', '1')->find_many(); - $ui->assign('d',$d); + $server = _post('server'); + $jenis = _post('jenis'); + $d = ORM::for_table('tbl_plans')->where('routers', $server)->where('type', $jenis)->where('enabled', '1')->find_many(); + $ui->assign('d', $d); $ui->display('autoload.tpl'); break; + case 'customer_select2': + $s = addslashes(_get('s')); + if (empty($s)) { + $c = ORM::for_table('tbl_customers')->limit(30)->find_many(); + } else { + $c = ORM::for_table('tbl_customers')->where_raw("(`username` LIKE '%$s%' OR `fullname` LIKE '%$s%' OR `phonenumber` LIKE '%$s%' OR `email` LIKE '%$s%')", [$s, $s, $s, $s])->limit(30)->find_many(); + } + header('Content-Type: application/json'); + foreach ($c as $cust) { + $json[] = [ + 'id' => $cust['id'], + 'text' => $cust['username'] . ' - ' . $cust['fullname'] . ' - ' . $cust['email'] + ]; + } + echo json_encode(['results' => $json]); + die(); + break; default: echo 'action not defined'; -} \ No newline at end of file +} diff --git a/system/controllers/customers.php b/system/controllers/customers.php index cd12ed48..c4c54603 100644 --- a/system/controllers/customers.php +++ b/system/controllers/customers.php @@ -24,25 +24,20 @@ switch ($action) { case 'list': $ui->assign('xfooter', ''); $search = _post('search'); - $what = _post('what'); - if(!in_array($what,['username','fullname','phonenumber','email'])){ - $what = 'username'; - } run_hook('list_customers'); #HOOK if ($search != '') { - $paginator = Paginator::bootstrap('tbl_customers', 'username', '%' . $search . '%'); + $paginator = Paginator::bootstrapRaw('tbl_customers', "(`username` LIKE '%$search%' OR `fullname` LIKE '%$search%' OR `phonenumber` LIKE '%$search%' OR `email` LIKE '%$search%')", [$search, $search, $search, $search]); $d = ORM::for_table('tbl_customers') - ->where_like($what, '%' . $search . '%') - ->offset($paginator['startpoint']) - ->limit($paginator['limit']) - ->order_by_desc('id')->find_many(); + ->where_raw("(`username` LIKE '%$search%' OR `fullname` LIKE '%$search%' OR `phonenumber` LIKE '%$search%' OR `email` LIKE '%$search%')", [$search, $search, $search, $search]) + ->offset($paginator['startpoint']) + ->limit($paginator['limit']) + ->order_by_desc('id')->find_many(); } else { $paginator = Paginator::bootstrap('tbl_customers'); $d = ORM::for_table('tbl_customers')->offset($paginator['startpoint'])->limit($paginator['limit'])->order_by_desc('id')->find_many(); } $ui->assign('search', htmlspecialchars($search)); - $ui->assign('what', $what); $ui->assign('d', $d); $ui->assign('paginator', $paginator); $ui->display('customers.tpl'); @@ -53,6 +48,48 @@ switch ($action) { $ui->display('customers-add.tpl'); break; + case 'viewu': + $customer = ORM::for_table('tbl_customers')->where('username', $routes['2'])->find_one(); + case 'view': + $id = $routes['2']; + run_hook('view_customer'); #HOOK + if(!$customer){ + $customer = ORM::for_table('tbl_customers')->find_one($id); + } + if ($customer) { + $v = $routes['3']; + if (empty($v) || $v == 'order') { + $v = 'order'; + // $paginator = Paginator::bootstrap('tbl_payment_gateway', 'username', $customer['username']); + // print_r($paginator); + $order = ORM::for_table('tbl_payment_gateway') + ->where('username', $customer['username']) + ->offset(0) + ->limit(30) + ->order_by_desc('id') + ->find_many(); + // $ui->assign('paginator', $paginator); + $ui->assign('order', $order); + } else if ($v == 'activation') { + // $paginator = Paginator::bootstrap('tbl_transactions', 'username', $customer['username']); + $activation = ORM::for_table('tbl_transactions') + ->where('username', $customer['username']) + ->offset(0) + ->limit(30) + ->order_by_desc('id') + ->find_many(); + // $ui->assign('paginator', $paginator); + $ui->assign('activation', $activation); + } + $package = ORM::for_table('tbl_user_recharges')->where('username',$customer['username'])->find_one(); + $ui->assign('package', $package); + $ui->assign('v', $v); + $ui->assign('d', $customer); + $ui->display('customers-view.tpl'); + } else { + r2(U . 'customers/list', 'e', $_L['Account_Not_Found']); + } + break; case 'edit': $id = $routes['2']; run_hook('edit_customer'); #HOOK @@ -74,22 +111,22 @@ switch ($action) { if ($c) { $mikrotik = Mikrotik::info($c['routers']); if ($c['type'] == 'Hotspot') { - if(!$config['radius_mode']){ + if (!$config['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); - Mikrotik::removeHotspotUser($client,$c['username']); - Mikrotik::removeHotspotActiveUser($client,$user['username']); + Mikrotik::removeHotspotUser($client, $c['username']); + Mikrotik::removeHotspotActiveUser($client, $c['username']); } } else { - if(!$config['radius_mode']){ + if (!$config['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); - Mikrotik::removePpoeUser($client,$c['username']); - Mikrotik::removePpoeActive($client,$user['username']); + Mikrotik::removePpoeUser($client, $c['username']); + Mikrotik::removePpoeActive($client, $c['username']); } } try { $d->delete(); } catch (Exception $e) { - } catch(Throwable $e){ + } catch (Throwable $e) { } try { $c->delete(); @@ -99,12 +136,12 @@ switch ($action) { try { $d->delete(); } catch (Exception $e) { - } catch(Throwable $e){ + } catch (Throwable $e) { } try { $c->delete(); } catch (Exception $e) { - } catch(Throwable $e){ + } catch (Throwable $e) { } } @@ -193,23 +230,23 @@ switch ($action) { if ($c) { $mikrotik = Mikrotik::info($c['routers']); if ($c['type'] == 'Hotspot') { - if(!$config['radius_mode']){ + if (!$config['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); - Mikrotik::setHotspotUser($client,$c['username'],$password); - Mikrotik::removeHotspotActiveUser($client,$user['username']); + Mikrotik::setHotspotUser($client, $c['username'], $password); + Mikrotik::removeHotspotActiveUser($client, $user['username']); } $d->password = $password; $d->save(); } else { - if(!$config['radius_mode']){ + if (!$config['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); - if(!empty($d['pppoe_password'])){ + if (!empty($d['pppoe_password'])) { Mikrotik::setPpoeUser($client, $c['username'], $d['pppoe_password']); - }else{ + } else { Mikrotik::setPpoeUser($client, $c['username'], $password); } - Mikrotik::removePpoeActive($client,$user['username']); + Mikrotik::removePpoeActive($client, $user['username']); } $d->password = $password; @@ -244,5 +281,5 @@ switch ($action) { break; default: - r2(U . 'customers/list', 'e', 'action not defined'); + r2(U . 'customers/list', 'e', 'action not defined'); } diff --git a/system/controllers/export.php b/system/controllers/export.php index 0d2ba727..b7a3a82b 100644 --- a/system/controllers/export.php +++ b/system/controllers/export.php @@ -168,7 +168,7 @@ $style $html EOF; $mpdf->WriteHTML($nhtml); - $mpdf->Output(date('Y-m-d') . _raid(4) . '.pdf', 'D'); + $mpdf->Output(date('Y-m-d') . Package::_raid(4) . '.pdf', 'D'); } else { echo 'No Data'; } @@ -340,7 +340,7 @@ $style $html EOF; $mpdf->WriteHTML($nhtml); - $mpdf->Output(date('Y-m-d') . _raid(4) . '.pdf', 'D'); + $mpdf->Output(date('Y-m-d') . Package::_raid(4) . '.pdf', 'D'); } else { echo 'No Data'; } diff --git a/system/controllers/home.php b/system/controllers/home.php index 6108d816..85e85836 100644 --- a/system/controllers/home.php +++ b/system/controllers/home.php @@ -9,11 +9,72 @@ $ui->assign('_title', $_L['Dashboard']); $user = User::_info(); $ui->assign('_user', $user); -if(isset($_GET['renewal'])){ +if (isset($_GET['renewal'])) { $user->auto_renewal = $_GET['renewal']; $user->save(); } +if (_post('send') == 'balance') { + if ($config['allow_balance_transfer'] == 'yes') { + $target = ORM::for_table('tbl_customers')->where('username', _post('username'))->find_one(); + if (!$target) { + r2(U . 'home', 'd', Lang::T('Username not found')); + } + $username = _post('username'); + $balance = _post('balance'); + if ($user['balance'] < $balance) { + r2(U . 'home', 'd', Lang::T('insufficient balance')); + } + if (intval($config['minimum_transfer']) >= intval($balance)) { + r2(U . 'home', 'd', Lang::T('Minimum Transfer') . ' ' . Lang::moneyFormat($config['minimum_transfer'])); + } + if ($user['username'] == $target['username']) { + r2(U . 'home', 'd', Lang::T('Cannot send to yourself')); + } + if (Balance::transfer($user['id'], $username, $balance)) { + //sender + $d = ORM::for_table('tbl_payment_gateway')->create(); + $d->username = $user['username']; + $d->gateway = $target['username']; + $d->plan_id = 0; + $d->plan_name = 'Send Balance'; + $d->routers_id = 0; + $d->routers = 'balance'; + $d->price = $balance; + $d->payment_method = "Customer"; + $d->payment_channel = "Balance"; + $d->created_date = date('Y-m-d H:i:s'); + $d->paid_date = date('Y-m-d H:i:s'); + $d->expired_date = date('Y-m-d H:i:s'); + $d->pg_url_payment = 'balance'; + $d->status = 2; + $d->save(); + //receiver + $d = ORM::for_table('tbl_payment_gateway')->create(); + $d->username = $target['username']; + $d->gateway = $user['username']; + $d->plan_id = 0; + $d->plan_name = 'Receive Balance'; + $d->routers_id = 0; + $d->routers = 'balance'; + $d->payment_method = "Customer"; + $d->payment_channel = "Balance"; + $d->price = $balance; + $d->created_date = date('Y-m-d H:i:s'); + $d->paid_date = date('Y-m-d H:i:s'); + $d->expired_date = date('Y-m-d H:i:s'); + $d->pg_url_payment = 'balance'; + $d->status = 2; + $d->save(); + Message::sendBalanceNotification($user['phonenumber'], $target['fullname'] . ' (' . $target['username'] . ')', $balance, Lang::getNotifText('balance_send'), $config['user_notification_payment']); + Message::sendBalanceNotification($target['phonenumber'], $user['fullname'] . ' (' . $user['username'] . ')', $balance, Lang::getNotifText('balance_received'), $config['user_notification_payment']); + Message::sendTelegram("#u$user[username] send balance to #u$target[username] \n" . Lang::moneyFormat($balance)); + r2(U . 'home', 's', Lang::T('Sending balance success')); + } + } else { + r2(U . 'home', 'd', 'Failed, balance is not available'); + } +} //Client Page $bill = User::_billing(); @@ -27,7 +88,7 @@ if (!empty($_SESSION['nux-mac']) && !empty($_SESSION['nux-ip'])) { $c = Mikrotik::getClient($m['ip_address'], $m['username'], $m['password']); Mikrotik::logMeIn($c, $user['username'], $user['password'], $_SESSION['nux-ip'], $_SESSION['nux-mac']); r2(U . 'home', 's', Lang::T('Login Request successfully')); - }else if ($_GET['mikrotik'] == 'logout') { + } else if ($_GET['mikrotik'] == 'logout') { $m = Mikrotik::info($bill['routers']); $c = Mikrotik::getClient($m['ip_address'], $m['username'], $m['password']); Mikrotik::logMeOut($c, $user['username']); diff --git a/system/controllers/order.php b/system/controllers/order.php index 19ee3e2d..d665889b 100644 --- a/system/controllers/order.php +++ b/system/controllers/order.php @@ -17,11 +17,12 @@ switch ($action) { break; case 'history': $ui->assign('_system_menu', 'history'); + $paginator = Paginator::bootstrap('tbl_payment_gateway', 'username', $user['username']); $d = ORM::for_table('tbl_payment_gateway') ->where('username', $user['username']) ->order_by_desc('id') + ->offset($paginator['startpoint'])->limit($paginator['limit']) ->find_many(); - $paginator = Paginator::bootstrap('tbl_payment_gateway', 'username', $user['username']); $ui->assign('paginator', $paginator); $ui->assign('d', $d); $ui->assign('_title', Lang::T('Order History')); @@ -62,7 +63,7 @@ switch ($action) { } break; case 'view': - $trxid = $routes['2'] * 1; + $trxid = $routes['2']; $trx = ORM::for_table('tbl_payment_gateway') ->where('username', $user['username']) ->find_one($trxid); @@ -97,7 +98,7 @@ switch ($action) { } } if (empty($trx)) { - r2(U . "home", 'e', Lang::T("Transaction Not found")); + r2(U . "order", 'e', Lang::T("Transaction Not found")); } $router = ORM::for_table('tbl_routers')->find_one($trx['routers_id']); $plan = ORM::for_table('tbl_plans')->find_one($trx['plan_id']); @@ -109,6 +110,30 @@ switch ($action) { $ui->assign('_title', 'TRX #' . $trxid); $ui->display('user-orderView.tpl'); break; + case 'pay': + if ($_c['enable_balance'] != 'yes'){ + r2(U . "order", 'e', Lang::T("Balance not enabled")); + } + $plan = ORM::for_table('tbl_plans')->where('enabled', '1')->find_one($routes['3']); + $router = ORM::for_table('tbl_routers')->where('enabled', '1')->find_one($routes['2']); + if (empty($router) || empty($plan)) { + r2(U . "order/package", 'e', Lang::T("Plan Not found")); + } + if ($plan && $plan['enabled'] && $user['balance'] >= $plan['price']) { + if (Package::rechargeUser($user['id'], $plan['routers'], $plan['id'], 'Customer', 'Balance')) { + // if success, then get the balance + Balance::min($user['id'], $plan['price']); + r2(U . "home", 's', Lang::T("Success to buy package")); + } else { + r2(U . "order/package", 'e', Lang::T("Failed to buy package")); + Message::sendTelegram("Buy Package with Balance Failed\n\n#u$c[username] #buy \n" . $plan['name_plan'] . + "\nRouter: " . $router_name . + "\nPrice: " . $p['price']); + } + }else{ + echo "no renewall | plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; + } + break; case 'buy': if (strpos($user['email'], '@') === false) { r2(U . 'accounts/profile', 'e', Lang::T("Please enter your email address")); diff --git a/system/controllers/pool.php b/system/controllers/pool.php index f46fd039..69b06371 100644 --- a/system/controllers/pool.php +++ b/system/controllers/pool.php @@ -145,5 +145,5 @@ switch ($action) { break; default: - echo 'action not defined'; + r2(U . 'pool/list/', 's', ''); } diff --git a/system/controllers/prepaid.php b/system/controllers/prepaid.php index 0ba294d0..8a9cb2d2 100644 --- a/system/controllers/prepaid.php +++ b/system/controllers/prepaid.php @@ -18,7 +18,24 @@ if ($admin['user_type'] != 'Admin' and $admin['user_type'] != 'Sales') { r2(U . "dashboard", 'e', $_L['Do_Not_Access']); } -use PEAR2\Net\RouterOS; +$select2_customer = << +document.addEventListener("DOMContentLoaded", function(event) { + $('#personSelect').select2({ + theme: "bootstrap", + ajax: { + url: function(params) { + if(params.term != undefined){ + return './index.php?_route=autoload/customer_select2&s='+params.term; + }else{ + return './index.php?_route=autoload/customer_select2'; + } + } + } + }); +}); + +EOT; require_once 'system/autoload/PEAR2/Autoload.php'; @@ -43,8 +60,7 @@ switch ($action) { break; case 'recharge': - $c = ORM::for_table('tbl_customers')->find_many(); - $ui->assign('c', $c); + $ui->assign('xfooter', $select2_customer); $p = ORM::for_table('tbl_plans')->where('enabled', '1')->find_many(); $ui->assign('p', $p); $r = ORM::for_table('tbl_routers')->where('enabled', '1')->find_many(); @@ -81,14 +97,14 @@ switch ($action) { } if ($msg == '') { - if(Package::rechargeUser($id_customer, $server, $plan, "Recharge", $admin['fullname'])){ + if (Package::rechargeUser($id_customer, $server, $plan, "Recharge", $admin['fullname'])) { $c = ORM::for_table('tbl_customers')->where('id', $id_customer)->find_one(); $in = ORM::for_table('tbl_transactions')->where('username', $c['username'])->order_by_desc('id')->find_one(); $ui->assign('in', $in); $ui->assign('date', date("Y-m-d H:i:s")); $ui->display('invoice.tpl'); - _log('[' . $admin['username'] . ']: ' . 'Recharge '.$c['username'].' ['.$in['plan_name'].']['.Lang::moneyFormat($in['price']).']', 'Admin', $admin['id']); - }else{ + _log('[' . $admin['username'] . ']: ' . 'Recharge ' . $c['username'] . ' [' . $in['plan_name'] . '][' . Lang::moneyFormat($in['price']) . ']', 'Admin', $admin['id']); + } else { r2(U . 'prepaid/recharge', 'e', "Failed to recharge account"); } } else { @@ -129,20 +145,20 @@ switch ($action) { if ($d) { run_hook('delete_customer_active_plan'); #HOOK if ($d['type'] == 'Hotspot') { - if(!$config['radius_mode']){ + if (!$config['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); - Mikrotik::removeHotspotUser($client,$c['username']); + Mikrotik::removeHotspotUser($client, $c['username']); } $d->delete(); } else { - if(!$config['radius_mode']){ + if (!$config['radius_mode']) { $client = Mikrotik::getClient($mikrotik['ip_address'], $mikrotik['username'], $mikrotik['password']); - Mikrotik::removePpoeUser($client,$c['username']); + Mikrotik::removePpoeUser($client, $c['username']); } $d->delete(); } - _log('[' . $admin['username'] . ']: ' . 'Delete Plan for Customer '.$c['username'].' ['.$in['plan_name'].']['.Lang::moneyFormat($in['price']).']', 'Admin', $admin['id']); + _log('[' . $admin['username'] . ']: ' . 'Delete Plan for Customer ' . $c['username'] . ' [' . $in['plan_name'] . '][' . Lang::moneyFormat($in['price']) . ']', 'Admin', $admin['id']); r2(U . 'prepaid/list', 's', $_L['Delete_Successfully']); } break; @@ -167,8 +183,8 @@ switch ($action) { $d->recharged_on = $recharged_on; $d->expiration = $expiration; $d->save(); - Package::changeTo($username,$id_plan); - _log('[' . $admin['username'] . ']: ' . 'Edit Plan for Customer '.$d['username'].' to ['.$d['plan_name'].']['.Lang::moneyFormat($d['price']).']', 'Admin', $admin['id']); + Package::changeTo($username, $id_plan); + _log('[' . $admin['username'] . ']: ' . 'Edit Plan for Customer ' . $d['username'] . ' to [' . $d['plan_name'] . '][' . Lang::moneyFormat($d['price']) . ']', 'Admin', $admin['id']); r2(U . 'prepaid/list', 's', $_L['Updated_Successfully']); } else { r2(U . 'prepaid/edit/' . $id, 'e', $msg); @@ -341,10 +357,8 @@ switch ($action) { break; case 'refill': - $ui->assign('xfooter', ''); + $ui->assign('xfooter', $select2_customer); $ui->assign('_title', $_L['Refill_Account']); - $c = ORM::for_table('tbl_customers')->find_many(); - $ui->assign('c', $c); run_hook('view_refill'); #HOOK $ui->display('refill.tpl'); @@ -358,7 +372,7 @@ switch ($action) { run_hook('refill_customer'); #HOOK if ($v1) { - if(Package::rechargeUser($user, $v1['routers'], $v1['id_plan'], "Refill", "Voucher")){ + if (Package::rechargeUser($user, $v1['routers'], $v1['id_plan'], "Refill", "Voucher")) { $v1->status = "1"; $v1->user = $c['username']; $v1->save(); @@ -367,7 +381,7 @@ switch ($action) { $ui->assign('in', $in); $ui->assign('date', date("Y-m-d H:i:s")); $ui->display('invoice.tpl'); - }else{ + } else { r2(U . 'prepaid/refill', 'e', "Failed to refill account"); } } else { @@ -376,7 +390,7 @@ switch ($action) { break; case 'deposit': $ui->assign('_title', Lang::T('Refill Balance')); - $ui->assign('c', ORM::for_table('tbl_customers')->find_many()); + $ui->assign('xfooter', $select2_customer); $ui->assign('p', ORM::for_table('tbl_plans')->where('enabled', '1')->where('type', 'Balance')->find_many()); run_hook('view_deposit'); #HOOK $ui->display('deposit.tpl'); @@ -387,13 +401,13 @@ switch ($action) { run_hook('deposit_customer'); #HOOK if (!empty($user) && !empty($plan)) { - if(Package::rechargeUser($user, 'balance', $plan, "Deposit", $admin['fullname'])){ + if (Package::rechargeUser($user, 'balance', $plan, "Deposit", $admin['fullname'])) { $c = ORM::for_table('tbl_customers')->where('id', $user)->find_one(); $in = ORM::for_table('tbl_transactions')->where('username', $c['username'])->order_by_desc('id')->find_one(); $ui->assign('in', $in); $ui->assign('date', date("Y-m-d H:i:s")); $ui->display('invoice.tpl'); - }else{ + } else { r2(U . 'prepaid/refill', 'e', "Failed to refill account"); } } else { diff --git a/system/controllers/routers.php b/system/controllers/routers.php index 26aa90dd..a798aec4 100644 --- a/system/controllers/routers.php +++ b/system/controllers/routers.php @@ -187,5 +187,5 @@ switch ($action) { break; default: - echo 'action not defined'; + r2(U . 'routers/list/', 's', ''); } \ No newline at end of file diff --git a/system/controllers/settings.php b/system/controllers/settings.php index 0de22c16..c764a33f 100644 --- a/system/controllers/settings.php +++ b/system/controllers/settings.php @@ -213,6 +213,7 @@ switch ($action) { $telegram_target_id = _post('telegram_target_id'); $sms_url = _post('sms_url'); $wa_url = _post('wa_url'); + $minimum_transfer = _post('minimum_transfer'); $user_notification_expired = _post('user_notification_expired'); $user_notification_reminder = _post('user_notification_reminder'); $user_notification_payment = _post('user_notification_payment'); @@ -281,6 +282,17 @@ switch ($action) { $d->save(); } + $d = ORM::for_table('tbl_appconfig')->where('setting', 'minimum_transfer')->find_one(); + if($d){ + $d->value = $minimum_transfer; + $d->save(); + }else{ + $d = ORM::for_table('tbl_appconfig')->create(); + $d->setting = 'minimum_transfer'; + $d->value = $minimum_transfer; + $d->save(); + } + $d = ORM::for_table('tbl_appconfig')->where('setting', 'telegram_bot')->find_one(); if($d){ $d->value = $telegram_bot; @@ -502,6 +514,7 @@ switch ($action) { }else{ $ui->assign('_json', json_decode(file_get_contents('system/uploads/notifications.default.json'), true)); } + $ui->assign('_default', json_decode(file_get_contents('system/uploads/notifications.default.json'), true)); $ui->display('app-notifications.tpl'); break; case 'notifications-post': diff --git a/system/controllers/voucher.php b/system/controllers/voucher.php index 22ee64dc..eb322db4 100644 --- a/system/controllers/voucher.php +++ b/system/controllers/voucher.php @@ -73,7 +73,7 @@ switch ($action) { // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -106,7 +106,7 @@ switch ($action) { // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -150,7 +150,7 @@ switch ($action) { // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; @@ -183,7 +183,7 @@ switch ($action) { // insert table transactions $t = ORM::for_table('tbl_transactions')->create(); - $t->invoice = "INV-" . _raid(5); + $t->invoice = "INV-" . Package::_raid(5); $t->username = $c['username']; $t->plan_name = $p['name_plan']; $t->price = $p['price']; diff --git a/system/cron.php b/system/cron.php index e7774867..75465898 100644 --- a/system/cron.php +++ b/system/cron.php @@ -19,11 +19,10 @@ ORM::configure('logging', true); include "autoload/Hookers.php"; // notification message -if (file_exists("uploads/notifications.json")) { - $_notifmsg = json_decode(file_get_contents('uploads/notifications.json'), true); -} else { - $_notifmsg = json_decode(file_get_contents('uploads/notifications.default.json'), true); +if(file_exists("system/uploads/notifications.json")){ + $_notifmsg =json_decode(file_get_contents('system/uploads/notifications.json'), true); } +$_notifmsg_default = json_decode(file_get_contents('system/uploads/notifications.default.json'), true); //register all plugin foreach (glob("plugin/*.php") as $filename) { @@ -59,12 +58,15 @@ $result = ORM::for_table('tbl_appconfig')->find_many(); foreach ($result as $value) { $config[$value['setting']] = $value['value']; } + +$_c = $config; + date_default_timezone_set($config['timezone']); -$textExpired = $_notifmsg['expired']; - -$d = ORM::for_table('tbl_user_recharges')->where('status', 'on')->find_many(); +$textExpired = Lang::getNotifText('expired'); +$d = ORM::for_table('tbl_user_recharges')->where('status', 'on')->where('expiration', date("Y-m-d"))->find_many(); +echo "Found ".count($d)." user(s)\n"; run_hook('cronjob'); #HOOK foreach ($d as $ds) { @@ -95,12 +97,20 @@ foreach ($d as $ds) { if (Package::rechargeUser($ds['customer_id'], $p['routers'], $p['id'], 'Customer', 'Balance')) { // if success, then get the balance Balance::min($ds['customer_id'], $p['price']); + echo "plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; + echo "auto renewall Success\n"; } else { + echo "plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; + echo "auto renewall Failed\n"; Message::sendTelegram("FAILED RENEWAL #cron\n\n#u$c[username] #buy #Hotspot \n" . $p['name_plan'] . "\nRouter: " . $router_name . "\nPrice: " . $p['price']); } + }else{ + echo "no renewall | plan enabled: $p[enabled] | User balance: $c[balance] | price $p[price]\n"; } + }else{ + echo "no renewall | balance $config[enable_balance] auto_renewal $c[auto_renewal]\n"; } } else echo " : ACTIVE \r\n"; } else { @@ -131,7 +141,7 @@ foreach ($d as $ds) { // if success, then get the balance Balance::min($ds['customer_id'], $p['price']); } else { - Message::sendTelegram("FAILED RENEWAL #cron\n\n#u$c[username] #buy #Hotspot \n" . $p['name_plan'] . + Message::sendTelegram("FAILED RENEWAL #cron\n\n#u$c[username] #buy #PPPOE \n" . $p['name_plan'] . "\nRouter: " . $router_name . "\nPrice: " . $p['price']); } diff --git a/system/cron_reminder.php b/system/cron_reminder.php index 8fd04c12..2985a4af 100644 --- a/system/cron_reminder.php +++ b/system/cron_reminder.php @@ -22,11 +22,10 @@ ORM::configure('logging', true); include "autoload/Hookers.php"; // notification message -if(file_exists("uploads/notifications.json")){ - $_notifmsg =json_decode(file_get_contents('uploads/notifications.json'), true); -}else{ - $_notifmsg = json_decode(file_get_contents('uploads/notifications.default.json'), true); +if(file_exists("system/uploads/notifications.json")){ + $_notifmsg =json_decode(file_get_contents('system/uploads/notifications.json'), true); } +$_notifmsg_default = json_decode(file_get_contents('system/uploads/notifications.default.json'), true); //register all plugin foreach (glob("plugin/*.php") as $filename) { @@ -74,17 +73,17 @@ run_hook('cronjob_reminder'); #HOOK $day7 = date('Y-m-d', strtotime("+7 day")); $day3 = date('Y-m-d', strtotime("+3 day")); $day1 = date('Y-m-d', strtotime("+1 day")); -print_r([$day1,$day3,$day7]); +print_r([$day1, $day3, $day7]); foreach ($d as $ds) { - if(in_array($ds['expiration'],[$day1,$day3,$day7])){ + if (in_array($ds['expiration'], [$day1, $day3, $day7])) { $u = ORM::for_table('tbl_user_recharges')->where('id', $ds['id'])->find_one(); $c = ORM::for_table('tbl_customers')->where('id', $ds['customer_id'])->find_one(); - if($ds['expiration']==$day7){ - echo Message::sendPackageNotification($c['phonenumber'], $c['fullname'], $u['namebp'], $_notifmsg['reminder_7_day'], $config['user_notification_reminder'])."\n"; - }else if($ds['expiration']==$day3){ - echo Message::sendPackageNotification($c['phonenumber'], $c['fullname'], $u['namebp'], $_notifmsg['reminder_3_day'], $config['user_notification_reminder'])."\n"; - }else if($ds['expiration']==$day1){ - echo Message::sendPackageNotification($c['phonenumber'], $c['fullname'], $u['namebp'], $_notifmsg['reminder_1_day'], $config['user_notification_reminder'])."\n"; + if ($ds['expiration'] == $day7) { + echo Message::sendPackageNotification($c['phonenumber'], $c['fullname'], $u['namebp'], Lang::getNotifText('reminder_7_day'), $config['user_notification_reminder']) . "\n"; + } else if ($ds['expiration'] == $day3) { + echo Message::sendPackageNotification($c['phonenumber'], $c['fullname'], $u['namebp'], Lang::getNotifText('reminder_3_day'), $config['user_notification_reminder']) . "\n"; + } else if ($ds['expiration'] == $day1) { + echo Message::sendPackageNotification($c['phonenumber'], $c['fullname'], $u['namebp'], Lang::getNotifText('reminder_1_day'), $config['user_notification_reminder']) . "\n"; } } } diff --git a/system/lan/english/common.lan.php b/system/lan/english/common.lan.php index 4e6ca82a..4a65d646 100644 --- a/system/lan/english/common.lan.php +++ b/system/lan/english/common.lan.php @@ -359,9 +359,28 @@ $_L['Balance_Plans'] = 'Balance Plans'; $_L['Buy_Balance'] = 'Buy Balance?'; $_L['Price'] = 'Price'; $_L['Validity'] = 'Validity'; -$_L['Disable_auto_renewal'] = 'Disable auto renewal?'; -$_L['Auto_Renewal_On'] = 'Auto Renewal On'; -$_L['Enable_auto_renewal'] = 'Enable auto renewal?'; -$_L['Auto_Renewal_Off'] = 'Auto Renewal Off'; -$_L['Refill_Balance'] = 'Refill Balance'; -$_L['Invoice_Footer'] = 'Invoice Footer'; +$_L['Disable_auto_renewal'] = 'Disable auto renewal?'; +$_L['Auto_Renewal_On'] = 'Auto Renewal On'; +$_L['Enable_auto_renewal'] = 'Enable auto renewal?'; +$_L['Auto_Renewal_Off'] = 'Auto Renewal Off'; +$_L['Refill_Balance'] = 'Refill Balance'; +$_L['Invoice_Footer'] = 'Invoice Footer'; +$_L['Pay_With_Balance'] = 'Pay With Balance'; +$_L['Pay_this_with_Balance_your_active_package_will_be_overwrite'] = 'Pay this with Balance? your active package will be overwrite'; +$_L['Success_to_buy_package'] = 'Success to buy package'; +$_L['Auto_Renewal'] = 'Auto Renewal'; +$_L['View'] = 'View'; +$_L['Back'] = 'Back'; +$_L['Active'] = 'Active'; +$_L['Transfer_Balance'] = 'Transfer Balance'; +$_L['Send_your_balance'] = 'Send your balance?'; +$_L['Send'] = 'Send'; +$_L['Cannot_send_to_yourself'] = 'Cannot send to yourself'; +$_L['Sending_balance_success'] = 'Sending balance success'; +$_L['From'] = 'From'; +$_L['To'] = 'To'; +$_L['insufficient_balance'] = 'insufficient balance'; +$_L['Send_Balance'] = 'Send Balance'; +$_L['Received_Balance'] = 'Received Balance'; +$_L['Minimum_Balance_Transfer'] = 'Minimum Balance Transfer'; +$_L['Minimum_Transfer'] = 'Minimum Transfer'; diff --git a/system/orm.php b/system/orm.php index ed3d28ba..2389efc0 100644 --- a/system/orm.php +++ b/system/orm.php @@ -1,2281 +1,2365 @@ 'sqlite::memory:', - 'id_column' => 'id', - 'id_column_overrides' => array(), - 'error_mode' => PDO::ERRMODE_EXCEPTION, - 'username' => null, - 'password' => null, - 'driver_options' => null, - 'identifier_quote_character' => null, // if this is null, will be autodetected - 'limit_clause_style' => null, // if this is null, will be autodetected - 'logging' => false, - 'logger' => null, - 'caching' => false, - 'caching_auto_clear' => false, - 'return_result_sets' => false, - ); - - // Map of configuration settings - protected static $_config = array(); - - // Map of database connections, instances of the PDO class - protected static $_db = array(); - - // Last query run, only populated if logging is enabled - protected static $_last_query; - - // Log of all queries run, mapped by connection key, only populated if logging is enabled - protected static $_query_log = array(); - - // Query cache, only used if query caching is enabled - protected static $_query_cache = array(); - - // Reference to previously used PDOStatement object to enable low-level access, if needed - protected static $_last_statement = null; - - // --------------------------- // - // --- INSTANCE PROPERTIES --- // - // --------------------------- // - - // Key name of the connections in self::$_db used by this instance - protected $_connection_name; - - // The name of the table the current ORM instance is associated with - protected $_table_name; - - // Alias for the table to be used in SELECT queries - protected $_table_alias = null; - - // Values to be bound to the query - protected $_values = array(); - - // Columns to select in the result - protected $_result_columns = array('*'); - - // Are we using the default result column or have these been manually changed? - protected $_using_default_result_columns = true; - - // Join sources - protected $_join_sources = array(); - - // Should the query include a DISTINCT keyword? - protected $_distinct = false; - - // Is this a raw query? - protected $_is_raw_query = false; - - // The raw query - protected $_raw_query = ''; - - // The raw query parameters - protected $_raw_parameters = array(); - - // Array of WHERE clauses - protected $_where_conditions = array(); - - // LIMIT - protected $_limit = null; - - // OFFSET - protected $_offset = null; - - // ORDER BY - protected $_order_by = array(); - - // GROUP BY - protected $_group_by = array(); - - // HAVING - protected $_having_conditions = array(); - - // The data for a hydrated instance of the class - protected $_data = array(); - - // Fields that have been modified during the - // lifetime of the object - protected $_dirty_fields = array(); - - // Fields that are to be inserted in the DB raw - protected $_expr_fields = array(); - - // Is this a new object (has create() been called)? - protected $_is_new = false; - - // Name of the column to use as the primary key for - // this instance only. Overrides the config settings. - protected $_instance_id_column = null; - - // ---------------------- // - // --- STATIC METHODS --- // - // ---------------------- // - /** - * Pass configuration settings to the class in the form of - * key/value pairs. As a shortcut, if the second argument - * is omitted and the key is a string, the setting is - * assumed to be the DSN string used by PDO to connect - * to the database (often, this will be the only configuration - * required to use Idiorm). If you have more than one setting - * you wish to configure, another shortcut is to pass an array - * of settings (and omit the second argument). - * @param string $key - * @param mixed $value - * @param string $connection_name Which connection to use - */ - public static function configure($key, $value = null, $connection_name = self::DEFAULT_CONNECTION) { - self::_setup_db_config($connection_name); //ensures at least default config is set - - if (is_array($key)) { - // Shortcut: If only one array argument is passed, - // assume it's an array of configuration settings - foreach ($key as $conf_key => $conf_value) { - self::configure($conf_key, $conf_value, $connection_name); - } - } else { - if (is_null($value)) { - // Shortcut: If only one string argument is passed, - // assume it's a connection string - $value = $key; - $key = 'connection_string'; - } - self::$_config[$connection_name][$key] = $value; - } - } - - /** - * Retrieve configuration options by key, or as whole array. - * @param string $key - * @param string $connection_name Which connection to use - */ - public static function get_config($key = null, $connection_name = self::DEFAULT_CONNECTION) { - if ($key) { - return self::$_config[$connection_name][$key]; - } else { - return self::$_config[$connection_name]; - } - } - - /** - * Delete all configs in _config array. - */ - public static function reset_config() { - self::$_config = array(); - } - - /** - * Despite its slightly odd name, this is actually the factory - * method used to acquire instances of the class. It is named - * this way for the sake of a readable interface, ie - * ORM::for_table('table_name')->find_one()-> etc. As such, - * this will normally be the first method called in a chain. - * @param string $table_name - * @param string $connection_name Which connection to use - * @return ORM - */ - public static function for_table($table_name, $connection_name = self::DEFAULT_CONNECTION) { - self::_setup_db($connection_name); - return new self($table_name, array(), $connection_name); - } - - - public static function execute($query) { - self::_setup_db(self::DEFAULT_CONNECTION); - return self::_execute($query); - } - - /** - * Set up the database connection used by the class - * @param string $connection_name Which connection to use - */ - protected static function _setup_db($connection_name = self::DEFAULT_CONNECTION) { - if (!array_key_exists($connection_name, self::$_db) || - !is_object(self::$_db[$connection_name])) { - self::_setup_db_config($connection_name); - - $db = new PDO( - self::$_config[$connection_name]['connection_string'], - self::$_config[$connection_name]['username'], - self::$_config[$connection_name]['password'], - self::$_config[$connection_name]['driver_options'] - ); - - $db->setAttribute(PDO::ATTR_ERRMODE, self::$_config[$connection_name]['error_mode']); - self::set_db($db, $connection_name); - } - } - - /** - * Ensures configuration (multiple connections) is at least set to default. - * @param string $connection_name Which connection to use - */ - protected static function _setup_db_config($connection_name) { - if (!array_key_exists($connection_name, self::$_config)) { - self::$_config[$connection_name] = self::$_default_config; - } - } - - /** - * Set the PDO object used by Idiorm to communicate with the database. - * This is public in case the ORM should use a ready-instantiated - * PDO object as its database connection. Accepts an optional string key - * to identify the connection if multiple connections are used. - * @param PDO $db - * @param string $connection_name Which connection to use - */ - public static function set_db($db, $connection_name = self::DEFAULT_CONNECTION) { - self::_setup_db_config($connection_name); - self::$_db[$connection_name] = $db; - if(!is_null(self::$_db[$connection_name])) { - self::_setup_identifier_quote_character($connection_name); - self::_setup_limit_clause_style($connection_name); - } - } - - /** - * Delete all registered PDO objects in _db array. - */ - public static function reset_db() { - self::$_db = array(); - } - - /** - * Detect and initialise the character used to quote identifiers - * (table names, column names etc). If this has been specified - * manually using ORM::configure('identifier_quote_character', 'some-char'), - * this will do nothing. - * @param string $connection_name Which connection to use - */ - protected static function _setup_identifier_quote_character($connection_name) { - if (is_null(self::$_config[$connection_name]['identifier_quote_character'])) { - self::$_config[$connection_name]['identifier_quote_character'] = - self::_detect_identifier_quote_character($connection_name); - } - } - - /** - * Detect and initialise the limit clause style ("SELECT TOP 5" / - * "... LIMIT 5"). If this has been specified manually using - * ORM::configure('limit_clause_style', 'top'), this will do nothing. - * @param string $connection_name Which connection to use - */ - public static function _setup_limit_clause_style($connection_name) { - if (is_null(self::$_config[$connection_name]['limit_clause_style'])) { - self::$_config[$connection_name]['limit_clause_style'] = - self::_detect_limit_clause_style($connection_name); - } - } - - /** - * Return the correct character used to quote identifiers (table - * names, column names etc) by looking at the driver being used by PDO. - * @param string $connection_name Which connection to use - * @return string - */ - protected static function _detect_identifier_quote_character($connection_name) { - switch(self::get_db($connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME)) { - case 'pgsql': - case 'sqlsrv': - case 'dblib': - case 'mssql': - case 'sybase': - case 'firebird': - return '"'; - case 'mysql': - case 'sqlite': - case 'sqlite2': - default: - return '`'; - } - } - - /** - * Returns a constant after determining the appropriate limit clause - * style - * @param string $connection_name Which connection to use - * @return string Limit clause style keyword/constant - */ - protected static function _detect_limit_clause_style($connection_name) { - switch(self::get_db($connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME)) { - case 'sqlsrv': - case 'dblib': - case 'mssql': - return ORM::LIMIT_STYLE_TOP_N; - default: - return ORM::LIMIT_STYLE_LIMIT; - } - } - - /** - * Returns the PDO instance used by the the ORM to communicate with - * the database. This can be called if any low-level DB access is - * required outside the class. If multiple connections are used, - * accepts an optional key name for the connection. - * @param string $connection_name Which connection to use - * @return PDO - */ - public static function get_db($connection_name = self::DEFAULT_CONNECTION) { - self::_setup_db($connection_name); // required in case this is called before Idiorm is instantiated - return self::$_db[$connection_name]; - } - - /** - * Executes a raw query as a wrapper for PDOStatement::execute. - * Useful for queries that can't be accomplished through Idiorm, - * particularly those using engine-specific features. - * @example raw_execute('SELECT `name`, AVG(`order`) FROM `customer` GROUP BY `name` HAVING AVG(`order`) > 10') - * @example raw_execute('INSERT OR REPLACE INTO `widget` (`id`, `name`) SELECT `id`, `name` FROM `other_table`') - * @param string $query The raw SQL query - * @param array $parameters Optional bound parameters - * @param string $connection_name Which connection to use - * @return bool Success - */ - public static function raw_execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) { - self::_setup_db($connection_name); - return self::_execute($query, $parameters, $connection_name); - } - - /** - * Returns the PDOStatement instance last used by any connection wrapped by the ORM. - * Useful for access to PDOStatement::rowCount() or error information - * @return PDOStatement - */ - public static function get_last_statement() { - return self::$_last_statement; - } - - /** - * Internal helper method for executing statments. Logs queries, and - * stores statement object in ::_last_statment, accessible publicly - * through ::get_last_statement() - * @param string $query - * @param array $parameters An array of parameters to be bound in to the query - * @param string $connection_name Which connection to use - * @return bool Response of PDOStatement::execute() - */ - protected static function _execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) { - $statement = self::get_db($connection_name)->prepare($query); - self::$_last_statement = $statement; - $time = microtime(true); - - foreach ($parameters as $key => &$param) { - if (is_null($param)) { - $type = PDO::PARAM_NULL; - } else if (is_bool($param)) { - $type = PDO::PARAM_BOOL; - } else if (is_int($param)) { - $type = PDO::PARAM_INT; - } else { - $type = PDO::PARAM_STR; - } - - $statement->bindParam(is_int($key) ? ++$key : $key, $param, $type); - } - - $q = $statement->execute(); - self::_log_query($query, $parameters, $connection_name, (microtime(true)-$time)); - - return $q; - } - - /** - * Add a query to the internal query log. Only works if the - * 'logging' config option is set to true. * - * This works by manually binding the parameters to the query - the - * query isn't executed like this (PDO normally passes the query and - * parameters to the database which takes care of the binding) but - * doing it this way makes the logged queries more readable. - * @param string $query - * @param array $parameters An array of parameters to be bound in to the query - * @param string $connection_name Which connection to use - * @param float $query_time Query time - * @return bool + * Idiorm + * + * http://github.com/j4mie/idiorm/ + * + * A single-class super-simple database abstraction layer for PHP. + * Provides (nearly) zero-configuration object-relational mapping + * and a fluent interface for building basic, commonly-used queries. + * + * BSD Licensed. + * + * Copyright (c) 2010, Jamie Matthews + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * + * The methods documented below are magic methods that conform to PSR-1. + * This documentation exposes these methods to doc generators and IDEs. + * @see http://www.php-fig.org/psr/psr-1/ + * + * @method static array|string getConfig($key = null, $connection_name = self::DEFAULT_CONNECTION) + * @method static null resetConfig() + * @method static \ORM forTable($table_name, $connection_name = self::DEFAULT_CONNECTION) + * @method static null setDb($db, $connection_name = self::DEFAULT_CONNECTION) + * @method static null resetDb() + * @method static null setupLimitClauseStyle($connection_name) + * @method static \PDO getDb($connection_name = self::DEFAULT_CONNECTION) + * @method static bool rawExecute($query, $parameters = array()) + * @method static \PDOStatement getLastStatement() + * @method static string getLastQuery($connection_name = null) + * @method static array getQueryLog($connection_name = self::DEFAULT_CONNECTION) + * @method array getConnectionNames() + * @method $this useIdColumn($id_column) + * @method \ORM|bool findOne($id=null) + * @method array|\IdiormResultSet findMany() + * @method \IdiormResultSet findResultSet() + * @method array findArray() + * @method $this forceAllDirty() + * @method $this rawQuery($query, $parameters = array()) + * @method $this tableAlias($alias) + * @method int countNullIdColumns() + * @method $this selectExpr($expr, $alias=null) + * @method \ORM selectMany($values) + * @method \ORM selectManyExpr($values) + * @method $this rawJoin($table, $constraint, $table_alias, $parameters = array()) + * @method $this innerJoin($table, $constraint, $table_alias=null) + * @method $this leftOuterJoin($table, $constraint, $table_alias=null) + * @method $this rightOuterJoin($table, $constraint, $table_alias=null) + * @method $this fullOuterJoin($table, $constraint, $table_alias=null) + * @method $this whereEqual($column_name, $value=null) + * @method $this whereNotEqual($column_name, $value=null) + * @method $this whereIdIs($id) + * @method $this whereAnyIs($values, $operator='=') + * @method array|string whereIdIn($ids) + * @method $this whereLike($column_name, $value=null) + * @method $this whereNotLike($column_name, $value=null) + * @method $this whereGt($column_name, $value=null) + * @method $this whereLt($column_name, $value=null) + * @method $this whereGte($column_name, $value=null) + * @method $this whereLte($column_name, $value=null) + * @method $this whereIn($column_name, $values) + * @method $this whereNotIn($column_name, $values) + * @method $this whereNull($column_name) + * @method $this whereNotNull($column_name) + * @method $this whereRaw($clause, $parameters=array()) + * @method $this orderByDesc($column_name) + * @method $this orderByAsc($column_name) + * @method $this orderByExpr($clause) + * @method $this groupBy($column_name) + * @method $this groupByExpr($expr) + * @method $this havingEqual($column_name, $value=null) + * @method $this havingNotEqual($column_name, $value=null) + * @method $this havingIdIs($id) + * @method $this havingLike($column_name, $value=null) + * @method $this havingNotLike($column_name, $value=null) + * @method $this havingGt($column_name, $value=null) + * @method $this havingLt($column_name, $value=null) + * @method $this havingGte($column_name, $value=null) + * @method $this havingLte($column_name, $value=null) + * @method $this havingIn($column_name, $values=null) + * @method $this havingNotIn($column_name, $values=null) + * @method $this havingNull($column_name) + * @method $this havingNotNull($column_name) + * @method $this havingRaw($clause, $parameters=array()) + * @method static this clearCache($table_name = null, $connection_name = self::DEFAULT_CONNECTION) + * @method array asArray() + * @method bool setExpr($key, $value = null) + * @method bool isDirty($key) + * @method bool isNew() */ - protected static function _log_query($query, $parameters, $connection_name, $query_time) { - // If logging is not enabled, do nothing - if (!self::$_config[$connection_name]['logging']) { - return false; - } - if (!isset(self::$_query_log[$connection_name])) { - self::$_query_log[$connection_name] = array(); - } + class ORM implements ArrayAccess { - // Strip out any non-integer indexes from the parameters - foreach($parameters as $key => $value) { - if (!is_int($key)) unset($parameters[$key]); - } + // ----------------------- // + // --- CLASS CONSTANTS --- // + // ----------------------- // - if (count($parameters) > 0) { - // Escape the parameters - $parameters = array_map(array(self::get_db($connection_name), 'quote'), $parameters); + // WHERE and HAVING condition array keys + const CONDITION_FRAGMENT = 0; + const CONDITION_VALUES = 1; - // Avoid %format collision for vsprintf - $query = str_replace("%", "%%", $query); + const DEFAULT_CONNECTION = 'default'; - // Replace placeholders in the query for vsprintf - if(false !== strpos($query, "'") || false !== strpos($query, '"')) { - $query = IdiormString::str_replace_outside_quotes("?", "%s", $query); + // Limit clause style + const LIMIT_STYLE_TOP_N = "top"; + const LIMIT_STYLE_LIMIT = "limit"; + + // ------------------------ // + // --- CLASS PROPERTIES --- // + // ------------------------ // + + // Class configuration + protected static $_default_config = array( + 'connection_string' => 'sqlite::memory:', + 'id_column' => 'id', + 'id_column_overrides' => array(), + 'error_mode' => PDO::ERRMODE_EXCEPTION, + 'username' => null, + 'password' => null, + 'driver_options' => null, + 'identifier_quote_character' => null, // if this is null, will be autodetected + 'limit_clause_style' => null, // if this is null, will be autodetected + 'logging' => false, + 'logger' => null, + 'caching' => false, + 'caching_auto_clear' => false, + 'return_result_sets' => false, + ); + + // Map of configuration settings + protected static $_config = array(); + + // Map of database connections, instances of the PDO class + protected static $_db = array(); + + // Last query run, only populated if logging is enabled + protected static $_last_query; + + // Log of all queries run, mapped by connection key, only populated if logging is enabled + protected static $_query_log = array(); + + // Query cache, only used if query caching is enabled + protected static $_query_cache = array(); + + // Reference to previously used PDOStatement object to enable low-level access, if needed + protected static $_last_statement = null; + + // --------------------------- // + // --- INSTANCE PROPERTIES --- // + // --------------------------- // + + // Key name of the connections in self::$_db used by this instance + protected $_connection_name; + + // The name of the table the current ORM instance is associated with + protected $_table_name; + + // Alias for the table to be used in SELECT queries + protected $_table_alias = null; + + // Values to be bound to the query + protected $_values = array(); + + // Columns to select in the result + protected $_result_columns = array('*'); + + // Are we using the default result column or have these been manually changed? + protected $_using_default_result_columns = true; + + // Join sources + protected $_join_sources = array(); + + // Should the query include a DISTINCT keyword? + protected $_distinct = false; + + // Is this a raw query? + protected $_is_raw_query = false; + + // The raw query + protected $_raw_query = ''; + + // The raw query parameters + protected $_raw_parameters = array(); + + // Array of WHERE clauses + protected $_where_conditions = array(); + + // LIMIT + protected $_limit = null; + + // OFFSET + protected $_offset = null; + + // ORDER BY + protected $_order_by = array(); + + // GROUP BY + protected $_group_by = array(); + + // HAVING + protected $_having_conditions = array(); + + // The data for a hydrated instance of the class + protected $_data = array(); + + // Fields that have been modified during the + // lifetime of the object + protected $_dirty_fields = array(); + + // Fields that are to be inserted in the DB raw + protected $_expr_fields = array(); + + // Is this a new object (has create() been called)? + protected $_is_new = false; + + // Name of the column to use as the primary key for + // this instance only. Overrides the config settings. + protected $_instance_id_column = null; + + // ---------------------- // + // --- STATIC METHODS --- // + // ---------------------- // + + /** + * Pass configuration settings to the class in the form of + * key/value pairs. As a shortcut, if the second argument + * is omitted and the key is a string, the setting is + * assumed to be the DSN string used by PDO to connect + * to the database (often, this will be the only configuration + * required to use Idiorm). If you have more than one setting + * you wish to configure, another shortcut is to pass an array + * of settings (and omit the second argument). + * @param string|array $key + * @param mixed $value + * @param string $connection_name Which connection to use + */ + public static function configure($key, $value = null, $connection_name = self::DEFAULT_CONNECTION) { + self::_setup_db_config($connection_name); //ensures at least default config is set + + if (is_array($key)) { + // Shortcut: If only one array argument is passed, + // assume it's an array of configuration settings + foreach ($key as $conf_key => $conf_value) { + self::configure($conf_key, $conf_value, $connection_name); + } } else { - $query = str_replace("?", "%s", $query); + if (is_null($value)) { + // Shortcut: If only one string argument is passed, + // assume it's a connection string + $value = $key; + $key = 'connection_string'; + } + self::$_config[$connection_name][$key] = $value; + } + } + + /** + * Retrieve configuration options by key, or as whole array. + * @param string $key + * @param string $connection_name Which connection to use + */ + public static function get_config($key = null, $connection_name = self::DEFAULT_CONNECTION) { + if ($key) { + return self::$_config[$connection_name][$key]; + } else { + return self::$_config[$connection_name]; + } + } + + /** + * Delete all configs in _config array. + */ + public static function reset_config() { + self::$_config = array(); + } + + /** + * Despite its slightly odd name, this is actually the factory + * method used to acquire instances of the class. It is named + * this way for the sake of a readable interface, ie + * ORM::for_table('table_name')->find_one()-> etc. As such, + * this will normally be the first method called in a chain. + * @param string $table_name + * @param string $connection_name Which connection to use + * @return ORM + */ + public static function for_table($table_name, $connection_name = self::DEFAULT_CONNECTION) { + self::_setup_db($connection_name); + return new self($table_name, array(), $connection_name); + } + + /** + * Set up the database connection used by the class + * @param string $connection_name Which connection to use + */ + protected static function _setup_db($connection_name = self::DEFAULT_CONNECTION) { + if (!array_key_exists($connection_name, self::$_db) || + !is_object(self::$_db[$connection_name])) { + self::_setup_db_config($connection_name); + + $db = new PDO( + self::$_config[$connection_name]['connection_string'], + self::$_config[$connection_name]['username'], + self::$_config[$connection_name]['password'], + self::$_config[$connection_name]['driver_options'] + ); + + $db->setAttribute(PDO::ATTR_ERRMODE, self::$_config[$connection_name]['error_mode']); + self::set_db($db, $connection_name); + } + } + + /** + * Ensures configuration (multiple connections) is at least set to default. + * @param string $connection_name Which connection to use + */ + protected static function _setup_db_config($connection_name) { + if (!array_key_exists($connection_name, self::$_config)) { + self::$_config[$connection_name] = self::$_default_config; + } + } + + /** + * Set the PDO object used by Idiorm to communicate with the database. + * This is public in case the ORM should use a ready-instantiated + * PDO object as its database connection. Accepts an optional string key + * to identify the connection if multiple connections are used. + * @param PDO $db + * @param string $connection_name Which connection to use + */ + public static function set_db($db, $connection_name = self::DEFAULT_CONNECTION) { + self::_setup_db_config($connection_name); + self::$_db[$connection_name] = $db; + if(!is_null(self::$_db[$connection_name])) { + self::_setup_identifier_quote_character($connection_name); + self::_setup_limit_clause_style($connection_name); + } + } + + /** + * Close and delete all registered PDO objects in _db array. + */ + public static function reset_db() { + self::$_db = null; + + self::$_db = array(); + } + + /** + * Detect and initialise the character used to quote identifiers + * (table names, column names etc). If this has been specified + * manually using ORM::configure('identifier_quote_character', 'some-char'), + * this will do nothing. + * @param string $connection_name Which connection to use + */ + protected static function _setup_identifier_quote_character($connection_name) { + if (is_null(self::$_config[$connection_name]['identifier_quote_character'])) { + self::$_config[$connection_name]['identifier_quote_character'] = + self::_detect_identifier_quote_character($connection_name); + } + } + + /** + * Detect and initialise the limit clause style ("SELECT TOP 5" / + * "... LIMIT 5"). If this has been specified manually using + * ORM::configure('limit_clause_style', 'top'), this will do nothing. + * @param string $connection_name Which connection to use + */ + public static function _setup_limit_clause_style($connection_name) { + if (is_null(self::$_config[$connection_name]['limit_clause_style'])) { + self::$_config[$connection_name]['limit_clause_style'] = + self::_detect_limit_clause_style($connection_name); + } + } + + /** + * Return the correct character used to quote identifiers (table + * names, column names etc) by looking at the driver being used by PDO. + * @param string $connection_name Which connection to use + * @return string + */ + protected static function _detect_identifier_quote_character($connection_name) { + switch(self::get_db($connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME)) { + case 'pgsql': + case 'sqlsrv': + case 'dblib': + case 'mssql': + case 'sybase': + case 'firebird': + return '"'; + case 'mysql': + case 'sqlite': + case 'sqlite2': + default: + return '`'; + } + } + + /** + * Returns a constant after determining the appropriate limit clause + * style + * @param string $connection_name Which connection to use + * @return string Limit clause style keyword/constant + */ + protected static function _detect_limit_clause_style($connection_name) { + switch(self::get_db($connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME)) { + case 'sqlsrv': + case 'dblib': + case 'mssql': + return ORM::LIMIT_STYLE_TOP_N; + default: + return ORM::LIMIT_STYLE_LIMIT; + } + } + + /** + * Returns the PDO instance used by the the ORM to communicate with + * the database. This can be called if any low-level DB access is + * required outside the class. If multiple connections are used, + * accepts an optional key name for the connection. + * @param string $connection_name Which connection to use + * @return PDO + */ + public static function get_db($connection_name = self::DEFAULT_CONNECTION) { + self::_setup_db($connection_name); // required in case this is called before Idiorm is instantiated + return self::$_db[$connection_name]; + } + + /** + * Executes a raw query as a wrapper for PDOStatement::execute. + * Useful for queries that can't be accomplished through Idiorm, + * particularly those using engine-specific features. + * @example raw_execute('SELECT `name`, AVG(`order`) FROM `customer` GROUP BY `name` HAVING AVG(`order`) > 10') + * @example raw_execute('INSERT OR REPLACE INTO `widget` (`id`, `name`) SELECT `id`, `name` FROM `other_table`') + * @param string $query The raw SQL query + * @param array $parameters Optional bound parameters + * @param string $connection_name Which connection to use + * @return bool Success + */ + public static function raw_execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) { + self::_setup_db($connection_name); + return self::_execute($query, $parameters, $connection_name); + } + + /** + * Returns the PDOStatement instance last used by any connection wrapped by the ORM. + * Useful for access to PDOStatement::rowCount() or error information + * @return PDOStatement + */ + public static function get_last_statement() { + return self::$_last_statement; + } + + /** + * Internal helper method for executing statments. Logs queries, and + * stores statement object in ::_last_statment, accessible publicly + * through ::get_last_statement() + * @param string $query + * @param array $parameters An array of parameters to be bound in to the query + * @param string $connection_name Which connection to use + * @return bool Response of PDOStatement::execute() + */ + protected static function _execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) { + $statement = self::get_db($connection_name)->prepare($query); + self::$_last_statement = $statement; + $time = microtime(true); + + foreach ($parameters as $key => &$param) { + if (is_null($param)) { + $type = PDO::PARAM_NULL; + } else if (is_bool($param)) { + $type = PDO::PARAM_BOOL; + } else if (is_int($param)) { + $type = PDO::PARAM_INT; + } else { + $type = PDO::PARAM_STR; + } + + $statement->bindParam(is_int($key) ? ++$key : $key, $param, $type); } - // Replace the question marks in the query with the parameters - $bound_query = vsprintf($query, $parameters); - } else { - $bound_query = $query; + $q = $statement->execute(); + self::_log_query($query, $parameters, $connection_name, (microtime(true)-$time)); + + return $q; } - self::$_last_query = $bound_query; - self::$_query_log[$connection_name][] = $bound_query; + /** + * Add a query to the internal query log. Only works if the + * 'logging' config option is set to true. + * + * This works by manually binding the parameters to the query - the + * query isn't executed like this (PDO normally passes the query and + * parameters to the database which takes care of the binding) but + * doing it this way makes the logged queries more readable. + * @param string $query + * @param array $parameters An array of parameters to be bound in to the query + * @param string $connection_name Which connection to use + * @param float $query_time Query time + * @return bool + */ + protected static function _log_query($query, $parameters, $connection_name, $query_time) { + // If logging is not enabled, do nothing + if (!self::$_config[$connection_name]['logging']) { + return false; + } + + if (!isset(self::$_query_log[$connection_name])) { + self::$_query_log[$connection_name] = array(); + } + + if (empty($parameters)) { + $bound_query = $query; + } else { + // Escape the parameters + $parameters = array_map(array(self::get_db($connection_name), 'quote'), $parameters); + + if (array_values($parameters) === $parameters) { + // ? placeholders + // Avoid %format collision for vsprintf + $query = str_replace("%", "%%", $query); + + // Replace placeholders in the query for vsprintf + if(false !== strpos($query, "'") || false !== strpos($query, '"')) { + $query = IdiormString::str_replace_outside_quotes("?", "%s", $query); + } else { + $query = str_replace("?", "%s", $query); + } + + // Replace the question marks in the query with the parameters + $bound_query = vsprintf($query, $parameters); + } else { + // named placeholders + foreach ($parameters as $key => $val) { + $query = str_replace($key, $val, $query); + } + $bound_query = $query; + } + } + + self::$_last_query = $bound_query; + self::$_query_log[$connection_name][] = $bound_query; - if(is_callable(self::$_config[$connection_name]['logger'])){ - $logger = self::$_config[$connection_name]['logger']; - $logger($bound_query, $query_time); + if(is_callable(self::$_config[$connection_name]['logger'])){ + $logger = self::$_config[$connection_name]['logger']; + $logger($bound_query, $query_time); + } + + return true; } - return true; - } + /** + * Get the last query executed. Only works if the + * 'logging' config option is set to true. Otherwise + * this will return null. Returns last query from all connections if + * no connection_name is specified + * @param null|string $connection_name Which connection to use + * @return string + */ + public static function get_last_query($connection_name = null) { + if ($connection_name === null) { + return self::$_last_query; + } + if (!isset(self::$_query_log[$connection_name])) { + return ''; + } - /** - * Get the last query executed. Only works if the - * 'logging' config option is set to true. Otherwise - * this will return null. Returns last query from all connections if - * no connection_name is specified - * @param null|string $connection_name Which connection to use - * @return string - */ - public static function get_last_query($connection_name = null) { - if ($connection_name === null) { - return self::$_last_query; + return end(self::$_query_log[$connection_name]); } - if (!isset(self::$_query_log[$connection_name])) { + + /** + * Get an array containing all the queries run on a + * specified connection up to now. + * Only works if the 'logging' config option is + * set to true. Otherwise, returned array will be empty. + * @param string $connection_name Which connection to use + */ + public static function get_query_log($connection_name = self::DEFAULT_CONNECTION) { + if (isset(self::$_query_log[$connection_name])) { + return self::$_query_log[$connection_name]; + } + return array(); + } + + /** + * Get a list of the available connection names + * @return array + */ + public static function get_connection_names() { + return array_keys(self::$_db); + } + + // ------------------------ // + // --- INSTANCE METHODS --- // + // ------------------------ // + + /** + * "Private" constructor; shouldn't be called directly. + * Use the ORM::for_table factory method instead. + */ + protected function __construct($table_name, $data = array(), $connection_name = self::DEFAULT_CONNECTION) { + $this->_table_name = $table_name; + $this->_data = $data; + + $this->_connection_name = $connection_name; + self::_setup_db_config($connection_name); + } + + /** + * Create a new, empty instance of the class. Used + * to add a new row to your database. May optionally + * be passed an associative array of data to populate + * the instance. If so, all fields will be flagged as + * dirty so all will be saved to the database when + * save() is called. + */ + public function create($data=null) { + $this->_is_new = true; + if (!is_null($data)) { + return $this->hydrate($data)->force_all_dirty(); + } + return $this; + } + + /** + * Specify the ID column to use for this instance or array of instances only. + * This overrides the id_column and id_column_overrides settings. + * + * This is mostly useful for libraries built on top of Idiorm, and will + * not normally be used in manually built queries. If you don't know why + * you would want to use this, you should probably just ignore it. + */ + public function use_id_column($id_column) { + $this->_instance_id_column = $id_column; + return $this; + } + + /** + * Create an ORM instance from the given row (an associative + * array of data fetched from the database) + */ + protected function _create_instance_from_row($row) { + $instance = self::for_table($this->_table_name, $this->_connection_name); + $instance->use_id_column($this->_instance_id_column); + $instance->hydrate($row); + return $instance; + } + + /** + * Tell the ORM that you are expecting a single result + * back from your query, and execute it. Will return + * a single instance of the ORM class, or false if no + * rows were returned. + * As a shortcut, you may supply an ID as a parameter + * to this method. This will perform a primary key + * lookup on the table. + */ + public function find_one($id=null) { + if (!is_null($id)) { + $this->where_id_is($id); + } + $this->limit(1); + $rows = $this->_run(); + + if (empty($rows)) { + return false; + } + + return $this->_create_instance_from_row($rows[0]); + } + + /** + * Tell the ORM that you are expecting multiple results + * from your query, and execute it. Will return an array + * of instances of the ORM class, or an empty array if + * no rows were returned. + * @return array|\IdiormResultSet + */ + public function find_many() { + if(self::$_config[$this->_connection_name]['return_result_sets']) { + return $this->find_result_set(); + } + return $this->_find_many(); + } + + /** + * Tell the ORM that you are expecting multiple results + * from your query, and execute it. Will return an array + * of instances of the ORM class, or an empty array if + * no rows were returned. + * @return array + */ + protected function _find_many() { + $rows = $this->_run(); + return array_map(array($this, '_create_instance_from_row'), $rows); + } + + /** + * Tell the ORM that you are expecting multiple results + * from your query, and execute it. Will return a result set object + * containing instances of the ORM class. + * @return \IdiormResultSet + */ + public function find_result_set() { + return new IdiormResultSet($this->_find_many()); + } + + /** + * Tell the ORM that you are expecting multiple results + * from your query, and execute it. Will return an array, + * or an empty array if no rows were returned. + * @return array + */ + public function find_array() { + return $this->_run(); + } + + /** + * Tell the ORM that you wish to execute a COUNT query. + * Will return an integer representing the number of + * rows returned. + */ + public function count($column = '*') { + return $this->_call_aggregate_db_function(__FUNCTION__, $column); + } + + /** + * Tell the ORM that you wish to execute a MAX query. + * Will return the max value of the choosen column. + */ + public function max($column) { + return $this->_call_aggregate_db_function(__FUNCTION__, $column); + } + + /** + * Tell the ORM that you wish to execute a MIN query. + * Will return the min value of the choosen column. + */ + public function min($column) { + return $this->_call_aggregate_db_function(__FUNCTION__, $column); + } + + /** + * Tell the ORM that you wish to execute a AVG query. + * Will return the average value of the choosen column. + */ + public function avg($column) { + return $this->_call_aggregate_db_function(__FUNCTION__, $column); + } + + /** + * Tell the ORM that you wish to execute a SUM query. + * Will return the sum of the choosen column. + */ + public function sum($column) { + return $this->_call_aggregate_db_function(__FUNCTION__, $column); + } + + /** + * Execute an aggregate query on the current connection. + * @param string $sql_function The aggregate function to call eg. MIN, COUNT, etc + * @param string $column The column to execute the aggregate query against + * @return int + */ + protected function _call_aggregate_db_function($sql_function, $column) { + $alias = strtolower($sql_function); + $sql_function = strtoupper($sql_function); + if('*' != $column) { + $column = $this->_quote_identifier($column); + } + $result_columns = $this->_result_columns; + $this->_result_columns = array(); + $this->select_expr("$sql_function($column)", $alias); + $result = $this->find_one(); + $this->_result_columns = $result_columns; + + $return_value = 0; + if($result !== false && isset($result->$alias)) { + if (!is_numeric($result->$alias)) { + $return_value = $result->$alias; + } + elseif((int) $result->$alias == (float) $result->$alias) { + $return_value = (int) $result->$alias; + } else { + $return_value = (float) $result->$alias; + } + } + return $return_value; + } + + /** + * This method can be called to hydrate (populate) this + * instance of the class from an associative array of data. + * This will usually be called only from inside the class, + * but it's public in case you need to call it directly. + */ + public function hydrate($data=array()) { + $this->_data = $data; + return $this; + } + + /** + * Force the ORM to flag all the fields in the $data array + * as "dirty" and therefore update them when save() is called. + */ + public function force_all_dirty() { + $this->_dirty_fields = $this->_data; + return $this; + } + + /** + * Perform a raw query. The query can contain placeholders in + * either named or question mark style. If placeholders are + * used, the parameters should be an array of values which will + * be bound to the placeholders in the query. If this method + * is called, all other query building methods will be ignored. + */ + public function raw_query($query, $parameters = array()) { + $this->_is_raw_query = true; + $this->_raw_query = $query; + $this->_raw_parameters = $parameters; + return $this; + } + + /** + * Add an alias for the main table to be used in SELECT queries + */ + public function table_alias($alias) { + $this->_table_alias = $alias; + return $this; + } + + /** + * Internal method to add an unquoted expression to the set + * of columns returned by the SELECT query. The second optional + * argument is the alias to return the expression as. + */ + protected function _add_result_column($expr, $alias=null) { + if (!is_null($alias)) { + $expr .= " AS " . $this->_quote_identifier($alias); + } + + if ($this->_using_default_result_columns) { + $this->_result_columns = array($expr); + $this->_using_default_result_columns = false; + } else { + $this->_result_columns[] = $expr; + } + return $this; + } + + /** + * Counts the number of columns that belong to the primary + * key and their value is null. + */ + public function count_null_id_columns() { + if (is_array($this->_get_id_column_name())) { + return count(array_filter($this->id(), 'is_null')); + } else { + return is_null($this->id()) ? 1 : 0; + } + } + + /** + * Add a column to the list of columns returned by the SELECT + * query. This defaults to '*'. The second optional argument is + * the alias to return the column as. + */ + public function select($column, $alias=null) { + $column = $this->_quote_identifier($column); + return $this->_add_result_column($column, $alias); + } + + /** + * Add an unquoted expression to the list of columns returned + * by the SELECT query. The second optional argument is + * the alias to return the column as. + */ + public function select_expr($expr, $alias=null) { + return $this->_add_result_column($expr, $alias); + } + + /** + * Add columns to the list of columns returned by the SELECT + * query. This defaults to '*'. Many columns can be supplied + * as either an array or as a list of parameters to the method. + * + * Note that the alias must not be numeric - if you want a + * numeric alias then prepend it with some alpha chars. eg. a1 + * + * @example select_many(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5'); + * @example select_many('column', 'column2', 'column3'); + * @example select_many(array('column', 'column2', 'column3'), 'column4', 'column5'); + * + * @return \ORM + */ + public function select_many() { + $columns = func_get_args(); + if(!empty($columns)) { + $columns = $this->_normalise_select_many_columns($columns); + foreach($columns as $alias => $column) { + if(is_numeric($alias)) { + $alias = null; + } + $this->select($column, $alias); + } + } + return $this; + } + + /** + * Add an unquoted expression to the list of columns returned + * by the SELECT query. Many columns can be supplied as either + * an array or as a list of parameters to the method. + * + * Note that the alias must not be numeric - if you want a + * numeric alias then prepend it with some alpha chars. eg. a1 + * + * @example select_many_expr(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5') + * @example select_many_expr('column', 'column2', 'column3') + * @example select_many_expr(array('column', 'column2', 'column3'), 'column4', 'column5') + * + * @return \ORM + */ + public function select_many_expr() { + $columns = func_get_args(); + if(!empty($columns)) { + $columns = $this->_normalise_select_many_columns($columns); + foreach($columns as $alias => $column) { + if(is_numeric($alias)) { + $alias = null; + } + $this->select_expr($column, $alias); + } + } + return $this; + } + + /** + * Take a column specification for the select many methods and convert it + * into a normalised array of columns and aliases. + * + * It is designed to turn the following styles into a normalised array: + * + * array(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5')) + * + * @param array $columns + * @return array + */ + protected function _normalise_select_many_columns($columns) { + $return = array(); + foreach($columns as $column) { + if(is_array($column)) { + foreach($column as $key => $value) { + if(!is_numeric($key)) { + $return[$key] = $value; + } else { + $return[] = $value; + } + } + } else { + $return[] = $column; + } + } + return $return; + } + + /** + * Add a DISTINCT keyword before the list of columns in the SELECT query + */ + public function distinct() { + $this->_distinct = true; + return $this; + } + + /** + * Internal method to add a JOIN source to the query. + * + * The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this + * will be prepended to JOIN. + * + * The table should be the name of the table to join to. + * + * The constraint may be either a string or an array with three elements. If it + * is a string, it will be compiled into the query as-is, with no escaping. The + * recommended way to supply the constraint is as an array with three elements: + * + * first_column, operator, second_column + * + * Example: array('user.id', '=', 'profile.user_id') + * + * will compile to + * + * ON `user`.`id` = `profile`.`user_id` + * + * The final (optional) argument specifies an alias for the joined table. + */ + protected function _add_join_source($join_operator, $table, $constraint, $table_alias=null) { + + $join_operator = trim("{$join_operator} JOIN"); + + $table = $this->_quote_identifier($table); + + // Add table alias if present + if (!is_null($table_alias)) { + $table_alias = $this->_quote_identifier($table_alias); + $table .= " {$table_alias}"; + } + + // Build the constraint + if (is_array($constraint)) { + list($first_column, $operator, $second_column) = $constraint; + $first_column = $this->_quote_identifier($first_column); + $second_column = $this->_quote_identifier($second_column); + $constraint = "{$first_column} {$operator} {$second_column}"; + } + + $this->_join_sources[] = "{$join_operator} {$table} ON {$constraint}"; + return $this; + } + + /** + * Add a RAW JOIN source to the query + */ + public function raw_join($table, $constraint, $table_alias, $parameters = array()) { + // Add table alias if present + if (!is_null($table_alias)) { + $table_alias = $this->_quote_identifier($table_alias); + $table .= " {$table_alias}"; + } + + $this->_values = array_merge($this->_values, $parameters); + + // Build the constraint + if (is_array($constraint)) { + list($first_column, $operator, $second_column) = $constraint; + $first_column = $this->_quote_identifier($first_column); + $second_column = $this->_quote_identifier($second_column); + $constraint = "{$first_column} {$operator} {$second_column}"; + } + + $this->_join_sources[] = "{$table} ON {$constraint}"; + return $this; + } + + /** + * Add a simple JOIN source to the query + */ + public function join($table, $constraint, $table_alias=null) { + return $this->_add_join_source("", $table, $constraint, $table_alias); + } + + /** + * Add an INNER JOIN souce to the query + */ + public function inner_join($table, $constraint, $table_alias=null) { + return $this->_add_join_source("INNER", $table, $constraint, $table_alias); + } + + /** + * Add a LEFT OUTER JOIN souce to the query + */ + public function left_outer_join($table, $constraint, $table_alias=null) { + return $this->_add_join_source("LEFT OUTER", $table, $constraint, $table_alias); + } + + /** + * Add an RIGHT OUTER JOIN souce to the query + */ + public function right_outer_join($table, $constraint, $table_alias=null) { + return $this->_add_join_source("RIGHT OUTER", $table, $constraint, $table_alias); + } + + /** + * Add an FULL OUTER JOIN souce to the query + */ + public function full_outer_join($table, $constraint, $table_alias=null) { + return $this->_add_join_source("FULL OUTER", $table, $constraint, $table_alias); + } + + /** + * Internal method to add a HAVING condition to the query + */ + protected function _add_having($fragment, $values=array()) { + return $this->_add_condition('having', $fragment, $values); + } + + /** + * Internal method to add a HAVING condition to the query + */ + protected function _add_simple_having($column_name, $separator, $value) { + return $this->_add_simple_condition('having', $column_name, $separator, $value); + } + + /** + * Internal method to add a HAVING clause with multiple values (like IN and NOT IN) + */ + public function _add_having_placeholder($column_name, $separator, $values) { + if (!is_array($column_name)) { + $data = array($column_name => $values); + } else { + $data = $column_name; + } + $result = $this; + foreach ($data as $key => $val) { + $column = $result->_quote_identifier($key); + $placeholders = $result->_create_placeholders($val); + $result = $result->_add_having("{$column} {$separator} ({$placeholders})", $val); + } + return $result; + } + + /** + * Internal method to add a HAVING clause with no parameters(like IS NULL and IS NOT NULL) + */ + public function _add_having_no_value($column_name, $operator) { + $conditions = (is_array($column_name)) ? $column_name : array($column_name); + $result = $this; + foreach($conditions as $column) { + $column = $this->_quote_identifier($column); + $result = $result->_add_having("{$column} {$operator}"); + } + return $result; + } + + /** + * Internal method to add a WHERE condition to the query + */ + protected function _add_where($fragment, $values=array()) { + return $this->_add_condition('where', $fragment, $values); + } + + /** + * Internal method to add a WHERE condition to the query + */ + protected function _add_simple_where($column_name, $separator, $value) { + return $this->_add_simple_condition('where', $column_name, $separator, $value); + } + + /** + * Add a WHERE clause with multiple values (like IN and NOT IN) + */ + public function _add_where_placeholder($column_name, $separator, $values) { + if (!is_array($column_name)) { + $data = array($column_name => $values); + } else { + $data = $column_name; + } + $result = $this; + foreach ($data as $key => $val) { + $column = $result->_quote_identifier($key); + $placeholders = $result->_create_placeholders($val); + $result = $result->_add_where("{$column} {$separator} ({$placeholders})", $val); + } + return $result; + } + + /** + * Add a WHERE clause with no parameters(like IS NULL and IS NOT NULL) + */ + public function _add_where_no_value($column_name, $operator) { + $conditions = (is_array($column_name)) ? $column_name : array($column_name); + $result = $this; + foreach($conditions as $column) { + $column = $this->_quote_identifier($column); + $result = $result->_add_where("{$column} {$operator}"); + } + return $result; + } + + /** + * Internal method to add a HAVING or WHERE condition to the query + */ + protected function _add_condition($type, $fragment, $values=array()) { + $conditions_class_property_name = "_{$type}_conditions"; + if (!is_array($values)) { + $values = array($values); + } + array_push($this->$conditions_class_property_name, array( + self::CONDITION_FRAGMENT => $fragment, + self::CONDITION_VALUES => $values, + )); + return $this; + } + + /** + * Helper method to compile a simple COLUMN SEPARATOR VALUE + * style HAVING or WHERE condition into a string and value ready to + * be passed to the _add_condition method. Avoids duplication + * of the call to _quote_identifier + * + * If column_name is an associative array, it will add a condition for each column + */ + protected function _add_simple_condition($type, $column_name, $separator, $value) { + $multiple = is_array($column_name) ? $column_name : array($column_name => $value); + $result = $this; + + foreach($multiple as $key => $val) { + // Add the table name in case of ambiguous columns + if (count($result->_join_sources) > 0 && strpos($key, '.') === false) { + $table = $result->_table_name; + if (!is_null($result->_table_alias)) { + $table = $result->_table_alias; + } + + $key = "{$table}.{$key}"; + } + $key = $result->_quote_identifier($key); + $result = $result->_add_condition($type, "{$key} {$separator} ?", $val); + } + return $result; + } + + /** + * Return a string containing the given number of question marks, + * separated by commas. Eg "?, ?, ?" + */ + protected function _create_placeholders($fields) { + if(!empty($fields)) { + $db_fields = array(); + foreach($fields as $key => $value) { + // Process expression fields directly into the query + if(array_key_exists($key, $this->_expr_fields)) { + $db_fields[] = $value; + } else { + $db_fields[] = '?'; + } + } + return implode(', ', $db_fields); + } + } + + /** + * Helper method that filters a column/value array returning only those + * columns that belong to a compound primary key. + * + * If the key contains a column that does not exist in the given array, + * a null value will be returned for it. + */ + protected function _get_compound_id_column_values($value) { + $filtered = array(); + foreach($this->_get_id_column_name() as $key) { + $filtered[$key] = isset($value[$key]) ? $value[$key] : null; + } + return $filtered; + } + + /** + * Helper method that filters an array containing compound column/value + * arrays. + */ + protected function _get_compound_id_column_values_array($values) { + $filtered = array(); + foreach($values as $value) { + $filtered[] = $this->_get_compound_id_column_values($value); + } + return $filtered; + } + + /** + * Add a WHERE column = value clause to your query. Each time + * this is called in the chain, an additional WHERE will be + * added, and these will be ANDed together when the final query + * is built. + * + * If you use an array in $column_name, a new clause will be + * added for each element. In this case, $value is ignored. + */ + public function where($column_name, $value=null) { + return $this->where_equal($column_name, $value); + } + + /** + * More explicitly named version of for the where() method. + * Can be used if preferred. + */ + public function where_equal($column_name, $value=null) { + return $this->_add_simple_where($column_name, '=', $value); + } + + /** + * Add a WHERE column != value clause to your query. + */ + public function where_not_equal($column_name, $value=null) { + return $this->_add_simple_where($column_name, '!=', $value); + } + + /** + * Special method to query the table by its primary key + * + * If primary key is compound, only the columns that + * belong to they key will be used for the query + */ + public function where_id_is($id) { + return (is_array($this->_get_id_column_name())) ? + $this->where($this->_get_compound_id_column_values($id), null) : + $this->where($this->_get_id_column_name(), $id); + } + + /** + * Allows adding a WHERE clause that matches any of the conditions + * specified in the array. Each element in the associative array will + * be a different condition, where the key will be the column name. + * + * By default, an equal operator will be used against all columns, but + * it can be overriden for any or every column using the second parameter. + * + * Each condition will be ORed together when added to the final query. + */ + public function where_any_is($values, $operator='=') { + $data = array(); + $query = array("(("); + $first = true; + foreach ($values as $value) { + if ($first) { + $first = false; + } else { + $query[] = ") OR ("; + } + $firstsub = true; + foreach($value as $key => $item) { + $op = is_string($operator) ? $operator : (isset($operator[$key]) ? $operator[$key] : '='); + if ($firstsub) { + $firstsub = false; + } else { + $query[] = "AND"; + } + $query[] = $this->_quote_identifier($key); + $data[] = $item; + $query[] = $op . " ?"; + } + } + $query[] = "))"; + return $this->where_raw(join(' ', $query), $data); + } + + /** + * Similar to where_id_is() but allowing multiple primary keys. + * + * If primary key is compound, only the columns that + * belong to they key will be used for the query + */ + public function where_id_in($ids) { + return (is_array($this->_get_id_column_name())) ? + $this->where_any_is($this->_get_compound_id_column_values_array($ids)) : + $this->where_in($this->_get_id_column_name(), $ids); + } + + /** + * Add a WHERE ... LIKE clause to your query. + */ + public function where_like($column_name, $value=null) { + return $this->_add_simple_where($column_name, 'LIKE', $value); + } + + /** + * Add where WHERE ... NOT LIKE clause to your query. + */ + public function where_not_like($column_name, $value=null) { + return $this->_add_simple_where($column_name, 'NOT LIKE', $value); + } + + /** + * Add a WHERE ... > clause to your query + */ + public function where_gt($column_name, $value=null) { + return $this->_add_simple_where($column_name, '>', $value); + } + + /** + * Add a WHERE ... < clause to your query + */ + public function where_lt($column_name, $value=null) { + return $this->_add_simple_where($column_name, '<', $value); + } + + /** + * Add a WHERE ... >= clause to your query + */ + public function where_gte($column_name, $value=null) { + return $this->_add_simple_where($column_name, '>=', $value); + } + + /** + * Add a WHERE ... <= clause to your query + */ + public function where_lte($column_name, $value=null) { + return $this->_add_simple_where($column_name, '<=', $value); + } + + /** + * Add a WHERE ... IN clause to your query + */ + public function where_in($column_name, $values) { + return $this->_add_where_placeholder($column_name, 'IN', $values); + } + + /** + * Add a WHERE ... NOT IN clause to your query + */ + public function where_not_in($column_name, $values) { + return $this->_add_where_placeholder($column_name, 'NOT IN', $values); + } + + /** + * Add a WHERE column IS NULL clause to your query + */ + public function where_null($column_name) { + return $this->_add_where_no_value($column_name, "IS NULL"); + } + + /** + * Add a WHERE column IS NOT NULL clause to your query + */ + public function where_not_null($column_name) { + return $this->_add_where_no_value($column_name, "IS NOT NULL"); + } + + /** + * Add a raw WHERE clause to the query. The clause should + * contain question mark placeholders, which will be bound + * to the parameters supplied in the second argument. + */ + public function where_raw($clause, $parameters=array()) { + return $this->_add_where($clause, $parameters); + } + + /** + * Add a LIMIT to the query + */ + public function limit($limit) { + $this->_limit = $limit; + return $this; + } + + /** + * Add an OFFSET to the query + */ + public function offset($offset) { + $this->_offset = $offset; + return $this; + } + + /** + * Add an ORDER BY clause to the query + */ + protected function _add_order_by($column_name, $ordering) { + $column_name = $this->_quote_identifier($column_name); + $this->_order_by[] = "{$column_name} {$ordering}"; + return $this; + } + + /** + * Add an ORDER BY column DESC clause + */ + public function order_by_desc($column_name) { + return $this->_add_order_by($column_name, 'DESC'); + } + + /** + * Add an ORDER BY column ASC clause + */ + public function order_by_asc($column_name) { + return $this->_add_order_by($column_name, 'ASC'); + } + + /** + * Add an unquoted expression as an ORDER BY clause + */ + public function order_by_expr($clause) { + $this->_order_by[] = $clause; + return $this; + } + + /** + * Add a column to the list of columns to GROUP BY + */ + public function group_by($column_name) { + $column_name = $this->_quote_identifier($column_name); + $this->_group_by[] = $column_name; + return $this; + } + + /** + * Add an unquoted expression to the list of columns to GROUP BY + */ + public function group_by_expr($expr) { + $this->_group_by[] = $expr; + return $this; + } + + /** + * Add a HAVING column = value clause to your query. Each time + * this is called in the chain, an additional HAVING will be + * added, and these will be ANDed together when the final query + * is built. + * + * If you use an array in $column_name, a new clause will be + * added for each element. In this case, $value is ignored. + */ + public function having($column_name, $value=null) { + return $this->having_equal($column_name, $value); + } + + /** + * More explicitly named version of for the having() method. + * Can be used if preferred. + */ + public function having_equal($column_name, $value=null) { + return $this->_add_simple_having($column_name, '=', $value); + } + + /** + * Add a HAVING column != value clause to your query. + */ + public function having_not_equal($column_name, $value=null) { + return $this->_add_simple_having($column_name, '!=', $value); + } + + /** + * Special method to query the table by its primary key. + * + * If primary key is compound, only the columns that + * belong to they key will be used for the query + */ + public function having_id_is($id) { + return (is_array($this->_get_id_column_name())) ? + $this->having($this->_get_compound_id_column_values($id), null) : + $this->having($this->_get_id_column_name(), $id); + } + + /** + * Add a HAVING ... LIKE clause to your query. + */ + public function having_like($column_name, $value=null) { + return $this->_add_simple_having($column_name, 'LIKE', $value); + } + + /** + * Add where HAVING ... NOT LIKE clause to your query. + */ + public function having_not_like($column_name, $value=null) { + return $this->_add_simple_having($column_name, 'NOT LIKE', $value); + } + + /** + * Add a HAVING ... > clause to your query + */ + public function having_gt($column_name, $value=null) { + return $this->_add_simple_having($column_name, '>', $value); + } + + /** + * Add a HAVING ... < clause to your query + */ + public function having_lt($column_name, $value=null) { + return $this->_add_simple_having($column_name, '<', $value); + } + + /** + * Add a HAVING ... >= clause to your query + */ + public function having_gte($column_name, $value=null) { + return $this->_add_simple_having($column_name, '>=', $value); + } + + /** + * Add a HAVING ... <= clause to your query + */ + public function having_lte($column_name, $value=null) { + return $this->_add_simple_having($column_name, '<=', $value); + } + + /** + * Add a HAVING ... IN clause to your query + */ + public function having_in($column_name, $values=null) { + return $this->_add_having_placeholder($column_name, 'IN', $values); + } + + /** + * Add a HAVING ... NOT IN clause to your query + */ + public function having_not_in($column_name, $values=null) { + return $this->_add_having_placeholder($column_name, 'NOT IN', $values); + } + + /** + * Add a HAVING column IS NULL clause to your query + */ + public function having_null($column_name) { + return $this->_add_having_no_value($column_name, 'IS NULL'); + } + + /** + * Add a HAVING column IS NOT NULL clause to your query + */ + public function having_not_null($column_name) { + return $this->_add_having_no_value($column_name, 'IS NOT NULL'); + } + + /** + * Add a raw HAVING clause to the query. The clause should + * contain question mark placeholders, which will be bound + * to the parameters supplied in the second argument. + */ + public function having_raw($clause, $parameters=array()) { + return $this->_add_having($clause, $parameters); + } + + /** + * Build a SELECT statement based on the clauses that have + * been passed to this instance by chaining method calls. + */ + protected function _build_select() { + // If the query is raw, just set the $this->_values to be + // the raw query parameters and return the raw query + if ($this->_is_raw_query) { + $this->_values = $this->_raw_parameters; + return $this->_raw_query; + } + + // Build and return the full SELECT statement by concatenating + // the results of calling each separate builder method. + return $this->_join_if_not_empty(" ", array( + $this->_build_select_start(), + $this->_build_join(), + $this->_build_where(), + $this->_build_group_by(), + $this->_build_having(), + $this->_build_order_by(), + $this->_build_limit(), + $this->_build_offset(), + )); + } + + /** + * Build the start of the SELECT statement + */ + protected function _build_select_start() { + $fragment = 'SELECT '; + $result_columns = join(', ', $this->_result_columns); + + if (!is_null($this->_limit) && + self::$_config[$this->_connection_name]['limit_clause_style'] === ORM::LIMIT_STYLE_TOP_N) { + $fragment .= "TOP {$this->_limit} "; + } + + if ($this->_distinct) { + $result_columns = 'DISTINCT ' . $result_columns; + } + + $fragment .= "{$result_columns} FROM " . $this->_quote_identifier($this->_table_name); + + if (!is_null($this->_table_alias)) { + $fragment .= " " . $this->_quote_identifier($this->_table_alias); + } + return $fragment; + } + + /** + * Build the JOIN sources + */ + protected function _build_join() { + if (count($this->_join_sources) === 0) { + return ''; + } + + return join(" ", $this->_join_sources); + } + + /** + * Build the WHERE clause(s) + */ + protected function _build_where() { + return $this->_build_conditions('where'); + } + + /** + * Build the HAVING clause(s) + */ + protected function _build_having() { + return $this->_build_conditions('having'); + } + + /** + * Build GROUP BY + */ + protected function _build_group_by() { + if (count($this->_group_by) === 0) { + return ''; + } + return "GROUP BY " . join(", ", $this->_group_by); + } + + /** + * Build a WHERE or HAVING clause + * @param string $type + * @return string + */ + protected function _build_conditions($type) { + $conditions_class_property_name = "_{$type}_conditions"; + // If there are no clauses, return empty string + if (count($this->$conditions_class_property_name) === 0) { + return ''; + } + + $conditions = array(); + foreach ($this->$conditions_class_property_name as $condition) { + $conditions[] = $condition[self::CONDITION_FRAGMENT]; + $this->_values = array_merge($this->_values, $condition[self::CONDITION_VALUES]); + } + + return strtoupper($type) . " " . join(" AND ", $conditions); + } + + /** + * Build ORDER BY + */ + protected function _build_order_by() { + if (count($this->_order_by) === 0) { + return ''; + } + return "ORDER BY " . join(", ", $this->_order_by); + } + + /** + * Build LIMIT + */ + protected function _build_limit() { + $fragment = ''; + if (!is_null($this->_limit) && + self::$_config[$this->_connection_name]['limit_clause_style'] == ORM::LIMIT_STYLE_LIMIT) { + if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'firebird') { + $fragment = 'ROWS'; + } else { + $fragment = 'LIMIT'; + } + $fragment .= " {$this->_limit}"; + } + return $fragment; + } + + /** + * Build OFFSET + */ + protected function _build_offset() { + if (!is_null($this->_offset)) { + $clause = 'OFFSET'; + if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'firebird') { + $clause = 'TO'; + } + return "$clause " . $this->_offset; + } return ''; } - return end(self::$_query_log[$connection_name]); - } - - /** - * Get an array containing all the queries run on a - * specified connection up to now. - * Only works if the 'logging' config option is - * set to true. Otherwise, returned array will be empty. - * @param string $connection_name Which connection to use - */ - public static function get_query_log($connection_name = self::DEFAULT_CONNECTION) { - if (isset(self::$_query_log[$connection_name])) { - return self::$_query_log[$connection_name]; + /** + * Wrapper around PHP's join function which + * only adds the pieces if they are not empty. + */ + protected function _join_if_not_empty($glue, $pieces) { + $filtered_pieces = array(); + foreach ($pieces as $piece) { + if (is_string($piece)) { + $piece = trim($piece); + } + if (!empty($piece)) { + $filtered_pieces[] = $piece; + } + } + return join($glue, $filtered_pieces); } - return array(); - } - /** - * Get a list of the available connection names - * @return array - */ - public static function get_connection_names() { - return array_keys(self::$_db); - } - - // ------------------------ // - // --- INSTANCE METHODS --- // - // ------------------------ // - - /** - * "Private" constructor; shouldn't be called directly. - * Use the ORM::for_table factory method instead. - */ - protected function __construct($table_name, $data = array(), $connection_name = self::DEFAULT_CONNECTION) { - $this->_table_name = $table_name; - $this->_data = $data; - - $this->_connection_name = $connection_name; - self::_setup_db_config($connection_name); - } - - /** - * Create a new, empty instance of the class. Used - * to add a new row to your database. May optionally - * be passed an associative array of data to populate - * the instance. If so, all fields will be flagged as - * dirty so all will be saved to the database when - * save() is called. - */ - public function create($data=null) { - $this->_is_new = true; - if (!is_null($data)) { - return $this->hydrate($data)->force_all_dirty(); + /** + * Quote a string that is used as an identifier + * (table names, column names etc). This method can + * also deal with dot-separated identifiers eg table.column + */ + protected function _quote_one_identifier($identifier) { + $parts = explode('.', $identifier); + $parts = array_map(array($this, '_quote_identifier_part'), $parts); + return join('.', $parts); } - return $this; - } - /** - * Specify the ID column to use for this instance or array of instances only. - * This overrides the id_column and id_column_overrides settings. - * - * This is mostly useful for libraries built on top of Idiorm, and will - * not normally be used in manually built queries. If you don't know why - * you would want to use this, you should probably just ignore it. - */ - public function use_id_column($id_column) { - $this->_instance_id_column = $id_column; - return $this; - } - - /** - * Create an ORM instance from the given row (an associative - * array of data fetched from the database) - */ - protected function _create_instance_from_row($row) { - $instance = self::for_table($this->_table_name, $this->_connection_name); - $instance->use_id_column($this->_instance_id_column); - $instance->hydrate($row); - return $instance; - } - - /** - * Tell the ORM that you are expecting a single result - * back from your query, and execute it. Will return - * a single instance of the ORM class, or false if no - * rows were returned. - * As a shortcut, you may supply an ID as a parameter - * to this method. This will perform a primary key - * lookup on the table. - */ - public function find_one($id=null) { - if (!is_null($id)) { - $this->where_id_is($id); + /** + * Quote a string that is used as an identifier + * (table names, column names etc) or an array containing + * multiple identifiers. This method can also deal with + * dot-separated identifiers eg table.column + */ + protected function _quote_identifier($identifier) { + if (is_array($identifier)) { + $result = array_map(array($this, '_quote_one_identifier'), $identifier); + return join(', ', $result); + } else { + return $this->_quote_one_identifier($identifier); + } } - $this->limit(1); - $rows = $this->_run(); - if (empty($rows)) { + /** + * This method performs the actual quoting of a single + * part of an identifier, using the identifier quote + * character specified in the config (or autodetected). + */ + protected function _quote_identifier_part($part) { + if ($part === '*') { + return $part; + } + + $quote_character = self::$_config[$this->_connection_name]['identifier_quote_character']; + // double up any identifier quotes to escape them + return $quote_character . + str_replace($quote_character, + $quote_character . $quote_character, + $part + ) . $quote_character; + } + + /** + * Create a cache key for the given query and parameters. + */ + protected static function _create_cache_key($query, $parameters, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) { + if(isset(self::$_config[$connection_name]['create_cache_key']) and is_callable(self::$_config[$connection_name]['create_cache_key'])){ + return call_user_func_array(self::$_config[$connection_name]['create_cache_key'], array($query, $parameters, $table_name, $connection_name)); + } + $parameter_string = join(',', $parameters); + $key = $query . ':' . $parameter_string; + return sha1($key); + } + + /** + * Check the query cache for the given cache key. If a value + * is cached for the key, return the value. Otherwise, return false. + */ + protected static function _check_query_cache($cache_key, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) { + if(isset(self::$_config[$connection_name]['check_query_cache']) and is_callable(self::$_config[$connection_name]['check_query_cache'])){ + return call_user_func_array(self::$_config[$connection_name]['check_query_cache'], array($cache_key, $table_name, $connection_name)); + } elseif (isset(self::$_query_cache[$connection_name][$cache_key])) { + return self::$_query_cache[$connection_name][$cache_key]; + } return false; } - return $this->_create_instance_from_row($rows[0]); - } - - /** - * Tell the ORM that you are expecting multiple results - * from your query, and execute it. Will return an array - * of instances of the ORM class, or an empty array if - * no rows were returned. - * @return array|\IdiormResultSet - */ - public function find_many() { - if(self::$_config[$this->_connection_name]['return_result_sets']) { - return $this->find_result_set(); - } - return $this->_find_many(); - } - - /** - * Tell the ORM that you are expecting multiple results - * from your query, and execute it. Will return an array - * of instances of the ORM class, or an empty array if - * no rows were returned. - * @return array - */ - protected function _find_many() { - $rows = $this->_run(); - return array_map(array($this, '_create_instance_from_row'), $rows); - } - - /** - * Tell the ORM that you are expecting multiple results - * from your query, and execute it. Will return a result set object - * containing instances of the ORM class. - * @return \IdiormResultSet - */ - public function find_result_set() { - return new IdiormResultSet($this->_find_many()); - } - - /** - * Tell the ORM that you are expecting multiple results - * from your query, and execute it. Will return an array, - * or an empty array if no rows were returned. - * @return array - */ - public function find_array() { - return $this->_run(); - } - - /** - * Tell the ORM that you wish to execute a COUNT query. - * Will return an integer representing the number of - * rows returned. - */ - public function count($column = '*') { - return $this->_call_aggregate_db_function(__FUNCTION__, $column); - } - - /** - * Tell the ORM that you wish to execute a MAX query. - * Will return the max value of the choosen column. - */ - public function max($column) { - return $this->_call_aggregate_db_function(__FUNCTION__, $column); - } - - /** - * Tell the ORM that you wish to execute a MIN query. - * Will return the min value of the choosen column. - */ - public function min($column) { - return $this->_call_aggregate_db_function(__FUNCTION__, $column); - } - - /** - * Tell the ORM that you wish to execute a AVG query. - * Will return the average value of the choosen column. - */ - public function avg($column) { - return $this->_call_aggregate_db_function(__FUNCTION__, $column); - } - - /** - * Tell the ORM that you wish to execute a SUM query. - * Will return the sum of the choosen column. - */ - public function sum($column) { - return $this->_call_aggregate_db_function(__FUNCTION__, $column); - } - - /** - * Execute an aggregate query on the current connection. - * @param string $sql_function The aggregate function to call eg. MIN, COUNT, etc - * @param string $column The column to execute the aggregate query against - * @return int - */ - protected function _call_aggregate_db_function($sql_function, $column) { - $alias = strtolower($sql_function); - $sql_function = strtoupper($sql_function); - if('*' != $column) { - $column = $this->_quote_identifier($column); - } - $result_columns = $this->_result_columns; - $this->_result_columns = array(); - $this->select_expr("$sql_function($column)", $alias); - $result = $this->find_one(); - $this->_result_columns = $result_columns; - - $return_value = 0; - if($result !== false && isset($result->$alias)) { - if (!is_numeric($result->$alias)) { - $return_value = $result->$alias; + /** + * Clear the query cache + */ + public static function clear_cache($table_name = null, $connection_name = self::DEFAULT_CONNECTION) { + self::$_query_cache = array(); + if(isset(self::$_config[$connection_name]['clear_cache']) and is_callable(self::$_config[$connection_name]['clear_cache'])){ + return call_user_func_array(self::$_config[$connection_name]['clear_cache'], array($table_name, $connection_name)); } - elseif((int) $result->$alias == (float) $result->$alias) { - $return_value = (int) $result->$alias; + } + + /** + * Add the given value to the query cache. + */ + protected static function _cache_query_result($cache_key, $value, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) { + if(isset(self::$_config[$connection_name]['cache_query_result']) and is_callable(self::$_config[$connection_name]['cache_query_result'])){ + return call_user_func_array(self::$_config[$connection_name]['cache_query_result'], array($cache_key, $value, $table_name, $connection_name)); + } elseif (!isset(self::$_query_cache[$connection_name])) { + self::$_query_cache[$connection_name] = array(); + } + self::$_query_cache[$connection_name][$cache_key] = $value; + } + + /** + * Execute the SELECT query that has been built up by chaining methods + * on this class. Return an array of rows as associative arrays. + */ + protected function _run() { + $query = $this->_build_select(); + $caching_enabled = self::$_config[$this->_connection_name]['caching']; + + if ($caching_enabled) { + $cache_key = self::_create_cache_key($query, $this->_values, $this->_table_name, $this->_connection_name); + $cached_result = self::_check_query_cache($cache_key, $this->_table_name, $this->_connection_name); + + if ($cached_result !== false) { + $this->_reset_idiorm_state(); + return $cached_result; + } + } + + self::_execute($query, $this->_values, $this->_connection_name); + $statement = self::get_last_statement(); + + $rows = array(); + while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { + $rows[] = $row; + } + + if ($caching_enabled) { + self::_cache_query_result($cache_key, $rows, $this->_table_name, $this->_connection_name); + } + + $this->_reset_idiorm_state(); + return $rows; + } + + /** + * Reset the Idiorm instance state + */ + private function _reset_idiorm_state() { + $this->_values = array(); + $this->_result_columns = array('*'); + $this->_using_default_result_columns = true; + } + + /** + * Return the raw data wrapped by this ORM + * instance as an associative array. Column + * names may optionally be supplied as arguments, + * if so, only those keys will be returned. + */ + public function as_array() { + if (func_num_args() === 0) { + return $this->_data; + } + $args = func_get_args(); + return array_intersect_key($this->_data, array_flip($args)); + } + + /** + * Return the value of a property of this object (database row) + * or null if not present. + * + * If a column-names array is passed, it will return a associative array + * with the value of each column or null if it is not present. + */ + public function get($key) { + if (is_array($key)) { + $result = array(); + foreach($key as $column) { + $result[$column] = isset($this->_data[$column]) ? $this->_data[$column] : null; + } + return $result; } else { - $return_value = (float) $result->$alias; + return isset($this->_data[$key]) ? $this->_data[$key] : null; } } - return $return_value; - } - /** - * This method can be called to hydrate (populate) this - * instance of the class from an associative array of data. - * This will usually be called only from inside the class, - * but it's public in case you need to call it directly. - */ - public function hydrate($data=array()) { - $this->_data = $data; - return $this; - } - - /** - * Force the ORM to flag all the fields in the $data array - * as "dirty" and therefore update them when save() is called. - */ - public function force_all_dirty() { - $this->_dirty_fields = $this->_data; - return $this; - } - - /** - * Perform a raw query. The query can contain placeholders in - * either named or question mark style. If placeholders are - * used, the parameters should be an array of values which will - * be bound to the placeholders in the query. If this method - * is called, all other query building methods will be ignored. - */ - public function raw_query($query, $parameters = array()) { - $this->_is_raw_query = true; - $this->_raw_query = $query; - $this->_raw_parameters = $parameters; - return $this; - } - - /** - * Add an alias for the main table to be used in SELECT queries - */ - public function table_alias($alias) { - $this->_table_alias = $alias; - return $this; - } - - /** - * Internal method to add an unquoted expression to the set - * of columns returned by the SELECT query. The second optional - * argument is the alias to return the expression as. - */ - protected function _add_result_column($expr, $alias=null) { - if (!is_null($alias)) { - $expr .= " AS " . $this->_quote_identifier($alias); + /** + * Return the name of the column in the database table which contains + * the primary key ID of the row. + */ + protected function _get_id_column_name() { + if (!is_null($this->_instance_id_column)) { + return $this->_instance_id_column; + } + if (isset(self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name])) { + return self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name]; + } + return self::$_config[$this->_connection_name]['id_column']; } - if ($this->_using_default_result_columns) { - $this->_result_columns = array($expr); - $this->_using_default_result_columns = false; - } else { - $this->_result_columns[] = $expr; - } - return $this; - } + /** + * Get the primary key ID of this object. + */ + public function id($disallow_null = false) { + $id = $this->get($this->_get_id_column_name()); - /** - * Counts the number of columns that belong to the primary - * key and their value is null. - */ - public function count_null_id_columns() { - if (is_array($this->_get_id_column_name())) { - return count(array_filter($this->id(), 'is_null')); - } else { - return is_null($this->id()) ? 1 : 0; - } - } - - /** - * Add a column to the list of columns returned by the SELECT - * query. This defaults to '*'. The second optional argument is - * the alias to return the column as. - */ - public function select($column, $alias=null) { - $column = $this->_quote_identifier($column); - return $this->_add_result_column($column, $alias); - } - - /** - * Add an unquoted expression to the list of columns returned - * by the SELECT query. The second optional argument is - * the alias to return the column as. - */ - public function select_expr($expr, $alias=null) { - return $this->_add_result_column($expr, $alias); - } - - /** - * Add columns to the list of columns returned by the SELECT - * query. This defaults to '*'. Many columns can be supplied - * as either an array or as a list of parameters to the method. - * - * Note that the alias must not be numeric - if you want a - * numeric alias then prepend it with some alpha chars. eg. a1 - * - * @example select_many(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5'); - * @example select_many('column', 'column2', 'column3'); - * @example select_many(array('column', 'column2', 'column3'), 'column4', 'column5'); - * - * @return \ORM - */ - public function select_many() { - $columns = func_get_args(); - if(!empty($columns)) { - $columns = $this->_normalise_select_many_columns($columns); - foreach($columns as $alias => $column) { - if(is_numeric($alias)) { - $alias = null; + if ($disallow_null) { + if (is_array($id)) { + foreach ($id as $id_part) { + if ($id_part === null) { + throw new Exception('Primary key ID contains null value(s)'); + } + } + } else if ($id === null) { + throw new Exception('Primary key ID missing from row or is null'); } - $this->select($column, $alias); } - } - return $this; - } - /** - * Add an unquoted expression to the list of columns returned - * by the SELECT query. Many columns can be supplied as either - * an array or as a list of parameters to the method. - * - * Note that the alias must not be numeric - if you want a - * numeric alias then prepend it with some alpha chars. eg. a1 - * - * @example select_many_expr(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5') - * @example select_many_expr('column', 'column2', 'column3') - * @example select_many_expr(array('column', 'column2', 'column3'), 'column4', 'column5') - * - * @return \ORM - */ - public function select_many_expr() { - $columns = func_get_args(); - if(!empty($columns)) { - $columns = $this->_normalise_select_many_columns($columns); - foreach($columns as $alias => $column) { - if(is_numeric($alias)) { - $alias = null; + return $id; + } + + /** + * Set a property to a particular value on this object. + * To set multiple properties at once, pass an associative array + * as the first parameter and leave out the second parameter. + * Flags the properties as 'dirty' so they will be saved to the + * database when save() is called. + */ + public function set($key, $value = null) { + return $this->_set_orm_property($key, $value); + } + + /** + * Set a property to a particular value on this object. + * To set multiple properties at once, pass an associative array + * as the first parameter and leave out the second parameter. + * Flags the properties as 'dirty' so they will be saved to the + * database when save() is called. + * @param string|array $key + * @param string|null $value + */ + public function set_expr($key, $value = null) { + return $this->_set_orm_property($key, $value, true); + } + + /** + * Set a property on the ORM object. + * @param string|array $key + * @param string|null $value + * @param bool $raw Whether this value should be treated as raw or not + */ + protected function _set_orm_property($key, $value = null, $expr = false) { + if (!is_array($key)) { + $key = array($key => $value); + } + foreach ($key as $field => $value) { + $this->_data[$field] = $value; + $this->_dirty_fields[$field] = $value; + if (false === $expr and isset($this->_expr_fields[$field])) { + unset($this->_expr_fields[$field]); + } else if (true === $expr) { + $this->_expr_fields[$field] = true; } - $this->select_expr($column, $alias); } + return $this; } - return $this; - } - /** - * Take a column specification for the select many methods and convert it - * into a normalised array of columns and aliases. - * - * It is designed to turn the following styles into a normalised array: - * - * array(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5')) - * - * @param array $columns - * @return array - */ - protected function _normalise_select_many_columns($columns) { - $return = array(); - foreach($columns as $column) { - if(is_array($column)) { - foreach($column as $key => $value) { - if(!is_numeric($key)) { - $return[$key] = $value; + /** + * Check whether the given field has been changed since this + * object was saved. + */ + public function is_dirty($key) { + return array_key_exists($key, $this->_dirty_fields); + } + + /** + * Check whether the model was the result of a call to create() or not + * @return bool + */ + public function is_new() { + return $this->_is_new; + } + + /** + * Save any fields which have been modified on this object + * to the database. + */ + public function save() { + $query = array(); + + // remove any expression fields as they are already baked into the query + $values = array_values(array_diff_key($this->_dirty_fields, $this->_expr_fields)); + + if (!$this->_is_new) { // UPDATE + // If there are no dirty values, do nothing + if (empty($values) && empty($this->_expr_fields)) { + return true; + } + $query = $this->_build_update(); + $id = $this->id(true); + if (is_array($id)) { + $values = array_merge($values, array_values($id)); + } else { + $values[] = $id; + } + } else { // INSERT + $query = $this->_build_insert(); + } + + $success = self::_execute($query, $values, $this->_connection_name); + $caching_auto_clear_enabled = self::$_config[$this->_connection_name]['caching_auto_clear']; + if($caching_auto_clear_enabled){ + self::clear_cache($this->_table_name, $this->_connection_name); + } + // If we've just inserted a new record, set the ID of this object + if ($this->_is_new) { + $this->_is_new = false; + if ($this->count_null_id_columns() != 0) { + $db = self::get_db($this->_connection_name); + if($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { + // it may return several columns if a compound primary + // key is used + $row = self::get_last_statement()->fetch(PDO::FETCH_ASSOC); + foreach($row as $key => $value) { + $this->_data[$key] = $value; + } } else { - $return[] = $value; + $column = $this->_get_id_column_name(); + // if the primary key is compound, assign the last inserted id + // to the first column + if (is_array($column)) { + $column = reset($column); + } + $this->_data[$column] = $db->lastInsertId(); } } - } else { - $return[] = $column; } - } - return $return; - } - /** - * Add a DISTINCT keyword before the list of columns in the SELECT query - */ - public function distinct() { - $this->_distinct = true; - return $this; - } - - /** - * Internal method to add a JOIN source to the query. - * - * The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this - * will be prepended to JOIN. - * - * The table should be the name of the table to join to. - * - * The constraint may be either a string or an array with three elements. If it - * is a string, it will be compiled into the query as-is, with no escaping. The - * recommended way to supply the constraint is as an array with three elements: - * - * first_column, operator, second_column - * - * Example: array('user.id', '=', 'profile.user_id') - * - * will compile to - * - * ON `user`.`id` = `profile`.`user_id` - * - * The final (optional) argument specifies an alias for the joined table. - */ - protected function _add_join_source($join_operator, $table, $constraint, $table_alias=null) { - - $join_operator = trim("{$join_operator} JOIN"); - - $table = $this->_quote_identifier($table); - - // Add table alias if present - if (!is_null($table_alias)) { - $table_alias = $this->_quote_identifier($table_alias); - $table .= " {$table_alias}"; + $this->_dirty_fields = $this->_expr_fields = array(); + return $success; } - // Build the constraint - if (is_array($constraint)) { - list($first_column, $operator, $second_column) = $constraint; - $first_column = $this->_quote_identifier($first_column); - $second_column = $this->_quote_identifier($second_column); - $constraint = "{$first_column} {$operator} {$second_column}"; - } - - $this->_join_sources[] = "{$join_operator} {$table} ON {$constraint}"; - return $this; - } - - /** - * Add a RAW JOIN source to the query - */ - public function raw_join($table, $constraint, $table_alias, $parameters = array()) { - // Add table alias if present - if (!is_null($table_alias)) { - $table_alias = $this->_quote_identifier($table_alias); - $table .= " {$table_alias}"; - } - - $this->_values = array_merge($this->_values, $parameters); - - // Build the constraint - if (is_array($constraint)) { - list($first_column, $operator, $second_column) = $constraint; - $first_column = $this->_quote_identifier($first_column); - $second_column = $this->_quote_identifier($second_column); - $constraint = "{$first_column} {$operator} {$second_column}"; - } - - $this->_join_sources[] = "{$table} ON {$constraint}"; - return $this; - } - - /** - * Add a simple JOIN source to the query - */ - public function join($table, $constraint, $table_alias=null) { - return $this->_add_join_source("", $table, $constraint, $table_alias); - } - - /** - * Add an INNER JOIN souce to the query - */ - public function inner_join($table, $constraint, $table_alias=null) { - return $this->_add_join_source("INNER", $table, $constraint, $table_alias); - } - - /** - * Add a LEFT OUTER JOIN souce to the query - */ - public function left_outer_join($table, $constraint, $table_alias=null) { - return $this->_add_join_source("LEFT OUTER", $table, $constraint, $table_alias); - } - - /** - * Add an RIGHT OUTER JOIN souce to the query - */ - public function right_outer_join($table, $constraint, $table_alias=null) { - return $this->_add_join_source("RIGHT OUTER", $table, $constraint, $table_alias); - } - - /** - * Add an FULL OUTER JOIN souce to the query - */ - public function full_outer_join($table, $constraint, $table_alias=null) { - return $this->_add_join_source("FULL OUTER", $table, $constraint, $table_alias); - } - - /** - * Internal method to add a HAVING condition to the query - */ - protected function _add_having($fragment, $values=array()) { - return $this->_add_condition('having', $fragment, $values); - } - - /** - * Internal method to add a HAVING condition to the query - */ - protected function _add_simple_having($column_name, $separator, $value) { - return $this->_add_simple_condition('having', $column_name, $separator, $value); - } - - /** - * Internal method to add a HAVING clause with multiple values (like IN and NOT IN) - */ - public function _add_having_placeholder($column_name, $separator, $values) { - if (!is_array($column_name)) { - $data = array($column_name => $values); - } else { - $data = $column_name; - } - $result = $this; - foreach ($data as $key => $val) { - $column = $result->_quote_identifier($key); - $placeholders = $result->_create_placeholders($val); - $result = $result->_add_having("{$column} {$separator} ({$placeholders})", $val); - } - return $result; - } - - /** - * Internal method to add a HAVING clause with no parameters(like IS NULL and IS NOT NULL) - */ - public function _add_having_no_value($column_name, $operator) { - $conditions = (is_array($column_name)) ? $column_name : array($column_name); - $result = $this; - foreach($conditions as $column) { - $column = $this->_quote_identifier($column); - $result = $result->_add_having("{$column} {$operator}"); - } - return $result; - } - - /** - * Internal method to add a WHERE condition to the query - */ - protected function _add_where($fragment, $values=array()) { - return $this->_add_condition('where', $fragment, $values); - } - - /** - * Internal method to add a WHERE condition to the query - */ - protected function _add_simple_where($column_name, $separator, $value) { - return $this->_add_simple_condition('where', $column_name, $separator, $value); - } - - /** - * Add a WHERE clause with multiple values (like IN and NOT IN) - */ - public function _add_where_placeholder($column_name, $separator, $values) { - if (!is_array($column_name)) { - $data = array($column_name => $values); - } else { - $data = $column_name; - } - $result = $this; - foreach ($data as $key => $val) { - $column = $result->_quote_identifier($key); - $placeholders = $result->_create_placeholders($val); - $result = $result->_add_where("{$column} {$separator} ({$placeholders})", $val); - } - return $result; - } - - /** - * Add a WHERE clause with no parameters(like IS NULL and IS NOT NULL) - */ - public function _add_where_no_value($column_name, $operator) { - $conditions = (is_array($column_name)) ? $column_name : array($column_name); - $result = $this; - foreach($conditions as $column) { - $column = $this->_quote_identifier($column); - $result = $result->_add_where("{$column} {$operator}"); - } - return $result; - } - - /** - * Internal method to add a HAVING or WHERE condition to the query - */ - protected function _add_condition($type, $fragment, $values=array()) { - $conditions_class_property_name = "_{$type}_conditions"; - if (!is_array($values)) { - $values = array($values); - } - array_push($this->$conditions_class_property_name, array( - self::CONDITION_FRAGMENT => $fragment, - self::CONDITION_VALUES => $values, - )); - return $this; - } - - /** - * Helper method to compile a simple COLUMN SEPARATOR VALUE - * style HAVING or WHERE condition into a string and value ready to - * be passed to the _add_condition method. Avoids duplication - * of the call to _quote_identifier - * - * If column_name is an associative array, it will add a condition for each column - */ - protected function _add_simple_condition($type, $column_name, $separator, $value) { - $multiple = is_array($column_name) ? $column_name : array($column_name => $value); - $result = $this; - - foreach($multiple as $key => $val) { - // Add the table name in case of ambiguous columns - if (count($result->_join_sources) > 0 && strpos($key, '.') === false) { - $table = $result->_table_name; - if (!is_null($result->_table_alias)) { - $table = $result->_table_alias; + /** + * Add a WHERE clause for every column that belongs to the primary key + */ + public function _add_id_column_conditions(&$query) { + $query[] = "WHERE"; + $keys = is_array($this->_get_id_column_name()) ? $this->_get_id_column_name() : array( $this->_get_id_column_name() ); + $first = true; + foreach($keys as $key) { + if ($first) { + $first = false; } - - $key = "{$table}.{$key}"; - } - $key = $result->_quote_identifier($key); - $result = $result->_add_condition($type, "{$key} {$separator} ?", $val); - } - return $result; - } - - /** - * Return a string containing the given number of question marks, - * separated by commas. Eg "?, ?, ?" - */ - protected function _create_placeholders($fields) { - if(!empty($fields)) { - $db_fields = array(); - foreach($fields as $key => $value) { - // Process expression fields directly into the query - if(array_key_exists($key, $this->_expr_fields)) { - $db_fields[] = $value; - } else { - $db_fields[] = '?'; - } - } - return implode(', ', $db_fields); - } - } - - /** - * Helper method that filters a column/value array returning only those - * columns that belong to a compound primary key. - * - * If the key contains a column that does not exist in the given array, - * a null value will be returned for it. - */ - protected function _get_compound_id_column_values($value) { - $filtered = array(); - foreach($this->_get_id_column_name() as $key) { - $filtered[$key] = isset($value[$key]) ? $value[$key] : null; - } - return $filtered; - } - - /** - * Helper method that filters an array containing compound column/value - * arrays. - */ - protected function _get_compound_id_column_values_array($values) { - $filtered = array(); - foreach($values as $value) { - $filtered[] = $this->_get_compound_id_column_values($value); - } - return $filtered; - } - - /** - * Add a WHERE column = value clause to your query. Each time - * this is called in the chain, an additional WHERE will be - * added, and these will be ANDed together when the final query - * is built. - * - * If you use an array in $column_name, a new clause will be - * added for each element. In this case, $value is ignored. - */ - public function where($column_name, $value=null) { - return $this->where_equal($column_name, $value); - } - - /** - * More explicitly named version of for the where() method. - * Can be used if preferred. - */ - public function where_equal($column_name, $value=null) { - return $this->_add_simple_where($column_name, '=', $value); - } - - /** - * Add a WHERE column != value clause to your query. - */ - public function where_not_equal($column_name, $value=null) { - return $this->_add_simple_where($column_name, '!=', $value); - } - - /** - * Special method to query the table by its primary key - * - * If primary key is compound, only the columns that - * belong to they key will be used for the query - */ - public function where_id_is($id) { - return (is_array($this->_get_id_column_name())) ? - $this->where($this->_get_compound_id_column_values($id), null) : - $this->where($this->_get_id_column_name(), $id); - } - - /** - * Allows adding a WHERE clause that matches any of the conditions - * specified in the array. Each element in the associative array will - * be a different condition, where the key will be the column name. - * - * By default, an equal operator will be used against all columns, but - * it can be overriden for any or every column using the second parameter. - * - * Each condition will be ORed together when added to the final query. - */ - public function where_any_is($values, $operator='=') { - $data = array(); - $query = array("(("); - $first = true; - foreach ($values as $item) { - if ($first) { - $first = false; - } else { - $query[] = ") OR ("; - } - $firstsub = true; - foreach($item as $key => $item) { - $op = is_string($operator) ? $operator : (isset($operator[$key]) ? $operator[$key] : '='); - if ($firstsub) { - $firstsub = false; - } else { + else { $query[] = "AND"; } $query[] = $this->_quote_identifier($key); - $data[] = $item; - $query[] = $op . " ?"; - } - } - $query[] = "))"; - return $this->where_raw(join($query, ' '), $data); - } - - /** - * Similar to where_id_is() but allowing multiple primary keys. - * - * If primary key is compound, only the columns that - * belong to they key will be used for the query - */ - public function where_id_in($ids) { - return (is_array($this->_get_id_column_name())) ? - $this->where_any_is($this->_get_compound_id_column_values_array($ids)) : - $this->where_in($this->_get_id_column_name(), $ids); - } - - /** - * Add a WHERE ... LIKE clause to your query. - */ - public function where_like($column_name, $value=null) { - return $this->_add_simple_where($column_name, 'LIKE', $value); - } - - /** - * Add where WHERE ... NOT LIKE clause to your query. - */ - public function where_not_like($column_name, $value=null) { - return $this->_add_simple_where($column_name, 'NOT LIKE', $value); - } - - /** - * Add a WHERE ... > clause to your query - */ - public function where_gt($column_name, $value=null) { - return $this->_add_simple_where($column_name, '>', $value); - } - - /** - * Add a WHERE ... < clause to your query - */ - public function where_lt($column_name, $value=null) { - return $this->_add_simple_where($column_name, '<', $value); - } - - /** - * Add a WHERE ... >= clause to your query - */ - public function where_gte($column_name, $value=null) { - return $this->_add_simple_where($column_name, '>=', $value); - } - - /** - * Add a WHERE ... <= clause to your query - */ - public function where_lte($column_name, $value=null) { - return $this->_add_simple_where($column_name, '<=', $value); - } - - /** - * Add a WHERE ... IN clause to your query - */ - public function where_in($column_name, $values) { - return $this->_add_where_placeholder($column_name, 'IN', $values); - } - - /** - * Add a WHERE ... NOT IN clause to your query - */ - public function where_not_in($column_name, $values) { - return $this->_add_where_placeholder($column_name, 'NOT IN', $values); - } - - /** - * Add a WHERE column IS NULL clause to your query - */ - public function where_null($column_name) { - return $this->_add_where_no_value($column_name, "IS NULL"); - } - - /** - * Add a WHERE column IS NOT NULL clause to your query - */ - public function where_not_null($column_name) { - return $this->_add_where_no_value($column_name, "IS NOT NULL"); - } - - /** - * Add a raw WHERE clause to the query. The clause should - * contain question mark placeholders, which will be bound - * to the parameters supplied in the second argument. - */ - public function where_raw($clause, $parameters=array()) { - return $this->_add_where($clause, $parameters); - } - - /** - * Add a LIMIT to the query - */ - public function limit($limit) { - $this->_limit = $limit; - return $this; - } - - /** - * Add an OFFSET to the query - */ - public function offset($offset) { - $this->_offset = $offset; - return $this; - } - - /** - * Add an ORDER BY clause to the query - */ - protected function _add_order_by($column_name, $ordering) { - $column_name = $this->_quote_identifier($column_name); - $this->_order_by[] = "{$column_name} {$ordering}"; - return $this; - } - - /** - * Add an ORDER BY column DESC clause - */ - public function order_by_desc($column_name) { - return $this->_add_order_by($column_name, 'DESC'); - } - - /** - * Add an ORDER BY column ASC clause - */ - public function order_by_asc($column_name) { - return $this->_add_order_by($column_name, 'ASC'); - } - - /** - * Add an unquoted expression as an ORDER BY clause - */ - public function order_by_expr($clause) { - $this->_order_by[] = $clause; - return $this; - } - - /** - * Add a column to the list of columns to GROUP BY - */ - public function group_by($column_name) { - $column_name = $this->_quote_identifier($column_name); - $this->_group_by[] = $column_name; - return $this; - } - - /** - * Add an unquoted expression to the list of columns to GROUP BY - */ - public function group_by_expr($expr) { - $this->_group_by[] = $expr; - return $this; - } - - /** - * Add a HAVING column = value clause to your query. Each time - * this is called in the chain, an additional HAVING will be - * added, and these will be ANDed together when the final query - * is built. - * - * If you use an array in $column_name, a new clause will be - * added for each element. In this case, $value is ignored. - */ - public function having($column_name, $value=null) { - return $this->having_equal($column_name, $value); - } - - /** - * More explicitly named version of for the having() method. - * Can be used if preferred. - */ - public function having_equal($column_name, $value=null) { - return $this->_add_simple_having($column_name, '=', $value); - } - - /** - * Add a HAVING column != value clause to your query. - */ - public function having_not_equal($column_name, $value=null) { - return $this->_add_simple_having($column_name, '!=', $value); - } - - /** - * Special method to query the table by its primary key. - * - * If primary key is compound, only the columns that - * belong to they key will be used for the query - */ - public function having_id_is($id) { - return (is_array($this->_get_id_column_name())) ? - $this->having($this->_get_compound_id_column_values($value)) : - $this->having($this->_get_id_column_name(), $id); - } - - /** - * Add a HAVING ... LIKE clause to your query. - */ - public function having_like($column_name, $value=null) { - return $this->_add_simple_having($column_name, 'LIKE', $value); - } - - /** - * Add where HAVING ... NOT LIKE clause to your query. - */ - public function having_not_like($column_name, $value=null) { - return $this->_add_simple_having($column_name, 'NOT LIKE', $value); - } - - /** - * Add a HAVING ... > clause to your query - */ - public function having_gt($column_name, $value=null) { - return $this->_add_simple_having($column_name, '>', $value); - } - - /** - * Add a HAVING ... < clause to your query - */ - public function having_lt($column_name, $value=null) { - return $this->_add_simple_having($column_name, '<', $value); - } - - /** - * Add a HAVING ... >= clause to your query - */ - public function having_gte($column_name, $value=null) { - return $this->_add_simple_having($column_name, '>=', $value); - } - - /** - * Add a HAVING ... <= clause to your query - */ - public function having_lte($column_name, $value=null) { - return $this->_add_simple_having($column_name, '<=', $value); - } - - /** - * Add a HAVING ... IN clause to your query - */ - public function having_in($column_name, $values=null) { - return $this->_add_having_placeholder($column_name, 'IN', $values); - } - - /** - * Add a HAVING ... NOT IN clause to your query - */ - public function having_not_in($column_name, $values=null) { - return $this->_add_having_placeholder($column_name, 'NOT IN', $values); - } - - /** - * Add a HAVING column IS NULL clause to your query - */ - public function having_null($column_name) { - return $this->_add_having_no_value($column_name, 'IS NULL'); - } - - /** - * Add a HAVING column IS NOT NULL clause to your query - */ - public function having_not_null($column_name) { - return $this->_add_having_no_value($column_name, 'IS NOT NULL'); - } - - /** - * Add a raw HAVING clause to the query. The clause should - * contain question mark placeholders, which will be bound - * to the parameters supplied in the second argument. - */ - public function having_raw($clause, $parameters=array()) { - return $this->_add_having($clause, $parameters); - } - - /** - * Build a SELECT statement based on the clauses that have - * been passed to this instance by chaining method calls. - */ - protected function _build_select() { - // If the query is raw, just set the $this->_values to be - // the raw query parameters and return the raw query - if ($this->_is_raw_query) { - $this->_values = $this->_raw_parameters; - return $this->_raw_query; - } - - // Build and return the full SELECT statement by concatenating - // the results of calling each separate builder method. - return $this->_join_if_not_empty(" ", array( - $this->_build_select_start(), - $this->_build_join(), - $this->_build_where(), - $this->_build_group_by(), - $this->_build_having(), - $this->_build_order_by(), - $this->_build_limit(), - $this->_build_offset(), - )); - } - - /** - * Build the start of the SELECT statement - */ - protected function _build_select_start() { - $fragment = 'SELECT '; - $result_columns = join(', ', $this->_result_columns); - - if (!is_null($this->_limit) && - self::$_config[$this->_connection_name]['limit_clause_style'] === ORM::LIMIT_STYLE_TOP_N) { - $fragment .= "TOP {$this->_limit} "; - } - - if ($this->_distinct) { - $result_columns = 'DISTINCT ' . $result_columns; - } - - $fragment .= "{$result_columns} FROM " . $this->_quote_identifier($this->_table_name); - - if (!is_null($this->_table_alias)) { - $fragment .= " " . $this->_quote_identifier($this->_table_alias); - } - return $fragment; - } - - /** - * Build the JOIN sources - */ - protected function _build_join() { - if (count($this->_join_sources) === 0) { - return ''; - } - - return join(" ", $this->_join_sources); - } - - /** - * Build the WHERE clause(s) - */ - protected function _build_where() { - return $this->_build_conditions('where'); - } - - /** - * Build the HAVING clause(s) - */ - protected function _build_having() { - return $this->_build_conditions('having'); - } - - /** - * Build GROUP BY - */ - protected function _build_group_by() { - if (count($this->_group_by) === 0) { - return ''; - } - return "GROUP BY " . join(", ", $this->_group_by); - } - - /** - * Build a WHERE or HAVING clause - * @param string $type - * @return string - */ - protected function _build_conditions($type) { - $conditions_class_property_name = "_{$type}_conditions"; - // If there are no clauses, return empty string - if (count($this->$conditions_class_property_name) === 0) { - return ''; - } - - $conditions = array(); - foreach ($this->$conditions_class_property_name as $condition) { - $conditions[] = $condition[self::CONDITION_FRAGMENT]; - $this->_values = array_merge($this->_values, $condition[self::CONDITION_VALUES]); - } - - return strtoupper($type) . " " . join(" AND ", $conditions); - } - - /** - * Build ORDER BY - */ - protected function _build_order_by() { - if (count($this->_order_by) === 0) { - return ''; - } - return "ORDER BY " . join(", ", $this->_order_by); - } - - /** - * Build LIMIT - */ - protected function _build_limit() { - $fragment = ''; - if (!is_null($this->_limit) && - self::$_config[$this->_connection_name]['limit_clause_style'] == ORM::LIMIT_STYLE_LIMIT) { - if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'firebird') { - $fragment = 'ROWS'; - } else { - $fragment = 'LIMIT'; - } - $fragment .= " {$this->_limit}"; - } - return $fragment; - } - - /** - * Build OFFSET - */ - protected function _build_offset() { - if (!is_null($this->_offset)) { - $clause = 'OFFSET'; - if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'firebird') { - $clause = 'TO'; - } - return "$clause " . $this->_offset; - } - return ''; - } - - /** - * Wrapper around PHP's join function which - * only adds the pieces if they are not empty. - */ - protected function _join_if_not_empty($glue, $pieces) { - $filtered_pieces = array(); - foreach ($pieces as $piece) { - if (is_string($piece)) { - $piece = trim($piece); - } - if (!empty($piece)) { - $filtered_pieces[] = $piece; - } - } - return join($glue, $filtered_pieces); - } - - /** - * Quote a string that is used as an identifier - * (table names, column names etc). This method can - * also deal with dot-separated identifiers eg table.column - */ - protected function _quote_one_identifier($identifier) { - $parts = explode('.', $identifier); - $parts = array_map(array($this, '_quote_identifier_part'), $parts); - return join('.', $parts); - } - - /** - * Quote a string that is used as an identifier - * (table names, column names etc) or an array containing - * multiple identifiers. This method can also deal with - * dot-separated identifiers eg table.column - */ - protected function _quote_identifier($identifier) { - if (is_array($identifier)) { - $result = array_map(array($this, '_quote_one_identifier'), $identifier); - return join(', ', $result); - } else { - return $this->_quote_one_identifier($identifier); - } - } - - /** - * This method performs the actual quoting of a single - * part of an identifier, using the identifier quote - * character specified in the config (or autodetected). - */ - protected function _quote_identifier_part($part) { - if ($part === '*') { - return $part; - } - - $quote_character = self::$_config[$this->_connection_name]['identifier_quote_character']; - // double up any identifier quotes to escape them - return $quote_character . - str_replace($quote_character, - $quote_character . $quote_character, - $part - ) . $quote_character; - } - - /** - * Create a cache key for the given query and parameters. - */ - protected static function _create_cache_key($query, $parameters, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) { - if(isset(self::$_config[$connection_name]['create_cache_key']) and is_callable(self::$_config[$connection_name]['create_cache_key'])){ - return call_user_func_array(self::$_config[$connection_name]['create_cache_key'], array($query, $parameters, $table_name, $connection_name)); - } - $parameter_string = join(',', $parameters); - $key = $query . ':' . $parameter_string; - return sha1($key); - } - - /** - * Check the query cache for the given cache key. If a value - * is cached for the key, return the value. Otherwise, return false. - */ - protected static function _check_query_cache($cache_key, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) { - if(isset(self::$_config[$connection_name]['check_query_cache']) and is_callable(self::$_config[$connection_name]['check_query_cache'])){ - return call_user_func_array(self::$_config[$connection_name]['check_query_cache'], array($cache_key, $table_name, $connection_name)); - } elseif (isset(self::$_query_cache[$connection_name][$cache_key])) { - return self::$_query_cache[$connection_name][$cache_key]; - } - return false; - } - - /** - * Clear the query cache - */ - public static function clear_cache($table_name = null, $connection_name = self::DEFAULT_CONNECTION) { - self::$_query_cache = array(); - if(isset(self::$_config[$connection_name]['clear_cache']) and is_callable(self::$_config[$connection_name]['clear_cache'])){ - return call_user_func_array(self::$_config[$connection_name]['clear_cache'], array($table_name, $connection_name)); - } - } - - /** - * Add the given value to the query cache. - */ - protected static function _cache_query_result($cache_key, $value, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) { - if(isset(self::$_config[$connection_name]['cache_query_result']) and is_callable(self::$_config[$connection_name]['cache_query_result'])){ - return call_user_func_array(self::$_config[$connection_name]['cache_query_result'], array($cache_key, $value, $table_name, $connection_name)); - } elseif (!isset(self::$_query_cache[$connection_name])) { - self::$_query_cache[$connection_name] = array(); - } - self::$_query_cache[$connection_name][$cache_key] = $value; - } - - /** - * Execute the SELECT query that has been built up by chaining methods - * on this class. Return an array of rows as associative arrays. - */ - protected function _run() { - $query = $this->_build_select(); - $caching_enabled = self::$_config[$this->_connection_name]['caching']; - - if ($caching_enabled) { - $cache_key = self::_create_cache_key($query, $this->_values, $this->_table_name, $this->_connection_name); - $cached_result = self::_check_query_cache($cache_key, $this->_table_name, $this->_connection_name); - - if ($cached_result !== false) { - return $cached_result; + $query[] = "= ?"; } } - self::_execute($query, $this->_values, $this->_connection_name); - $statement = self::get_last_statement(); + /** + * Build an UPDATE query + */ + protected function _build_update() { + $query = array(); + $query[] = "UPDATE {$this->_quote_identifier($this->_table_name)} SET"; - $rows = array(); - while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { - $rows[] = $row; - } - - if ($caching_enabled) { - self::_cache_query_result($cache_key, $rows, $this->_table_name, $this->_connection_name); - } - - // reset Idiorm after executing the query - $this->_values = array(); - $this->_result_columns = array('*'); - $this->_using_default_result_columns = true; - - return $rows; - } - - /** - * Return the raw data wrapped by this ORM - * instance as an associative array. Column - * names may optionally be supplied as arguments, - * if so, only those keys will be returned. - */ - public function as_array() { - if (func_num_args() === 0) { - return $this->_data; - } - $args = func_get_args(); - return array_intersect_key($this->_data, array_flip($args)); - } - - /** - * Return the value of a property of this object (database row) - * or null if not present. - * - * If a column-names array is passed, it will return a associative array - * with the value of each column or null if it is not present. - */ - public function get($key) { - if (is_array($key)) { - $result = array(); - foreach($key as $column) { - $result[$column] = isset($this->_data[$column]) ? $this->_data[$column] : null; - } - return $result; - } else { - return isset($this->_data[$key]) ? $this->_data[$key] : null; - } - } - - /** - * Return the name of the column in the database table which contains - * the primary key ID of the row. - */ - protected function _get_id_column_name() { - if (!is_null($this->_instance_id_column)) { - return $this->_instance_id_column; - } - if (isset(self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name])) { - return self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name]; - } - return self::$_config[$this->_connection_name]['id_column']; - } - - /** - * Get the primary key ID of this object. - */ - public function id($disallow_null = false) { - $id = $this->get($this->_get_id_column_name()); - - if ($disallow_null) { - if (is_array($id)) { - foreach ($id as $id_part) { - if ($id_part === null) { - throw new Exception('Primary key ID contains null value(s)'); - } + $field_list = array(); + foreach ($this->_dirty_fields as $key => $value) { + if(!array_key_exists($key, $this->_expr_fields)) { + $value = '?'; } - } else if ($id === null) { - throw new Exception('Primary key ID missing from row or is null'); + $field_list[] = "{$this->_quote_identifier($key)} = $value"; } + $query[] = join(", ", $field_list); + $this->_add_id_column_conditions($query); + return join(" ", $query); } - return $id; - } + /** + * Build an INSERT query + */ + protected function _build_insert() { + $query[] = "INSERT INTO"; + $query[] = $this->_quote_identifier($this->_table_name); + $field_list = array_map(array($this, '_quote_identifier'), array_keys($this->_dirty_fields)); + $query[] = "(" . join(", ", $field_list) . ")"; + $query[] = "VALUES"; - /** - * Set a property to a particular value on this object. - * To set multiple properties at once, pass an associative array - * as the first parameter and leave out the second parameter. - * Flags the properties as 'dirty' so they will be saved to the - * database when save() is called. - */ - public function set($key, $value = null) { - return $this->_set_orm_property($key, $value); - } + $placeholders = $this->_create_placeholders($this->_dirty_fields); + $query[] = "({$placeholders})"; - /** - * Set a property to a particular value on this object. - * To set multiple properties at once, pass an associative array - * as the first parameter and leave out the second parameter. - * Flags the properties as 'dirty' so they will be saved to the - * database when save() is called. - * @param string|array $key - * @param string|null $value - */ - public function set_expr($key, $value = null) { - return $this->_set_orm_property($key, $value, true); - } - - /** - * Set a property on the ORM object. - * @param string|array $key - * @param string|null $value - * @param bool $raw Whether this value should be treated as raw or not - */ - protected function _set_orm_property($key, $value = null, $expr = false) { - if (!is_array($key)) { - $key = array($key => $value); - } - foreach ($key as $field => $value) { - $this->_data[$field] = $value; - $this->_dirty_fields[$field] = $value; - if (false === $expr and isset($this->_expr_fields[$field])) { - unset($this->_expr_fields[$field]); - } else if (true === $expr) { - $this->_expr_fields[$field] = true; + if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { + $query[] = 'RETURNING ' . $this->_quote_identifier($this->_get_id_column_name()); } + + return join(" ", $query); } - return $this; - } - /** - * Check whether the given field has been changed since this - * object was saved. - */ - public function is_dirty($key) { - return isset($this->_dirty_fields[$key]); - } + /** + * Delete this record from the database + */ + public function delete() { + $query = array( + "DELETE FROM", + $this->_quote_identifier($this->_table_name) + ); + $this->_add_id_column_conditions($query); + return self::_execute(join(" ", $query), is_array($this->id(true)) ? array_values($this->id(true)) : array($this->id(true)), $this->_connection_name); + } - /** - * Check whether the model was the result of a call to create() or not - * @return bool - */ - public function is_new() { - return $this->_is_new; - } + /** + * Delete many records from the database + */ + public function delete_many() { + // Build and return the full DELETE statement by concatenating + // the results of calling each separate builder method. + $query = $this->_join_if_not_empty(" ", array( + "DELETE FROM", + $this->_quote_identifier($this->_table_name), + $this->_build_where(), + )); - /** - * Save any fields which have been modified on this object - * to the database. - */ - public function save() { - $query = array(); + return self::_execute($query, $this->_values, $this->_connection_name); + } - // remove any expression fields as they are already baked into the query - $values = array_values(array_diff_key($this->_dirty_fields, $this->_expr_fields)); + // --------------------- // + // --- ArrayAccess --- // + // --------------------- // - if (!$this->_is_new) { // UPDATE - // If there are no dirty values, do nothing - if (empty($values) && empty($this->_expr_fields)) { - return true; + #[\ReturnTypeWillChange] + public function offsetExists($key) { + return array_key_exists($key, $this->_data); + } + + #[\ReturnTypeWillChange] + public function offsetGet($key) { + return $this->get($key); + } + + #[\ReturnTypeWillChange] + public function offsetSet($key, $value) { + if(is_null($key)) { + throw new InvalidArgumentException('You must specify a key/array index.'); } - $query = $this->_build_update(); - $id = $this->id(true); - if (is_array($id)) { - $values = array_merge($values, array_values($id)); + $this->set($key, $value); + } + + #[\ReturnTypeWillChange] + public function offsetUnset($key) { + unset($this->_data[$key]); + unset($this->_dirty_fields[$key]); + } + + // --------------------- // + // --- MAGIC METHODS --- // + // --------------------- // + public function __get($key) { + return $this->offsetGet($key); + } + + public function __set($key, $value) { + $this->offsetSet($key, $value); + } + + public function __unset($key) { + $this->offsetUnset($key); + } + + + public function __isset($key) { + return $this->offsetExists($key); + } + + /** + * Magic method to capture calls to undefined class methods. + * In this case we are attempting to convert camel case formatted + * methods into underscore formatted methods. + * + * This allows us to call ORM methods using camel case and remain + * backwards compatible. + * + * @param string $name + * @param array $arguments + * @return ORM + */ + public function __call($name, $arguments) + { + $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)); + + if (method_exists($this, $method)) { + return call_user_func_array(array($this, $method), $arguments); } else { - $values[] = $id; - } - } else { // INSERT - $query = $this->_build_insert(); - } - - $success = self::_execute($query, $values, $this->_connection_name); - $caching_auto_clear_enabled = self::$_config[$this->_connection_name]['caching_auto_clear']; - if($caching_auto_clear_enabled){ - self::clear_cache($this->_table_name, $this->_connection_name); - } - // If we've just inserted a new record, set the ID of this object - if ($this->_is_new) { - $this->_is_new = false; - if ($this->count_null_id_columns() != 0) { - $db = self::get_db($this->_connection_name); - if($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { - // it may return several columns if a compound primary - // key is used - $row = self::get_last_statement()->fetch(PDO::FETCH_ASSOC); - foreach($row as $key => $value) { - $this->_data[$key] = $value; - } - } else { - $column = $this->_get_id_column_name(); - // if the primary key is compound, assign the last inserted id - // to the first column - if (is_array($column)) { - $column = array_slice($column, 0, 1); - } - $this->_data[$column] = $db->lastInsertId(); - } + throw new IdiormMethodMissingException("Method $name() does not exist in class " . get_class($this)); } } - $this->_dirty_fields = $this->_expr_fields = array(); - return $success; - } + /** + * Magic method to capture calls to undefined static class methods. + * In this case we are attempting to convert camel case formatted + * methods into underscore formatted methods. + * + * This allows us to call ORM methods using camel case and remain + * backwards compatible. + * + * @param string $name + * @param array $arguments + * @return ORM + */ + public static function __callStatic($name, $arguments) + { + $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)); - /** - * Add a WHERE clause for every column that belongs to the primary key - */ - public function _add_id_column_conditions(&$query) { - $query[] = "WHERE"; - $keys = is_array($this->_get_id_column_name()) ? $this->_get_id_column_name() : array( $this->_get_id_column_name() ); - $first = true; - foreach($keys as $key) { - if ($first) { - $first = false; - } - else { - $query[] = "AND"; - } - $query[] = $this->_quote_identifier($key); - $query[] = "= ?"; + return call_user_func_array(array('ORM', $method), $arguments); } } /** - * Build an UPDATE query - */ - protected function _build_update() { - $query = array(); - $query[] = "UPDATE {$this->_quote_identifier($this->_table_name)} SET"; - - $field_list = array(); - foreach ($this->_dirty_fields as $key => $value) { - if(!array_key_exists($key, $this->_expr_fields)) { - $value = '?'; - } - $field_list[] = "{$this->_quote_identifier($key)} = $value"; - } - $query[] = join(", ", $field_list); - $this->_add_id_column_conditions($query); - return join(" ", $query); - } - - /** - * Build an INSERT query - */ - protected function _build_insert() { - $query[] = "INSERT INTO"; - $query[] = $this->_quote_identifier($this->_table_name); - $field_list = array_map(array($this, '_quote_identifier'), array_keys($this->_dirty_fields)); - $query[] = "(" . join(", ", $field_list) . ")"; - $query[] = "VALUES"; - - $placeholders = $this->_create_placeholders($this->_dirty_fields); - $query[] = "({$placeholders})"; - - if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { - $query[] = 'RETURNING ' . $this->_quote_identifier($this->_get_id_column_name()); - } - - return join(" ", $query); - } - - /** - * Delete this record from the database - */ - public function delete() { - $query = array( - "DELETE FROM", - $this->_quote_identifier($this->_table_name) - ); - $this->_add_id_column_conditions($query); - return self::_execute(join(" ", $query), is_array($this->id(true)) ? array_values($this->id(true)) : array($this->id(true)), $this->_connection_name); - } - - /** - * Delete many records from the database - */ - public function delete_many() { - // Build and return the full DELETE statement by concatenating - // the results of calling each separate builder method. - $query = $this->_join_if_not_empty(" ", array( - "DELETE FROM", - $this->_quote_identifier($this->_table_name), - $this->_build_where(), - )); - - return self::_execute($query, $this->_values, $this->_connection_name); - } - - // --------------------- // - // --- ArrayAccess --- // - // --------------------- // - - public function offsetExists($key) { - return array_key_exists($key, $this->_data); - } - - public function offsetGet($key) { - return $this->get($key); - } - - public function offsetSet($key, $value) { - if(is_null($key)) { - throw new InvalidArgumentException('You must specify a key/array index.'); - } - $this->set($key, $value); - } - - public function offsetUnset($key) { - unset($this->_data[$key]); - unset($this->_dirty_fields[$key]); - } - - // --------------------- // - // --- MAGIC METHODS --- // - // --------------------- // - public function __get($key) { - return $this->offsetGet($key); - } - - public function __set($key, $value) { - $this->offsetSet($key, $value); - } - - public function __unset($key) { - $this->offsetUnset($key); - } - - - public function __isset($key) { - return $this->offsetExists($key); - } - - /** - * Magic method to capture calls to undefined class methods. - * In this case we are attempting to convert camel case formatted - * methods into underscore formatted methods. - * - * This allows us to call ORM methods using camel case and remain - * backwards compatible. - * - * @param string $name - * @param array $arguments - * @return ORM - */ - public function __call($name, $arguments) - { - $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)); - - if (method_exists($this, $method)) { - return call_user_func_array(array($this, $method), $arguments); - } else { - throw new IdiormMethodMissingException("Method $name() does not exist in class " . get_class($this)); - } - } - - /** - * Magic method to capture calls to undefined static class methods. - * In this case we are attempting to convert camel case formatted - * methods into underscore formatted methods. - * - * This allows us to call ORM methods using camel case and remain - * backwards compatible. - * - * @param string $name - * @param array $arguments - * @return ORM - */ - public static function __callStatic($name, $arguments) - { - $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)); - - return call_user_func_array(array('ORM', $method), $arguments); - } -} - -/** - * A class to handle str_replace operations that involve quoted strings - * @example IdiormString::str_replace_outside_quotes('?', '%s', 'columnA = "Hello?" AND columnB = ?'); - * @example IdiormString::value('columnA = "Hello?" AND columnB = ?')->replace_outside_quotes('?', '%s'); - * @author Jeff Roberson - * @author Simon Holywell - * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer - */ -class IdiormString { - protected $subject; - protected $search; - protected $replace; - - /** - * Get an easy to use instance of the class - * @param string $subject - * @return \self - */ - public static function value($subject) { - return new self($subject); - } - - /** - * Shortcut method: Replace all occurrences of the search string with the replacement - * string where they appear outside quotes. - * @param string $search - * @param string $replace - * @param string $subject - * @return string - */ - public static function str_replace_outside_quotes($search, $replace, $subject) { - return self::value($subject)->replace_outside_quotes($search, $replace); - } - - /** - * Set the base string object - * @param string $subject - */ - public function __construct($subject) { - $this->subject = (string) $subject; - } - - /** - * Replace all occurrences of the search string with the replacement - * string where they appear outside quotes - * @param string $search - * @param string $replace - * @return string - */ - public function replace_outside_quotes($search, $replace) { - $this->search = $search; - $this->replace = $replace; - return $this->_str_replace_outside_quotes(); - } - - /** - * Validate an input string and perform a replace on all ocurrences - * of $this->search with $this->replace + * A class to handle str_replace operations that involve quoted strings + * @example IdiormString::str_replace_outside_quotes('?', '%s', 'columnA = "Hello?" AND columnB = ?'); + * @example IdiormString::value('columnA = "Hello?" AND columnB = ?')->replace_outside_quotes('?', '%s'); * @author Jeff Roberson + * @author Simon Holywell * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer - * @return string */ - protected function _str_replace_outside_quotes(){ - $re_valid = '/ + class IdiormString { + protected $subject; + protected $search; + protected $replace; + + /** + * Get an easy to use instance of the class + * @param string $subject + * @return \self + */ + public static function value($subject) { + return new self($subject); + } + + /** + * Shortcut method: Replace all occurrences of the search string with the replacement + * string where they appear outside quotes. + * @param string $search + * @param string $replace + * @param string $subject + * @return string + */ + public static function str_replace_outside_quotes($search, $replace, $subject) { + return self::value($subject)->replace_outside_quotes($search, $replace); + } + + /** + * Set the base string object + * @param string $subject + */ + public function __construct($subject) { + $this->subject = (string) $subject; + } + + /** + * Replace all occurrences of the search string with the replacement + * string where they appear outside quotes + * @param string $search + * @param string $replace + * @return string + */ + public function replace_outside_quotes($search, $replace) { + $this->search = $search; + $this->replace = $replace; + return $this->_str_replace_outside_quotes(); + } + + /** + * Validate an input string and perform a replace on all ocurrences + * of $this->search with $this->replace + * @author Jeff Roberson + * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer + * @return string + */ + protected function _str_replace_outside_quotes(){ + $re_valid = '/ # Validate string having embedded quoted substrings. ^ # Anchor to start of string. (?: # Zero or more string chunks. @@ -2285,10 +2369,10 @@ class IdiormString { )* # Zero or more string chunks. \z # Anchor to end of string. /sx'; - if (!preg_match($re_valid, $this->subject)) { - throw new IdiormStringException("Subject string is not valid in the replace_outside_quotes context."); - } - $re_parse = '/ + if (!preg_match($re_valid, $this->subject)) { + throw new IdiormStringException("Subject string is not valid in the replace_outside_quotes context."); + } + $re_parse = '/ # Match one chunk of a valid string having embedded quoted substrings. ( # Either $1: Quoted chunk. "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" # Either a double quoted chunk, @@ -2296,162 +2380,178 @@ class IdiormString { ) # End $1: Quoted chunk. | ([^\'"\\\\]+) # or $2: an unquoted chunk (no escapes). /sx'; - return preg_replace_callback($re_parse, array($this, '_str_replace_outside_quotes_cb'), $this->subject); - } - - /** - * Process each matching chunk from preg_replace_callback replacing - * each occurrence of $this->search with $this->replace - * @author Jeff Roberson - * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer - * @param array $matches - * @return string - */ - protected function _str_replace_outside_quotes_cb($matches) { - // Return quoted string chunks (in group $1) unaltered. - if ($matches[1]) return $matches[1]; - // Process only unquoted chunks (in group $2). - return preg_replace('/'. preg_quote($this->search, '/') .'/', - $this->replace, $matches[2]); - } -} - -/** - * A result set class for working with collections of model instances - * @author Simon Holywell - */ -class IdiormResultSet implements Countable, IteratorAggregate, ArrayAccess, Serializable { - /** - * The current result set as an array - * @var array - */ - protected $_results = array(); - - /** - * Optionally set the contents of the result set by passing in array - * @param array $results - */ - public function __construct(array $results = array()) { - $this->set_results($results); - } - - /** - * Set the contents of the result set by passing in array - * @param array $results - */ - public function set_results(array $results) { - $this->_results = $results; - } - - /** - * Get the current result set as an array - * @return array - */ - public function get_results() { - return $this->_results; - } - - /** - * Get the current result set as an array - * @return array - */ - public function as_array() { - return $this->get_results(); - } - - /** - * Get the number of records in the result set - * @return int - */ - public function count() { - return count($this->_results); - } - - /** - * Get an iterator for this object. In this case it supports foreaching - * over the result set. - * @return \ArrayIterator - */ - public function getIterator() { - return new ArrayIterator($this->_results); - } - - /** - * ArrayAccess - * @param int|string $offset - * @return bool - */ - public function offsetExists($offset) { - return isset($this->_results[$offset]); - } - - /** - * ArrayAccess - * @param int|string $offset - * @return mixed - */ - public function offsetGet($offset) { - return $this->_results[$offset]; - } - - /** - * ArrayAccess - * @param int|string $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) { - $this->_results[$offset] = $value; - } - - /** - * ArrayAccess - * @param int|string $offset - */ - public function offsetUnset($offset) { - unset($this->_results[$offset]); - } - - /** - * Serializable - * @return string - */ - public function serialize() { - return serialize($this->_results); - } - - /** - * Serializable - * @param string $serialized - * @return array - */ - public function unserialize($serialized) { - return unserialize($serialized); - } - - /** - * Call a method on all models in a result set. This allows for method - * chaining such as setting a property on all models in a result set or - * any other batch operation across models. - * @example ORM::for_table('Widget')->find_many()->set('field', 'value')->save(); - * @param string $method - * @param array $params - * @return \IdiormResultSet - */ - public function __call($method, $params = array()) { - foreach($this->_results as $model) { - if (method_exists($model, $method)) { - call_user_func_array(array($model, $method), $params); - } else { - throw new IdiormMethodMissingException("Method $method() does not exist in class " . get_class($this)); - } + return preg_replace_callback($re_parse, array($this, '_str_replace_outside_quotes_cb'), $this->subject); + } + + /** + * Process each matching chunk from preg_replace_callback replacing + * each occurrence of $this->search with $this->replace + * @author Jeff Roberson + * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer + * @param array $matches + * @return string + */ + protected function _str_replace_outside_quotes_cb($matches) { + // Return quoted string chunks (in group $1) unaltered. + if ($matches[1]) return $matches[1]; + // Process only unquoted chunks (in group $2). + return preg_replace('/'. preg_quote($this->search, '/') .'/', + $this->replace, $matches[2]); } - return $this; } -} -/** - * A placeholder for exceptions eminating from the IdiormString class - */ -class IdiormStringException extends Exception {} + /** + * A result set class for working with collections of model instances + * @author Simon Holywell + * @method null setResults(array $results) + * @method array getResults() + */ + class IdiormResultSet implements Countable, IteratorAggregate, ArrayAccess, Serializable { + /** + * The current result set as an array + * @var array + */ + protected $_results = array(); -class IdiormMethodMissingException extends Exception {} \ No newline at end of file + /** + * Optionally set the contents of the result set by passing in array + * @param array $results + */ + public function __construct(array $results = array()) { + $this->set_results($results); + } + + /** + * Set the contents of the result set by passing in array + * @param array $results + */ + public function set_results(array $results) { + $this->_results = $results; + } + + /** + * Get the current result set as an array + * @return array + */ + public function get_results() { + return $this->_results; + } + + /** + * Get the current result set as an array + * @return array + */ + public function as_array() { + return $this->get_results(); + } + + /** + * Get the number of records in the result set + * @return int + */ + #[\ReturnTypeWillChange] + public function count() { + return count($this->_results); + } + + /** + * Get an iterator for this object. In this case it supports foreaching + * over the result set. + * @return \ArrayIterator + */ + #[\ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->_results); + } + + /** + * ArrayAccess + * @param int|string $offset + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return isset($this->_results[$offset]); + } + + /** + * ArrayAccess + * @param int|string $offset + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->_results[$offset]; + } + + /** + * ArrayAccess + * @param int|string $offset + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->_results[$offset] = $value; + } + + /** + * ArrayAccess + * @param int|string $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->_results[$offset]); + } + + public function __serialize() { + return $this->serialize(); + } + + public function __unserialize($data) { + $this->unserialize($data); + } + + /** + * Serializable + * @return string + */ + public function serialize() { + return serialize($this->_results); + } + + /** + * Serializable + * @param string $serialized + * @return array + */ + public function unserialize($serialized) { + return unserialize($serialized); + } + + /** + * Call a method on all models in a result set. This allows for method + * chaining such as setting a property on all models in a result set or + * any other batch operation across models. + * @example ORM::for_table('Widget')->find_many()->set('field', 'value')->save(); + * @param string $method + * @param array $params + * @return \IdiormResultSet + */ + public function __call($method, $params = array()) { + foreach($this->_results as $model) { + if (method_exists($model, $method)) { + call_user_func_array(array($model, $method), $params); + } else { + throw new IdiormMethodMissingException("Method $method() does not exist in class " . get_class($this)); + } + } + return $this; + } + } + + /** + * A placeholder for exceptions eminating from the IdiormString class + */ + class IdiormStringException extends Exception {} + + class IdiormMethodMissingException extends Exception {} diff --git a/system/updates.json b/system/updates.json index 7dce27f7..a0151c59 100644 --- a/system/updates.json +++ b/system/updates.json @@ -10,5 +10,8 @@ "ALTER TABLE `tbl_plans` CHANGE `type` `type` ENUM('Hotspot','PPPOE','Balance') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;", "ALTER TABLE `tbl_transactions` CHANGE `type` `type` ENUM('Hotspot','PPPOE','Balance') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;", "ALTER TABLE `tbl_customers` ADD `auto_renewal` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Auto renewall using balance' AFTER `balance`;" + ], + "2023.8.23" : [ + "ALTER TABLE `tbl_customers` CHANGE `pppoe_password` `pppoe_password` VARCHAR(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'For PPPOE Login';" ] } \ No newline at end of file diff --git a/system/uploads/notifications.default.json b/system/uploads/notifications.default.json index 21e9c1cc..a1485439 100644 --- a/system/uploads/notifications.default.json +++ b/system/uploads/notifications.default.json @@ -1,5 +1,7 @@ { "expired": "Hello [[name]], your internet package [[package]] has been expired.", + "balance_send": "You sent [[balance]] to [[name]].", + "balance_received": "You have received [[balance]] from [[name]].", "reminder_7_day": "Hello *[[name]]*, \r\nyour internet package *[[package]]* will be expired in 7 days.", "reminder_3_day": "Hello *[[name]]*, \r\nyour internet package *[[package]]* will be expired in 3 days.", "reminder_1_day": "Hello *[[name]]*,\r\n your internet package *[[package]]* will be expired tomorrow.", diff --git a/ui/ui/app-notifications.tpl b/ui/ui/app-notifications.tpl index 6446adf1..a453f864 100644 --- a/ui/ui/app-notifications.tpl +++ b/ui/ui/app-notifications.tpl @@ -113,6 +113,32 @@

    +
    +
    + +
    + +
    +

    + [[name]] Receiver name.
    + [[balance]] how much balance have been send. +

    +
    +
    +
    +
    + +
    + +
    +

    + [[name]] Sender name.
    + [[balance]] how much balance have been received. +

    +
    +
    diff --git a/ui/ui/app-settings.tpl b/ui/ui/app-settings.tpl index 02e44903..566849df 100644 --- a/ui/ui/app-settings.tpl +++ b/ui/ui/app-settings.tpl @@ -107,6 +107,13 @@

    {Lang::T('Allow balance transfer between customers')}

    +
    + +
    + +
    +
    diff --git a/ui/ui/customers-add.tpl b/ui/ui/customers-add.tpl index c367095e..c4bcf59c 100644 --- a/ui/ui/customers-add.tpl +++ b/ui/ui/customers-add.tpl @@ -1,76 +1,91 @@ {include file="sections/header.tpl"} -
    -
    -
    -
    {$_L['Add_Contact']}
    -
    +
    +
    +
    +
    {$_L['Add_Contact']}
    +
    -
    -
    - -
    -
    - + - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - + - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - - {Lang::T('User Cannot change this, only admin. if it Empty it will use user password')} -
    + +
    + +
    +
    + {if $_c['country_code_phone']!= ''} + + + {else} + + {/if} +
    -
    - -
    - -
    -
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + {if $_c['country_code_phone']!= ''} + + + {else} + + {/if} + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + + {Lang::T('User Cannot change this, only admin. if it Empty it will use user password')} +
    +
    +
    + +
    + +
    +
    -
    -
    - - Or {$_L['Cancel']} -
    -
    - -
    -
    -
    -
    -
    +
    +
    + + Or {$_L['Cancel']} +
    +
    + +
    +
    +
    +
    +
    -{include file="sections/footer.tpl"} +{include file="sections/footer.tpl"} \ No newline at end of file diff --git a/ui/ui/customers-edit.tpl b/ui/ui/customers-edit.tpl index 4886620e..cdbca703 100644 --- a/ui/ui/customers-edit.tpl +++ b/ui/ui/customers-edit.tpl @@ -1,77 +1,95 @@ {include file="sections/header.tpl"} -
    -
    -
    -
    {$_L['Edit_Contact']}
    -
    +
    +
    +
    +
    {$_L['Edit_Contact']}
    +
    -
    - -
    - -
    -
    - + - -
    -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - + - -
    -
    -
    -
    - -
    - - {$_L['password_change_help']} -
    -
    -
    - -
    - - {Lang::T('User Cannot change this, only admin. if it Empty it will use user password')} -
    -
    -
    - -
    - -
    -
    + + +
    + +
    +
    + {if $_c['country_code_phone']!= ''} + + + {else} + + {/if} + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + {if $_c['country_code_phone']!= ''} + + + {else} + + {/if} + +
    +
    +
    +
    + +
    + + {$_L['password_change_help']} +
    +
    +
    + +
    + + {Lang::T('User Cannot change this, only admin. if it Empty it will use user password')} +
    +
    +
    + +
    + +
    +
    -
    -
    - - Or {$_L['Cancel']} -
    -
    -
    -
    -
    -
    -
    -
    +
    +
    + + Or {$_L['Cancel']} +
    +
    + +
    +
    +
    +
    + -{include file="sections/footer.tpl"} +{include file="sections/footer.tpl"} \ No newline at end of file diff --git a/ui/ui/customers-view.tpl b/ui/ui/customers-view.tpl new file mode 100644 index 00000000..68559ccf --- /dev/null +++ b/ui/ui/customers-view.tpl @@ -0,0 +1,171 @@ +{include file="sections/header.tpl"} + +
    +
    +
    +
    + avatar + +

    {$d['fullname']}

    + +
      +
    • + {$_L['Username']} {$d['username']} +
    • +
    • + {$_L['Phone_Number']} {$d['phonenumber']} +
    • +
    • + {$_L['Email']} {$d['email']} +
    • +
    +

    {Lang::nl2br($d['address'])}

    +
      +
    • + {$_L['Password']} {$d['password']} +
    • + {if $d['pppoe_password'] != ''} +
    • + PPPOE {$_L['Password']} {$d['pppoe_password']} +
    • + {/if} +
    • + {Lang::T('Balance')} {Lang::moneyFormat($d['balance'])} +
    • +
    • + {Lang::T('Auto Renewal')} {if $d['auto_renewal']}yes{else}no{/if} +
    • +
    • + {$_L['Created_On']} {Lang::dateTimeFormat($d['created_at'])} +
    • +
    • + {Lang::T('Last Login')} {Lang::dateTimeFormat($d['last_login'])} +
    • +
    +
    +
    + +
    + +
    +
    +
    + {if $package} +
    +
    +

    {$package['type']} - {$package['namebp']}

    +
      +
    • + {Lang::T('Active')} {if $package['status']=='on'}yes{else}no{/if} +
    • +
    • + {$_L['Created_On']} {Lang::dateFormat($package['recharged_on'])} +
    • +
    • + {$_L['Expires_On']} {Lang::dateTimeFormat($package['expiration']+' '+$package['time'])} +
    • +
    • + {$package['routers']} {$package['method']} +
    • +
    +
    +
    + {/if} + {Lang::T('Back')}
    +
    +
    + +
    + + {if Lang::arrayCount($activation)} + + + + + + + + + + + + + {foreach $activation as $ds} + + + + + + + + + + {/foreach} + + {/if} + {if Lang::arrayCount($order)} + + + + + + + + + + + + + + + {foreach $order as $ds} + + + + + + + + + + + + {/foreach} + + {/if} +
    {$_L['Username']}{$_L['Plan_Name']}{$_L['Plan_Price']}{$_L['Type']}{$_L['Created_On']}{$_L['Expires_On']}{$_L['Method']}
    {$ds['username']}{$ds['plan_name']}{Lang::moneyFormat($ds['price'])}{$ds['type']}{date($_c['date_format'], strtotime($ds['recharged_on']))}{date($_c['date_format'], strtotime($ds['expiration']))} + {$ds['time']}{$ds['method']}
    {$_L['Plan_Name']}{Lang::T('Gateway')}{Lang::T('Routers')}{$_L['Type']}{$_L['Plan_Price']}{$_L['Created_On']}{$_L['Expires_On']}{Lang::T('Date Done')}{$_L['Method']}
    {$ds['plan_name']}{$ds['gateway']}{$ds['routers']}{$ds['payment_channel']}{Lang::moneyFormat($ds['price'])}{date("{$_c['date_format']} H:i", + strtotime($ds['created_date']))}{date("{$_c['date_format']} H:i", + strtotime($ds['expired_date']))}{if $ds['status']!=1}{date("{$_c['date_format']} H:i", + strtotime($ds['paid_date']))}{/if}{if $ds['status']==1}{$_L['UNPAID']} + {elseif $ds['status']==2}{$_L['PAID']} + {elseif $ds['status']==3}{$_L['FAILED']} + {elseif $ds['status']==4}{$_L['CANCELED']} + {elseif $ds['status']==5}{$_L['UNKNOWN']} + {/if}
    +
    + {$paginator['contents']} +
    +
    + +{include file="sections/footer.tpl"} \ No newline at end of file diff --git a/ui/ui/customers.tpl b/ui/ui/customers.tpl index 495e0e8e..38cd5d4e 100644 --- a/ui/ui/customers.tpl +++ b/ui/ui/customers.tpl @@ -9,19 +9,12 @@
    @@ -34,6 +27,7 @@ + @@ -41,27 +35,23 @@ - {foreach $d as $ds} + - + - {/foreach} diff --git a/ui/ui/dashboard.tpl b/ui/ui/dashboard.tpl index 15d56945..6d8151fc 100644 --- a/ui/ui/dashboard.tpl +++ b/ui/ui/dashboard.tpl @@ -107,7 +107,7 @@ {foreach $expire as $expired} - +
    {$_L['Manage']} {$_L['Username']} {$_L['Full_Name']} {Lang::T('Balance')}{$_L['Email']} {$_L['Created_On']} {$_L['Recharge']}{$_L['Manage']}
    + {Lang::T('View')} + {$ds['username']} {$ds['fullname']} {Lang::moneyFormat($ds['balance'])} {$ds['phonenumber']} {$ds['email']}{$ds['created_at']}{Lang::dateTimeFormat($ds['created_at'])} {$_L['Recharge']} - {$_L['Edit']} - {$_L['Delete']} -
    {$no++}{$expired['username']}{$expired['username']} {date($_c['date_format'], strtotime($expired['recharged_on']))} {date($_c['date_format'], strtotime($expired['expiration']))} {$expired['time']} diff --git a/ui/ui/deposit.tpl b/ui/ui/deposit.tpl index 02e5810e..a76311fa 100644 --- a/ui/ui/deposit.tpl +++ b/ui/ui/deposit.tpl @@ -11,10 +11,6 @@
    diff --git a/ui/ui/hotspot-add.tpl b/ui/ui/hotspot-add.tpl index 06b361e2..448dc26b 100644 --- a/ui/ui/hotspot-add.tpl +++ b/ui/ui/hotspot-add.tpl @@ -69,7 +69,7 @@
    - +
    {foreach $r as $rs} diff --git a/ui/ui/hotspot-edit.tpl b/ui/ui/hotspot-edit.tpl index 3dab50b6..769c2ff4 100644 --- a/ui/ui/hotspot-edit.tpl +++ b/ui/ui/hotspot-edit.tpl @@ -70,7 +70,7 @@
    - +
    diff --git a/ui/ui/pool-add.tpl b/ui/ui/pool-add.tpl index d0306f92..813be474 100644 --- a/ui/ui/pool-add.tpl +++ b/ui/ui/pool-add.tpl @@ -20,7 +20,7 @@
    - +
    @@ -58,7 +58,7 @@
    - +
    diff --git a/ui/ui/pppoe-edit.tpl b/ui/ui/pppoe-edit.tpl index 582645fc..aa23412a 100644 --- a/ui/ui/pppoe-edit.tpl +++ b/ui/ui/pppoe-edit.tpl @@ -25,7 +25,7 @@
    - +
    {foreach $p as $ps} diff --git a/ui/ui/recharge.tpl b/ui/ui/recharge.tpl index 596b57dc..ddfd28fc 100644 --- a/ui/ui/recharge.tpl +++ b/ui/ui/recharge.tpl @@ -10,10 +10,6 @@
    diff --git a/ui/ui/refill.tpl b/ui/ui/refill.tpl index 4377d6ac..9e6ef850 100644 --- a/ui/ui/refill.tpl +++ b/ui/ui/refill.tpl @@ -9,12 +9,8 @@
    -
    @@ -39,4 +35,5 @@
    + {include file="sections/footer.tpl"} \ No newline at end of file diff --git a/ui/ui/reports-period.tpl b/ui/ui/reports-period.tpl index a1926078..67a378b6 100644 --- a/ui/ui/reports-period.tpl +++ b/ui/ui/reports-period.tpl @@ -32,6 +32,7 @@ + diff --git a/ui/ui/routers-edit.tpl b/ui/ui/routers-edit.tpl index e33d0327..b6afb78d 100644 --- a/ui/ui/routers-edit.tpl +++ b/ui/ui/routers-edit.tpl @@ -41,7 +41,8 @@
    - +
    diff --git a/ui/ui/scripts/select2.min.js b/ui/ui/scripts/select2.min.js deleted file mode 100644 index 49a988c7..00000000 --- a/ui/ui/scripts/select2.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! Select2 4.0.0 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(n=n.slice(0,n.length-1),a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.concat(a),k=0;k0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){return n.apply(b,v.call(arguments,0).concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;hc;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e":">",'"':""","'":"'","/":"/"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('
      ');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('
    • '),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),this.$results.append(d)},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")});var f=e.filter("[aria-selected=true]");f.length>0?f.first().trigger("mouseenter"):e.first().trigger("mouseenter")})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";{a(h)}this.template(b,h);for(var i=[],j=0;j",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b){var c=this,d=b.id+"-results";this.$results.attr("id",d),b.on("results:all",function(a){c.clear(),c.append(a.data),b.isOpen()&&c.setClasses()}),b.on("results:append",function(a){c.append(a.data),b.isOpen()&&c.setClasses()}),b.on("query",function(a){c.showLoading(a)}),b.on("select",function(){b.isOpen()&&c.setClasses()}),b.on("unselect",function(){b.isOpen()&&c.setClasses()}),b.on("open",function(){c.$results.attr("aria-expanded","true"),c.$results.attr("aria-hidden","false"),c.setClasses(),c.ensureHighlightVisible()}),b.on("close",function(){c.$results.attr("aria-expanded","false"),c.$results.attr("aria-hidden","true"),c.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=c.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=c.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?c.trigger("close"):c.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=c.getHighlightedResults(),b=c.$results.find("[aria-selected]"),d=b.index(a);if(0!==d){var e=d-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=c.$results.offset().top,h=f.offset().top,i=c.$results.scrollTop()+(h-g);0===e?c.$results.scrollTop(0):0>h-g&&c.$results.scrollTop(i)}}),b.on("results:next",function(){var a=c.getHighlightedResults(),b=c.$results.find("[aria-selected]"),d=b.index(a),e=d+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=c.$results.offset().top+c.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=c.$results.scrollTop()+h-g;0===e?c.$results.scrollTop(0):h>g&&c.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){c.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=c.$results.scrollTop(),d=c.$results.get(0).scrollHeight-c.$results.scrollTop()+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&d<=c.$results.height();e?(c.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(c.$results.scrollTop(c.$results.get(0).scrollHeight-c.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var d=a(this),e=d.data("data");return"true"===d.attr("aria-selected")?void(c.options.get("multiple")?c.trigger("unselect",{originalEvent:b,data:e}):c.trigger("close")):void c.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(){var b=a(this).data("data");c.getHighlightedResults().removeClass("select2-results__option--highlighted"),c.trigger("results:focus",{data:b,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a){var b=this,d=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){b.trigger("focus",a)}),this.$selection.on("blur",function(a){b.trigger("blur",a)}),this.$selection.on("keydown",function(a){b.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){b.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){b.update(a.data)}),a.on("open",function(){b.$selection.attr("aria-expanded","true"),b.$selection.attr("aria-owns",d),b._attachCloseHandler(a)}),a.on("close",function(){b.$selection.attr("aria-expanded","false"),b.$selection.removeAttr("aria-activedescendant"),b.$selection.removeAttr("aria-owns"),b.$selection.focus(),b._detachCloseHandler(a)}),a.on("enable",function(){b.$selection.attr("tabindex",b._tabindex)}),a.on("disable",function(){b.$selection.attr("tabindex","-1")})},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c){function d(){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html(''),a},d.prototype.bind=function(a){var b=this;d.__super__.bind.apply(this,arguments);var c=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",c),this.$selection.attr("aria-labelledby",c),this.$selection.on("mousedown",function(a){1===a.which&&b.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(){}),this.$selection.on("blur",function(){}),a.on("selection:update",function(a){b.update(a.data)})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a){var b=this.options.get("templateSelection"),c=this.options.get("escapeMarkup");return c(b(a))},d.prototype.selectionContainer=function(){return a("")},d.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.display(b),d=this.$selection.find(".select2-selection__rendered");d.empty().append(c),d.prop("title",b.title||b.text)},d}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('
        '),a},d.prototype.bind=function(){var b=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){b.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(c){var d=a(this),e=d.parent(),f=e.data("data");b.trigger("unselect",{originalEvent:c,data:f})})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a){var b=this.options.get("templateSelection"),c=this.options.get("escapeMarkup");return c(b(a))},d.prototype.selectionContainer=function(){var b=a('
      • ×
      • ');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},a}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e0||0===c.length)){var d=a('×');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus()}),b.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val(""),e.$search.focus()}),b.on("enable",function(){e.$search.prop("disabled",!1)}),b.on("disable",function(){e.$search.prop("disabled",!0)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e.trigger("blur",a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}}),this.$selection.on("input",".select2-search--inline",function(){e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input",".select2-search--inline",function(a){e.handleSearch(a)})},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.trigger("open"),this.$search.val(b.text+" ")},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=(c.extend(!0,{},l,j),this.option(l));k.replaceWith(m)}else{var n=this.option(j);if(j.children){var o=this.convertToOptions(j.children);b.appendMany(n,o)}h.push(n)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(b,c){this.ajaxOptions=this._applyDefaults(c.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),a.__super__.constructor.call(this,b,c)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return{q:a.term}},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url(a)),"function"==typeof f.data&&(f.data=f.data(a)),this.ajaxOptions.delay&&""!==a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");if(void 0!==f&&(this.createTag=f),b.call(this,c,d),a.isArray(e))for(var g=0;g0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.position=function(){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a){function b(){}return b.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},b.prototype.handleSearch=function(){if(!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},b.prototype.showSearch=function(){return!0},b}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
      • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(a,b,c){this.$dropdownParent=c.get("dropdownParent")||document.body,a.call(this,b,c)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c){var d=this,e="scroll.select2."+c.id,f="resize.select2."+c.id,g="orientationchange.select2."+c.id,h=this.$container.parents().filter(b.hasScroll);h.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),h.on(e,function(){var b=a(this).data("select2-scroll-position");a(this).scrollTop(b.y)}),a(window).on(e+" "+f+" "+g,function(){d._positionDropdown(),d._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c){var d="scroll.select2."+c.id,e="resize.select2."+c.id,f="orientationchange.select2."+c.id,g=this.$container.parents().filter(b.hasScroll);g.off(d),a(window).off(d+" "+e+" "+f)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=(this.$container.position(),this.$container.offset());f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom};c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){this.$dropdownContainer.width();var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.selectionAdapter=l.multiple?e:d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(this.options.dir=a.prop("dir")?a.prop("dir"):a.closest("[dir]").prop("dir")?a.closest("[dir]").prop("dir"):"ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this._sync=c.bind(this._syncAttributes,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._sync);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._sync)}),this._observer.observe(this.$element[0],{attributes:!0,subtree:!1})):this.$element[0].addEventListener&&this.$element[0].addEventListener("DOMAttrModified",b._sync,!1)},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("focus",function(){a.$container.addClass("select2-container--focus")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open"),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ENTER?(a.trigger("results:select"),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle"),b.preventDefault()):c===d.UP?(a.trigger("results:previous"),b.preventDefault()):c===d.DOWN?(a.trigger("results:next"),b.preventDefault()):(c===d.ESC||c===d.TAB)&&(a.close(),b.preventDefault()):(c===d.ENTER||c===d.SPACE||(c===d.DOWN||c===d.UP)&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable")):this.trigger("enable")},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||(this.trigger("query",{}),this.trigger("open"))},e.prototype.close=function(){this.isOpen()&&this.trigger("close")},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._sync),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&this.$element[0].removeEventListener("DOMAttrModified",this._sync,!1),this._sync=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery.select2",["jquery","require","./select2/core","./select2/defaults"],function(a,b,c,d){if(b("jquery.mousewheel"),null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){{var d=a.extend({},b,!0);new c(a(this),d)}}),this;if("string"==typeof b){var d=this.data("select2");null==d&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2.");var f=Array.prototype.slice.call(arguments,1),g=d[b](f);return a.inArray(b,e)>-1?this:g}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),b.define("jquery.mousewheel",["jquery"],function(a){return a}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); \ No newline at end of file diff --git a/ui/ui/sections/footer.tpl b/ui/ui/sections/footer.tpl index e68d9aec..daca93fe 100644 --- a/ui/ui/sections/footer.tpl +++ b/ui/ui/sections/footer.tpl @@ -11,10 +11,8 @@ - - {if isset($xfooter)} {$xfooter} diff --git a/ui/ui/styles/plugins/select2.css b/ui/ui/styles/plugins/select2.css deleted file mode 100644 index d27b8184..00000000 --- a/ui/ui/styles/plugins/select2.css +++ /dev/null @@ -1,281 +0,0 @@ - -.select2-container { - box-sizing: border-box; - display: block; - margin: 0; - position: relative; - } - .select2-container .select2-selection--single { - box-sizing: border-box; - cursor: pointer; - display: block; - height: 36px; - padding: 0 5px; - user-select: none; - -webkit-user-select: none; } - .select2-container .select2-selection--single .select2-selection__rendered { - display: block; - padding-left: 8px; - padding-right: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } - .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { - padding-right: 8px; - padding-left: 20px; } - .select2-container .select2-selection--multiple { - cursor: pointer; - display: block; - user-select: none; - -webkit-user-select: none; } - .select2-container .select2-selection--multiple .select2-selection__rendered { - display: inline-block; - overflow: hidden; - padding-left: 8px; - text-overflow: ellipsis; - white-space: nowrap; } - .select2-container .select2-search--inline { - float: left; } - .select2-container .select2-search--inline .select2-search__field { - box-sizing: border-box; - border: none; - font-size: 100%; - margin-top: 5px; } - .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { - -webkit-appearance: none; } - -.select2-dropdown { - background-color: white; - border-radius: 0 0 2px 2px; - border: 1px solid #dedede; - box-sizing: border-box; - display: block; - position: absolute; - left: -100000px; - width: 100%; - z-index: 1051; } - -.select2-results { - display: block; } - -.select2-results__options { - list-style: none; - margin: 0; - padding: 0; } - -.select2-results__option { - padding: 6px 10px; - user-select: none; - -webkit-user-select: none; } - .select2-results__option[aria-selected] { - cursor: pointer; } - -.select2-container--open .select2-dropdown { - left: 0; - border-top: 0; - box-shadow: 0 1px 0 rgba(0,0,0,.05); -} - -.select2-container--open .select2-dropdown--above { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--open .select2-dropdown--below { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-search--dropdown { - display: block; - padding: 4px; } - .select2-search--dropdown .select2-search__field { - padding: 4px; - width: 100%; - box-sizing: border-box; } - .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { - -webkit-appearance: none; } - .select2-search--dropdown.select2-search--hide { - display: none; } - -.select2-close-mask { - border: 0; - margin: 0; - padding: 0; - display: block; - position: fixed; - left: 0; - top: 0; - min-height: 100%; - min-width: 100%; - height: auto; - width: auto; - opacity: 0; - z-index: 99; - background-color: #fff; - filter: alpha(opacity=0); } - -.select2-hidden-accessible { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; } - -/*** Default Style ***/ -.select2-container--default .select2-selection--single { - background-color: #fff; - border: 1px solid #dedede; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - border-radius:0px; } - .select2-container--default .select2-selection--single .select2-selection__rendered { - color: #333; - line-height: 36px;} - .select2-container--default .select2-selection--single .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; } - .select2-container--default .select2-selection--single .select2-selection__placeholder { - color: #999; } - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 26px; - position: absolute; - top: 5px; - right: 4px; - width: 20px; } - .select2-container--default .select2-selection--single .select2-selection__arrow b { - border-color: #555 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - left: 50%; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - width: 0; } -.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { - float: left; } -.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { - left: 1px; - right: auto; } -.select2-container--default.select2-container--disabled .select2-selection--single { - background-color: #eee; - cursor: default; } - .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { - display: none; } -.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; } -.select2-container--default .select2-selection--multiple { - background-color: #fff; - height: 36px; - border: none; - border-bottom: 1px solid #dedede; - cursor: text; } - .select2-container--default .select2-selection--multiple .select2-selection__rendered { - list-style: none; - margin: 0; - min-height: 36px; - padding: 0 5px; - margin-bottom: 0; - width: 100%; } - .select2-container--default .select2-selection--multiple .select2-selection__placeholder { - color: #999; - margin-top: 5px; - float: left; } - .select2-container--default .select2-selection--multiple .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; - margin-top: 5px; - margin-right: 10px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice { - background-color: #fff; - border: 1px solid #dedede; - border-radius: 1px; - cursor: default; - float: left; - font-size: 12px; - margin-right: 5px; - margin-top: 4px; - position: relative; - top: 1px; - box-shadow: 0 1px 1px rgba(0,0,0,.1); - padding: 2px 8px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { - color: #999; - cursor: pointer; - display: inline-block; - font-weight: bold; - margin-right: 2px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { - color: #333; } -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder { - float: right; } -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - margin-left: 5px; - margin-right: auto; } -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { - margin-left: 2px; - margin-right: auto; } -.select2-container--default.select2-container--focus .select2-selection--multiple { - border: none; - border-bottom: solid #dedede 1px; - outline: 0; } -.select2-container--default.select2-container--disabled .select2-selection--multiple { - background-color: #eee; - cursor: default; } -.select2-container--default.select2-container--disabled .select2-selection__choice__remove { - display: none; } -.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { - border-top-left-radius: 0; - border-top-right-radius: 0; } -.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } -.select2-container--default .select2-search--dropdown .select2-search__field { - border: 1px solid #eee; } -.select2-container--default .select2-search--inline .select2-search__field { - background: transparent; - border: none; - outline: 0; } -.select2-container--default .select2-results > .select2-results__options { - max-height: 200px; - overflow-y: auto; } -.select2-container--default .select2-results__option[role=group] { - padding: 0; } -.select2-container--default .select2-results__option[aria-disabled=true] { - color: #999; } -.select2-container--default .select2-results__option[aria-selected=true] { - background-color: transparent; } -.select2-container--default .select2-results__option .select2-results__option { - padding-left: 1em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__group { - padding-left: 0; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option { - margin-left: -1em; - padding-left: 2em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -2em; - padding-left: 3em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -3em; - padding-left: 4em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -4em; - padding-left: 5em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -5em; - padding-left: 6em; } -.select2-container--default .select2-results__option--highlighted[aria-selected] { - background-color:#3f51b5; - color: white; } -.select2-container--default .select2-results__group { - cursor: default; - display: block; - padding: 6px; } - diff --git a/ui/ui/user-dashboard.tpl b/ui/ui/user-dashboard.tpl index 80851465..12c7e358 100644 --- a/ui/ui/user-dashboard.tpl +++ b/ui/ui/user-dashboard.tpl @@ -41,7 +41,7 @@

        {$_L['Announcement']}

        -
        +
        {include file="$_path/../pages/Announcement.html"}
        @@ -138,6 +138,39 @@ }, 2000); {/if} + {if $_c['enable_balance'] == 'yes'} +
        +
        +

        {Lang::T("Transfer Balance")}

        +
        +
        +
        +
        +
        + +
        +
        + +
        +
        + +
        +
        +
        + +
        +
        + {/if}
        {if $_c['disable_voucher'] != 'yes'}
        diff --git a/ui/ui/user-orderPlan.tpl b/ui/ui/user-orderPlan.tpl index 6315baad..d8a2c91b 100644 --- a/ui/ui/user-orderPlan.tpl +++ b/ui/ui/user-orderPlan.tpl @@ -33,6 +33,10 @@ {/foreach}
        + +
        +
        {Lang::T('Balance')} {Lang::moneyFormat($_user['balance'])}
        +
        {/if} {foreach $routers as $router}
        @@ -69,9 +73,16 @@
        - Buy +
        + Buy + {if $_c['enable_balance'] == 'yes' && $_user['balance']>=$plan['price']} + {Lang::T('Pay With Balance')} + {/if} +
        @@ -106,9 +117,16 @@
        - Buy +
        + Buy + {if $_c['enable_balance'] == 'yes' && $_user['balance']>=$plan['price']} + {Lang::T('Pay With Balance')} + {/if} +
        diff --git a/ui/ui/user-orderView.tpl b/ui/ui/user-orderView.tpl index 48c2006a..6a090412 100644 --- a/ui/ui/user-orderView.tpl +++ b/ui/ui/user-orderView.tpl @@ -6,7 +6,7 @@
        - {if $plan['type']!='Balance'} + {if $trx['routers']!='balance'}
        {$router['name']}
        @@ -17,70 +17,98 @@
        {/if}
        - - - - - - - - - - - {if $trx['status']==2} + {if $trx['pg_url_payment']=='balance'} +
        {Lang::T('Status')}{if $trx['status']==1}{Lang::T('UNPAID')}{elseif $trx['status']==2}{Lang::T('PAID')}{elseif $trx['status']==3}{Lang::T('FAILED')}{elseif $trx['status']==4}{Lang::T('CANCELED')}{else}{Lang::T('UNKNOWN')}{/if} -
        {Lang::T('expired')}{date($_c['date_format'], strtotime($trx['expired_date']))} - {date('H:i', strtotime($trx['expired_date']))}
        + + + + + - {/if} - - - - - - - - - - - - - {if $plan['type']!='Balance'} - {if $plan['type'] eq 'Hotspot'} - - - - - {if $plan['typebp'] eq 'Limited'} - {if $plan['limit_type'] eq 'Time_Limit' or $plan['limit_type'] eq 'Both_Limit'} - - - - - {/if} - {if $plan['limit_type'] eq 'Data_Limit' or $plan['limit_type'] eq 'Both_Limit'} - - - - - {/if} - {/if} - {/if} - - + {if $trx['plan_name'] == 'Receive Balance'} + + {else} + + {/if} + - - + + + +
        {Lang::T('Type')}{$trx['plan_name']}
        {Lang::T('Paid Date')} {date($_c['date_format'], strtotime($trx['paid_date']))} {date('H:i', strtotime($trx['paid_date']))}
        {$_L['Plan_Name']}{$plan['name_plan']}
        {$_L['Plan_Price']}{Lang::moneyFormat($plan['price'])}
        {Lang::T('Type')}{$plan['type']}
        {Lang::T('Plan_Type')}{Lang::T($plan['typebp'])}
        {Lang::T('Time_Limit')}{$ds['time_limit']} {$ds['time_unit']}
        {Lang::T('Data_Limit')}{$ds['data_limit']} {$ds['data_unit']}
        {$_L['Plan_Validity']}{$plan['validity']} {$plan['validity_unit']}{Lang::T('From')}{Lang::T('To')}{$trx['gateway']}
        {$_L['Bandwidth_Plans']}{$bandw['name_bw']}
        {$bandw['rate_down']}{$bandw['rate_down_unit']}/{$bandw['rate_up']}{$bandw['rate_up_unit']} +
        {$_L['Balance']}{Lang::moneyFormat($trx['price'])}
        + {else} + + + + + - {/if} - -
        {Lang::T('Status')}{if $trx['status']==1}{Lang::T('UNPAID')}{elseif $trx['status']==2}{Lang::T('PAID')}{elseif $trx['status']==3}{Lang::T('FAILED')}{elseif $trx['status']==4}{Lang::T('CANCELED')}{else}{Lang::T('UNKNOWN')}{/if}
        + + {Lang::T('expired')} + {date($_c['date_format'], strtotime($trx['expired_date']))} + {date('H:i', strtotime($trx['expired_date']))} + + {if $trx['status']==2} + + {Lang::T('Paid Date')} + {date($_c['date_format'], strtotime($trx['paid_date']))} + {date('H:i', strtotime($trx['paid_date']))} + + {/if} + + {$_L['Plan_Name']} + {$plan['name_plan']} + + + {$_L['Plan_Price']} + {Lang::moneyFormat($plan['price'])} + + + {Lang::T('Type')} + {$plan['type']} + + {if $plan['type']!='Balance'} + {if $plan['type'] eq 'Hotspot'} + + {Lang::T('Plan_Type')} + {Lang::T($plan['typebp'])} + + {if $plan['typebp'] eq 'Limited'} + {if $plan['limit_type'] eq 'Time_Limit' or $plan['limit_type'] eq 'Both_Limit'} + + {Lang::T('Time_Limit')} + {$ds['time_limit']} {$ds['time_unit']} + + {/if} + {if $plan['limit_type'] eq 'Data_Limit' or $plan['limit_type'] eq 'Both_Limit'} + + {Lang::T('Data_Limit')} + {$ds['data_limit']} {$ds['data_unit']} + + {/if} + {/if} + {/if} + + {$_L['Plan_Validity']} + {$plan['validity']} {$plan['validity_unit']} + + + {$_L['Bandwidth_Plans']} + {$bandw['name_bw']}
        {$bandw['rate_down']}{$bandw['rate_down_unit']}/{$bandw['rate_up']}{$bandw['rate_up_unit']} + + + {/if} + + + {/if}
        {if $trx['status']==1}
        -
        -
        Update Finished
        +
        +
        Update Finished
        PHPNuxBill has been updated to Version
        diff --git a/version.json b/version.json index 167ffc44..89bbc3fb 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2023.8.16" + "version": "2023.8.24" } \ No newline at end of file