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'+data.point[i].value+' - '+data.point[i].yLabels[0]+'
';
+ }
+ 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
+{include file="global/block_footer.tpl"}
+
+{include file="global/block_header.tpl" ALIGN="right" BLOCK_HEADER="Notification History"}
+
+ {include file="global/pagination.tpl"}
+
+
+
+ ID
+ Time
+ Type
+ Active
+
+
+
+{section notification $NOTIFICATIONS}
+
+ {$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}
+
+
+
+
+
+{/section}
+
+
+
+{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'
)}
{$TRANSACTIONS[transaction].id}
@@ -27,7 +32,7 @@
{$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}
{/if}
{/section}
@@ -42,7 +47,8 @@
-
+ {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 @@
{$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}
- {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 @@
-
+ {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'
)}
{$TRANSACTIONS[transaction].id}
@@ -110,9 +118,9 @@
{$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}
- {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.
-
-
+{include file="global/block_footer.tpl"}
+
+{include file="global/block_header.tpl" BLOCK_HEADER="User Information"}
+
+{include file="global/pagination.tpl"}
+
+
+{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 @@
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 @@
+
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 @@
{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/register/disabled.tpl b/public/templates/mmcFE/register/disabled.tpl
new file mode 100644
index 00000000..5e5ac8ee
--- /dev/null
+++ b/public/templates/mmcFE/register/disabled.tpl
@@ -0,0 +1,3 @@
+{include file="global/block_header.tpl" BLOCK_HEADER="Registration disabled" BLOCK_STYLE="clear:none;"}
+We are currently not accepting new user registrations.
+{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/blocks/blocks_found.tpl b/public/templates/mmcFE/statistics/blocks/blocks_found.tpl
deleted file mode 100644
index 474fd87c..00000000
--- a/public/templates/mmcFE/statistics/blocks/blocks_found.tpl
+++ /dev/null
@@ -1,37 +0,0 @@
-{include file="global/block_header.tpl" BLOCK_HEADER="Last $BLOCKLIMIT Blocks Found" BLOCK_STYLE="clear:none;"}
-
-
-
-
- Block
- Validity
- Finder
- Date / Time
- Difficulty
- Shares
-
-
-
-{assign var=rank value=1}
-{section block $BLOCKSFOUND}
-
- {$BLOCKSFOUND[block].height}
-
- {if $BLOCKSFOUND[block].confirmations >= $GLOBAL.confirmations}
- Confirmed
- {else if $BLOCKSFOUND[block].confirmations == -1}
- Orphan
- {else}{$GLOBAL.confirmations - $BLOCKSFOUND[block].confirmations} left{/if}
- {$BLOCKSFOUND[block].finder|default:"unknown"}
- {$BLOCKSFOUND[block].time|date_format:"%d/%m/%Y %H:%M:%S"}
- {$BLOCKSFOUND[block].difficulty|number_format:"8"}
- {$BLOCKSFOUND[block].shares|number_format}
-
-{/section}
-
-
-
-
- Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
-
-{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/blocks/default.tpl b/public/templates/mmcFE/statistics/blocks/default.tpl
new file mode 100644
index 00000000..3d7afdde
--- /dev/null
+++ b/public/templates/mmcFE/statistics/blocks/default.tpl
@@ -0,0 +1,73 @@
+{include file="global/block_header.tpl" BLOCK_HEADER="Block Shares" BLOCK_STYLE="clear:none;"}
+
+ Block Shares
+
+
+{section block $BLOCKSFOUND step=-1 max=20}
+ {$BLOCKSFOUND[block].height}
+{/section}
+
+
+
+
+ Expected
+{section block $BLOCKSFOUND step=-1 max=20}
+ {round(pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty)}
+{/section}
+
+
+ Actual
+{section block $BLOCKSFOUND step=-1 max=20}
+ {$BLOCKSFOUND[block].shares}
+{/section}
+
+
+
+
+
+The graph above illustrates N shares to find a block vs. E Shares expected to find a block based on
+target and network difficulty and assuming a zero variance scenario.
+
+{include file="global/block_footer.tpl"}
+
+{include file="global/block_header.tpl" BLOCK_HEADER="Last $BLOCKLIMIT Blocks Found" BLOCK_STYLE="clear:none;"}
+
+
+
+
+ Block
+ Validity
+ Finder
+ Time
+ Difficulty
+ Expected Shares
+ Actual Shares
+ Percentage
+
+
+
+{assign var=rank value=1}
+{section block $BLOCKSFOUND}
+
+ {$BLOCKSFOUND[block].height}
+
+ {if $BLOCKSFOUND[block].confirmations >= $GLOBAL.confirmations}
+ Confirmed
+ {else if $BLOCKSFOUND[block].confirmations == -1}
+ Orphan
+ {else}{$GLOBAL.confirmations - $BLOCKSFOUND[block].confirmations} left{/if}
+ {$BLOCKSFOUND[block].finder|default:"unknown"}
+ {$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"}
+ {$BLOCKSFOUND[block].difficulty|number_format:"2"}
+ {(pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty)|number_format}
+ {$BLOCKSFOUND[block].shares|number_format}
+ {($BLOCKSFOUND[block].shares / (pow(2,32 - $GLOBAL.config.targetdiff) * $BLOCKSFOUND[block].difficulty) * 100)|number_format:"2"}
+
+{/section}
+
+
+
+
+ Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
+
+{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/blocks/small_table.tpl b/public/templates/mmcFE/statistics/blocks/small_table.tpl
new file mode 100644
index 00000000..9152fcc7
--- /dev/null
+++ b/public/templates/mmcFE/statistics/blocks/small_table.tpl
@@ -0,0 +1,28 @@
+{include file="global/block_header.tpl" BLOCK_HEADER="Last $BLOCKLIMIT Blocks Found" BLOCK_STYLE="clear:none;"}
+
+
+
+
+ Block
+ Finder
+ Time
+ Actual Shares
+
+
+
+{assign var=rank value=1}
+{section block $BLOCKSFOUND}
+
+ {$BLOCKSFOUND[block].height}
+ {$BLOCKSFOUND[block].finder|default:"unknown"}
+ {$BLOCKSFOUND[block].time|date_format:"%d/%m %H:%M:%S"}
+ {$BLOCKSFOUND[block].shares|number_format}
+
+{/section}
+
+
+
+
+ Note: Round Earnings are not credited until {$GLOBAL.confirmations} confirms.
+
+{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/default.tpl b/public/templates/mmcFE/statistics/default.tpl
index d1dfd202..a8e9a4c2 100644
--- a/public/templates/mmcFE/statistics/default.tpl
+++ b/public/templates/mmcFE/statistics/default.tpl
@@ -19,4 +19,5 @@
+These stats are also available in JSON format HERE
{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/graphs/both.tpl b/public/templates/mmcFE/statistics/graphs/both.tpl
new file mode 100644
index 00000000..ff760be1
--- /dev/null
+++ b/public/templates/mmcFE/statistics/graphs/both.tpl
@@ -0,0 +1,43 @@
+{if is_array($YOURHASHRATES) && is_array($POOLHASHRATES)}
+
+{foreach from=array('area','pie') item=chartType}
+
+ Your vs Pool Hashrate
+
+
+
+{for $i=date('G') to 23}
+ {$i}:00
+{/for}
+{for $i=0 to date('G', time () - 60 * 60)}
+ {$i}:00
+{/for}
+
+
+
+
+ {$smarty.session.USERDATA.username}
+{for $i=date('G') to 23}
+ {$YOURHASHRATES.$i|default:"0"}
+{/for}
+{for $i=0 to date('G', time() - 60 * 60)}
+ {$YOURHASHRATES.$i|default:"0"}
+{/for}
+
+
+ Pool
+{for $i=date('G') to 23}
+ {$POOLHASHRATES.$i|default:"0"}
+{/for}
+{for $i=0 to date('G', time() - 60 * 60)}
+ {$POOLHASHRATES.$i|default:"0"}
+{/for}
+
+
+
+
+{/foreach}
+
+{else}
+
No shares available to start calculations
+{/if}
diff --git a/public/templates/mmcFE/statistics/graphs/default.tpl b/public/templates/mmcFE/statistics/graphs/default.tpl
new file mode 100644
index 00000000..c325a4c6
--- /dev/null
+++ b/public/templates/mmcFE/statistics/graphs/default.tpl
@@ -0,0 +1,5 @@
+{include file="global/block_header.tpl" BLOCK_HEADER="24h Hashrate Statistics" BUTTONS=array(mine,pool,both)}
+{include file="{$smarty.request.page}/{$smarty.request.action}/mine.tpl"}
+{include file="{$smarty.request.page}/{$smarty.request.action}/pool.tpl"}
+{include file="{$smarty.request.page}/{$smarty.request.action}/both.tpl"}
+{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/graphs/mine.tpl b/public/templates/mmcFE/statistics/graphs/mine.tpl
new file mode 100644
index 00000000..fe217f2f
--- /dev/null
+++ b/public/templates/mmcFE/statistics/graphs/mine.tpl
@@ -0,0 +1,31 @@
+{if is_array($YOURHASHRATES)}
+
+
+ Your Hashrate
+
+
+
+{for $i=date('G') to 23}
+ {$i}:00
+{/for}
+{for $i=0 to date('G', time () - 60 * 60)}
+ {$i}:00
+{/for}
+
+
+
+
+ {$smarty.session.USERDATA.username}
+{for $i=date('G') to 23}
+ {$YOURHASHRATES.$i|default:"0"}
+{/for}
+{for $i=0 to date('G', time() - 60 * 60)}
+ {$YOURHASHRATES.$i|default:"0"}
+{/for}
+
+
+
+
+{else}
+
No shares available to start calculations
+{/if}
diff --git a/public/templates/mmcFE/statistics/graphs/pool.tpl b/public/templates/mmcFE/statistics/graphs/pool.tpl
new file mode 100644
index 00000000..ba2ed4e5
--- /dev/null
+++ b/public/templates/mmcFE/statistics/graphs/pool.tpl
@@ -0,0 +1,31 @@
+{if is_array($POOLHASHRATES)}
+
+
+ Pool Hashrate
+
+
+
+{for $i=date('G') to 23}
+ {$i}:00
+{/for}
+{for $i=0 to date('G', time () - 60 * 60)}
+ {$i}:00
+{/for}
+
+
+
+
+ Pool
+{for $i=date('G') to 23}
+ {$POOLHASHRATES.$i|default:"0"}
+{/for}
+{for $i=0 to date('G', time() - 60 * 60)}
+ {$POOLHASHRATES.$i|default:"0"}
+{/for}
+
+
+
+
+{else}
+
No shares available to start calculations
+{/if}
diff --git a/public/templates/mmcFE/statistics/pool/authenticated.tpl b/public/templates/mmcFE/statistics/pool/authenticated.tpl
index d0ce7803..edc0e0ba 100644
--- a/public/templates/mmcFE/statistics/pool/authenticated.tpl
+++ b/public/templates/mmcFE/statistics/pool/authenticated.tpl
@@ -4,15 +4,19 @@
{include file="statistics/pool/contributors_hashrate.tpl"}
-{include file="global/block_header.tpl" BLOCK_HEADER="Server Stats" BLOCK_STYLE="clear:all;" STYLE="padding-left:5px;padding-right:5px;"}
+{include file="global/block_header.tpl" ALIGN="left" BLOCK_HEADER="Server Stats" BLOCK_STYLE="clear:all;" STYLE="padding-left:5px;padding-right:5px;"}
- {$GLOBAL.hashrate / 1000} Mhash/s
+ {($GLOBAL.hashrate / 1000)|number_format:"3"} Mhash/s
-
+
+ {(100 - (100 / $GLOBAL.roundshares.valid * $GLOBAL.roundshares.invalid))|number_format:"2"} %
+
+
+
{$GLOBAL.workers}
@@ -32,8 +36,8 @@
{$ESTTIME|seconds_to_words}
-
- {($ESTTIME * $GLOBAL.sharerate)|number_format:"0"}
+
+ {(pow(2, 32 - $GLOBAL.config.targetdiff) * $DIFFICULTY)|number_format:"0"} (done: {(100 / (pow(2, 32 - $GLOBAL.config.targetdiff) * $DIFFICULTY) * $GLOBAL.roundshares.valid)|number_format:"2"} %)
@@ -44,6 +48,6 @@
{include file="global/block_footer.tpl"}
-{include file="statistics/blocks/blocks_found.tpl"}
+{include file="statistics/blocks/small_table.tpl" ALIGN="right" SHORT=true}
{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl
index b97c1515..222902c7 100644
--- a/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl
+++ b/public/templates/mmcFE/statistics/pool/contributors_hashrate.tpl
@@ -6,18 +6,21 @@
Rank
User Name
KH/s
- Ł/Day (est)
+ {$GLOBAL.config.currency}/Day
+ {$GLOBAL.config.price.currency}/Day
{assign var=rank value=1}
{assign var=listed value=0}
{section contrib $CONTRIBHASHES}
+ {math assign="estday" equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$CONTRIBHASHES[contrib].hashrate}
{$rank++}
{$CONTRIBHASHES[contrib].account}
{$CONTRIBHASHES[contrib].hashrate|number_format}
- {math equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$CONTRIBHASHES[contrib].hashrate}
+ {$estday|number_format:"3"}
+ {($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}
{/section}
{if $listed != 1}
@@ -25,7 +28,8 @@
n/a
{$GLOBAL.userdata.username}
{$GLOBAL.userdata.hashrate}
- {math equation="round(reward / ( diff * pow(2,32) / ( hashrate * 1000 ) / 3600 / 24), 3)" diff=$DIFFICULTY reward=$REWARD hashrate=$GLOBAL.userdata.hashrate}
+ {$estday|number_format:"3"|default:"n/a"}
+ {($estday * $GLOBAL.price)|default:"n/a"|number_format:"2"}
{/if}
diff --git a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl
index 04e9c3e0..444effa4 100644
--- a/public/templates/mmcFE/statistics/pool/contributors_shares.tpl
+++ b/public/templates/mmcFE/statistics/pool/contributors_shares.tpl
@@ -22,7 +22,7 @@
n/a
{$GLOBAL.userdata.username}
- {$GLOBAL.userdata.shares.valid}
+ {$GLOBAL.userdata.shares.valid|number_format}
{/if}
diff --git a/public/templates/mmcFE/statistics/pool/default.tpl b/public/templates/mmcFE/statistics/pool/default.tpl
deleted file mode 100644
index d1dfd202..00000000
--- a/public/templates/mmcFE/statistics/pool/default.tpl
+++ /dev/null
@@ -1,22 +0,0 @@
-{include file="global/block_header.tpl" BLOCK_HEADER="Pool Statistics"}
-
-{include file="global/block_footer.tpl"}
diff --git a/public/templates/mmcFE/statistics/user/default.tpl b/public/templates/mmcFE/statistics/user/default.tpl
deleted file mode 100644
index ffc5e8c8..00000000
--- a/public/templates/mmcFE/statistics/user/default.tpl
+++ /dev/null
@@ -1,23 +0,0 @@
-{include file="global/block_header.tpl" BLOCK_HEADER="Your Average Hourly Hash Rate" BUTTONS=array(mine,pool,both)}
-
-
- Your Hashrate
-
-
-
-{section hashrate $YOURHASHRATES}
- {$YOURHASHRATES[hashrate].hour}
-{/section}
-
-
-
-
- {$GLOBAL.USERDATA.username}
-{section hashrate $YOURHASHRATES}
- {$YOURHASHRATES[hashrate].hashrate|number_format}
-{/section}
-
-
-
-
-{include file="global/block_footer.tpl"}
diff --git a/sql/issue_144_notification_upgrade.sql b/sql/issue_144_notification_upgrade.sql
new file mode 100644
index 00000000..73985979
--- /dev/null
+++ b/sql/issue_144_notification_upgrade.sql
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS `notifications` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `type` varchar(25) NOT NULL,
+ `data` varchar(255) NOT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `account_id` int(10) unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `active` (`active`),
+ KEY `data` (`data`),
+ KEY `account_id` (`account_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE IF NOT EXISTS `notification_settings` (
+ `type` varchar(15) NOT NULL,
+ `account_id` int(11) NOT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '0'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/sql/issue_147_accounts_upgrade.sql b/sql/issue_147_accounts_upgrade.sql
new file mode 100644
index 00000000..ecff0f6f
--- /dev/null
+++ b/sql/issue_147_accounts_upgrade.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `accounts` ADD `is_locked` BOOLEAN NOT NULL DEFAULT FALSE AFTER `email` ;
+ALTER TABLE `accounts` CHANGE `admin` `is_admin` BOOLEAN NOT NULL DEFAULT FALSE ;
diff --git a/sql/issue_148_transactions_upgrade.sql b/sql/issue_148_transactions_upgrade.sql
new file mode 100644
index 00000000..2c0a47a7
--- /dev/null
+++ b/sql/issue_148_transactions_upgrade.sql
@@ -0,0 +1 @@
+ALTER TABLE `transactions` CHANGE `type` `type` ENUM( 'Credit', 'Debit_MP', 'Debit_AP', 'Donation', 'Fee', 'Orphan_Credit', 'Orphan_Fee', 'Orphan_Donation', 'Bonus', 'Orphan_Bonus' ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;
diff --git a/sql/issue_167_settings_upgrade.sql b/sql/issue_167_settings_upgrade.sql
new file mode 100644
index 00000000..dff0378a
--- /dev/null
+++ b/sql/issue_167_settings_upgrade.sql
@@ -0,0 +1 @@
+ALTER TABLE `settings` CHANGE `setting` `name` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ;
diff --git a/sql/issue_182_accounts_upgrade.sql b/sql/issue_182_accounts_upgrade.sql
new file mode 100644
index 00000000..62266515
--- /dev/null
+++ b/sql/issue_182_accounts_upgrade.sql
@@ -0,0 +1 @@
+ALTER TABLE `accounts` ADD `failed_logins` INT( 5 ) UNSIGNED NULL DEFAULT '0' AFTER `is_locked` ;
diff --git a/sql/issue_203_transactions_upgrade.sql b/sql/issue_203_transactions_upgrade.sql
new file mode 100644
index 00000000..9c824b89
--- /dev/null
+++ b/sql/issue_203_transactions_upgrade.sql
@@ -0,0 +1 @@
+ALTER TABLE `transactions` CHANGE `type` `type` ENUM( 'Credit', 'Debit_MP', 'Debit_AP', 'Donation', 'Fee', 'Orphan_Credit', 'Orphan_Fee', 'Orphan_Donation', 'Credit_PPS', 'Fee_PPS', 'Donation_PPS', 'TXFee' ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ;
diff --git a/sql/issue_212_notification_settings_upgrade.sql b/sql/issue_212_notification_settings_upgrade.sql
new file mode 100644
index 00000000..1a07b26e
--- /dev/null
+++ b/sql/issue_212_notification_settings_upgrade.sql
@@ -0,0 +1 @@
+ALTER TABLE `notification_settings` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST ;
diff --git a/sql/issue_70_transactions_upgrade.sql b/sql/issue_70_transactions_upgrade.sql
new file mode 100644
index 00000000..90561397
--- /dev/null
+++ b/sql/issue_70_transactions_upgrade.sql
@@ -0,0 +1 @@
+ALTER TABLE `transactions` CHANGE `type` `type` ENUM( 'Credit', 'Debit_MP', 'Debit_AP', 'Donation', 'Fee', 'Orphan_Credit', 'Orphan_Fee', 'Orphan_Donation', 'Bonus', 'Orphan_Bonus', 'Credit_PPS', 'Debit_PPS', 'Donation_PPS' ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;
diff --git a/sql/mmcfe_ng_structure.sql b/sql/mmcfe_ng_structure.sql
index 4b6bf27d..614d3f23 100644
--- a/sql/mmcfe_ng_structure.sql
+++ b/sql/mmcfe_ng_structure.sql
@@ -1,37 +1,22 @@
--- phpMyAdmin SQL Dump
--- version 3.5.8.1deb1
--- http://www.phpmyadmin.net
---
--- Host: localhost
--- Generation Time: May 31, 2013 at 02:31 PM
--- Server version: 5.5.31-0ubuntu0.13.04.1
--- PHP Version: 5.4.9-4ubuntu2
-
-SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
-
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
---
--- Database: `mmcfe_ng`
---
-
--- --------------------------------------------------------
-
---
--- Table structure for table `accounts`
---
+CREATE DATABASE IF NOT EXISTS `mmcfe_ng` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
+USE `mmcfe_ng`;
CREATE TABLE IF NOT EXISTS `accounts` (
`id` int(255) NOT NULL AUTO_INCREMENT,
- `admin` int(1) NOT NULL DEFAULT '0',
+ `is_admin` tinyint(1) NOT NULL DEFAULT '0',
`username` varchar(40) NOT NULL,
`pass` varchar(255) NOT NULL,
`email` varchar(255) DEFAULT NULL COMMENT 'Assocaited email: used for validating users, and re-setting passwords',
+ `is_locked` tinyint(1) NOT NULL DEFAULT '0',
+ `failed_logins` int(5) unsigned DEFAULT '0',
`loggedIp` varchar(255) DEFAULT NULL,
`sessionTimeoutStamp` int(255) DEFAULT NULL,
`pin` varchar(255) NOT NULL COMMENT 'four digit pin to allow account changes',
@@ -44,12 +29,6 @@ CREATE TABLE IF NOT EXISTS `accounts` (
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--- --------------------------------------------------------
-
---
--- Table structure for table `blocks`
---
-
CREATE TABLE IF NOT EXISTS `blocks` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`height` int(10) unsigned NOT NULL,
@@ -65,13 +44,39 @@ CREATE TABLE IF NOT EXISTS `blocks` (
PRIMARY KEY (`id`),
UNIQUE KEY `height` (`height`,`blockhash`),
KEY `time` (`time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Discovered blocks persisted from Litecoin Service';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Discovered blocks persisted from Litecoin Service';
--- --------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `notifications` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `type` varchar(25) NOT NULL,
+ `data` varchar(255) NOT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `account_id` int(10) unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `active` (`active`),
+ KEY `data` (`data`),
+ KEY `account_id` (`account_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
---
--- Table structure for table `settings`
---
+CREATE TABLE IF NOT EXISTS `notification_settings` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `type` varchar(15) NOT NULL,
+ `account_id` int(11) NOT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `pool_worker` (
+ `id` int(255) NOT NULL AUTO_INCREMENT,
+ `account_id` int(255) NOT NULL,
+ `username` char(50) DEFAULT NULL,
+ `password` char(255) DEFAULT NULL,
+ `monitor` tinyint(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `username` (`username`),
+ KEY `account_id` (`account_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `settings` (
`name` varchar(255) NOT NULL,
@@ -80,12 +85,6 @@ CREATE TABLE IF NOT EXISTS `settings` (
UNIQUE KEY `setting` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--- --------------------------------------------------------
-
---
--- Table structure for table `shares`
---
-
CREATE TABLE IF NOT EXISTS `shares` (
`id` bigint(30) NOT NULL AUTO_INCREMENT,
`rem_host` varchar(255) NOT NULL,
@@ -100,13 +99,7 @@ CREATE TABLE IF NOT EXISTS `shares` (
KEY `upstream_result` (`upstream_result`),
KEY `our_result` (`our_result`),
KEY `username` (`username`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `shares_archive`
---
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `shares_archive` (
`id` int(255) unsigned NOT NULL AUTO_INCREMENT,
@@ -119,13 +112,7 @@ CREATE TABLE IF NOT EXISTS `shares_archive` (
PRIMARY KEY (`id`),
UNIQUE KEY `share_id` (`share_id`),
KEY `time` (`time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Archive shares for potential later debugging purposes';
-
--- --------------------------------------------------------
-
---
--- Table structure for table `statistics_shares`
---
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Archive shares for potential later debugging purposes';
CREATE TABLE IF NOT EXISTS `statistics_shares` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
@@ -136,18 +123,12 @@ CREATE TABLE IF NOT EXISTS `statistics_shares` (
PRIMARY KEY (`id`),
KEY `account_id` (`account_id`),
KEY `block_id` (`block_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `transactions`
---
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `transactions` (
`id` int(255) NOT NULL AUTO_INCREMENT,
`account_id` int(255) unsigned NOT NULL,
- `type` enum('Credit','Debit_MP','Debit_AP','Donation','Fee','Orphan_Credit','Orphan_Fee','Orphan_Donation') DEFAULT NULL,
+ `type` enum('Credit','Debit_MP','Debit_AP','Donation','Fee','Orphan_Credit','Orphan_Fee','Orphan_Donation','Credit_PPS','Fee_PPS','Donation_PPS','TXFee') DEFAULT NULL,
`coin_address` varchar(255) DEFAULT NULL,
`amount` double DEFAULT '0',
`block_id` int(255) DEFAULT NULL,
@@ -156,24 +137,7 @@ CREATE TABLE IF NOT EXISTS `transactions` (
KEY `block_id` (`block_id`),
KEY `account_id` (`account_id`),
KEY `type` (`type`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `pool_worker`
---
-
-CREATE TABLE IF NOT EXISTS `pool_worker` (
- `id` int(255) NOT NULL AUTO_INCREMENT,
- `account_id` int(255) NOT NULL,
- `username` char(50) DEFAULT NULL,
- `password` char(255) DEFAULT NULL,
- `hashrate` int(11) DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `username` (`username`),
- KEY `account_id` (`account_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;