diff --git a/README.md b/README.md index 66e3dcc2..817aab98 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ Donors These people have supported this project with a donation: * [obigal](https://github.com/obigal) +* [vias](https://github.com/vias79) +* [WKNiGHT](https://github.com/WKNiGHT-) Requirements ============ @@ -57,10 +59,11 @@ The following feature have been implemented so far: * Reward Systems * Propotional - * (Planned) PPS + * PPS * (Planned) PPLNS * Use of memcache for statistics instead of a cronjob * Web User accounts + * Re-Captcha protected registration form * Worker accounts * Worker activity (live, past 10 minutes) * Worker hashrates (live, past 10 minutes) @@ -68,9 +71,23 @@ The following feature have been implemented so far: * Minimal Block statistics * Pool donations * Pool fees -* Manual payout with 0.1 LTC fee -* Auto payout with 0.1 LTC fee +* Manual payout +* Auto payout * Transaction list (confirmed and unconfirmed) +* Admin Panel + * User Listing including statistics + * Wallet information + * (Planned) News Posts + * (Planned) Pool Donations +* Notification system + * IDLE Workers + * New blocks found in pool + * Auto Payout + * Manual Payout +* Support for various Scrypt based coins via config + * MNC + * LTC + * ... Installation ============ diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index 3454ad9c..7e5609fc 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -27,6 +27,9 @@ if ($bitcoin->can_connect() !== true) { exit(1); } +// Mark this job as active +$setting->setValue('auto_payout_active', 1); + // Fetch all users with setup AP $users = $user->getAllAutoPayout(); @@ -35,11 +38,12 @@ if (! empty($users)) { verbose("UserID\tUsername\tBalance\tThreshold\tAddress\t\t\t\t\tStatus\n\n"); foreach ($users as $aUserData) { - $dBalance = $transaction->getBalance($aUserData['id']); + $aBalance = $transaction->getBalance($aUserData['id']); + $dBalance = $aBalance['confirmed']; verbose($aUserData['id'] . "\t" . $aUserData['username'] . "\t" . $dBalance . "\t" . $aUserData['ap_threshold'] . "\t\t" . $aUserData['coin_address'] . "\t"); - // Only run if balance meets threshold and can pay the transaction fee - if ($dBalance > $aUserData['ap_threshold'] && $dBalance > 0.1) { + // Only run if balance meets threshold and can pay the potential transaction fee + if ($dBalance > $aUserData['ap_threshold'] && $dBalance > $config['txfee']) { // Validate address against RPC try { $bitcoin->validateaddress($aUserData['coin_address']); @@ -48,20 +52,29 @@ if (! empty($users)) { continue; } - // Send balance - 0.1 Fee to address + // Send balance, fees are reduced later by RPC Server try { - $bitcoin->sendtoaddress($aUserData['coin_address'], $dBalance - 0.1); + $bitcoin->sendtoaddress($aUserData['coin_address'], $dBalance); } catch (BitcoinClientException $e) { verbose("SEND FAILED\n"); continue; } // Create transaction record - if ($transaction->addTransaction($aUserData['id'], $dBalance, 'Debit_AP', NULL, $aUserData['coin_address'], 0.1)) { - verbose("OK\n"); + if ($transaction->addTransaction($aUserData['id'], $dBalance - $config['txfee'], 'Debit_AP', NULL, $aUserData['coin_address']) && $transaction->addTransaction($aUserData['id'], $config['txfee'], 'TXFee', NULL, $aUserData['coin_address'])) { + // Notify user via mail + $aMailData['email'] = $user->getUserEmail($user->getUserName($aUserData['id'])); + $aMailData['subject'] = 'Auto Payout Completed'; + $aMailData['amount'] = $dBalance; + if (!$notification->sendNotification($aUserData['id'], 'auto_payout', $aMailData)) { + verbose("NOTIFY FAILED\n"); + } else { + verbose("OK\n"); + } } else { verbose("FAILED\n"); } + } else { verbose("SKIPPED\n"); } @@ -69,3 +82,8 @@ if (! empty($users)) { } else { verbose("No user has configured their AP > 0\n"); } + +// Mark this job as inactive +$setting->setValue('auto_payout_active', 0); + +?> diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index 025c1d3d..e68d9ff4 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -39,9 +39,8 @@ if ( $bitcoin->can_connect() === true ){ // Nothing to do so bail out if (empty($aTransactions['transactions'])) { - verbose("No new transactions since last block\n"); + verbose("No new RPC transactions since last block\n"); } else { - // Table header verbose("Blockhash\t\tHeight\tAmount\tConfirmations\tDiff\t\tTime\t\t\tStatus\n"); @@ -66,36 +65,67 @@ if (empty($aTransactions['transactions'])) { } } +verbose("\n"); // Now with our blocks added we can scan for their upstream shares $aAllBlocks = $block->getAllUnaccounted('ASC'); +if (empty($aAllBlocks)) { + verbose("No new unaccounted blocks found\n"); +} else { + // Loop through our unaccounted blocks + verbose("\nBlock ID\tBlock Height\tShare ID\tShares\tFinder\t\t\tStatus\n"); + foreach ($aAllBlocks as $iIndex => $aBlock) { + if (empty($aBlock['share_id'])) { + // Fetch this blocks upstream ID + if ($share->setUpstream($block->getLastUpstreamId())) { + $iCurrentUpstreamId = $share->getUpstreamId(); + $iAccountId = $user->getUserId($share->getUpstreamFinder()); + } else { + verbose("\nUnable to fetch blocks upstream share. Aborting!\n"); + verbose($share->getError() . "\n"); + exit; + } + // Fetch share information + if (!$iPreviousShareId = $block->getLastShareId()) { + $iPreviousShareId = 0; + verbose("\nUnable to find highest share ID found so far\n"); + verbose("If this is your first block, this is normal\n\n"); + } + $iRoundShares = $share->getRoundShares($iPreviousShareId, $iCurrentUpstreamId); -// Loop through our unaccounted blocks -verbose("Block ID\tBlock Height\tShare ID\tFinder\t\t\tStatus\n"); -foreach ($aAllBlocks as $iIndex => $aBlock) { - if (empty($aBlock['share_id'])) { - // Fetch this blocks upstream ID - if ($share->setUpstream($block->getLastUpstreamId())) { - $iCurrentUpstreamId = $share->getUpstreamId(); - $iAccountId = $user->getUserId($share->getUpstreamFinder()); - } else { - verbose("Unable to fetch blocks upstream share\n"); - verbose($share->getError() . "\n"); - continue; + // Store new information + $strStatus = "OK"; + if (!$block->setShareId($aBlock['id'], $iCurrentUpstreamId)) + $strStatus = "Share ID Failed"; + if (!$block->setFinder($aBlock['id'], $iAccountId)) + $strStatus = "Finder Failed"; + if (!$block->setShares($aBlock['id'], $iRoundShares)) + $strStatus = "Shares Failed"; + if ($config['block_bonus'] > 0 && !$transaction->addTransaction($iAccountId, $config['block_bonus'], 'Bonus', $aBlock['id'])) { + $strStatus = "Bonus Failed"; + } + + verbose( + $aBlock['id'] . "\t\t" + . $aBlock['height'] . "\t\t" + . $iCurrentUpstreamId . "\t\t" + . $iRoundShares . "\t" + . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t\t" + . $strStatus + . "\n" + ); + + // Notify users + $aAccounts = $notification->getNotificationAccountIdByType('new_block'); + if (is_array($aAccounts)) { + foreach ($aAccounts as $aData) { + $aMailData['height'] = $aBlock['height']; + $aMailData['subject'] = 'New Block'; + $aMailData['email'] = $user->getUserEmail($user->getUserName($aData['account_id'])); + $aMailData['shares'] = $iRoundShares; + $notification->sendNotification($aData['account_id'], 'new_block', $aMailData); + } + } } - // Store new information - $strStatus = "OK"; - if (!$block->setShareId($aBlock['id'], $iCurrentUpstreamId)) - $strStatus = "Share ID Failed"; - if (!$block->setFinder($aBlock['id'], $iAccountId)) - $strStatus = "Finder Failed"; - verbose( - $aBlock['id'] . "\t\t" - . $aBlock['height'] . "\t\t" - . $iCurrentUpstreamId . "\t\t" - . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t\t" - . $strStatus - . "\n" - ); } } ?> diff --git a/cronjobs/notifications.php b/cronjobs/notifications.php new file mode 100755 index 00000000..ec61002a --- /dev/null +++ b/cronjobs/notifications.php @@ -0,0 +1,56 @@ +#!/usr/bin/php +getAllIdleWorkers(); +if (empty($aWorkers)) { + verbose("No idle workers found\n"); +} else { + foreach ($aWorkers as $aWorker) { + $aData = $aWorker; + $aData['username'] = $user->getUserName($aWorker['account_id']); + $aData['subject'] = 'IDLE Worker : ' . $aWorker['username']; + $aData['worker'] = $aWorker['username']; + $aData['email'] = $user->getUserEmail($aData['username']); + if (!$notification->sendNotification($aWorker['account_id'], 'idle_worker', $aData)) + verbose($notification->getError() . "\n"); + } +} + +// We notified, lets check which recovered +$aNotifications = $notification->getAllActive('idle_worker'); +if (!empty($aNotifications)) { + foreach ($aNotifications as $aNotification) { + $aData = json_decode($aNotification['data'], true); + $aWorker = $worker->getWorker($aData['id']); + if ($aWorker['active'] == 1) { + if ($notification->setInactive($aNotification['id'])) { + verbose("Marked notification " . $aNotification['id'] . " as inactive\n"); + } else { + verbose("Failed to set notification inactive for " . $aWorker['username'] . "\n"); + } + } + } +} +?> diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php new file mode 100755 index 00000000..459384d2 --- /dev/null +++ b/cronjobs/pps_payout.php @@ -0,0 +1,127 @@ +#!/usr/bin/php +can_connect() === true ){ + $dDifficulty = $bitcoin->getdifficulty(); +} else { + verbose("Aborted: " . $bitcoin->can_connect() . "\n"); + exit(1); +} + +// Value per share calculation +$pps_value = number_format(round(50 / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); + +// Find our last share accounted and last inserted share for PPS calculations +$iPreviousShareId = $setting->getValue('pps_last_share_id'); +$iLastShareId = $share->getLastInsertedShareId(); + +// Check for all new shares, we start one higher as our last accounted share to avoid duplicates +$aAccountShares = $share->getSharesForAccounts($iPreviousShareId + 1, $iLastShareId); + +verbose("ID\tUsername\tInvalid\tValid\t\tPPS Value\t\tPayout\t\tDonation\tFee\t\tStatus\n"); + +foreach ($aAccountShares as $aData) { + // Take our valid shares and multiply by per share value + $aData['payout'] = number_format(round($aData['valid'] * $pps_value, 8), 8); + + // Defaults + $aData['fee' ] = 0; + $aData['donation'] = 0; + + // Calculate block fees + if ($config['fees'] > 0) + $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 8); + // Calculate donation amount + $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 8); + + verbose($aData['id'] . "\t" . + $aData['username'] . "\t" . + $aData['invalid'] . "\t" . + $aData['valid'] . "\t*\t" . + $pps_value . "\t=\t" . + $aData['payout'] . "\t" . + $aData['donation'] . "\t" . + $aData['fee'] . "\t"); + + $strStatus = "OK"; + // Add new credit transaction + if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit_PPS')) + $strStatus = "Transaction Failed"; + // Add new fee debit for this block + if ($aData['fee'] > 0 && $config['fees'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['fee'], 'Fee_PPS')) + $strStatus = "Fee Failed"; + // Add new donation debit + if ($aData['donation'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['donation'], 'Donation_PPS')) + $strStatus = "Donation Failed"; + verbose($strStatus . "\n"); +} + +// Store our last inserted ID for the next run +$setting->setValue('pps_last_share_id', $iLastShareId); + +verbose("\n\n------------------------------------------------------------------------------------\n\n"); + +// Fetch all unaccounted blocks +$aAllBlocks = $block->getAllUnaccounted('ASC'); +if (empty($aAllBlocks)) { + verbose("No new unaccounted blocks found\n"); +} + +// Go through blocks and archive/delete shares that have been accounted for +foreach ($aAllBlocks as $iIndex => $aBlock) { + // If we are running through more than one block, check for previous share ID + $iLastBlockShare = @$aAllBlocks[$iIndex - 1]['share_id'] ? @$aAllBlocks[$iIndex - 1]['share_id'] : 0; + // Per account statistics + $aAccountShares = $share->getSharesForAccounts(@$iLastBlockShare, $aBlock['share_id']); + foreach ($aAccountShares as $key => $aData) { + if (!$statistics->updateShareStatistics($aData, $aBlock['id'])) + verbose("Failed to update stats for this block on : " . $aData['username'] . "\n"); + } + // Move shares to archive + if ($config['archive_shares'] && $aBlock['share_id'] < $iLastShareId) { + if (!$share->moveArchive($aBlock['share_id'], $aBlock['id'], @$iLastBlockShare)) + verbose("Archving failed\n"); + } + // Delete shares + if ($aBlock['share_id'] < $iLastShareId && !$share->deleteAccountedShares($aBlock['share_id'], $iLastBlockShare)) { + verbose("\nERROR : Failed to delete accounted shares from " . $aBlock['share_id'] . " to " . $iLastBlockShare . ", aborting!\n"); + exit(1); + } + // Mark this block as accounted for + if (!$block->setAccounted($aBlock['id'])) { + verbose("\nERROR : Failed to mark block as accounted! Aborting!\n"); + exit(1); + } +} +?> diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index 30d0b018..61092006 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -22,6 +22,12 @@ limitations under the License. // Include all settings and classes require_once('shared.inc.php'); +// Check if we are set as the payout system +if ($config['payout_system'] != 'prop') { + verbose("Please activate this cron in configuration via payout_system = prop\n"); + exit(0); +} + // Fetch all unaccounted blocks $aAllBlocks = $block->getAllUnaccounted('ASC'); if (empty($aAllBlocks)) { @@ -32,25 +38,16 @@ if (empty($aAllBlocks)) { $count = 0; foreach ($aAllBlocks as $iIndex => $aBlock) { if (!$aBlock['accounted']) { - $iPreviousShareId = $aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; + $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; $iCurrentUpstreamId = $aBlock['share_id']; $aAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); - // Table header for block details - verbose("ID\tHeight\tTime\t\tShares\tFinder\t\tShare ID\tPrev Share\t\tStatus\n"); - verbose($aBlock['id'] . "\t" . $aBlock['height'] . "\t" . $aBlock['time'] . "\t" . $iRoundShares . "\t" . $user->getUserName($aBlock['account_id']) . "\t" . $iCurrentUpstreamId . "\t\t" . $iPreviousShareId); - if (empty($aAccountShares)) { verbose("\nNo shares found for this block\n\n"); sleep(2); continue; } - $strStatus = "OK"; - // Store share information for this block - if (!$block->setShares($aBlock['id'], $iRoundShares)) - $strStatus = "Shares Failed"; - verbose("\t\t$strStatus\n\n"); // Table header for account shares verbose("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee\t\tStatus\n"); diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index 730a9063..c9dfd949 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -16,7 +16,7 @@ PIDFILE='/tmp/mmcfe-ng-cron.pid' CRONHOME='.' # List of cruns to execute -CRONS="findblock.php proportional_payout.php blockupdate.php auto_payout.php tickerupdate.php" +CRONS="findblock.php proportional_payout.php pps_payout.php blockupdate.php auto_payout.php tickerupdate.php notifications.php statistics.php" # Additional arguments to pass to cronjobs CRONARGS="-v" diff --git a/cronjobs/shared.inc.php b/cronjobs/shared.inc.php index b12a7f77..a0b1dc5d 100644 --- a/cronjobs/shared.inc.php +++ b/cronjobs/shared.inc.php @@ -26,10 +26,10 @@ define("BASEPATH", "../public/"); define("SECURITY", 1); // Include our configuration (holding defines for the requires) -require_once(BASEPATH . '/include/config/global.inc.php'); +require_once(BASEPATH . 'include/config/global.inc.php'); // We include all needed files here, even though our templates could load them themself -require_once(BASEPATH . INCLUDE_DIR . '/autoloader.inc.php'); +require_once(INCLUDE_DIR . '/autoloader.inc.php'); // Parse command line $options = getopt("v"); diff --git a/cronjobs/statistics.php b/cronjobs/statistics.php new file mode 100755 index 00000000..d4fe65e0 --- /dev/null +++ b/cronjobs/statistics.php @@ -0,0 +1,49 @@ +#!/usr/bin/php +setGetCache(false); + +// Since fetching from cache is disabled, overwrite our stats +if (!$statistics->getRoundShares()) + verbose("Unable to fetch and store current round shares\n"); +if (!$statistics->getTopContributors('shares')) + verbose("Unable to fetch and store top share contributors\n"); +if (!$statistics->getTopContributors('hashes')) + verbose("Unable to fetch and store top hashrate contributors\n"); +if (!$statistics->getCurrentHashrate()) + verbose("Unable to fetch and store pool hashrate\n"); +// Admin specific statistics, we cache the global query due to slowness +if (!$statistics->getAllUserStats('%')) + verbose("Unable to fetch and store admin panel full user list\n"); + +// Per user share statistics based on all shares submitted +$stmt = $mysqli->prepare("SELECT DISTINCT SUBSTRING_INDEX( `username` , '.', 1 ) AS username FROM " . $share->getTableName()); +if ($stmt && $stmt->execute() && $result = $stmt->get_result()) { + while ($row = $result->fetch_assoc()) { + if (!$statistics->getUserShares($user->getUserId($row['username']))) + verbose("Failed to fetch and store user stats for " . $row['username'] . "\n"); + } +} +?> diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index ea16564e..a78d4a62 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -14,3 +14,5 @@ require_once(CLASS_DIR . '/worker.class.php'); require_once(CLASS_DIR . '/statistics.class.php'); require_once(CLASS_DIR . '/transaction.class.php'); require_once(CLASS_DIR . '/setting.class.php'); +require_once(CLASS_DIR . '/mail.class.php'); +require_once(CLASS_DIR . '/notification.class.php'); diff --git a/public/include/classes/bitcoin.class.php b/public/include/classes/bitcoin.class.php index fa88d415..ad26267d 100644 --- a/public/include/classes/bitcoin.class.php +++ b/public/include/classes/bitcoin.class.php @@ -246,8 +246,8 @@ class BitcoinClientException extends ErrorException { } } -require_once(BASEPATH . INCLUDE_DIR . "/xmlrpc.inc.php"); -require_once(BASEPATH . INCLUDE_DIR . "/jsonrpc.inc.php"); +require_once(INCLUDE_DIR . "/xmlrpc.inc.php"); +require_once(INCLUDE_DIR . "/jsonrpc.inc.php"); /** * Bitcoin client class for access to a Bitcoin server via JSON-RPC-HTTP[S] diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index aad32c08..743ce67e 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -43,6 +43,18 @@ class Block { return false; } + /** + * Get our last, highest share ID inserted for a block + * @param none + * @return int data Share ID + **/ + public function getLastShareId() { + $stmt = $this->mysqli->prepare("SELECT MAX(share_id) AS share_id FROM $this->table LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->share_id; + return false; + } + /** * Fetch all unaccounted blocks * @param order string Sort order, default ASC diff --git a/public/include/classes/mail.class.php b/public/include/classes/mail.class.php new file mode 100644 index 00000000..e34423c5 --- /dev/null +++ b/public/include/classes/mail.class.php @@ -0,0 +1,67 @@ +debug = $debug; + } + public function setMysql($mysqli) { + $this->mysqli = $mysqli; + } + public function setSmarty($smarty) { + $this->smarty = $smarty; + } + public function setUser($user) { + $this->user = $user; + } + public function setConfig($config) { + $this->config = $config; + } + public function setErrorMessage($msg) { + $this->sError = $msg; + } + public function getError() { + return $this->sError; + } + function checkStmt($bState) { + $this->debug->append("STA " . __METHOD__, 4); + if ($bState ===! true) { + $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); + $this->setErrorMessage('Internal application Error'); + return false; + } + return true; + } + + public function sendMail($template, $aData) { + $this->smarty->assign('WEBSITENAME', $this->config['website']['name']); + $this->smarty->assign('SUBJECT', $aData['subject']); + $this->smarty->assign('DATA', $aData); + $headers = 'From: Website Administration <' . $this->config['website']['email'] . ">\n"; + $headers .= "MIME-Version: 1.0\n"; + $headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n"; + if (mail($aData['email'], + $this->smarty->fetch(BASEPATH . 'templates/mail/subject.tpl'), + $this->smarty->fetch(BASEPATH . 'templates/mail/' . $template . '.tpl'), + $headers)) { + return true; + } else { + $this->setErrorMessage("Unable to send mail"); + return false; + } + return false; + } +} + +// Make our class available automatically +$mail = new Mail (); +$mail->setDebug($debug); +$mail->setMysql($mysqli); +$mail->setSmarty($smarty); +$mail->setConfig($config); +?> diff --git a/public/include/classes/notification.class.php b/public/include/classes/notification.class.php new file mode 100644 index 00000000..b38bac2e --- /dev/null +++ b/public/include/classes/notification.class.php @@ -0,0 +1,191 @@ + 'active', + 'type' => 'i', + 'value' => 0 + ); + return $this->updateSingle($id, $field); + } + + /** + * Update a single row in a table + * @param userID int Account ID + * @param field string Field to update + * @return bool + **/ + private function updateSingle($id, $field, $table='') { + if (empty($table)) $table = $this->table; + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("UPDATE $table SET " . $field['name'] . " = ? WHERE id = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param($field['type'].'i', $field['value'], $id) && $stmt->execute()) + return true; + $this->debug->append("Unable to update " . $field['name'] . " with " . $field['value'] . " for ID $id"); + return false; + } + /** + * We check our notification table for existing data + * so we can avoid duplicate entries + **/ + public function isNotified($aData) { + $this->debug->append("STA " . __METHOD__, 4); + $data = json_encode($aData); + $stmt = $this->mysqli->prepare("SELECT id FROM $this->table WHERE data = ? AND active = 1 LIMIT 1"); + if ($stmt && $stmt->bind_param('s', $data) && $stmt->execute() && $stmt->store_result() && $stmt->num_rows == 1) + return true; + // Catchall + // Does not seem to have a notification set + return false; + } + + /** + * Get all active notifications + **/ + public function getAllActive($strType) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt =$this->mysqli->prepare("SELECT id, data FROM $this->table WHERE active = 1 AND type = ?"); + if ($stmt && $stmt->bind_param('s', $strType) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + // Catchall + return false; + } + + /** + * Add a new notification to the table + * @param type string Type of the notification + * @return bool + **/ + public function addNotification($account_id, $type, $data) { + $this->debug->append("STA " . __METHOD__, 4); + // Store notification data as json + $data = json_encode($data); + $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, type, data, active) VALUES (?, ?,?,1)"); + if ($stmt && $stmt->bind_param('iss', $account_id, $type, $data) && $stmt->execute()) + return true; + $this->debug->append("Failed to add notification for $type with $data: " . $this->mysqli->error); + $this->setErrorMessage("Unable to add new notification " . $this->mysqli->error); + return false; + } + + /** + * Fetch notifications for a user account + * @param id int Account ID + * @return array Notification data + **/ + public function getNofifications($account_id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE account_id = ? ORDER BY time DESC"); + if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + // Catchall + return false; + } + + /** + * Fetch notification settings for user account + * @param id int Account ID + * @return array Notification settings + **/ + public function getNotificationSettings($account_id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT * FROM $this->tableSettings WHERE account_id = ?"); + if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute() && $result = $stmt->get_result()) { + while ($row = $result->fetch_assoc()) { + $aData[$row['type']] = $row['active']; + } + return $aData; + } + // Catchall + return false; + } + + /** + * Get all accounts that wish to receive a specific notification + * @param strType string Notification type + * @return data array User Accounts + **/ + public function getNotificationAccountIdByType($strType) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT account_id FROM $this->tableSettings WHERE type = ? AND active = 1"); + if ($stmt && $stmt->bind_param('s', $strType) && $stmt->execute() && $result = $stmt->get_result()) { + return $result->fetch_all(MYSQLI_ASSOC); + } + // Catchall + return false; + } + + /** + * Update accounts notification settings + * @param account_id int Account ID + * @param data array Data array + * @return bool + **/ + public function updateSettings($account_id, $data) { + $this->debug->append("STA " . __METHOD__, 4); + $failed = $ok = 0; + foreach ($data as $type => $active) { + // Does an entry exist already + $stmt = $this->mysqli->prepare("SELECT * FROM $this->tableSettings WHERE account_id = ? AND type = ?"); + if ($stmt && $stmt->bind_param('is', $account_id, $type) && $stmt->execute() && $stmt->store_result() && $stmt->num_rows() > 0) { + // We found a matching row + $stmt = $this->mysqli->prepare("UPDATE $this->tableSettings SET active = ? WHERE type = ? AND account_id = ?"); + if ($stmt && $stmt->bind_param('isi', $active, $type, $account_id) && $stmt->execute() && $stmt->close()) { + $ok++; + } else { + $failed++; + } + } else { + $stmt = $this->mysqli->prepare("INSERT INTO $this->tableSettings (active, type, account_id) VALUES (?,?,?)"); + if ($stmt && $stmt->bind_param('isi', $active, $type, $account_id) && $stmt->execute()) { + $ok++; + } else { + $failed++; + } + } + } + if ($failed > 0) { + $this->setErrorMessage('Failed to update ' . $failed . ' settings'); + return false; + } + return true; + } + + /** + * Send a specific notification setup in notification_settings + * @param type string Notification type + * @return bool + **/ + public function sendNotification($account_id, $strType, $aMailData) { + // Check if we notified for this event already + if ( $this->isNotified($aMailData) ) { + $this->setErrorMessage('A notification for this event has been sent already'); + return false; + } + // Check if this user wants strType notifications + $stmt = $this->mysqli->prepare("SELECT account_id FROM $this->tableSettings WHERE type = ? AND active = 1 AND account_id = ?"); + if ($stmt && $stmt->bind_param('si', $strType, $account_id) && $stmt->execute() && $stmt->bind_result($id) && $stmt->fetch()) { + if ($stmt->close() && $this->sendMail('notifications/' . $strType, $aMailData) && $this->addNotification($account_id, $strType, $aMailData)) + return true; + } else { + $this->setErrorMessage('User disabled ' . $strType . ' notifications'); + } + return false; + } +} + +$notification = new Notification(); +$notification->setDebug($debug); +$notification->setMysql($mysqli); +$notification->setSmarty($smarty); +$notification->setConfig($config); + +?> diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index b0a3b249..85935a2d 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -44,6 +44,21 @@ class Share { return $this->table; } + /** + * Get last inserted Share ID from Database + * Used for PPS calculations without moving to archive + **/ + public function getLastInsertedShareId() { + $stmt = $this->mysqli->prepare(" + SELECT MAX(id) AS id FROM $this->table + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->id; + // Catchall + $this->setErrorMessage('Failed to fetch last inserted share ID'); + return false; + } + /** * Get all valid shares for this round * @param previous_upstream int Previous found share accepted by upstream to limit results @@ -172,7 +187,8 @@ class Share { ORDER BY id ASC LIMIT 1"); if ($this->checkStmt($stmt) && $stmt->bind_param('i', $last) && $stmt->execute() && $result = $stmt->get_result()) { $this->oUpstream = $result->fetch_object(); - return true; + if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) + return true; } // Catchall return false; diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index 8b12391e..a30aba1b 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -13,6 +13,7 @@ if (!defined('SECURITY')) class Statistics { private $sError = ''; private $table = 'statistics_shares'; + private $getcache = true; public function __construct($debug, $mysqli, $config, $share, $user, $block, $memcache) { $this->debug = $debug; @@ -34,6 +35,14 @@ class Statistics { return $this->sError; } + // Disable fetching values from cache + public function setGetCache($set=false) { + $this->getcache = $set; + } + public function getGetCache() { + return $this->getcache; + } + private function checkStmt($bState) { if ($bState ===! true) { $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); @@ -54,7 +63,7 @@ class Statistics { $stmt = $this->mysqli->prepare(" SELECT b.*, a.username as finder FROM " . $this->block->getTableName() . " AS b - LEFT JOIN accounts AS a + LEFT JOIN " . $this->user->getTableName() . " AS a ON b.account_id = a.id ORDER BY height DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $limit) && $stmt->execute() && $result = $stmt->get_result()) @@ -88,14 +97,10 @@ class Statistics { **/ public function getCurrentHashrate() { $this->debug->append("STA " . __METHOD__, 4); - if ($data = $this->memcache->get(__FUNCTION__)) return $data; + if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" - SELECT SUM(hashrate) AS hashrate FROM - ( - SELECT ROUND(COUNT(id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) AS hashrate FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) - UNION - SELECT ROUND(COUNT(id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) AS hashrate FROM " . $this->share->getArchiveTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) - ) AS sum"); + SELECT ROUND(COUNT(id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) AS hashrate FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) + "); // Catchall if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__, $result->fetch_object()->hashrate); $this->debug->append("Failed to get hashrate: " . $this->mysqli->error); @@ -111,12 +116,8 @@ class Statistics { $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" - SELECT ROUND(SUM(sharerate) / 600, 2) AS sharerate FROM - ( - SELECT COUNT(id) AS sharerate FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) - UNION ALL - SELECT COUNT(id) AS sharerate FROM " . $this->share->getArchiveTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) - ) AS sum"); + SELECT ROUND(COUNT(id) / 600, 2) AS sharerate FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) + "); if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__, $result->fetch_object()->sharerate); // Catchall $this->debug->append("Failed to fetch share rate: " . $this->mysqli->error); @@ -130,7 +131,7 @@ class Statistics { **/ public function getRoundShares() { $this->debug->append("STA " . __METHOD__, 4); - if ($data = $this->memcache->get(__FUNCTION__)) return $data; + if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" SELECT ( SELECT IFNULL(count(id), 0) @@ -155,7 +156,7 @@ class Statistics { **/ public function getUserShares($account_id) { $this->debug->append("STA " . __METHOD__, 4); - if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; + if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT ( @@ -183,12 +184,42 @@ class Statistics { return false; } + /** + * Admin panel specific query + * @return data array invlid and valid shares for all accounts + **/ + public function getAllUserStats($filter='%') { + $this->debug->append("STA " . __METHOD__, 4); + if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__ . $filter)) return $data; + $stmt = $this->mysqli->prepare(" + SELECT + a.id AS id, + a.is_admin as is_admin, + a.is_locked as is_locked, + a.username AS username, + a.donate_percent AS donate_percent, + a.email AS email, + COUNT(s.id) AS shares + FROM " . $this->user->getTableName() . " AS a + LEFT JOIN " . $this->share->getTableName() . " AS s + ON a.username = SUBSTRING_INDEX( s.username, '.', 1 ) + WHERE + a.username LIKE ? + GROUP BY username + ORDER BY username + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('s', $filter) && $stmt->execute() && $result = $stmt->get_result()) { + return $this->memcache->setCache(__FUNCTION__ . $filter, $result->fetch_all(MYSQLI_ASSOC)); + } + } + /** * Same as getUserShares for Hashrate * @param account_id integer User ID * @return data integer Current Hashrate in khash/s **/ public function getUserHashrate($account_id) { + $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT ROUND(COUNT(s.id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) AS hashrate @@ -204,6 +235,28 @@ class Statistics { return false; } + /** + * Same as getUserHashrate for Sharerate + * @param account_id integer User ID + * @return data integer Current Sharerate in shares/s + **/ + public function getUserSharerate($account_id) { + $this->debug->append("STA " . __METHOD__, 4); + if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; + $stmt = $this->mysqli->prepare(" + SELECT COUNT(s.id)/600 AS sharerate + FROM " . $this->share->getTableName() . " AS s, + " . $this->user->getTableName() . " AS u + WHERE u.username = SUBSTRING_INDEX( s.username, '.', 1 ) + AND s.time > DATE_SUB(now(), INTERVAL 10 MINUTE) + AND u.id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param("i", $account_id) && $stmt->execute() && $result = $stmt->get_result() ) + return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_object()->sharerate); + // Catchall + $this->debug->append("Failed to fetch sharerate: " . $this->mysqli->error); + return false; + } + /** * Get hashrate for a specific worker * @param worker_id int Worker ID to fetch hashrate for @@ -234,7 +287,7 @@ class Statistics { **/ public function getTopContributors($type='shares', $limit=15) { $this->debug->append("STA " . __METHOD__, 4); - if ($data = $this->memcache->get(__FUNCTION__ . $type . $limit)) return $data; + if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__ . $type . $limit)) return $data; switch ($type) { case 'shares': $stmt = $this->mysqli->prepare(" @@ -242,6 +295,7 @@ class Statistics { COUNT(id) AS shares, SUBSTRING_INDEX( username, '.', 1 ) AS account FROM " . $this->share->getTableName() . " + WHERE our_result = 'Y' GROUP BY account ORDER BY shares DESC LIMIT ?"); @@ -258,6 +312,7 @@ class Statistics { SUBSTRING_INDEX( username, '.', 1 ) AS account FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) + AND our_result = 'Y' GROUP BY account ORDER BY hashrate DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $limit) && $stmt->execute() && $result = $stmt->get_result()) @@ -270,7 +325,6 @@ class Statistics { /** * get Hourly hashrate for a user - * Not working yet since I was not able to solve this via SQL queries * @param account_id int User ID * @return data array NOT FINISHED YET **/ @@ -279,24 +333,50 @@ class Statistics { if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT - ROUND(COUNT(s.id) * POW(2, 12)/600/1000) AS hashrate, + ROUND(COUNT(s.id) * POW(2, " . $this->config['difficulty'] . ") / 3600 / 1000) AS hashrate, HOUR(s.time) AS hour - FROM " . $this->share->getTableName() . " AS s, accounts AS a - WHERE time < NOW() - INTERVAL 1 HOUR AND time > NOW() - INTERVAL 25 HOUR + FROM " . $this->share->getTableName() . " AS s, accounts AS a + WHERE time < NOW() - INTERVAL 1 HOUR + AND time > NOW() - INTERVAL 25 HOUR AND a.username = SUBSTRING_INDEX( s.username, '.', 1 ) AND a.id = ? - GROUP BY HOUR(time) - UNION ALL - SELECT - ROUND(COUNT(s.id) * POW(2, 12)/600/1000) AS hashrate, - HOUR(s.time) AS hour - FROM " . $this->share->getArchiveTableName() . " AS s, accounts AS a - WHERE time < NOW() - INTERVAL 1 HOUR AND time > NOW() - INTERVAL 25 HOUR - AND a.username = SUBSTRING_INDEX( s.username, '.', 1 ) - AND a.id = ? - GROUP BY HOUR(time)"); - if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $account_id, $account_id) && $stmt->execute() && $result = $stmt->get_result()) - return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_all(MYSQLI_ASSOC), 3600); + GROUP BY HOUR(time) + "); + if ($this->checkStmt($stmt) && $stmt->bind_param("i", $account_id) && $stmt->execute() && $result = $stmt->get_result()) { + $aData = array(); + while ($row = $result->fetch_assoc()) { + $aData[$row['hour']] = $row['hashrate']; + } + return $this->memcache->setCache(__FUNCTION__ . $account_id, $aData); + } + // Catchall + $this->debug->append("Failed to fetch hourly hashrate: " . $this->mysqli->error); + return false; + } + + /** + * get Hourly hashrate for the pool + * @param none + * @return data array NOT FINISHED YET + **/ + public function getHourlyHashrateByPool() { + $this->debug->append("STA " . __METHOD__, 4); + if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; + $stmt = $this->mysqli->prepare(" + SELECT + ROUND(COUNT(s.id) * POW(2, " . $this->config['difficulty'] . ") / 3600 / 1000) AS hashrate, + HOUR(s.time) AS hour + FROM " . $this->share->getTableName() . " AS s + WHERE time < NOW() - INTERVAL 1 HOUR + AND time > NOW() - INTERVAL 25 HOUR + GROUP BY HOUR(time) + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) { + while ($row = $result->fetch_assoc()) { + $aData[$row['hour']] = $row['hashrate']; + } + return $this->memcache->setCache(__FUNCTION__, @$aData); + } // Catchall $this->debug->append("Failed to fetch hourly hashrate: " . $this->mysqli->error); return false; diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 15f29c77..38e1b429 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -55,35 +55,23 @@ class Transaction { **/ public function setOrphan($block_id) { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare(" - UPDATE $this->table - SET type = 'Orphan_Credit' - WHERE type = 'Credit' - AND block_id = ? - "); - if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { - $this->debug->append("Failed to set orphan credit transactions for $block_id"); - return false; - } - $stmt = $this->mysqli->prepare(" - UPDATE $this->table - SET type = 'Orphan_Fee' - WHERE type = 'Fee' - AND block_id = ? - "); - if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { - $this->debug->append("Failed to set orphan fee transactions for $block_id"); - return false; - } - $stmt = $this->mysqli->prepare(" - UPDATE $this->table - SET type = 'Orphan_Donation' - WHERE type = 'Donation' - AND block_id = ? - "); - if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { - $this->debug->append("Failed to set orphan donation transactions for $block_id"); - return false; + $aOrphans = array( + 'Credit' => 'Orphan_Credit', + 'Fee' => 'Orphan_Fee', + 'Donation' => 'Orphan_Donation', + 'Bonus' => 'Orphan_Bonus' + ); + foreach ($aOrphans as $from => $to) { + $stmt = $this->mysqli->prepare(" + UPDATE $this->table + SET type = '$to' + WHERE type = '$from' + AND block_id = ? + "); + if (!($this->checkStmt($stmt) && $stmt->bind_param('i', $block_id) && $stmt->execute())) { + $this->debug->append("Failed to set orphan $from => $to transactions for $block_id"); + return false; + } } return true; } @@ -130,6 +118,7 @@ class Transaction { /** * Get total balance for all users locked in wallet + * This includes any outstanding unconfirmed transactions! * @param none * @return data double Amount locked for users **/ @@ -142,8 +131,10 @@ class Transaction { SELECT sum(t.amount) AS credit FROM $this->table AS t LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id - WHERE t.type = 'Credit' - AND b.confirmations >= ? + WHERE ( + ( t.type IN ('Credit','Bonus') AND b.confirmations >= ? ) OR + ( t.type = 'Credit_PPS' ) + ) ) AS t1, ( SELECT sum(t.amount) AS debit @@ -152,10 +143,12 @@ class Transaction { ) AS t2, ( SELECT sum(t.amount) AS other - FROM transactions AS t + FROM " . $this->table . " AS t LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id - WHERE t.type IN ('Donation','Fee') - AND b.confirmations >= ? + WHERE ( + ( t.type IN ('Donation','Fee') AND b.confirmations >= ? ) OR + t.type IN ('Donation_PPS','Fee_PPS','TXFee') + ) ) AS t3"); if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $this->config['confirmations'], $this->config['confirmations']) && $stmt->execute() && $stmt->bind_result($dBalance) && $stmt->fetch()) return $dBalance; @@ -173,14 +166,19 @@ class Transaction { public function getBalance($account_id) { $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare(" - SELECT ROUND(IFNULL(t1.credit, 0) - IFNULL(t2.debit, 0) - IFNULL(t3.other, 0), 8) AS balance + SELECT + ROUND(IFNULL(t1.credit, 0) - IFNULL(t2.debit, 0) - IFNULL(t3.other, 0), 8) AS confirmed, + ROUND(IFNULL(t4.credit, 0) - IFNULL(t5.other, 0), 8) AS unconfirmed FROM ( SELECT sum(t.amount) AS credit FROM $this->table AS t LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id - WHERE t.type = 'Credit' - AND b.confirmations >= ? + WHERE + ( + ( t.type IN ('Credit','Bonus') AND b.confirmations >= ? ) OR + ( t.type = 'Credit_PPS' ) + ) AND t.account_id = ? ) AS t1, ( @@ -193,20 +191,41 @@ class Transaction { SELECT sum(t.amount) AS other FROM $this->table AS t LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id - WHERE t.type IN ('Donation','Fee') - AND b.confirmations >= ? + WHERE + ( + ( t.type IN ('Donation','Fee') AND b.confirmations >= ? ) OR + ( t.type IN ('Donation_PPS', 'Fee_PPS', 'TXFee') ) + ) AND t.account_id = ? - ) AS t3 + ) AS t3, + ( + SELECT sum(t.amount) AS credit + FROM $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id + WHERE + t.type IN ('Credit','Bonus') AND b.confirmations < ? + AND t.account_id = ? + ) AS t4, + ( + SELECT sum(t.amount) AS other + FROM $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id + WHERE + ( + t.type IN ('Donation','Fee') AND b.confirmations < ? + ) + AND t.account_id = ? + ) AS t5 "); if ($this->checkStmt($stmt)) { - $stmt->bind_param("iiiii", $this->config['confirmations'], $account_id, $account_id, $this->config['confirmations'], $account_id); + $stmt->bind_param("iiiiiiiii", $this->config['confirmations'], $account_id, $account_id, $this->config['confirmations'], $account_id, $this->config['confirmations'], $account_id, $this->config['confirmations'], $account_id); if (!$stmt->execute()) { $this->debug->append("Unable to execute statement: " . $stmt->error); $this->setErrorMessage("Fetching balance failed"); } $result = $stmt->get_result(); $stmt->close(); - return $result->fetch_object()->balance; + return $result->fetch_assoc(); } return false; } diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index d6582676..49de7b5f 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -26,35 +26,75 @@ class User { public function getError() { return $this->sError; } - public function getUserName($id) { return $this->getSingle($id, 'username', 'id'); } - public function getUserId($username) { return $this->getSingle($username, 'id', 'username', 's'); } - public function getUserEmail($username) { return $this->getSingle($username, 'email', 'username', 's'); } - + public function getUserAdmin($id) { + return $this->getSingle($id, 'is_admin', 'id'); + } + public function getUserLocked($id) { + return $this->getSingle($id, 'is_locked', 'id'); + } public function getUserToken($id) { return $this->getSingle($id, 'token', 'id'); } - + public function getUserIp($id) { + return $this->getSingle($id, 'loggedIp', 'id'); + } + public function getUserFailed($id) { + return $this->getSingle($id, 'failed_logins', 'id'); + } public function getIdFromToken($token) { return $this->getSingle($token, 'id', 'token', 's'); } - - public function setUserToken($id) { - $field = array( - 'name' => 'token', - 'type' => 's', - 'value' => hash('sha256', $id.time().$this->salt) - ); + public function isLocked($id) { + return $this->getUserLocked($id); + } + public function isAdmin($id) { + return $this->getUserAdmin($id); + } + public function changeLocked($id) { + $field = array('name' => 'is_locked', 'type' => 'i', 'value' => !$this->isLocked($id)); return $this->updateSingle($id, $field); } + public function changeAdmin($id) { + $field = array('name' => 'is_admin', 'type' => 'i', 'value' => !$this->isAdmin($id)); + return $this->updateSingle($id, $field); + } + public function setUserToken($id) { + $field = array('name' => 'token', 'type' => 's', 'value' => hash('sha256', $id.time().$this->salt)); + return $this->updateSingle($id, $field); + } + private function setUserFailed($id, $value) { + $field = array( 'name' => 'failed_logins', 'type' => 'i', 'value' => $value); + return $this->updateSingle($id, $field); + } + private function incUserFailed($id) { + $field = array( 'name' => 'failed_logins', 'type' => 'i', 'value' => $this->getUserFailed($id) + 1); + return $this->updateSingle($id, $field); + } + private function setUserIp($id, $ip) { + $field = array( 'name' => 'loggedIp', 'type' => 's', 'value' => $ip ); + return $this->updateSingle($id, $field); + } + + /** + * Fetch all users for administrative tasks + * @param none + * @return data array All users with db columns as array fields + **/ + public function getUsers($filter='%') { + $stmt = $this->mysqli->prepare("SELECT * FROM " . $this->getTableName() . " WHERE username LIKE ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('s', $filter) && $stmt->execute() && $result = $stmt->get_result()) { + return $result->fetch_all(MYSQLI_ASSOC); + } + } /** * Check user login @@ -65,10 +105,20 @@ class User { public function checkLogin($username, $password) { $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Checking login for $username with password $password", 2); - if ( $this->checkUserPassword($username, $password) ) { + if ($this->isLocked($this->getUserId($username))) { + $this->setErrorMessage("Account is locked. Please contact site support."); + return false; + } + if ( $this->checkUserPassword($username, $password)) { $this->createSession($username); + $this->setUserFailed($this->getUserId($username), 0); + $this->setUserIp($this->getUserId($username), $_SERVER['REMOTE_ADDR']); return true; } + $this->setErrorMessage("Invalid username or password"); + if ($id = $this->getUserId($username)) + $this->incUserFailed($id); + return false; } @@ -166,7 +216,7 @@ class User { **/ private function updateSingle($id, $field) { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare("UPDATE $this->table SET " . $field['name'] . " = ? WHERE id = ? LIMIT 1"); + $stmt = $this->mysqli->prepare("UPDATE $this->table SET `" . $field['name'] . "` = ? WHERE id = ? LIMIT 1"); if ($this->checkStmt($stmt) && $stmt->bind_param($field['type'].'i', $field['value'], $id) && $stmt->execute()) return true; $this->debug->append("Unable to update " . $field['name'] . " with " . $field['value'] . " for ID $id"); @@ -224,20 +274,40 @@ class User { * @param donat float donation % of income * @return bool **/ - public function updateAccount($userID, $address, $threshold, $donate) { + public function updateAccount($userID, $address, $threshold, $donate, $email) { $this->debug->append("STA " . __METHOD__, 4); $bUser = false; - $threshold = min(250, max(0, floatval($threshold))); - if ($threshold < 1) $threshold = 0.0; + + // number validation checks + if ($threshold < $this->config['ap_threshold']['min'] && $threshold != 0) { + $this->setErrorMessage('Threshold below configured minimum of ' . $this->config['ap_threshold']['min']); + return false; + } else if ($threshold > $this->config['ap_threshold']['max']) { + $this->setErrorMessage('Threshold above configured maximum of ' . $this->config['ap_threshold']['max']); + return false; + } + if ($donate < 0) { + $this->setErrorMessage('Donation below allowed 0% limit'); + return false; + } else if ($donate > 100) { + $this->setErrorMessage('Donation above allowed 100% limit'); + return false; + } + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $this->setErrorMessage('Invalid email address'); + return false; + } + // Number sanitizer, just in case we fall through above + $threshold = min($this->config['ap_threshold']['max'], max(0, floatval($threshold))); $donate = min(100, max(0, floatval($donate))); - $stmt = $this->mysqli->prepare("UPDATE $this->table SET coin_address = ?, ap_threshold = ?, donate_percent = ? WHERE id = ?"); - $stmt->bind_param('sddi', $address, $threshold, $donate, $userID); - $stmt->execute(); - if ( $stmt->errno == 0 ) { - $stmt->close(); + // We passed all validation checks so update the account + $stmt = $this->mysqli->prepare("UPDATE $this->table SET coin_address = ?, ap_threshold = ?, donate_percent = ?, email = ? WHERE id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('sddsi', $address, $threshold, $donate, $email, $userID) && $stmt->execute()) return true; - } + // Catchall + $this->setErrorMessage('Failed to update your account'); + $this->debug->append('Account update failed: ' . $this->mysqli->error); return false; } @@ -266,15 +336,15 @@ class User { private function checkUserPassword($username, $password) { $this->debug->append("STA " . __METHOD__, 4); $user = array(); - $stmt = $this->mysqli->prepare("SELECT username, id FROM $this->table WHERE username=? AND pass=? LIMIT 1"); + $stmt = $this->mysqli->prepare("SELECT username, id, is_admin FROM $this->table WHERE username=? AND pass=? LIMIT 1"); if ($this->checkStmt($stmt)) { $stmt->bind_param('ss', $username, hash('sha256', $password.$this->salt)); $stmt->execute(); - $stmt->bind_result($row_username, $row_id); + $stmt->bind_result($row_username, $row_id, $row_admin); $stmt->fetch(); $stmt->close(); // Store the basic login information - $this->user = array('username' => $row_username, 'id' => $row_id); + $this->user = array('username' => $row_username, 'id' => $row_id, 'is_admin' => $row_admin); return $username === $row_username; } return false; @@ -303,7 +373,8 @@ class User { $this->debug->append("STA " . __METHOD__, 4); session_destroy(); session_regenerate_id(true); - return true; + // Enforce a page reload + header("Location: index.php"); } /** @@ -325,7 +396,7 @@ class User { $this->debug->append("Fetching user information for user id: $userID"); $stmt = $this->mysqli->prepare(" SELECT - id, username, pin, api_key, admin, + id, username, pin, api_key, is_admin, email, IFNULL(donate_percent, '0') as donate_percent, coin_address, ap_threshold FROM $this->table WHERE id = ? LIMIT 0,1"); @@ -363,7 +434,7 @@ class User { $this->setErrorMessage( 'Password do not match' ); return false; } - if (!empty($email1) && !filter_var($email1, FILTER_VALIDATE_EMAIL)) { + if (empty($email1) || !filter_var($email1, FILTER_VALIDATE_EMAIL)) { $this->setErrorMessage( 'Invalid e-mail address' ); return false; } @@ -383,7 +454,7 @@ class User { "); } else { $stmt = $this->mysqli->prepare(" - INSERT INTO $this->table (username, pass, email, pin, api_key, admin) + INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin) VALUES (?, ?, ?, ?, ?, 1) "); } @@ -455,6 +526,7 @@ class User { } $smarty->assign('TOKEN', $token); $smarty->assign('USERNAME', $username); + $smarty->assign('SUBJECT', 'Password Reset Request'); $smarty->assign('WEBSITENAME', $this->config['website']['name']); $headers = 'From: Website Administration <' . $this->config['website']['email'] . ">\n"; $headers .= "MIME-Version: 1.0\n"; @@ -470,6 +542,22 @@ class User { } return false; } + + /** + * Check if a user is authenticated and allowed to login + * Checks the $_SESSION for existing data + * Destroys the session if account is now locked + * @param none + * @return bool + **/ + public function isAuthenticated() { + $this->debug->append("STA " . __METHOD__, 4); + if (@$_SESSION['AUTHENTICATED'] == true && ! $this->isLocked($_SESSION['USERDATA']['id']) && $this->getUserIp($_SESSION['USERDATA']['id']) == $_SERVER['REMOTE_ADDR']) + return true; + // Catchall + $this->logoutUser(); + return false; + } } // Make our class available automatically diff --git a/public/include/classes/worker.class.php b/public/include/classes/worker.class.php index 5a5c19c4..6a9f6524 100644 --- a/public/include/classes/worker.class.php +++ b/public/include/classes/worker.class.php @@ -43,17 +43,60 @@ class Worker { public function updateWorkers($account_id, $data) { $this->debug->append("STA " . __METHOD__, 4); $username = $this->user->getUserName($account_id); + $iFailed = 0; foreach ($data as $key => $value) { // Prefix the WebUser to Worker name $value['username'] = "$username." . $value['username']; - $stmt = $this->mysqli->prepare("UPDATE $this->table SET password = ?, username = ? WHERE account_id = ? AND id = ?"); - if ($this->checkStmt($stmt)) { - if (!$stmt->bind_param('ssii', $value['password'], $value['username'], $account_id, $key)) return false; - if (!$stmt->execute()) return false; - $stmt->close(); - } + $stmt = $this->mysqli->prepare("UPDATE $this->table SET password = ?, username = ?, monitor = ? WHERE account_id = ? AND id = ?"); + if ( ! ( $this->checkStmt($stmt) && $stmt->bind_param('ssiii', $value['password'], $value['username'], $value['monitor'], $account_id, $key) && $stmt->execute()) ) + $iFailed++; } - return true; + if ($iFailed == 0) + return true; + // Catchall + $this->setErrorMessage('Failed to update ' . $iFailed . ' worker.'); + return false; + } + + /** + * Fetch all IDLE workers that have monitoring enabled + * @param none + * @return data array Workers in IDLE state and monitoring enabled + **/ + public function getAllIdleWorkers() { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT account_id, id, username + FROM " . $this->table . " + WHERE monitor = 1 AND ( SELECT SIGN(COUNT(id)) FROM " . $this->share->getTableName() . " WHERE username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) = 0"); + + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + // Catchall + $this->setErrorMessage("Unable to fetch IDLE, monitored workers"); + echo $this->mysqli->error; + return false; + } + + /** + * Fetch a specific worker and its status + * @param id int Worker ID + * @return mixed array Worker details + **/ + public function getWorker($id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT id, username, password, monitor, + ( SELECT SIGN(COUNT(id)) FROM " . $this->share->getTableName() . " WHERE username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS active, + ( SELECT ROUND(COUNT(id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) FROM " . $this->share->getTableName() . " WHERE username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS hashrate + FROM $this->table + WHERE id = ? + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + // Catchall + echo $this->mysqli->error; + return false; } /** @@ -64,7 +107,7 @@ class Worker { public function getWorkers($account_id) { $this->debug->append("STA " . __METHOD__, 4); $stmt = $this->mysqli->prepare(" - SELECT id, username, password, + SELECT id, username, password, monitor, ( SELECT SIGN(COUNT(id)) FROM " . $this->share->getTableName() . " WHERE username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS active, ( SELECT ROUND(COUNT(id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) FROM " . $this->share->getTableName() . " WHERE username = $this->table.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS hashrate FROM $this->table diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 2bb6069f..29a46658 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -23,13 +23,29 @@ define('SALT', 'PLEASEMAKEMESOMETHINGRANDOM'); $config = array( 'price' => array( 'url' => 'https://btc-e.com/api/2', - 'target' => '/ltc_usd/ticker' + 'target' => '/ltc_usd/ticker', + 'currency' => 'USD' // Used in ministats template + ), + 'ap_threshold' => array( + 'min' => 1, + 'max' => 250 ), 'website' => array( + 'registration' => true, // Allow new users to register 'name' => 'The Pool', 'slogan' => 'Resistance is futile', 'email' => 'test@example.com', // Mail address used for notifications ), + // See: http://www.google.com/recaptcha + 'recaptcha' => array( + 'enabled' => false, // Enable re-captcha during registraion + 'public_key' => 'YOUR_PUBLIC_RECAPTCHA_KEY', + 'private_key' => 'YOUR_PRIVATE_RECAPTCHA_KEY' + ), + 'currency' => 'LTC', // Currency name to be used on website + 'txfee' => 0.1, // Default tx fee added by RPC server + 'block_bonus' => 0, + 'payout_system' => 'prop', // Set your payout here so template changes are activated 'archive_shares' => true, // Store accounted shares in archive table? 'blockexplorer' => 'http://explorer.litecoin.net/search?q=', // URL for block searches, prefixed to each block number 'chaininfo' => 'http://allchains.info', // Link to Allchains for Difficulty information diff --git a/public/include/pages/account.inc.php b/public/include/pages/account.inc.php index 859575d9..9e43518e 100644 --- a/public/include/pages/account.inc.php +++ b/public/include/pages/account.inc.php @@ -1,13 +1,10 @@ isAuthenticated()) { + // Tempalte specifics + $smarty->assign("CONTENT", "default.tpl"); } - -// Tempalte specifics -$smarty->assign("CONTENT", "default.tpl"); ?> diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index bb29cace..5b717cee 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -4,61 +4,77 @@ if (!defined('SECURITY')) die('Hacking attempt'); -if (!$_SESSION['AUTHENTICATED']) { - header('Location: index.php?page=home'); -} - -if ( ! $user->checkPin($_SESSION['USERDATA']['id'], $_POST['authPin']) && $_POST['do']) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid PIN','TYPE' => 'errormsg'); -} else { - switch ($_POST['do']) { - case 'cashOut': - $continue = true; - $dBalance = $transaction->getBalance($_SESSION['USERDATA']['id']); - $sCoinAddress = $user->getCoinAddress($_SESSION['USERDATA']['id']); - if ($dBalance > 0.1) { - if ($bitcoin->can_connect() === true) { - try { - $bitcoin->validateaddress($sCoinAddress); - } catch (BitcoinClientException $e) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid payment address: ' . $sUserSendAddress, 'TYPE' => 'errormsg'); - $continue = false; - } - if ($continue == true) { - // Remove the transfer fee and send to address - try { - $bitcoin->sendtoaddress($sCoinAddress, $dBalance - 0.1); - } catch (BitcoinClientException $e) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to send LTC, please contact site support immidiately', 'TYPE' => 'errormsg'); - $continue = false; - } - } - // Set balance to 0, add to paid out, insert to ledger - if ($continue == true && $transaction->addTransaction($_SESSION['USERDATA']['id'], $dBalance, 'Debit_MP', NULL, $sCoinAddress)) - $_SESSION['POPUP'][] = array('CONTENT' => 'Transaction completed', 'TYPE' => 'success'); +if ($user->isAuthenticated()) { + if ( ! $user->checkPin($_SESSION['USERDATA']['id'], @$_POST['authPin']) && @$_POST['do']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid PIN','TYPE' => 'errormsg'); + } else { + switch (@$_POST['do']) { + case 'cashOut': + if ($setting->getValue('manual_payout_active') == 1) { + $_SESSION['POPUP'][] = array('CONTENT' => 'A manual payout is in progress. Please try again later.', 'TYPE' => 'errormsg'); } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service', 'TYPE' => 'errormsg'); + $setting->setValue('manual_payout_active', 1); + $continue = true; + $aBalance = $transaction->getBalance($_SESSION['USERDATA']['id']); + $dBalance = $aBalance['confirmed']; + $sCoinAddress = $user->getCoinAddress($_SESSION['USERDATA']['id']); + // Ensure we can cover the potential transaction fee + if ($dBalance > $config['txfee']) { + if ($bitcoin->can_connect() === true) { + try { + $bitcoin->validateaddress($sCoinAddress); + } catch (BitcoinClientException $e) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid payment address: ' . $sUserSendAddress, 'TYPE' => 'errormsg'); + $continue = false; + } + if ($continue == true) { + // Send balance to address, mind fee for transaction! + try { + if ($setting->getValue('auto_payout_active') == 0) { + $bitcoin->sendtoaddress($sCoinAddress, $dBalance); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Auto-payout active, please contact site support immidiately to revoke invalid transactions.', 'TYPE' => 'errormsg'); + $continue = false; + } + } catch (BitcoinClientException $e) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to send ' . $config['currency'] . ', please contact site support immidiately', 'TYPE' => 'errormsg'); + $continue = false; + } + } + // Set balance to 0, add to paid out, insert to ledger + if ($continue == true && $transaction->addTransaction($_SESSION['USERDATA']['id'], $dBalance - $config['txfee'], 'Debit_MP', NULL, $sCoinAddress) && $transaction->addTransaction($_SESSION['USERDATA']['id'], $config['txfee'], 'TXFee', NULL, $sCoinAddress) ) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Transaction completed', 'TYPE' => 'success'); + $aMailData['email'] = $user->getUserEmail($user->getUserName($_SESSION['USERDATA']['id'])); + $aMailData['amount'] = $dBalance; + $aMailData['subject'] = 'Manual Payout Completed'; + $notification->sendNotification($_SESSION['USERDATA']['id'], 'manual_payout', $aMailData); + } + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service', 'TYPE' => 'errormsg'); + } + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Insufficient funds, you need more than ' . $config['txfee'] . ' ' . $conifg['currency'] . ' to cover transaction fees', 'TYPE' => 'errormsg'); + } + $setting->setValue('manual_payout_active', 0); } - } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Insufficient funds, you need more than 0.1 LTC to cover transaction fees', 'TYPE' => 'errormsg'); - } - break; + break; - case 'updateAccount': - if ($user->updateAccount($_SESSION['USERDATA']['id'], $_POST['paymentAddress'], $_POST['payoutThreshold'], $_POST['donatePercent'])) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Account details updated', 'TYPE' => 'success'); - } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to update your account', 'TYPE' => 'errormsg'); - } - break; + case 'updateAccount': + if ($user->updateAccount($_SESSION['USERDATA']['id'], $_POST['paymentAddress'], $_POST['payoutThreshold'], $_POST['donatePercent'], $_POST['email'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account details updated', 'TYPE' => 'success'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to update your account: ' . $user->getError(), 'TYPE' => 'errormsg'); + } + break; - case 'updatePassword': - if ($user->updatePassword($_SESSION['USERDATA']['id'], $_POST['currentPassword'], $_POST['newPassword'], $_POST['newPassword2'])) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Password updated', 'TYPE' => 'success'); - } else { - $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); + case 'updatePassword': + if ($user->updatePassword($_SESSION['USERDATA']['id'], $_POST['currentPassword'], $_POST['newPassword'], $_POST['newPassword2'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Password updated', 'TYPE' => 'success'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); + } + break; } - break; } } diff --git a/public/include/pages/account/notifications.inc.php b/public/include/pages/account/notifications.inc.php new file mode 100644 index 00000000..3f013ed0 --- /dev/null +++ b/public/include/pages/account/notifications.inc.php @@ -0,0 +1,25 @@ +isAuthenticated()) { + if (@$_REQUEST['do'] == 'save') { + if ($notification->updateSettings($_SESSION['USERDATA']['id'], $_REQUEST['data'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Updated notification settings'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to update settings', 'TYPE' => 'errormsg'); + } + } + + // Fetch notifications + $aNotifications = $notification->getNofifications($_SESSION['USERDATA']['id']); + if (!$aNotifications) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any notifications', 'TYPE' => 'errormsg'); + + // Fetch user notification settings + $aSettings = $notification->getNotificationSettings($_SESSION['USERDATA']['id']); + + $smarty->assign('NOTIFICATIONS', $aNotifications); + $smarty->assign('SETTINGS', $aSettings); + $smarty->assign('CONTENT', 'default.tpl'); +} +?> diff --git a/public/include/pages/account/transactions.inc.php b/public/include/pages/account/transactions.inc.php index 6e83e292..db927ca7 100644 --- a/public/include/pages/account/transactions.inc.php +++ b/public/include/pages/account/transactions.inc.php @@ -2,11 +2,10 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if (!$_SESSION['AUTHENTICATED']) header('Location: index.php?page=home'); - -$aTransactions = $transaction->getTransactions($_SESSION['USERDATA']['id']); -if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); - -$smarty->assign('TRANSACTIONS', $aTransactions); -$smarty->assign('CONTENT', 'default.tpl'); +if ($user->isAuthenticated()) { + $aTransactions = $transaction->getTransactions($_SESSION['USERDATA']['id']); + if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); + $smarty->assign('TRANSACTIONS', $aTransactions); + $smarty->assign('CONTENT', 'default.tpl'); +} ?> diff --git a/public/include/pages/account/workers.inc.php b/public/include/pages/account/workers.inc.php index 67bd0e19..ccdae2b8 100644 --- a/public/include/pages/account/workers.inc.php +++ b/public/include/pages/account/workers.inc.php @@ -2,35 +2,38 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if (!$_SESSION['AUTHENTICATED']) header('Location: index.php?page=home'); -switch ($_REQUEST['do']) { -case 'delete': - if ($worker->deleteWorker($_SESSION['USERDATA']['id'], $_GET['id'])) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Worker removed'); - } else { - $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); +if ($user->isAuthenticated()) { + switch (@$_REQUEST['do']) { + case 'delete': + if ($worker->deleteWorker($_SESSION['USERDATA']['id'], $_GET['id'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Worker removed'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); + } + break; + case 'add': + if ($worker->addWorker($_SESSION['USERDATA']['id'], $_POST['username'], $_POST['password'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Worker added'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); + } + break; + case 'update': + if ($worker->updateWorkers($_SESSION['USERDATA']['id'], $_POST['data'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Worker updated'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); + } + break; } - break; -case 'add': - if ($worker->addWorker($_SESSION['USERDATA']['id'], $_POST['username'], $_POST['password'])) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Worker added'); - } else { - $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); - } - break; -case 'update': - if ($worker->updateWorkers($_SESSION['USERDATA']['id'], $_POST['data'])) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Worker updated'); - } else { - $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); - } - break; + + $aWorkers = $worker->getWorkers($_SESSION['USERDATA']['id']); + if (!$aWorkers) $_SESSION['POPUP'][] = array('CONTENT' => 'You have no workers configured', 'TYPE' => 'errormsg'); + + $smarty->assign('WORKERS', $aWorkers); } -$aWorkers = $worker->getWorkers($_SESSION['USERDATA']['id']); -if (!$aWorkers) $_SESSION['POPUP'][] = array('CONTENT' => 'You have no workers configured', 'TYPE' => 'errormsg'); - $smarty->assign('CONTENT', 'default.tpl'); -$smarty->assign('WORKERS', $aWorkers); + ?> diff --git a/public/include/pages/admin.inc.php b/public/include/pages/admin.inc.php new file mode 100644 index 00000000..5305b030 --- /dev/null +++ b/public/include/pages/admin.inc.php @@ -0,0 +1,14 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/admin/user.inc.php b/public/include/pages/admin/user.inc.php new file mode 100644 index 00000000..9bb9ecee --- /dev/null +++ b/public/include/pages/admin/user.inc.php @@ -0,0 +1,50 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +$aRoundShares = $statistics->getRoundShares(); + +// Change account lock +if (@$_POST['do'] == 'lock') { + $supress_master = 1; + $user->changeLocked($_POST['account_id']); +} + +// Change account admin +if (@$_POST['do'] == 'admin') { + $supress_master = 1; + $user->changeAdmin($_POST['account_id']); +} + +if (@$_POST['query']) { + // Fetch requested users + $aUsers = $statistics->getAllUserStats($_POST['query']); + + // Add additional stats to each user + // This is not optimized yet, best is a proper SQL + // Query against the stats table? Currently cached though. + foreach ($aUsers as $iKey => $aUser) { + $aBalance = $transaction->getBalance($aUser['id']); + $aUser['balance'] = $aBalance['confirmed']; + $aUser['hashrate'] = $statistics->getUserHashrate($aUser['id']); + $aUser['payout']['est_block'] = round(( (int)$aUser['shares'] / (int)$aRoundShares['valid'] ) * (int)$config['reward'], 3); + $aUser['payout']['est_fee'] = round(($config['fees'] / 100) * $aUser['payout']['est_block'], 3); + $aUser['payout']['est_donation'] = round((( $aUser['donate_percent'] / 100) * ($aUser['payout']['est_block'] - $aUser['payout']['est_fee'])), 3); + $aUser['payout']['est_payout'] = round($aUser['payout']['est_block'] - $aUser['payout']['est_donation'] - $aUser['payout']['est_fee'], 3); + $aUsers[$iKey] = $aUser; + } + // Assign our variables + $smarty->assign("USERS", $aUsers); +} + + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/admin/wallet.inc.php b/public/include/pages/admin/wallet.inc.php new file mode 100644 index 00000000..45ff5af4 --- /dev/null +++ b/public/include/pages/admin/wallet.inc.php @@ -0,0 +1,24 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +if ($bitcoin->can_connect() === true){ + $dBalance = $bitcoin->query('getbalance'); +} else { + $dBalance = 0; + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); +} + +$smarty->assign("BALANCE", $dBalance); +$smarty->assign("LOCKED", $transaction->getLockedBalance()); + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/api/getuserstatus.inc.php b/public/include/pages/api/getuserstatus.inc.php new file mode 100644 index 00000000..c91ade94 --- /dev/null +++ b/public/include/pages/api/getuserstatus.inc.php @@ -0,0 +1,29 @@ +checkApiKey($_REQUEST['api_key']); + +// We have to check if that user is admin too +if ( ! $user->isAdmin($id) ) { + header("HTTP/1.1 401 Unauthorized"); + die("Access denied"); +} + +// Is it a username or a user ID +ctype_digit($_REQUEST['id']) ? $username = $user->getUserName($_REQUEST['id']) : $username = $_REQUEST['id']; +ctype_digit($_REQUEST['id']) ? $id = $_REQUEST['id'] : $id = $user->getUserId($_REQUEST['id']); + +// Output JSON format +echo json_encode(array('getuserstatus' => array( + 'username' => $username, + 'shares' => $statistics->getUserShares($id), + 'hashrate' => $statistics->getUserHashrate($id) +))); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getuserworkers.inc.php b/public/include/pages/api/getuserworkers.inc.php new file mode 100644 index 00000000..23bdcf5d --- /dev/null +++ b/public/include/pages/api/getuserworkers.inc.php @@ -0,0 +1,24 @@ +checkApiKey($_REQUEST['api_key']); + +// We have to check if that user is admin too +if ( ! $user->isAdmin($id) ) { + header("HTTP/1.1 401 Unauthorized"); + die("Access denied"); +} + +// Is it a username or a user ID +ctype_digit($_REQUEST['id']) ? $id = $_REQUEST['id'] : $id = $user->getUserId($_REQUEST['id']); + +// Output JSON format +echo json_encode(array('getuserworkers' => $worker->getWorkers($id))); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/public.inc.php b/public/include/pages/api/public.inc.php new file mode 100644 index 00000000..162e9134 --- /dev/null +++ b/public/include/pages/api/public.inc.php @@ -0,0 +1,26 @@ +getLast(); +$aShares = $statistics->getRoundShares(); + +echo json_encode( + array( + 'pool_name' => $config['website']['name'], + 'hashrate' => $statistics->getCurrentHashrate(), + 'workers' => $worker->getCountAllActiveWorkers(), + 'shares_this_round' => $aShares['valid'], + 'last_block' => $aLastBlock['height'], + 'network_hashrate' => '0' + ) +); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/login.inc.php b/public/include/pages/login.inc.php index 6600a872..c157d720 100644 --- a/public/include/pages/login.inc.php +++ b/public/include/pages/login.inc.php @@ -7,7 +7,7 @@ if (!defined('SECURITY')) if ( $user->checkLogin($_POST['username'],$_POST['password']) ) { header('Location: index.php?page=home'); } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid username or password', 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to login: '. $user->getError(), 'TYPE' => 'errormsg'); } $smarty->assign('CONTENT', 'default.tpl'); ?> diff --git a/public/include/pages/register.inc.php b/public/include/pages/register.inc.php index aecab054..d0a1d713 100644 --- a/public/include/pages/register.inc.php +++ b/public/include/pages/register.inc.php @@ -1,9 +1,17 @@ assign("CONTENT", "default.tpl"); +if (!$config['website']['registration']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account registration is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); + $smarty->assign("CONTENT", "disabled.tpl"); +} else { + if ($config['recaptcha']['enabled']) { + require_once(INCLUDE_DIR . '/recaptchalib.php'); + $smarty->assign("RECAPTCHA", recaptcha_get_html($config['recaptcha']['public_key'])); + } + // Tempalte specifics + $smarty->assign("CONTENT", "default.tpl"); +} ?> diff --git a/public/include/pages/register/register.inc.php b/public/include/pages/register/register.inc.php index 53e941bf..ce41630e 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -1,14 +1,46 @@ register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'])) { - $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); +// Check if recaptcha is enabled, process form data if valid +if($config['recaptcha']['enabled'] && $_POST["recaptcha_response_field"] && $_POST["recaptcha_response_field"]!=''){ + if ($rsp->is_valid) { + $smarty->assign("RECAPTCHA", recaptcha_get_html($config['recaptcha']['public_key'])); + if (!$config['website']['registration']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account registration is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); + } else if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2']) && $config['website']['registration']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to create account: ' . $user->getError(), 'TYPE' => 'errormsg'); + } + } else { + $smarty->assign("RECAPTCHA", recaptcha_get_html($config['recaptcha']['public_key'], $rsp->error)); + $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid Captcha, please try again. (' . $rsp->error . ')', 'TYPE' => 'errormsg'); + } +// Empty captcha +} else if ($config['recaptcha']['enabled']) { + $smarty->assign("RECAPTCHA", recaptcha_get_html($config['recaptcha']['public_key'], $rsp->error)); + $_SESSION['POPUP'][] = array('CONTENT' => 'Empty Captcha, please try again.', 'TYPE' => 'errormsg'); +// Captcha disabled } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to create account: ' . $user->getError(), 'TYPE' => 'errormsg'); + if (!$config['website']['registration']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account registration is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); + } else if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2']) && $config['website']['registration']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account created, please login'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to create account: ' . $user->getError(), 'TYPE' => 'errormsg'); + } } // We load the default registration template instead of an action specific one diff --git a/public/include/pages/statistics.inc.php b/public/include/pages/statistics.inc.php index c465f091..195e3545 100644 --- a/public/include/pages/statistics.inc.php +++ b/public/include/pages/statistics.inc.php @@ -5,14 +5,14 @@ if (!defined('SECURITY')) die('Hacking attempt'); if ($bitcoin->can_connect() === true){ - $iDifficulty = $bitcoin->query('getdifficulty'); + $dDifficulty = $bitcoin->query('getdifficulty'); $iBlock = $bitcoin->query('getblockcount'); } else { - $iDifficulty = 1; + $dDifficulty = 1; $iBlock = 0; $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); } $smarty->assign("CURRENTBLOCK", $iBlock); -$smarty->assign("CURRENTDIFFICULTY", $iDifficulty); -$smarty->assign("CONTENT", "pool/default.tpl"); +$smarty->assign("DIFFICULTY", $dDifficulty); +$smarty->assign("CONTENT", "default.tpl"); diff --git a/public/include/pages/statistics/blocks.inc.php b/public/include/pages/statistics/blocks.inc.php index 7ba0bfdd..e83aa8a8 100644 --- a/public/include/pages/statistics/blocks.inc.php +++ b/public/include/pages/statistics/blocks.inc.php @@ -1,9 +1,8 @@ isAuthenticated()) header("Location: index.php?page=home"); // Grab the last blocks found $iLimit = 30; @@ -14,9 +13,5 @@ $aBlockData = $aBlocksFoundData[0]; $smarty->assign("BLOCKSFOUND", $aBlocksFoundData); $smarty->assign("BLOCKLIMIT", $iLimit); -if ($_SESSION['AUTHENTICATED']) { - $smarty->assign("CONTENT", "blocks_found.tpl"); -} else { - $smarty->assign("CONTENT", "default.tpl"); -} +$smarty->assign("CONTENT", "default.tpl"); ?> diff --git a/public/include/pages/statistics/graphs.inc.php b/public/include/pages/statistics/graphs.inc.php new file mode 100644 index 00000000..f7016835 --- /dev/null +++ b/public/include/pages/statistics/graphs.inc.php @@ -0,0 +1,16 @@ +isAuthenticated()) { + $aHourlyHashRates = $statistics->getHourlyHashrateByAccount($_SESSION['USERDATA']['id']); + $aPoolHourlyHashRates = $statistics->getHourlyHashrateByPool(); +} + +// Propagate content our template +$smarty->assign("YOURHASHRATES", @$aHourlyHashRates); +$smarty->assign("POOLHASHRATES", @$aPoolHourlyHashRates); +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/statistics/pool.inc.php b/public/include/pages/statistics/pool.inc.php index 0c9c2eb7..9650681e 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -21,12 +21,14 @@ $aContributorsShares = $statistics->getTopContributors('shares', 15); $aContributorsHashes = $statistics->getTopContributors('hashes', 15); // Grab the last 10 blocks found -$iLimit = 10; +$iLimit = 5; $aBlocksFoundData = $statistics->getBlocksFound($iLimit); $aBlockData = $aBlocksFoundData[0]; // Estimated time to find the next block $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); +$iCurrentPoolHashrate == 0 ? $iCurrentPoolHashrate = 1 : true; + // Time in seconds, not hours, using modifier in smarty to translate $iEstTime = $dDifficulty * pow(2,32) / ($iCurrentPoolHashrate * 1000); @@ -50,9 +52,9 @@ $smarty->assign("LASTBLOCK", $aBlockData['height']); $smarty->assign("DIFFICULTY", $dDifficulty); $smarty->assign("REWARD", $config['reward']); -if ($_SESSION['AUTHENTICATED']) { +if ($user->isAuthenticated()) { $smarty->assign("CONTENT", "authenticated.tpl"); } else { - $smarty->assign("CONTENT", "default.tpl"); + $smarty->assign("CONTENT", "../default.tpl"); } ?> diff --git a/public/include/pages/statistics/user.inc.php b/public/include/pages/statistics/user.inc.php deleted file mode 100644 index 41440b99..00000000 --- a/public/include/pages/statistics/user.inc.php +++ /dev/null @@ -1,26 +0,0 @@ -can_connect() === true){ - $dDifficulty = $bitcoin->getdifficulty(); - $iBlock = $bitcoin->getblockcount(); -} else { - $iDifficulty = 1; - $iBlock = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); -} - -$aHourlyHashRates = $statistics->getHourlyHashrateByAccount($_SESSION['USERDATA']['id']); - -// Propagate content our template -$smarty->assign("YOURHASHRATES", $aHourlyHashRates); -$smarty->assign("DIFFICULTY", $dDifficulty); - -if ($_SESSION['AUTHENTICATED']) { - $smarty->assign("CONTENT", "default.tpl"); -} -?> diff --git a/public/include/recaptchalib.php b/public/include/recaptchalib.php new file mode 100644 index 00000000..32c4f4d7 --- /dev/null +++ b/public/include/recaptchalib.php @@ -0,0 +1,277 @@ + $value ) + $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; + + // Cut the last '&' + $req=substr($req,0,strlen($req)-1); + return $req; +} + + + +/** + * Submits an HTTP POST to a reCAPTCHA server + * @param string $host + * @param string $path + * @param array $data + * @param int port + * @return array response + */ +function _recaptcha_http_post($host, $path, $data, $port = 80) { + + $req = _recaptcha_qsencode ($data); + + $http_request = "POST $path HTTP/1.0\r\n"; + $http_request .= "Host: $host\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; + $http_request .= "Content-Length: " . strlen($req) . "\r\n"; + $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; + $http_request .= "\r\n"; + $http_request .= $req; + + $response = ''; + if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { + die ('Could not open socket'); + } + + fwrite($fs, $http_request); + + while ( !feof($fs) ) + $response .= fgets($fs, 1160); // One TCP-IP packet + fclose($fs); + $response = explode("\r\n\r\n", $response, 2); + + return $response; +} + + + +/** + * Gets the challenge HTML (javascript and non-javascript version). + * This is called from the browser, and the resulting reCAPTCHA HTML widget + * is embedded within the HTML form it was called from. + * @param string $pubkey A public key for reCAPTCHA + * @param string $error The error given by reCAPTCHA (optional, default is null) + * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) + + * @return string - The HTML to be embedded in the user's form. + */ +function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) +{ + if ($pubkey == null || $pubkey == '') { + die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($use_ssl) { + $server = RECAPTCHA_API_SECURE_SERVER; + } else { + $server = RECAPTCHA_API_SERVER; + } + + $errorpart = ""; + if ($error) { + $errorpart = "&error=" . $error; + } + return ' + + '; +} + + + + +/** + * A ReCaptchaResponse is returned from recaptcha_check_answer() + */ +class ReCaptchaResponse { + var $is_valid; + var $error; +} + + +/** + * Calls an HTTP POST function to verify if the user's guess was correct + * @param string $privkey + * @param string $remoteip + * @param string $challenge + * @param string $response + * @param array $extra_params an array of extra variables to post to the server + * @return ReCaptchaResponse + */ +function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) +{ + if ($privkey == null || $privkey == '') { + die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($remoteip == null || $remoteip == '') { + die ("For security reasons, you must pass the remote ip to reCAPTCHA"); + } + + + + //discard spam submissions + if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { + $recaptcha_response = new ReCaptchaResponse(); + $recaptcha_response->is_valid = false; + $recaptcha_response->error = 'incorrect-captcha-sol'; + return $recaptcha_response; + } + + $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify", + array ( + 'privatekey' => $privkey, + 'remoteip' => $remoteip, + 'challenge' => $challenge, + 'response' => $response + ) + $extra_params + ); + + $answers = explode ("\n", $response [1]); + $recaptcha_response = new ReCaptchaResponse(); + + if (trim ($answers [0]) == 'true') { + $recaptcha_response->is_valid = true; + } + else { + $recaptcha_response->is_valid = false; + $recaptcha_response->error = $answers [1]; + } + return $recaptcha_response; + +} + +/** + * gets a URL where the user can sign up for reCAPTCHA. If your application + * has a configuration page where you enter a key, you should provide a link + * using this function. + * @param string $domain The domain where the page is hosted + * @param string $appname The name of your application + */ +function recaptcha_get_signup_url ($domain = null, $appname = null) { + return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname)); +} + +function _recaptcha_aes_pad($val) { + $block_size = 16; + $numpad = $block_size - (strlen ($val) % $block_size); + return str_pad($val, strlen ($val) + $numpad, chr($numpad)); +} + +/* Mailhide related code */ + +function _recaptcha_aes_encrypt($val,$ky) { + if (! function_exists ("mcrypt_encrypt")) { + die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); + } + $mode=MCRYPT_MODE_CBC; + $enc=MCRYPT_RIJNDAEL_128; + $val=_recaptcha_aes_pad($val); + return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); +} + + +function _recaptcha_mailhide_urlbase64 ($x) { + return strtr(base64_encode ($x), '+/', '-_'); +} + +/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ +function recaptcha_mailhide_url($pubkey, $privkey, $email) { + if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { + die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . + "you can do so at http://www.google.com/recaptcha/mailhide/apikey"); + } + + + $ky = pack('H*', $privkey); + $cryptmail = _recaptcha_aes_encrypt ($email, $ky); + + return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); +} + +/** + * gets the parts of the email to expose to the user. + * eg, given johndoe@example,com return ["john", "example.com"]. + * the email is then displayed as john...@example.com + */ +function _recaptcha_mailhide_email_parts ($email) { + $arr = preg_split("/@/", $email ); + + if (strlen ($arr[0]) <= 4) { + $arr[0] = substr ($arr[0], 0, 1); + } else if (strlen ($arr[0]) <= 6) { + $arr[0] = substr ($arr[0], 0, 3); + } else { + $arr[0] = substr ($arr[0], 0, 4); + } + return $arr; +} + +/** + * Gets html to display an email address given a public an private key. + * to get a key, go to: + * + * http://www.google.com/recaptcha/mailhide/apikey + */ +function recaptcha_mailhide_html($pubkey, $privkey, $email) { + $emailparts = _recaptcha_mailhide_email_parts ($email); + $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); + + return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); + +} + + +?> diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index b2e67f37..8a320581 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -5,7 +5,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); $debug->append('Loading Smarty libraries', 2); -define('SMARTY_DIR', BASEPATH . INCLUDE_DIR . '/smarty/libs/'); +define('SMARTY_DIR', INCLUDE_DIR . '/smarty/libs/'); // Include the actual smarty class file include(SMARTY_DIR . 'Smarty.class.php'); @@ -16,10 +16,10 @@ $smarty = new Smarty; // Assign our local paths $debug->append('Define Smarty Paths', 3); -$smarty->template_dir = 'templates/' . THEME . '/'; -$smarty->compile_dir = 'templates/compile/'; +$smarty->template_dir = BASEPATH . 'templates/' . THEME . '/'; +$smarty->compile_dir = BASEPATH . 'templates/compile/'; // Optional smarty caching, check Smarty documentation for details $smarty->caching = $config['cache']; -$smarty->cache_dir = "templates/cache"; +$smarty->cache_dir = BASEPATH . "templates/cache"; ?> diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index f34b3b4a..923b7263 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -7,17 +7,29 @@ if (!defined('SECURITY')) // Globally available variables $debug->append('Global smarty variables', 3); +// Defaults to get rid of PHP Notice warnings +$dDifficulty = 1; +$aRoundShares = 1; + +// Only run these if the user is logged in +if (@$_SESSION['AUTHENTICATED']) { + $aRoundShares = $statistics->getRoundShares(); + if ($bitcoin->can_connect() === true) + $dDifficulty = $bitcoin->query('getdifficulty'); +} + // Fetch some data -$aRoundShares = $statistics->getRoundShares(); $iCurrentActiveWorkers = $worker->getCountAllActiveWorkers(); $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); $iCurrentPoolShareRate = $statistics->getCurrentShareRate(); +// Global data for Smarty $aGlobal = array( 'slogan' => $config['website']['slogan'], 'websitename' => $config['website']['name'], 'hashrate' => $iCurrentPoolHashrate, 'sharerate' => $iCurrentPoolShareRate, + 'ppsvalue' => number_format(round(50 / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12), 'workers' => $iCurrentActiveWorkers, 'roundshares' => $aRoundShares, 'fees' => $config['fees'], @@ -25,7 +37,18 @@ $aGlobal = array( 'reward' => $config['reward'], 'price' => $setting->getValue('price'), 'blockexplorer' => $config['blockexplorer'], - 'chaininfo' => $config['chaininfo'] + 'chaininfo' => $config['chaininfo'], + 'config' => array( + 'price' => array( 'currency' => $config['price']['currency'] ), + 'targetdiff' => $config['difficulty'], + 'currency' => $config['currency'], + 'txfee' => $config['txfee'], + 'payout_system' => $config['payout_system'], + 'ap_threshold' => array( + 'min' => $config['ap_threshold']['min'], + 'max' => $config['ap_threshold']['max'] + ) + ) ); // We don't want these session infos cached @@ -36,12 +59,19 @@ if (@$_SESSION['USERDATA']['id']) { // Other userdata that we can cache savely $aGlobal['userdata']['shares'] = $statistics->getUserShares($_SESSION['USERDATA']['id']); $aGlobal['userdata']['hashrate'] = $statistics->getUserHashrate($_SESSION['USERDATA']['id']); + $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); - // Some estimations - $aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (int)$config['reward'], 3); - $aGlobal['userdata']['est_fee'] = round(($config['fees'] / 100) * $aGlobal['userdata']['est_block'], 3); - $aGlobal['userdata']['est_donation'] = round((( $aGlobal['userdata']['donate_percent'] / 100) * ($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_fee'])), 3); - $aGlobal['userdata']['est_payout'] = round($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_donation'] - $aGlobal['userdata']['est_fee'], 3); + switch ($config['payout_system']) { + case 'pps': + break; + default: + // Some estimations + $aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (int)$config['reward'], 3); + $aGlobal['userdata']['est_fee'] = round(($config['fees'] / 100) * $aGlobal['userdata']['est_block'], 3); + $aGlobal['userdata']['est_donation'] = round((( $aGlobal['userdata']['donate_percent'] / 100) * ($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_fee'])), 3); + $aGlobal['userdata']['est_payout'] = round($aGlobal['userdata']['est_block'] - $aGlobal['userdata']['est_donation'] - $aGlobal['userdata']['est_fee'], 3); + break; + } } // Make it available in Smarty diff --git a/public/site_assets/mmcFE/css/style.css b/public/site_assets/mmcFE/css/style.css index a41aef4c..6243fbfc 100644 --- a/public/site_assets/mmcFE/css/style.css +++ b/public/site_assets/mmcFE/css/style.css @@ -465,6 +465,12 @@ a:hover { .block table tr th.right{ text-align: right; } +.block table tr td.center{ + text-align: center; +} +.block table tr th.center{ + text-align: center; +} .block table tr td.delete a { color: #666; } @@ -1032,3 +1038,25 @@ a:hover { padding-left:5px; } +/* Custom checkboxes */ +input[type=checkbox] { + display:none; +} + +input[type=checkbox] + label +{ + background: url('../images/error.gif'); + height: 16px; + width: 16px; + display:inline-block; + padding: 0 0 0 0px; +} + +input[type=checkbox]:checked + label +{ + background: url('../images/success.gif'); + height: 16px; + width: 16px; + display:inline-block; + padding: 0 0 0 0px; +} diff --git a/public/site_assets/mmcFE/images/first.png b/public/site_assets/mmcFE/images/first.png new file mode 100644 index 00000000..6f11fcb0 Binary files /dev/null and b/public/site_assets/mmcFE/images/first.png differ diff --git a/public/site_assets/mmcFE/images/last.png b/public/site_assets/mmcFE/images/last.png new file mode 100644 index 00000000..72079357 Binary files /dev/null and b/public/site_assets/mmcFE/images/last.png differ diff --git a/public/site_assets/mmcFE/images/next.png b/public/site_assets/mmcFE/images/next.png new file mode 100644 index 00000000..4a2f9d4e Binary files /dev/null and b/public/site_assets/mmcFE/images/next.png differ diff --git a/public/site_assets/mmcFE/images/prev.png b/public/site_assets/mmcFE/images/prev.png new file mode 100644 index 00000000..15d1584b Binary files /dev/null and b/public/site_assets/mmcFE/images/prev.png differ diff --git a/public/site_assets/mmcFE/js/custom.js b/public/site_assets/mmcFE/js/custom.js index 1e047555..76e967c6 100644 --- a/public/site_assets/mmcFE/js/custom.js +++ b/public/site_assets/mmcFE/js/custom.js @@ -16,19 +16,12 @@ $(function () { var statsType = 'area'; } - // calculate width of graph so it doesnt overflow its parent div - //var chart_width = ($(this).parent('div').width()) - 60; - // hack to statically set width as something is broken with div width calculation - anni - var chart_width = $(document).width() - 340; - if (statsType == 'line' || statsType == 'pie') { $(this).hide().visualize({ type: statsType, // 'bar', 'area', 'pie', 'line' - width: chart_width, height: '240px', colors: ['#6fb9e8', '#ec8526', '#9dc453', '#ddd74c'], - lineDots: 'double', interaction: true, multiHover: 5, @@ -43,9 +36,8 @@ $(function () { }); } else { $(this).hide().visualize({ - type: statsType, // 'bar', 'area', 'pie', 'line' - width: chart_width, + type: statsType, height: '240px', colors: ['#6fb9e8', '#ec8526', '#9dc453', '#ddd74c'] }); @@ -66,6 +58,16 @@ $(function () { widgets: ['zebra'] }); + $("table.pagesort") + .tablesorter({ widgets: ['zebra'] }) + .tablesorterPager({ positionFixed: false, container: $("#pager") }); + $("table.pagesort2") + .tablesorter({ widgets: ['zebra'] }) + .tablesorterPager({ positionFixed: false, container: $("#pager2") }); + $("table.pagesort4") + .tablesorter({ widgets: ['zebra'] }) + .tablesorterPager({ positionFixed: false, container: $("#pager3") }); + $('.block table tr th.header').css('cursor', 'pointer'); // Check / uncheck all checkboxes diff --git a/public/site_assets/mmcFE/js/jquery.tooltip.visualize.js b/public/site_assets/mmcFE/js/jquery.tooltip.visualize.js new file mode 100644 index 00000000..f6f71fd9 --- /dev/null +++ b/public/site_assets/mmcFE/js/jquery.tooltip.visualize.js @@ -0,0 +1,106 @@ +/** + * -------------------------------------------------------------------- + * Tooltip plugin for the jQuery-Plugin "Visualize" + * Tolltip by Iraê Carvalho, irae@irae.pro.br, http://irae.pro.br/en/ + * Copyright (c) 2010 Iraê Carvalho + * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses. + * + * Visualize plugin by Scott Jehl, scott@filamentgroup.com + * Copyright (c) 2009 Filament Group, http://www.filamentgroup.com + * + * -------------------------------------------------------------------- + */ + +(function($){ + $.visualizePlugins.push(function visualizeTooltip(options,tableData) { + //configuration + var o = $.extend({ + tooltip: false, + tooltipalign: 'auto', // also available 'left' and 'right' + tooltipvalign: 'top', + tooltipclass: 'visualize-tooltip', + tooltiphtml: function(data){ + if(options.multiHover) { + var html=''; + for(var i=0;i'; + } + return html; + } else { + return '

'+data.point.value+' - '+data.point.yLabels[0]+'

'; + } + }, + delay:false + },options); + + // don't go any further if we are not to show anything + if(!o.tooltip) {return;} + + var self = $(this), + canvasContain = self.next(), + scroller = canvasContain.find('.visualize-scroller'), + scrollerW = scroller.width(), + tracker = canvasContain.find('.visualize-interaction-tracker'); + + // IE needs background color and opacity white or the tracker stays behind the tooltip + tracker.css({ + backgroundColor:'white', + opacity:0, + zIndex:100 + }); + + var tooltip = $('
').css({ + position:'absolute', + display:'none', + zIndex:90 + }) + .insertAfter(scroller.find('canvas')); + + var usescroll = true; + + if( typeof(G_vmlCanvasManager) != 'undefined' ){ + scroller.css({'position':'absolute'}); + tracker.css({marginTop:'-'+(o.height)+'px'}); + } + + + self.bind('vizualizeOver',function visualizeTooltipOver(e,data){ + if(data.canvasContain.get(0) != canvasContain.get(0)) {return;} // for multiple graphs originated from same table + if(o.multiHover) { + var p = data.point[0].canvasCords; + } else { + var p = data.point.canvasCords; + } + var left,right,top,clasRem,clasAd,bottom,x=Math.round(p[0]+data.tableData.zeroLocX),y=Math.round(p[1]+data.tableData.zeroLocY); + if(o.tooltipalign == 'left' || ( o.tooltipalign=='auto' && x-scroller.scrollLeft()<=scrollerW/2 ) ) { + if($.browser.msie && ($.browser.version == 7 || $.browser.version == 6) ) {usescroll=false;} else {usescroll=true;} + left = (x-(usescroll?scroller.scrollLeft():0))+'px'; + right = ''; + clasAdd="tooltipleft"; + clasRem="tooltipright"; + } else { + if($.browser.msie && $.browser.version == 7) {usescroll=false;} else {usescroll=true;} + left = ''; + right = (Math.abs(x-o.width)- (o.width-(usescroll?scroller.scrollLeft():0)-scrollerW) )+'px'; + clasAdd="tooltipright"; + clasRem="tooltipleft"; + } + + tooltip + .addClass(clasAdd) + .removeClass(clasRem) + .html(o.tooltiphtml(data)) + .css({ + display:'block', + top: y+'px', + left: left, + right: right + }); + }); + + self.bind('vizualizeOut',function visualizeTooltipOut(e,data){ + tooltip.css({display:'none'}); + }); + + }); +})(jQuery); \ No newline at end of file diff --git a/public/site_assets/mmcFE/js/jquery.visualize.js b/public/site_assets/mmcFE/js/jquery.visualize.js index 3daf800a..1043aaca 100644 --- a/public/site_assets/mmcFE/js/jquery.visualize.js +++ b/public/site_assets/mmcFE/js/jquery.visualize.js @@ -1,806 +1,795 @@ -/** - * -------------------------------------------------------------------- - * jQuery-Plugin "visualize" - * by Scott Jehl, scott@filamentgroup.com - * http://www.filamentgroup.com - * Copyright (c) 2009 Filament Group - * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses. - * - * -------------------------------------------------------------------- - */ -(function ($) { - $.fn.visualize = function (options, container) { - return $(this).each(function () { - //configuration - var o = $.extend({ - type: 'bar', - //also available: area, pie, line - width: $(this).width(), - //height of canvas - defaults to table height - height: $(this).height(), - //height of canvas - defaults to table height - appendTitle: true, - //table caption text is added to chart - title: null, - //grabs from table caption if null - appendKey: true, - //color key is added to chart - colors: ['#be1e2d', '#666699', '#92d5ea', '#ee8310', '#8d10ee', '#5a3b16', '#26a4ed', '#f45a90', '#e9e744'], - textColors: [], - //corresponds with colors array. null/undefined items will fall back to CSS - parseDirection: 'x', - //which direction to parse the table data - pieMargin: 10, - //pie charts only - spacing around pie - pieLabelsAsPercent: true, - pieLabelPos: 'inside', - lineWeight: 4, - //for line and area - stroke weight - lineDots: false, - //also available: 'single', 'double' - dotInnerColor: "#ffffff", - // only used for lineDots:'double' - lineMargin: (options.lineDots ? 15 : 0), - //for line and area - spacing around lines - barGroupMargin: 10, - chartId: '', - xLabelParser: null, - // function to parse labels as values - valueParser: null, - // function to parse values. must return a Number - chartId: '', - chartClass: '', - barMargin: 1, - //space around bars in bar chart (added to both sides of bar) - yLabelInterval: 30, - //distance between y labels - interaction: false // only used for lineDots != false -- triggers mouseover and mouseout on original table - }, options); - - //reset width, height to numbers - o.width = parseFloat(o.width); - o.height = parseFloat(o.height); - - // reset padding if graph is not lines - if (o.type != 'line' && o.type != 'area') { - o.lineMargin = 0; - } - - var self = $(this); - - // scrape data from html table - var tableData = {}; - var colors = o.colors; - var textColors = o.textColors; - - - var parseLabels = function (direction) { - var labels = []; - if (direction == 'x') { - self.find('thead tr').each(function (i) { - $(this).find('th').each(function (j) { - if (!labels[j]) { - labels[j] = []; - } - labels[j][i] = $(this).text() - }) - }); - } else { - self.find('tbody tr').each(function (i) { - $(this).find('th').each(function (j) { - if (!labels[i]) { - labels[i] = []; - } - labels[i][j] = $(this).text() - }); - }); - } - return labels; - }; - - var fnParse = o.valueParser || parseFloat; - var dataGroups = tableData.dataGroups = []; - if (o.parseDirection == 'x') { - self.find('tbody tr').each(function (i, tr) { - dataGroups[i] = {}; - dataGroups[i].points = []; - dataGroups[i].color = colors[i]; - if (textColors[i]) { - dataGroups[i].textColor = textColors[i]; - } - $(tr).find('td').each(function (j, td) { - dataGroups[i].points.push({ - value: fnParse($(td).text()), - elem: td, - tableCords: [i, j] - }); - }); - }); - } else { - var cols = self.find('tbody tr:eq(0) td').size(); - for (var i = 0; i < cols; i++) { - dataGroups[i] = {}; - dataGroups[i].points = []; - dataGroups[i].color = colors[i]; - if (textColors[i]) { - dataGroups[i].textColor = textColors[i]; - } - self.find('tbody tr').each(function (j) { - dataGroups[i].points.push({ - value: $(this).find('td').eq(i).text() * 1, - elem: this, - tableCords: [i, j] - }); - }); - }; - } - - - var allItems = tableData.allItems = []; - $(dataGroups).each(function (i, row) { - var count = 0; - $.each(row.points, function (j, point) { - allItems.push(point); - count += point.value; - }); - row.groupTotal = count; - }); - - tableData.dataSum = 0; - tableData.topValue = 0; - tableData.bottomValue = Infinity; - $.each(allItems, function (i, item) { - tableData.dataSum += fnParse(item.value); - if (fnParse(item.value, 10) > tableData.topValue) { - tableData.topValue = fnParse(item.value, 10); - } - if (item.value < tableData.bottomValue) { - tableData.bottomValue = fnParse(item.value); - } - }); - var dataSum = tableData.dataSum; - var topValue = tableData.topValue; - var bottomValue = tableData.bottomValue; - - var xAllLabels = tableData.xAllLabels = parseLabels(o.parseDirection); - var yAllLabels = tableData.yAllLabels = parseLabels(o.parseDirection === 'x' ? 'y' : 'x'); - - var xLabels = tableData.xLabels = []; - $.each(tableData.xAllLabels, function (i, labels) { - tableData.xLabels.push(labels[0]); - }); - - var totalYRange = tableData.totalYRange = tableData.topValue - tableData.bottomValue; - - var zeroLocX = tableData.zeroLocX = 0; - - if ($.isFunction(o.xLabelParser)) { - - var xTopValue = null; - var xBottomValue = null; - - $.each(xLabels, function (i, label) { - label = xLabels[i] = o.xLabelParser(label); - if (i === 0) { - xTopValue = label; - xBottomValue = label; - } - if (label > xTopValue) { - xTopValue = label; - } - if (label < xBottomValue) { - xBottomValue = label; - } - }); - - var totalXRange = tableData.totalXRange = xTopValue - xBottomValue; - - - var xScale = tableData.xScale = (o.width - 2 * o.lineMargin) / totalXRange; - var marginDiffX = 0; - if (o.lineMargin) { - var marginDiffX = -2 * xScale - o.lineMargin; - } - zeroLocX = tableData.zeroLocX = xBottomValue + o.lineMargin; - - tableData.xBottomValue = xBottomValue; - tableData.xTopValue = xTopValue; - tableData.totalXRange = totalXRange; - } - - var yScale = tableData.yScale = (o.height - 2 * o.lineMargin) / totalYRange; - var zeroLocY = tableData.zeroLocY = (o.height - 2 * o.lineMargin) * (tableData.topValue / tableData.totalYRange) + o.lineMargin; - - var yLabels = tableData.yLabels = []; - - var numLabels = Math.floor((o.height - 2 * o.lineMargin) / 30); - - var loopInterval = tableData.totalYRange / numLabels; //fix provided from lab - loopInterval = Math.round(parseFloat(loopInterval) / 5) * 5; - loopInterval = Math.max(loopInterval, 1); - - // var start = - for (var j = Math.round(parseInt(tableData.bottomValue) / 5) * 5; j <= tableData.topValue - loopInterval; j += loopInterval) { - yLabels.push(j); - } - if (yLabels[yLabels.length - 1] > tableData.topValue + loopInterval) { - yLabels.pop(); - } else if (yLabels[yLabels.length - 1] <= tableData.topValue - 10) { - yLabels.push(tableData.topValue); - } - - // populate some data - $.each(dataGroups, function (i, row) { - row.yLabels = tableData.yAllLabels[i]; - $.each(row.points, function (j, point) { - point.zeroLocY = tableData.zeroLocY; - point.zeroLocX = tableData.zeroLocX; - point.xLabels = tableData.xAllLabels[j]; - point.yLabels = tableData.yAllLabels[i]; - point.color = row.color; - }); - }); - - try { - console.log(tableData); - } catch (e) {} - - var charts = {}; - - charts.pie = { - interactionPoints: dataGroups, - - setup: function () { - charts.pie.draw(true); - }, - draw: function (drawHtml) { - - var centerx = Math.round(canvas.width() / 2); - var centery = Math.round(canvas.height() / 2); - var radius = centery - o.pieMargin; - var counter = 0.0; - - if (drawHtml) { - canvasContain.addClass('visualize-pie'); - - if (o.pieLabelPos == 'outside') { - canvasContain.addClass('visualize-pie-outside'); - } - - var toRad = function (integer) { - return (Math.PI / 180) * integer; - }; - var labels = $('').insertAfter(canvas); - } - - - //draw the pie pieces - $.each(dataGroups, function (i, row) { - var fraction = row.groupTotal / dataSum; - if (fraction <= 0 || isNaN(fraction)) return; - ctx.beginPath(); - ctx.moveTo(centerx, centery); - ctx.arc(centerx, centery, radius, counter * Math.PI * 2 - Math.PI * 0.5, (counter + fraction) * Math.PI * 2 - Math.PI * 0.5, false); - ctx.lineTo(centerx, centery); - ctx.closePath(); - ctx.fillStyle = dataGroups[i].color; - ctx.fill(); - // draw labels - if (drawHtml) { - var sliceMiddle = (counter + fraction / 2); - var distance = o.pieLabelPos == 'inside' ? radius / 1.5 : radius + radius / 5; - var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance)); - var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) * (distance)); - var leftRight = (labelx > centerx) ? 'right' : 'left'; - var topBottom = (labely > centery) ? 'bottom' : 'top'; - var percentage = parseFloat((fraction * 100).toFixed(2)); - - // interaction variables - row.canvasCords = [labelx, labely]; - row.zeroLocY = tableData.zeroLocY = 0; // related to zeroLocY and plugin API - row.zeroLocX = tableData.zeroLocX = 0; // related to zeroLocX and plugin API - row.value = row.groupTotal; - - - if (percentage) { - var labelval = (o.pieLabelsAsPercent) ? percentage + '%' : row.groupTotal; - var labeltext = $('' + labelval + '').css(leftRight, 0).css(topBottom, 0); - if (labeltext) var label = $('
  • ').appendTo(labels).css({ - left: labelx, - top: labely - }).append(labeltext); - labeltext.css('font-size', radius / 8).css('margin-' + leftRight, -labeltext.width() / 2).css('margin-' + topBottom, -labeltext.outerHeight() / 2); - - if (dataGroups[i].textColor) { - labeltext.css('color', dataGroups[i].textColor); - } - - } - } - counter += fraction; - }); - } - }; - - (function () { - - var xInterval; - - var drawPoint = function (ctx, x, y, color, size) { - ctx.moveTo(x, y); - ctx.beginPath(); - ctx.arc(x, y, size / 2, 0, 2 * Math.PI, false); - ctx.closePath(); - ctx.fillStyle = color; - ctx.fill(); - }; - - charts.line = { - - interactionPoints: allItems, - - setup: function (area) { - - if (area) { - canvasContain.addClass('visualize-area'); - } else { - canvasContain.addClass('visualize-line'); - } - - //write X labels - var xlabelsUL = $('').width(canvas.width()).height(canvas.height()).insertBefore(canvas); - - if (!o.customXLabels) { - xInterval = (canvas.width() - 2 * o.lineMargin) / (xLabels.length - 1); - $.each(xLabels, function (i) { - var thisLi = $('
  • ' + this + '
  • ').prepend('').css('left', o.lineMargin + xInterval * i).appendTo(xlabelsUL); - var label = thisLi.find('span:not(.line)'); - var leftOffset = label.width() / -2; - if (i == 0) { - leftOffset = -20; - } else if (i == xLabels.length - 1) { - leftOffset = -label.width() + 20; - } - label.css('margin-left', leftOffset).addClass('label'); - }); - } else { - o.customXLabels(tableData, xlabelsUL); - } - - //write Y labels - var liBottom = (canvas.height() - 2 * o.lineMargin) / (yLabels.length - 1); - var ylabelsUL = $('
      ').width(canvas.width()).height(canvas.height()) - // .css('margin-top',-o.lineMargin) - .insertBefore(scroller); - - $.each(yLabels, function (i) { - var value = Math.floor(this); - var posB = (value - bottomValue) * yScale + o.lineMargin; - if (posB >= o.height - 1 || posB < 0) { - return; - } - var thisLi = $('
    • ' + value + '
    • ').css('bottom', posB); - if (Math.abs(posB) < o.height - 1) { - thisLi.prepend(''); - } - thisLi.prependTo(ylabelsUL); - - var label = thisLi.find('span:not(.line)'); - var topOffset = label.height() / -2; - if (!o.lineMargin) { - if (i == 0) { - topOffset = -label.height(); - } else if (i == yLabels.length - 1) { - topOffset = 0; - } - } - label.css('margin-top', topOffset).addClass('label'); - }); - - //start from the bottom left - ctx.translate(zeroLocX, zeroLocY); - - charts.line.draw(area); - - }, - - draw: function (area) { - // prevent drawing on top of previous draw - ctx.clearRect(-zeroLocX, -zeroLocY, o.width, o.height); - // Calculate each point properties before hand - var integer; - $.each(dataGroups, function (i, row) { - integer = o.lineMargin; // the current offset - $.each(row.points, function (j, point) { - if (o.xLabelParser) { - point.canvasCords = [(xLabels[j] - zeroLocX) * xScale - xBottomValue, -(point.value * yScale)]; - } else { - point.canvasCords = [integer, -(point.value * yScale)]; - } - - if (o.lineDots) { - point.dotSize = o.dotSize || o.lineWeight * Math.PI; - point.dotInnerSize = o.dotInnerSize || o.lineWeight * Math.PI / 2; - if (o.lineDots == 'double') { - point.innerColor = o.dotInnerColor; - } - } - integer += xInterval; - }); - }); - // fire custom event so we can enable rich interaction - self.trigger('vizualizeBeforeDraw', { - options: o, - table: self, - canvasContain: canvasContain, - tableData: tableData - }); - // draw lines and areas - $.each(dataGroups, function (h) { - // Draw lines - ctx.beginPath(); - ctx.lineWidth = o.lineWeight; - ctx.lineJoin = 'round'; - $.each(this.points, function (g) { - var loc = this.canvasCords; - if (g == 0) { - ctx.moveTo(loc[0], loc[1]); - } - ctx.lineTo(loc[0], loc[1]); - }); - ctx.strokeStyle = this.color; - ctx.stroke(); - // Draw fills - if (area) { - var integer = this.points[this.points.length - 1].canvasCords[0]; - if (isFinite(integer)) ctx.lineTo(integer, 0); - ctx.lineTo(o.lineMargin, 0); - ctx.closePath(); - ctx.fillStyle = this.color; - ctx.globalAlpha = .3; - ctx.fill(); - ctx.globalAlpha = 1.0; - } else { - ctx.closePath(); - } - }); - // draw points - if (o.lineDots) { - $.each(dataGroups, function (h) { - $.each(this.points, function (g) { - drawPoint(ctx, this.canvasCords[0], this.canvasCords[1], this.color, this.dotSize); - if (o.lineDots === 'double') { - drawPoint(ctx, this.canvasCords[0], this.canvasCords[1], this.innerColor, this.dotInnerSize); - } - }); - }); - } - - } - }; - - })(); - - charts.area = { - setup: function () { - charts.line.setup(true); - }, - draw: charts.line.draw - }; - - (function () { - - var horizontal, bottomLabels; - - charts.bar = { - setup: function () { - /** - * We can draw horizontal or vertical bars depending on the - * value of the 'barDirection' option (which may be 'vertical' or - * 'horizontal'). - */ - - horizontal = (o.barDirection == 'horizontal'); - - canvasContain.addClass('visualize-bar'); - - /** - * Write labels along the bottom of the chart. If we're drawing - * horizontal bars, these will be the yLabels, otherwise they - * will be the xLabels. The positioning also varies slightly: - * yLabels are values, hence they will span the whole width of - * the canvas, whereas xLabels are supposed to line up with the - * bars. - */ - bottomLabels = horizontal ? yLabels : xLabels; - - var xInterval = canvas.width() / (bottomLabels.length - (horizontal ? 1 : 0)); - - var xlabelsUL = $('
        ').width(canvas.width()).height(canvas.height()).insertBefore(canvas); - - $.each(bottomLabels, function (i) { - var thisLi = $('
      • ' + this + '
      • ').prepend('').css('left', xInterval * i).width(xInterval).appendTo(xlabelsUL); - - if (horizontal) { - var label = thisLi.find('span.label'); - label.css("margin-left", -label.width() / 2); - } - }); - - /** - * Write labels along the left of the chart. Follows the same idea - * as the bottom labels. - */ - var leftLabels = horizontal ? xLabels : yLabels; - var liBottom = canvas.height() / (leftLabels.length - (horizontal ? 0 : 1)); - - var ylabelsUL = $('
          ').width(canvas.width()).height(canvas.height()).insertBefore(canvas); - - $.each(leftLabels, function (i) { - var thisLi = $('
        • ' + this + '
        • ').prependTo(ylabelsUL); - - var label = thisLi.find('span:not(.line)').addClass('label'); - - if (horizontal) { - /** - * For left labels, we want to vertically align the text - * to the middle of its container, but we don't know how - * many lines of text we will have, since the labels could - * be very long. - * - * So we set a min-height of liBottom, and a max-height - * of liBottom + 1, so we can then check the label's actual - * height to determine if it spans one line or more lines. - */ - label.css({ - 'min-height': liBottom, - 'max-height': liBottom + 1, - 'vertical-align': 'middle' - }); - thisLi.css({ - 'top': liBottom * i, - 'min-height': liBottom - }); - - var r = label[0].getClientRects()[0]; - if (r.bottom - r.top == liBottom) { -/* This means we have only one line of text; hence - * we can centre the text vertically by setting the line-height, - * as described at: - * http://www.ampsoft.net/webdesign-l/vertical-aligned-nav-list.html - * - * (Although firefox has .height on the rectangle, IE doesn't, - * so we use r.bottom - r.top rather than r.height.) - */ - label.css('line-height', parseInt(liBottom) + 'px'); - } else { -/* - * If there is more than one line of text, then we shouldn't - * touch the line height, but we should make sure the text - * doesn't overflow the container. - */ - label.css("overflow", "hidden"); - } - } else { - thisLi.css('bottom', liBottom * i).prepend(''); - label.css('margin-top', -label.height() / 2) - } - }); - - charts.bar.draw(); - - }, - - draw: function () { - // Draw bars - if (horizontal) { - // for horizontal, keep the same code, but rotate everything 90 degrees - // clockwise. - ctx.rotate(Math.PI / 2); - } else { - // for vertical, translate to the top left corner. - ctx.translate(0, zeroLocY); - } - - // Don't attempt to draw anything if all the values are zero, - // otherwise we will get weird exceptions from the canvas methods. - if (totalYRange <= 0) return; - - var yScale = (horizontal ? canvas.width() : canvas.height()) / totalYRange; - var barWidth = horizontal ? (canvas.height() / xLabels.length) : (canvas.width() / (bottomLabels.length)); - var linewidth = (barWidth - o.barGroupMargin * 2) / dataGroups.length; - - for (var h = 0; h < dataGroups.length; h++) { - ctx.beginPath(); - - var strokeWidth = linewidth - (o.barMargin * 2); - ctx.lineWidth = strokeWidth; - var points = dataGroups[h].points; - var integer = 0; - for (var i = 0; i < points.length; i++) { - // If the last value is zero, IE will go nuts and not draw anything, - // so don't try to draw zero values at all. - if (points[i].value != 0) { - var xVal = (integer - o.barGroupMargin) + (h * linewidth) + linewidth / 2; - xVal += o.barGroupMargin * 2; - ctx.moveTo(xVal, 0); - ctx.lineTo(xVal, Math.round(-points[i].value * yScale)); - } - integer += barWidth; - } - ctx.strokeStyle = dataGroups[h].color; - ctx.stroke(); - ctx.closePath(); - } - - } - }; - - })(); - - //create new canvas, set w&h attrs (not inline styles) - var canvasNode = document.createElement("canvas"); - var canvas = $(canvasNode).attr({ - 'height': o.height, - 'width': o.width - }); - - //get title for chart - var title = o.title || self.find('caption').text(); - - //create canvas wrapper div, set inline w&h, append - var canvasContain = (container || $('
          ')).height(o.height).width(o.width); - - var scroller = $('
          ').appendTo(canvasContain).append(canvas); - - //title/key container - if (o.appendTitle || o.appendKey) { - var infoContain = $('
          ').appendTo(canvasContain); - } - - //append title - if (o.appendTitle) { - $('
          ' + title + '
          ').appendTo(infoContain); - } - - - //append key - if (o.appendKey) { - var newKey = $('
            '); - $.each(yAllLabels, function (i, label) { - $('
          • ' + label + '
          • ').appendTo(newKey); - }); - newKey.appendTo(infoContain); - }; - - // init interaction - if (o.interaction) { - // sets the canvas to track interaction - // IE needs one div on top of the canvas since the VML shapes prevent mousemove from triggering correctly. - // Pie charts needs tracker because labels goes on top of the canvas and also messes up with mousemove - var tracker = $('
            ').css({ - 'height': o.height + 'px', - 'width': o.width + 'px', - 'position': 'relative', - 'z-index': 200 - }).insertAfter(canvas); - - var triggerInteraction = function (overOut, data) { - var data = $.extend({ - canvasContain: canvasContain, - tableData: tableData - }, data); - self.trigger('vizualize' + overOut, data); - }; - - var over = false, - last = false, - started = false; - tracker.mousemove(function (e) { - var x, y, x1, y1, data, dist, i, current, selector, zLabel, elem, color, minDist, found, ev = e.originalEvent; - - // get mouse position relative to the tracker/canvas - x = ev.layerX || ev.offsetX || 0; - y = ev.layerY || ev.offsetY || 0; - - found = false; - minDist = started ? 30000 : (o.type == 'pie' ? (Math.round(canvas.height() / 2) - o.pieMargin) / 3 : o.lineWeight * 4); - // iterate datagroups to find points with matching - $.each(charts[o.type].interactionPoints, function (i, current) { - x1 = current.canvasCords[0] + zeroLocX; - y1 = current.canvasCords[1] + (o.type == "pie" ? 0 : zeroLocY); - dist = Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y)); - if (dist < minDist) { - found = current; - minDist = dist; - } - }); - - if (o.multiHover && found) { - x = found.canvasCords[0] + zeroLocX; - y = found.canvasCords[1] + (o.type == "pie" ? 0 : zeroLocY); - found = [found]; - $.each(charts[o.type].interactionPoints, function (i, current) { - if (current == found[0]) { - return; - } - x1 = current.canvasCords[0] + zeroLocX; - y1 = current.canvasCords[1] + zeroLocY; - dist = Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y)); - if (dist <= o.multiHover) { - found.push(current); - } - }); - } - // trigger over and out only when state changes, instead of on every mousemove - over = found; - if (over != last) { - if (over) { - if (last) { - triggerInteraction('Out', { - point: last - }); - } - triggerInteraction('Over', { - point: over - }); - last = over; - } - if (last && !over) { - triggerInteraction('Out', { - point: last - }); - last = false; - } - started = true; - } - }); - tracker.mouseleave(function () { - triggerInteraction('Out', { - point: last, - mouseOutGraph: true - }); - over = (last = false); - }); - } - - //append new canvas to page - if (!container) { - canvasContain.insertAfter(this); - } - if (typeof (G_vmlCanvasManager) != 'undefined') { - G_vmlCanvasManager.init(); - G_vmlCanvasManager.initElement(canvas[0]); - } - - //set up the drawing board - var ctx = canvas[0].getContext('2d'); - - // Scroll graphs - scroller.scrollLeft(o.width - scroller.width()); - - // init plugins - $.each($.visualizePlugins, function (i, plugin) { - plugin.call(self, o, tableData); - }); - - //create chart - charts[o.type].setup(); - - if (!container) { - //add event for updating - self.bind('visualizeRefresh', function () { - self.visualize(o, $(this).empty()); - }); - //add event for redraw - self.bind('visualizeRedraw', function () { - charts[o.type].draw(); - }); - } - }).next(); //returns canvas(es) - }; - // create array for plugins. if you wish to make a plugin, - // just push your init funcion into this array - $.visualizePlugins = []; - -})(jQuery); +/** + * -------------------------------------------------------------------- + * jQuery-Plugin "visualize" + * by Scott Jehl, scott@filamentgroup.com + * http://www.filamentgroup.com + * Copyright (c) 2009 Filament Group + * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses. + * + * -------------------------------------------------------------------- + */ +(function($) { +$.fn.visualize = function(options, container){ + return $(this).each(function(){ + //configuration + var o = $.extend({ + type: 'bar', //also available: area, pie, line + width: $(this).width(), //height of canvas - defaults to table height + height: $(this).height(), //height of canvas - defaults to table height + appendTitle: true, //table caption text is added to chart + title: null, //grabs from table caption if null + appendKey: true, //color key is added to chart + colors: ['#be1e2d','#666699','#92d5ea','#ee8310','#8d10ee','#5a3b16','#26a4ed','#f45a90','#e9e744'], + textColors: [], //corresponds with colors array. null/undefined items will fall back to CSS + parseDirection: 'x', //which direction to parse the table data + pieMargin: 10, //pie charts only - spacing around pie + pieLabelsAsPercent: true, + pieLabelPos: 'inside', + lineWeight: 4, //for line and area - stroke weight + lineDots: false, //also available: 'single', 'double' + dotInnerColor: "#ffffff", // only used for lineDots:'double' + lineMargin: (options.lineDots?15:0), //for line and area - spacing around lines + barGroupMargin: 10, + chartId: '', + xLabelParser: null, // function to parse labels as values + valueParser: null, // function to parse values. must return a Number + chartId: '', + chartClass: '', + barMargin: 1, //space around bars in bar chart (added to both sides of bar) + yLabelInterval: 30, //distance between y labels + interaction: false // only used for lineDots != false -- triggers mouseover and mouseout on original table + },options); + + //reset width, height to numbers + o.width = parseFloat(o.width); + o.height = parseFloat(o.height); + + // reset padding if graph is not lines + if(o.type != 'line' && o.type != 'area' ) { + o.lineMargin = 0; + } + + var self = $(this); + + // scrape data from html table + var tableData = {}; + var colors = o.colors; + var textColors = o.textColors; + + + var parseLabels = function(direction){ + var labels = []; + if(direction == 'x'){ + self.find('thead tr').each(function(i){ + $(this).find('th').each(function(j){ + if(!labels[j]) { + labels[j] = []; + } + labels[j][i] = $(this).text() + }) + }); + } + else { + self.find('tbody tr').each(function(i){ + $(this).find('th').each(function(j) { + if(!labels[i]) { + labels[i] = []; + } + labels[i][j] = $(this).text() + }); + }); + } + return labels; + }; + + var fnParse = o.valueParser || parseFloat; + var dataGroups = tableData.dataGroups = []; + if(o.parseDirection == 'x'){ + self.find('tbody tr').each(function(i,tr){ + dataGroups[i] = {}; + dataGroups[i].points = []; + dataGroups[i].color = colors[i]; + if(textColors[i]){ dataGroups[i].textColor = textColors[i]; } + $(tr).find('td').each(function(j,td){ + dataGroups[i].points.push( { + value: fnParse($(td).text()), + elem: td, + tableCords: [i,j] + } ); + }); + }); + } else { + var cols = self.find('tbody tr:eq(0) td').size(); + for(var i=0; itableData.topValue) { + tableData.topValue = fnParse(item.value,10); + } + if(item.valuexTopValue) { + xTopValue = label; + } + if(label tableData.topValue+loopInterval) { + yLabels.pop(); + } else if (yLabels[yLabels.length-1] <= tableData.topValue-10) { + yLabels.push(tableData.topValue); + } + + // populate some data + $.each(dataGroups,function(i,row){ + row.yLabels = tableData.yAllLabels[i]; + $.each(row.points, function(j,point){ + point.zeroLocY = tableData.zeroLocY; + point.zeroLocX = tableData.zeroLocX; + point.xLabels = tableData.xAllLabels[j]; + point.yLabels = tableData.yAllLabels[i]; + point.color = row.color; + }); + }); + + try{console.log(tableData);}catch(e){} + + var charts = {}; + + charts.pie = { + interactionPoints: dataGroups, + + setup: function() { + charts.pie.draw(true); + }, + draw: function(drawHtml){ + + var centerx = Math.round(canvas.width()/2); + var centery = Math.round(canvas.height()/2); + var radius = centery - o.pieMargin; + var counter = 0.0; + + if(drawHtml) { + canvasContain.addClass('visualize-pie'); + + if(o.pieLabelPos == 'outside'){ canvasContain.addClass('visualize-pie-outside'); } + + var toRad = function(integer){ return (Math.PI/180)*integer; }; + var labels = $('
              ') + .insertAfter(canvas); + } + + + //draw the pie pieces + $.each(dataGroups, function(i,row){ + var fraction = row.groupTotal / dataSum; + if (fraction <= 0 || isNaN(fraction)) + return; + ctx.beginPath(); + ctx.moveTo(centerx, centery); + ctx.arc(centerx, centery, radius, + counter * Math.PI * 2 - Math.PI * 0.5, + (counter + fraction) * Math.PI * 2 - Math.PI * 0.5, + false); + ctx.lineTo(centerx, centery); + ctx.closePath(); + ctx.fillStyle = dataGroups[i].color; + ctx.fill(); + // draw labels + if(drawHtml) { + var sliceMiddle = (counter + fraction/2); + var distance = o.pieLabelPos == 'inside' ? radius/1.5 : radius + radius / 5; + var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance)); + var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) * (distance)); + var leftRight = (labelx > centerx) ? 'right' : 'left'; + var topBottom = (labely > centery) ? 'bottom' : 'top'; + var percentage = parseFloat((fraction*100).toFixed(2)); + + // interaction variables + row.canvasCords = [labelx,labely]; + row.zeroLocY = tableData.zeroLocY = 0; // related to zeroLocY and plugin API + row.zeroLocX = tableData.zeroLocX = 0; // related to zeroLocX and plugin API + row.value = row.groupTotal; + + + if(percentage){ + var labelval = (o.pieLabelsAsPercent) ? percentage + '%' : row.groupTotal; + var labeltext = $('' + labelval +'') + .css(leftRight, 0) + .css(topBottom, 0); + if(labeltext) + var label = $('
            • ') + .appendTo(labels) + .css({left: labelx, top: labely}) + .append(labeltext); + labeltext + .css('font-size', radius / 8) + .css('margin-'+leftRight, -labeltext.width()/2) + .css('margin-'+topBottom, -labeltext.outerHeight()/2); + + if(dataGroups[i].textColor){ labeltext.css('color', dataGroups[i].textColor); } + + } + } + counter+=fraction; + }); + } + }; + + (function(){ + + var xInterval; + + var drawPoint = function (ctx,x,y,color,size) { + ctx.moveTo(x,y); + ctx.beginPath(); + ctx.arc(x,y,size/2,0,2*Math.PI,false); + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + }; + + charts.line = { + + interactionPoints: allItems, + + setup: function(area){ + + if(area){ canvasContain.addClass('visualize-area'); } + else{ canvasContain.addClass('visualize-line'); } + + //write X labels + var xlabelsUL = $('
                ') + .width(canvas.width()) + .height(canvas.height()) + .insertBefore(canvas); + + if(!o.customXLabels) { + xInterval = (canvas.width() - 2*o.lineMargin) / (xLabels.length -1); + $.each(xLabels, function(i){ + var thisLi = $('
              • '+this+'
              • ') + .prepend('') + .css('left', o.lineMargin + xInterval * i) + .appendTo(xlabelsUL); + var label = thisLi.find('span:not(.line)'); + var leftOffset = label.width()/-2; + if(i == 0){ leftOffset = 0; } + else if(i== xLabels.length-1){ leftOffset = -label.width(); } + label + .css('margin-left', leftOffset) + .addClass('label'); + }); + } else { + o.customXLabels(tableData,xlabelsUL); + } + + //write Y labels + var liBottom = (canvas.height() - 2*o.lineMargin) / (yLabels.length-1); + var ylabelsUL = $('
                  ') + .width(canvas.width()) + .height(canvas.height()) + // .css('margin-top',-o.lineMargin) + .insertBefore(scroller); + + $.each(yLabels, function(i){ + var value = Math.floor(this); + var posB = (value-bottomValue)*yScale + o.lineMargin; + if(posB >= o.height-1 || posB < 0) { + return; + } + var thisLi = $('
                • '+value+'
                • ') + .css('bottom', posB); + if(Math.abs(posB) < o.height-1) { + thisLi.prepend(''); + } + thisLi.prependTo(ylabelsUL); + + var label = thisLi.find('span:not(.line)'); + var topOffset = label.height()/-2; + if(!o.lineMargin) { + if(i == 0){ topOffset = -label.height(); } + else if(i== yLabels.length-1){ topOffset = 0; } + } + label + .css('margin-top', topOffset) + .addClass('label'); + }); + + //start from the bottom left + ctx.translate(zeroLocX,zeroLocY); + + charts.line.draw(area); + + }, + + draw: function(area) { + // prevent drawing on top of previous draw + ctx.clearRect(-zeroLocX,-zeroLocY,o.width,o.height); + // Calculate each point properties before hand + var integer; + $.each(dataGroups,function(i,row){ + integer = o.lineMargin; // the current offset + $.each(row.points, function(j,point){ + if(o.xLabelParser) { + point.canvasCords = [(xLabels[j]-zeroLocX)*xScale - xBottomValue,-(point.value*yScale)]; + } else { + point.canvasCords = [integer,-(point.value*yScale)]; + } + + if(o.lineDots) { + point.dotSize = o.dotSize||o.lineWeight*Math.PI; + point.dotInnerSize = o.dotInnerSize||o.lineWeight*Math.PI/2; + if(o.lineDots == 'double') { + point.innerColor = o.dotInnerColor; + } + } + integer+=xInterval; + }); + }); + // fire custom event so we can enable rich interaction + self.trigger('vizualizeBeforeDraw',{options:o,table:self,canvasContain:canvasContain,tableData:tableData}); + // draw lines and areas + $.each(dataGroups,function(h){ + // Draw lines + ctx.beginPath(); + ctx.lineWidth = o.lineWeight; + ctx.lineJoin = 'round'; + $.each(this.points, function(g){ + var loc = this.canvasCords; + if(g == 0) { + ctx.moveTo(loc[0],loc[1]); + } + ctx.lineTo(loc[0],loc[1]); + }); + ctx.strokeStyle = this.color; + ctx.stroke(); + // Draw fills + if(area){ + var integer = this.points[this.points.length-1].canvasCords[0]; + if (isFinite(integer)) + ctx.lineTo(integer,0); + ctx.lineTo(o.lineMargin,0); + ctx.closePath(); + ctx.fillStyle = this.color; + ctx.globalAlpha = .3; + ctx.fill(); + ctx.globalAlpha = 1.0; + } + else {ctx.closePath();} + }); + // draw points + if(o.lineDots) { + $.each(dataGroups,function(h){ + $.each(this.points, function(g){ + drawPoint(ctx,this.canvasCords[0],this.canvasCords[1],this.color,this.dotSize); + if(o.lineDots === 'double') { + drawPoint(ctx,this.canvasCords[0],this.canvasCords[1],this.innerColor,this.dotInnerSize); + } + }); + }); + } + + } + }; + + })(); + + charts.area = { + setup: function() { + charts.line.setup(true); + }, + draw: charts.line.draw + }; + + (function(){ + + var horizontal,bottomLabels; + + charts.bar = { + setup:function(){ + /** + * We can draw horizontal or vertical bars depending on the + * value of the 'barDirection' option (which may be 'vertical' or + * 'horizontal'). + */ + + horizontal = (o.barDirection == 'horizontal'); + + canvasContain.addClass('visualize-bar'); + + /** + * Write labels along the bottom of the chart. If we're drawing + * horizontal bars, these will be the yLabels, otherwise they + * will be the xLabels. The positioning also varies slightly: + * yLabels are values, hence they will span the whole width of + * the canvas, whereas xLabels are supposed to line up with the + * bars. + */ + bottomLabels = horizontal ? yLabels : xLabels; + + var xInterval = canvas.width() / (bottomLabels.length - (horizontal ? 1 : 0)); + + var xlabelsUL = $('
                    ') + .width(canvas.width()) + .height(canvas.height()) + .insertBefore(canvas); + + $.each(bottomLabels, function(i){ + var thisLi = $('
                  • '+this+'
                  • ') + .prepend('') + .css('left', xInterval * i) + .width(xInterval) + .appendTo(xlabelsUL); + + if (horizontal) { + var label = thisLi.find('span.label'); + label.css("margin-left", -label.width() / 2); + } + }); + + /** + * Write labels along the left of the chart. Follows the same idea + * as the bottom labels. + */ + var leftLabels = horizontal ? xLabels : yLabels; + var liBottom = canvas.height() / (leftLabels.length - (horizontal ? 0 : 1)); + + var ylabelsUL = $('
                      ') + .width(canvas.width()) + .height(canvas.height()) + .insertBefore(canvas); + + $.each(leftLabels, function(i){ + var thisLi = $('
                    • '+this+'
                    • ').prependTo(ylabelsUL); + + var label = thisLi.find('span:not(.line)').addClass('label'); + + if (horizontal) { + /** + * For left labels, we want to vertically align the text + * to the middle of its container, but we don't know how + * many lines of text we will have, since the labels could + * be very long. + * + * So we set a min-height of liBottom, and a max-height + * of liBottom + 1, so we can then check the label's actual + * height to determine if it spans one line or more lines. + */ + label.css({ + 'min-height': liBottom, + 'max-height': liBottom + 1, + 'vertical-align': 'middle' + }); + thisLi.css({'top': liBottom * i, 'min-height': liBottom}); + + var r = label[0].getClientRects()[0]; + if (r.bottom - r.top == liBottom) { + /* This means we have only one line of text; hence + * we can centre the text vertically by setting the line-height, + * as described at: + * http://www.ampsoft.net/webdesign-l/vertical-aligned-nav-list.html + * + * (Although firefox has .height on the rectangle, IE doesn't, + * so we use r.bottom - r.top rather than r.height.) + */ + label.css('line-height', parseInt(liBottom) + 'px'); + } + else { + /* + * If there is more than one line of text, then we shouldn't + * touch the line height, but we should make sure the text + * doesn't overflow the container. + */ + label.css("overflow", "hidden"); + } + } + else { + thisLi.css('bottom', liBottom * i).prepend(''); + label.css('margin-top', -label.height() / 2) + } + }); + + charts.bar.draw(); + + }, + + draw: function() { + // Draw bars + + if (horizontal) { + // for horizontal, keep the same code, but rotate everything 90 degrees + // clockwise. + ctx.rotate(Math.PI / 2); + } + else { + // for vertical, translate to the top left corner. + ctx.translate(0, zeroLocY); + } + + // Don't attempt to draw anything if all the values are zero, + // otherwise we will get weird exceptions from the canvas methods. + if (totalYRange <= 0) + return; + + var yScale = (horizontal ? canvas.width() : canvas.height()) / totalYRange; + var barWidth = horizontal ? (canvas.height() / xLabels.length) : (canvas.width() / (bottomLabels.length)); + var linewidth = (barWidth - o.barGroupMargin*2) / dataGroups.length; + + for(var h=0; h')) + .height(o.height) + .width(o.width); + + var scroller = $('
                      ') + .appendTo(canvasContain) + .append(canvas); + + //title/key container + if(o.appendTitle || o.appendKey){ + var infoContain = $('
                      ') + .appendTo(canvasContain); + } + + //append title + if(o.appendTitle){ + $('
                      '+ title +'
                      ').appendTo(infoContain); + } + + + //append key + if(o.appendKey){ + var newKey = $('
                        '); + $.each(yAllLabels, function(i,label){ + $('
                      • '+ label +'
                      • ') + .appendTo(newKey); + }); + newKey.appendTo(infoContain); + }; + + // init interaction + if(o.interaction) { + // sets the canvas to track interaction + // IE needs one div on top of the canvas since the VML shapes prevent mousemove from triggering correctly. + // Pie charts needs tracker because labels goes on top of the canvas and also messes up with mousemove + var tracker = $('
                        ') + .css({ + 'height': o.height + 'px', + 'width': o.width + 'px', + 'position':'relative', + 'z-index': 200 + }) + .insertAfter(canvas); + + var triggerInteraction = function(overOut,data) { + var data = $.extend({ + canvasContain:canvasContain, + tableData:tableData + },data); + self.trigger('vizualize'+overOut,data); + }; + + var over=false, last=false, started=false; + tracker.mousemove(function(e){ + var x,y,x1,y1,data,dist,i,current,selector,zLabel,elem,color,minDist,found,ev=e.originalEvent; + + // get mouse position relative to the tracker/canvas + x = ev.layerX || ev.offsetX || 0; + y = ev.layerY || ev.offsetY || 0; + + found = false; + minDist = started?30000:(o.type=='pie'?(Math.round(canvas.height()/2)-o.pieMargin)/3:o.lineWeight*4); + // iterate datagroups to find points with matching + $.each(charts[o.type].interactionPoints,function(i,current){ + x1 = current.canvasCords[0] + zeroLocX; + y1 = current.canvasCords[1] + (o.type=="pie"?0:zeroLocY); + dist = Math.sqrt( (x1 - x)*(x1 - x) + (y1 - y)*(y1 - y) ); + if(dist < minDist) { + found = current; + minDist = dist; + } + }); + + if(o.multiHover && found) { + x = found.canvasCords[0] + zeroLocX; + y = found.canvasCords[1] + (o.type=="pie"?0:zeroLocY); + found = [found]; + $.each(charts[o.type].interactionPoints,function(i,current){ + if(current == found[0]) {return;} + x1 = current.canvasCords[0] + zeroLocX; + y1 = current.canvasCords[1] + zeroLocY; + dist = Math.sqrt( (x1 - x)*(x1 - x) + (y1 - y)*(y1 - y) ); + if(dist <= o.multiHover) { + found.push(current); + } + }); + } + // trigger over and out only when state changes, instead of on every mousemove + over = found; + if(over != last) { + if(over) { + if(last) { + triggerInteraction('Out',{point:last}); + } + triggerInteraction('Over',{point:over}); + last = over; + } + if(last && !over) { + triggerInteraction('Out',{point:last}); + last=false; + } + started=true; + } + }); + tracker.mouseleave(function(){ + triggerInteraction('Out',{ + point:last, + mouseOutGraph:true + }); + over = (last = false); + }); + } + + //append new canvas to page + if(!container){canvasContain.insertAfter(this); } + if( typeof(G_vmlCanvasManager) != 'undefined' ){ G_vmlCanvasManager.init(); G_vmlCanvasManager.initElement(canvas[0]); } + + //set up the drawing board + var ctx = canvas[0].getContext('2d'); + + // Scroll graphs + scroller.scrollLeft(o.width-scroller.width()); + + // init plugins + $.each($.visualizePlugins,function(i,plugin){ + plugin.call(self,o,tableData); + }); + + //create chart + charts[o.type].setup(); + + if(!container){ + //add event for updating + self.bind('visualizeRefresh', function(){ + self.visualize(o, $(this).empty()); + }); + //add event for redraw + self.bind('visualizeRedraw', function(){ + charts[o.type].draw(); + }); + } + }).next(); //returns canvas(es) +}; +// create array for plugins. if you wish to make a plugin, +// just push your init funcion into this array +$.visualizePlugins = []; + +})(jQuery); + + diff --git a/public/templates/mail/notifications/auto_payout.tpl b/public/templates/mail/notifications/auto_payout.tpl new file mode 100644 index 00000000..6d045357 --- /dev/null +++ b/public/templates/mail/notifications/auto_payout.tpl @@ -0,0 +1,8 @@ + + +

                        An automated payout completed.

                        +

                        Amount: {$DATA.amount}

                        +
                        +
                        + + diff --git a/public/templates/mail/notifications/idle_worker.tpl b/public/templates/mail/notifications/idle_worker.tpl new file mode 100644 index 00000000..3ef12840 --- /dev/null +++ b/public/templates/mail/notifications/idle_worker.tpl @@ -0,0 +1,11 @@ + + +

                        One of your workers is currently IDLE: {$DATA.worker}

                        +

                        We have not received any shares for this worker in the past 10 minutes.

                        +

                        Since monitoring is enabled for this worker, this notification was sent.

                        +
                        +

                        Please check your workers!

                        +
                        +
                        + + diff --git a/public/templates/mail/notifications/manual_payout.tpl b/public/templates/mail/notifications/manual_payout.tpl new file mode 100644 index 00000000..75198d7b --- /dev/null +++ b/public/templates/mail/notifications/manual_payout.tpl @@ -0,0 +1,8 @@ + + +

                        An manual payout request completed.

                        +

                        Amount: {$DATA.amount}

                        +
                        +
                        + + diff --git a/public/templates/mail/notifications/new_block.tpl b/public/templates/mail/notifications/new_block.tpl new file mode 100644 index 00000000..9d17d06e --- /dev/null +++ b/public/templates/mail/notifications/new_block.tpl @@ -0,0 +1,7 @@ + + +

                        A new block has been discovered!

                        +
                        +
                        + + diff --git a/public/templates/mail/subject.tpl b/public/templates/mail/subject.tpl index 665e26f5..94fd6a28 100644 --- a/public/templates/mail/subject.tpl +++ b/public/templates/mail/subject.tpl @@ -1 +1 @@ -[ {$WEBSITENAME} ] Password Reset Request +[ {$WEBSITENAME} ] {$SUBJECT} diff --git a/public/templates/mmcFE/account/edit/default.tpl b/public/templates/mmcFE/account/edit/default.tpl index 9d885b16..69861d61 100644 --- a/public/templates/mmcFE/account/edit/default.tpl +++ b/public/templates/mmcFE/account/edit/default.tpl @@ -7,22 +7,23 @@ Username: {$GLOBAL.userdata.username} User Id: {$GLOBAL.userdata.id} API Key: {$GLOBAL.userdata.api_key} + E-Mail: Payment Address: Donation %: [donation amount in percent (example: 0.5)] - Automatic Payout Threshold: [1-250 LTC. Set to '0' for no auto payout] + Automatic Payout Threshold: [{$GLOBAL.config.ap_threshold.min}-{$GLOBAL.config.ap_threshold.max} {$GLOBAL.config.currency}. Set to '0' for no auto payout] 4 digit PIN: [The 4 digit PIN you chose when registering] {include file="global/block_footer.tpl"} {include file="global/block_header.tpl" BLOCK_HEADER="Cash Out"} -
                        • Please note: a 0.1 ltc transaction will apply when processing "On-Demand" manual payments
                        +
                        • Please note: a {$GLOBAL.config.txfee} {$GLOBAL.config.currency} transaction will apply when processing "On-Demand" manual payments
                        - +
                        Account Balance:    {$GLOBAL.userdata.balance|escape} LTC
                        Account Balance:    {$GLOBAL.userdata.balance.confirmed|escape} {$GLOBAL.config.currency}
                        Payout to:
                        {$GLOBAL.userdata.coin_address|escape}
                        4 digit PIN:
                        diff --git a/public/templates/mmcFE/account/notifications/default.tpl b/public/templates/mmcFE/account/notifications/default.tpl new file mode 100644 index 00000000..1d54729b --- /dev/null +++ b/public/templates/mmcFE/account/notifications/default.tpl @@ -0,0 +1,84 @@ +{include file="global/block_header.tpl" ALIGN="left" BLOCK_HEADER="Notification Settings"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        TypeActive
                        IDLE Worker + + + +
                        New Blocks + + + +
                        Auto Payout + + + +
                        Manual Payout + + + +
                        + +
                        +
                        +{include file="global/block_footer.tpl"} + +{include file="global/block_header.tpl" ALIGN="right" BLOCK_HEADER="Notification History"} +
                        + {include file="global/pagination.tpl"} + + + + + + + + + + +{section notification $NOTIFICATIONS} + + + + + + +{/section} + +
                        IDTimeTypeActive
                        {$NOTIFICATIONS[notification].id}{$NOTIFICATIONS[notification].time} + {if $NOTIFICATIONS[notification].type == new_block}New Block + {else if $NOTIFICATIONS[notification].type == auto_payout}Auto Payout + {else if $NOTIFICATIONS[notification].type == idle_worker}IDLE Worker + {else if $NOTIFICATIONS[notification].type == manual_payout}Manual Payout + {/if} + + +
                        +
                        +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/account/transactions/default.tpl b/public/templates/mmcFE/account/transactions/default.tpl index 8f5282e1..c76e9fae 100644 --- a/public/templates/mmcFE/account/transactions/default.tpl +++ b/public/templates/mmcFE/account/transactions/default.tpl @@ -1,7 +1,8 @@ {include file="global/block_header.tpl" BLOCK_HEADER="Transaction Log" BUTTONS=array(Confirmed,Unconfirmed,Orphan)}
                        - + {include file="global/pagination.tpl"} +
                        @@ -15,11 +16,15 @@ {section transaction $TRANSACTIONS} {if ( - ($TRANSACTIONS[transaction].type == 'Credit' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) + (($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus')and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations >= $GLOBAL.confirmations) + or $TRANSACTIONS[transaction].type == 'Credit_PPS' + or $TRANSACTIONS[transaction].type == 'Fee_PPS' + or $TRANSACTIONS[transaction].type == 'Donation_PPS' or $TRANSACTIONS[transaction].type == 'Debit_AP' or $TRANSACTIONS[transaction].type == 'Debit_MP' + or $TRANSACTIONS[transaction].type == 'TXFee' )} @@ -27,7 +32,7 @@ - + {/if} {/section} @@ -42,7 +47,8 @@
                        -
                        TX #
                        {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}{$TRANSACTIONS[transaction].amount}
                        + {include file="global/pagination.tpl" ID=2} +
                        @@ -56,7 +62,7 @@ {section transaction $TRANSACTIONS} {if ( - $TRANSACTIONS[transaction].type == 'Credit' && $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations + ($TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus') and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations or ($TRANSACTIONS[transaction].type == 'Donation' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) or ($TRANSACTIONS[transaction].type == 'Fee' and $TRANSACTIONS[transaction].confirmations < $GLOBAL.confirmations) )} @@ -66,9 +72,9 @@ - + - {if $TRANSACTIONS[transaction].type == Credit} + {if $TRANSACTIONS[transaction].type == 'Credit' or $TRANSACTIONS[transaction].type == 'Bonus'} {assign var="credits" value="`$credits+$TRANSACTIONS[transaction].amount`"} {else} {assign var="debits" value="`$debits+$TRANSACTIONS[transaction].amount`"} @@ -86,7 +92,8 @@
                        -
                        TX #
                        {$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}{$TRANSACTIONS[transaction].amount}
                        + {include file="global/pagination.tpl"} +
                        @@ -103,6 +110,7 @@ $TRANSACTIONS[transaction].type == 'Orphan_Credit' or $TRANSACTIONS[transaction].type == 'Orphan_Donation' or $TRANSACTIONS[transaction].type == 'Orphan_Fee' + or $TRANSACTIONS[transaction].type == 'Orphan_Bonus' )} @@ -110,9 +118,9 @@ - + - {if $TRANSACTIONS[transaction].type == Orphan_Credit} + {if $TRANSACTIONS[transaction].type == 'Orphan_Credit' or $TRANSACTIONS[transaction].type == 'Orphan_Bonus'} {assign var="orphan_credits" value="`$orphan_credits+$TRANSACTIONS[transaction].amount`"} {else} {assign var="orphan_debits" value="`$orphan_debits+$TRANSACTIONS[transaction].amount`"} diff --git a/public/templates/mmcFE/account/workers/default.tpl b/public/templates/mmcFE/account/workers/default.tpl index c09cdde3..8c127400 100644 --- a/public/templates/mmcFE/account/workers/default.tpl +++ b/public/templates/mmcFE/account/workers/default.tpl @@ -1,9 +1,4 @@ {include file="global/block_header.tpl" BLOCK_HEADER="My Workers"} -
                        • - CAUTION! Deletion of a worker could cause all associated shares for that worker to be lost. - Do not delete Workers unless you are certain all of their shares have been counted or that you have never used that worker account. -
                        -
                        @@ -14,18 +9,23 @@
                        - - + + + {section worker $WORKERS} {assign var="username" value="."|escape|explode:$WORKERS[worker].username:2} - {$username.0|escape}. + {$username.0|escape}. - - + + + {/section} diff --git a/public/templates/mmcFE/admin/default.tpl b/public/templates/mmcFE/admin/default.tpl new file mode 100644 index 00000000..60cdf352 --- /dev/null +++ b/public/templates/mmcFE/admin/default.tpl @@ -0,0 +1,3 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="Admin Panel"} +

                        Welcome to the admin panel. Please select an option from the drop-down menu.

                        +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/admin/user/default.tpl b/public/templates/mmcFE/admin/user/default.tpl new file mode 100644 index 00000000..82955741 --- /dev/null +++ b/public/templates/mmcFE/admin/user/default.tpl @@ -0,0 +1,89 @@ + + +{include file="global/block_header.tpl" BLOCK_HEADER="Query User Database"} + + + + + + +{include file="global/block_footer.tpl"} + +{include file="global/block_header.tpl" BLOCK_HEADER="User Information"} +
                        +{include file="global/pagination.tpl"} +
                        +
                        TX #
                        {$TRANSACTIONS[transaction].id}{$TRANSACTIONS[transaction].type} {$TRANSACTIONS[transaction].coin_address} {if $TRANSACTIONS[transaction].height == 0}n/a{else}{$TRANSACTIONS[transaction].height}{/if}{$TRANSACTIONS[transaction].amount}{$TRANSACTIONS[transaction].amount}
                        Worker Name PasswordActiveKhash/sActiveMonitorKhash/s    
                        {if $WORKERS[worker].active == 1}Y{else}N{/if}{$WORKERS[worker].hashrate} + + + {$WORKERS[worker].hashrate|number_format}
                        + + + + + + + + + + + + + + + +{section name=user loop=$USERS|default} + + + + + + + + + + + + +{sectionelse} + + + +{/section} + + + + + + + + + + + + + + + +
                        IDUsernameE-MailShares  Hashrate  Est. Donation  Est. Payout   Balance   AdminLocked
                        {$USERS[user].id}{$USERS[user].username}{$USERS[user].email}{$USERS[user].shares}{$USERS[user].hashrate}{$USERS[user].payout.est_donation|number_format:"8"}{$USERS[user].payout.est_payout|number_format:"8"}{$USERS[user].balance|number_format:"8"} + + + + + + + +
                        IDUsernameE-MailShares  Hashrate  Est. Donation  Est. Payout   Balance   AdminLocked
                        +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/admin/wallet/default.tpl b/public/templates/mmcFE/admin/wallet/default.tpl new file mode 100644 index 00000000..42b8c8b3 --- /dev/null +++ b/public/templates/mmcFE/admin/wallet/default.tpl @@ -0,0 +1,16 @@ +{include file="global/block_header.tpl" BLOCK_HEADER="Wallet Information"} + + + + + + + + + + + + + +
                        Wallet Balance{$BALANCE|number_format:"8"}
                        Locked for users{$LOCKED|number_format:"8"}
                        Liquid Assets{($BALANCE - $LOCKED)|number_format:"8"}
                        +{include file="global/block_footer.tpl"} diff --git a/public/templates/mmcFE/global/footer.tpl b/public/templates/mmcFE/global/footer.tpl index bf425d19..2450da79 100644 --- a/public/templates/mmcFE/global/footer.tpl +++ b/public/templates/mmcFE/global/footer.tpl @@ -1,5 +1,5 @@
                        - Litecoin Pool using litecoind, pushpoold
                        - mmcfe-ng Website based on mmcfe, overhauled by TheSerapher, available on GitHub
                        + Litecoin Pool using litecoind, pushpoold, stratum-mining
                        + mmcfe-ng Website based on mmcfe by AnnihilaT overhauled by TheSerapher, available on GitHub
                        LTC: Lge95QR2frp9y1wJufjUPCycVsg5gLJPW8


                        diff --git a/public/templates/mmcFE/global/header.tpl b/public/templates/mmcFE/global/header.tpl index 19f1a788..fd625758 100644 --- a/public/templates/mmcFE/global/header.tpl +++ b/public/templates/mmcFE/global/header.tpl @@ -4,9 +4,9 @@
                        - - - + + +
                      • LTC/usd: {$GLOBAL.price|default:"n/a"}    
                      • Pool Hashrate: {$GLOBAL.hashrate / 1000} MH/s    
                      • Pool Sharerate: {$GLOBAL.sharerate} Shares/s    
                      • {$GLOBAL.config.currency}/{$GLOBAL.config.price.currency}: {$GLOBAL.price|default:"n/a"|number_format:"4"}    
                      • Pool Hashrate: {($GLOBAL.hashrate / 1000)|number_format:"3"} MH/s    
                      • Pool Sharerate: {$GLOBAL.sharerate|number_format:"2"} Shares/s    
                      • Pool Workers: {$GLOBAL.workers}    
                      • diff --git a/public/templates/mmcFE/global/navigation.tpl b/public/templates/mmcFE/global/navigation.tpl index 3830006b..c53f61c7 100644 --- a/public/templates/mmcFE/global/navigation.tpl +++ b/public/templates/mmcFE/global/navigation.tpl @@ -5,16 +5,24 @@ {/if} - {if $smarty.session.AUTHENTICATED|default:"0" == 1 && $GLOBAL.userdata.admin == 1}
                      • Admin Panel
                      • {/if} -
                      • Statistics + {if $smarty.session.AUTHENTICATED|default:"0" == 1 && $GLOBAL.userdata.is_admin == 1} +
                      • Admin Panel + +
                      • + {/if} +
                      • Statistics
                      • Getting Started
                      • diff --git a/public/templates/mmcFE/global/pagination.tpl b/public/templates/mmcFE/global/pagination.tpl new file mode 100644 index 00000000..22bdb0e3 --- /dev/null +++ b/public/templates/mmcFE/global/pagination.tpl @@ -0,0 +1,15 @@ +
                        +
                        + + + + + + +
                        +
                        diff --git a/public/templates/mmcFE/global/sidebar.tpl b/public/templates/mmcFE/global/sidebar.tpl index 21a8e0aa..58d49c67 100644 --- a/public/templates/mmcFE/global/sidebar.tpl +++ b/public/templates/mmcFE/global/sidebar.tpl @@ -6,8 +6,13 @@
                        - - + + + + + + + @@ -35,7 +40,7 @@ - + @@ -54,8 +59,9 @@ - - + + +
                        diff --git a/public/templates/mmcFE/global/sidebar_pps.tpl b/public/templates/mmcFE/global/sidebar_pps.tpl new file mode 100644 index 00000000..b7aa8617 --- /dev/null +++ b/public/templates/mmcFE/global/sidebar_pps.tpl @@ -0,0 +1,61 @@ +
                        +
                        +
                        +
                        +

                        Dashboard

                        +
                        +
                        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                        +
                        +
                        +
                        diff --git a/public/templates/mmcFE/master.tpl b/public/templates/mmcFE/master.tpl index aff628f4..8709e974 100644 --- a/public/templates/mmcFE/master.tpl +++ b/public/templates/mmcFE/master.tpl @@ -11,6 +11,7 @@ + @@ -46,7 +47,11 @@