diff --git a/.gitignore b/.gitignore index d67a566e..788a1bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /public/include/config/global.inc.php /public/templates/compile/*.php +/cronjobs/logs/*.txt +/cronjobs/logs/*.txt.*.gz +/public/templates/cache/*.php diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0187d0a..3c2cfd08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ consider following this contribution guide! * [General GitHub documentation][2] * [GitHub pull request documentation][3] -[1]: https://github.com/TheSerapher/php-mmcfe-ng/issues "Issue" +[1]: https://github.com/TheSerapher/php-mpos/issues "Issue" [2]: http://help.github.com/ "GitHub documentation" [3]: http://help.github.com/send-pull-requests/ "GitHub pull request documentation" [4]: https://github.com/signup/free "GitHub account" diff --git a/POOLS.md b/POOLS.md new file mode 100644 index 00000000..e1217ec3 --- /dev/null +++ b/POOLS.md @@ -0,0 +1,122 @@ +Pools running MPOS +================== + +If you are not sure if you want to use `MPOS` on your own pool, +maybe this list will push you over the edge of decision making. Some +small and large pools are already running on it and have succesfully +tested it on various coins. + +These tables represent their users pools. Be aware that all values are +as of this writing and may have changed since then. + + +### vias79 + +Vias is using MPOS with a small group of people to mine various coins. +They have succesfully mined blocks on each of those pools listed. +All pools are running on Stratum only. + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | -----------------: | ----- | +| http://wdc.nordicminers.eu | Worldcoin | n/a | n/a | | +| http://lky.nordicminers.eu | Luckycoin | n/a | n/a | | +| http://fst.nordicminers.eu | Fastcoin | n/a | n/a | | +| http://dgc.nordicminers.eu | Digitalcoin | n/a | n/a | | +| http://ezc.nordicminers.eu | Ezcoin | n/a | n/a | | +| http://sbc.nordicminers.eu | Stablecoin | n/a | n/a | | +| http://mnc.nordicminers.eu | Mincoin | n/a | n/a | | +| http://arg.nordicminers.eu | Argentum | n/a | n/a | | +| http://mem.nordicminers.eu | Memecoin | n/a | n/a | | +| http://frk.nordicminers.eu | Franko | n/a | n/a | | +| http://pxc.nordicminers.eu | Phenixcoin | n/a | n/a | | + +### WKNiGHT + +WKNiGHT was an early adopter of `MPOS`. He has been around since a first stable release +which only featured proportional payouts. He successfully moved to PPS since then and is +running more or less without any issues (related to `MPOS` that is ;-)). He is also running +the most powerful pool! + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| http://www.ejpool.info | Litecoin | 155 MHash | 120 | | + +### Obigal + +Small Time Miners are running various stratum only pools for different coins. + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------- | ----- | +| http://meg.smalltimeminer.com | Megacoin | 5 - 10 MHash | n/a | | +| http://flo.smalltimeminer.com | Florincoin | 5 - 6 MHash | n/a | | +| http://alf.smalltimeminer.com | Alphacoin | 2 - 4 MHash | n/a | | +| http://cgb.smalltimeminer.com | Cryptobullion | 2 - 4 MHash | n/a | PoS/PoW type coin + +### Feeleep75 + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| http://bot.coinmine.pl | Bottlecaps | 3 - 50 MHash | n/a | PoS/PoW type coin | +| http://yacp.coinmine.pl | YaCoin | 19 MHash | n/a | | + +### LiteSaber + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------- | ------------------: | ----- | +| http://coinhuntr.com | Litecoin | 200 MHash | 250 | Custom Frontend template | + +### Sheinsha + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| http://str.minar.cc | StarCoin | 35 MHash | 35 | PPLNS+VARDIFF, PoS/PoW type coin | +| http://dmd.minar.cc | DiamondCoin | 90 MHash | 65 | PPLNS+VARDIFF, PoS/PoW type coin | +| http://cgb.minar.cc | CryptogenicBullion | 18 MHash | 15 | PPLNS+VARDIFF, PoS/PoW type coin | +| http://phs.minar.cc | Philosopherstone Coin | 170 MHash | 130 | PPLNS+VARDIFF, PoS/PoW type coin | + +### IainKay & Nushor + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------- | ------------------: | ----- | +| http://ltc.nushor.net | Litecoin | 130 MHash | 300 | Succesfully migrated from `mmcfe` | + +### nrpatten + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| http://www.litecoinfor.me | Litecoin | 0 | 0 | | +| http://www.fastcoinfor.me | Fastcoin | 0.830 MHash | 2 | | + +### ZC + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| https://ltc.hashfaster.com | LTC | 70 MHash | 80 | Custom Template | + +### nutnut + + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| http://ftc.nut2pools.com | Feathercoin | 45-50Mhs | 25 workers | New style, PPLNS | +| http://wdc.nut2pools.com | Worldcoin | 3.5 Mhs | 3 workers | New style, PPLNS | +| http://pxc.nut2pools.com | Phenixcoin | 0 | 0 | New style | PPLNS | + +### ahmedbodi + + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| http://gme.crypto-expert.com | Gamecoin | 1.5Mhs | 3 workers | Custom Template, Prop | +| http://orb.crypto-expert.com | Orbitcoin | 0.2 Mhs | 1 workers | Custom Template, PPLNS | +| http://src.crypto-expert.com | Securecoin | 25 | 20 | Custom Template, Prop | +| http://dgc.crypto-expert.com | Digitalcoin | 0 | 0 | Custom Template, Prop | +| http://arg.crypto-expert.com | Argentum | 0 | 0 | Custom Template, Prop| +| http://crypto-expert.com/TIX | Tix | 0.5 | 1 | Custom Template, Prop| + +### Neozonz + + +| Pool URL | Coin | Avg. Hashrate | Avg. Active Workers | Notes | +| -------- | ---- | ------------: | ------------------: | ----- | +| https://www.mine-litecoin.com | Litecoin | 10Mhs | 75 workers | Custom Template, Prop | diff --git a/README.md b/README.md index 817aab98..ce2ca5b0 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,11 @@ Description =========== -mmcFE-ng is a web frontend for Pooled LTC Mining. A pool using this interface is running at http://pool.grewe.ca +MPOS is a web based Mining Portal for various crypto currencies. A few pools using this interface are running at: -The web frontend layout is based on mmcFE, the original work by Greedi: -https://github.com/Greedi/mmcFE - -After working a few days trying to learn to run my own pool and the -systems behind it I figured I'd look a bit deeper in the code to -understand how it works. While doing so I also migrated the existing -code over to my own framework so maintenance would be easier in the -future. +* http://ltc.pool.grewe.ca +* http://fst.pool.grewe.ca +* http://nvc.pool.grewe.ca **NOTE**: This project is still under development and commits are happening on a daily basis. I do not recommend using this for a live setup as of yet. Wait for the later Release Candidate @@ -21,7 +16,17 @@ Donations I was hoping to keep this out of the README but apparently people remove or change the LTC address at the bottom of the page. For those of you finding my project and are willing to appreciate the work -with some hard earned LTC feel free to donate to my LTC address: `Lge95QR2frp9y1wJufjUPCycVsg5gLJPW8` +with some hard earned coins feel free to donate: + +* LTC address: `Lge95QR2frp9y1wJufjUPCycVsg5gLJPW8` +* FTC address: `6jDgGaUzMVyac5uqBhJCMiFMKCtH1LagTA` +* NVC address: `4Guct6z7NVPVALHRAVn517TTmvqQve4WYr` +* FST address: `g17CfFHqNqR5JnUjtG8RNBYh2WrhEirV67` + +Website Footer +============== + +When you decide to use `MPOS` please be so kind and leave the footer intact. You are not the author of the software and should honor those that have worked on it. I don't mind changing the LTC donation address at the bottom, but keep in mind who really wrote this software and would deserve those ;-). Donors ====== @@ -31,6 +36,13 @@ These people have supported this project with a donation: * [obigal](https://github.com/obigal) * [vias](https://github.com/vias79) * [WKNiGHT](https://github.com/WKNiGHT-) +* [ZC](https://github.com/zccopwrx) +* Nutnut + +Pools running MPOS +================== + +You can find a list of active pools [here](POOLS.md). Requirements ============ @@ -40,6 +52,11 @@ It should also work on any related distribution (RHEL, Debian). For support on how to get `litecoind` or `pushpoold` to work, please ask in the appropriate forums. +Be aware that `MPOS` is **only** for pooled mining. Solo Mining is not +supported. They will never match an upstream share, solo miners do not create +any shares, only blocks. Expect weird behavior if trying to mix them. See #299 +for full information. + * Apache2 * libapache2-mod-php5 * PHP 5.4+ @@ -57,33 +74,44 @@ Features The following feature have been implemented so far: +* Fully re-written GUI with [Smarty][2] templates +* Mobile WebUI +* **NEW** VARDIFF Support * Reward Systems * Propotional * PPS - * (Planned) PPLNS -* Use of memcache for statistics instead of a cronjob + * PPLNS +* Statistics are cached in Memcache by Cronjob for quick data access +* **NEW** New Theme + * **NEW** Live Dashboard + * **NEW** AJAX Support + * **NEW** Overhauled API * Web User accounts * Re-Captcha protected registration form * Worker accounts - * Worker activity (live, past 10 minutes) - * Worker hashrates (live, past 10 minutes) + * Worker activity + * Worker hashrates * Pool statistics * Minimal Block statistics * Pool donations * Pool fees +* Block Bonus Payouts * Manual payout * Auto payout -* Transaction list (confirmed and unconfirmed) +* Transaction list * Admin Panel + * Cron Monitoring Overview * User Listing including statistics * Wallet information - * (Planned) News Posts - * (Planned) Pool Donations + * User Transactions + * News Posts + * Pool Settings * Notification system * IDLE Workers * New blocks found in pool * Auto Payout * Manual Payout +* User-to-user Invitation System * Support for various Scrypt based coins via config * MNC * LTC @@ -92,9 +120,28 @@ The following feature have been implemented so far: Installation ============ -Please take a look at the [Quick Start Guide](https://github.com/TheSerapher/php-mmcfe-ng/wiki/Quick-Start-Guide). This will give you -an idea how to setup `mmcfe-ng`. +Please take a look at the [Quick Start Guide](https://github.com/TheSerapher/php-mpos/wiki/Quick-Start-Guide). This will give you +an idea how to setup `MPOS`. +Customization +============= + +This project was meant to allow users to easily cusomize the system and templates. Hence no upstream framework was used to keep it as simple as possible. +If you are just using the system, there will be no need to adjust anything. Things will work out of the box! But if you plan on creating +your own theme, things are pretty easy: + +* Create a new theme folder in `public/templates/` +* Create a new site_assets folder in `public/site_assets` +* Create your own complete custom template or copy from an existing one +* Change your theme in the `Admin Panel` and point it to the newly created folder + +The good thing with this approach: You can keep the backend code updated! Since your new theme will never conflict with existing themes, a simple git pull will +keep your installation updated. You decide which new feature you'd like to integrate on your own theme. Bugfixes to the code will work out of the box! + +Other customizations are also possible but will require merging changes together. Usually users would not need to change the backend code unless they wish to work +on non-existing features in `MPOS`. For the vast majority, adjusting themes should be enough to highlight your pool from others. + +In all that, I humbly ask to keep the `MPOS` author reference and Github URL intact. Contributing ============ @@ -107,7 +154,7 @@ You can contribute to this project in different ways: Contact ======= -You can find me on Freenode.net, #mmcfe-ng. +You can find me on Freenode.net, #MPOS. License and Author ================== @@ -127,4 +174,5 @@ See the License for the specific language governing permissions and limitations under the License. - [1]: https://github.com/TheSerapher/php-mmcfe-ng/issues "Issue" + [1]: https://github.com/TheSerapher/php-mpos/issues "Issue" + [2]: http://www.smarty.net/docs/en/ "Smarty" diff --git a/cronjobs/archive_cleanup.php b/cronjobs/archive_cleanup.php new file mode 100755 index 00000000..9910979f --- /dev/null +++ b/cronjobs/archive_cleanup.php @@ -0,0 +1,39 @@ +#!/usr/bin/php +purgeArchive()) { + $log->logError("Failed to delete archived shares, not critical but should be checked!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to delete archived shares"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); +} + +// Cron cleanup and monitoring +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/auto_payout.php b/cronjobs/auto_payout.php index 7e5609fc..c65a18ce 100755 --- a/cronjobs/auto_payout.php +++ b/cronjobs/auto_payout.php @@ -19,71 +19,86 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); -if ($bitcoin->can_connect() !== true) { - verbose("Unable to connect to RPC server, exiting"); - exit(1); +if ($setting->getValue('disable_ap') == 1) { + $log->logInfo(" auto payout disabled via admin panel"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Auto-Payout disabled"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + $monitoring->setStatus($cron_name . "_endtime", "date", time()); + exit(0); } -// Mark this job as active -$setting->setValue('auto_payout_active', 1); +if ($bitcoin->can_connect() !== true) { + $log->logFatal(" unable to connect to RPC server, exiting"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); +} // Fetch all users with setup AP $users = $user->getAllAutoPayout(); +// Quick summary +if (count($users) > 0) $log->logInfo(" found " . count($users) . " queued payout(s)"); + // Go through users and run transactions if (! empty($users)) { - verbose("UserID\tUsername\tBalance\tThreshold\tAddress\t\t\t\t\tStatus\n\n"); + $log->logInfo("\tUserID\tUsername\tBalance\tThreshold\tAddress"); foreach ($users as $aUserData) { $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"); + $log->logInfo("\t" . $aUserData['id'] . "\t" . $aUserData['username'] . "\t" . $dBalance . "\t" . $aUserData['ap_threshold'] . "\t\t" . $aUserData['coin_address']); // 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']); + $aStatus = $bitcoin->validateaddress($aUserData['coin_address']); + if (!$aStatus['isvalid']) { + $log->logError('Failed to verify this users coin address, skipping payout'); + continue; + } } catch (BitcoinClientException $e) { - verbose("VERIFY FAILED\n"); + $log->logError('Failed to verifu this users coin address, skipping payout'); continue; } // Send balance, fees are reduced later by RPC Server try { - $bitcoin->sendtoaddress($aUserData['coin_address'], $dBalance); + $bitcoin->sendtoaddress($aUserData['coin_address'], $dBalance - $config['txfee']); } catch (BitcoinClientException $e) { - verbose("SEND FAILED\n"); + $log->logError('Failed to send requested balance to coin address, please check payout process'); continue; } // Create transaction record 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'])) { + // Mark all older transactions as archived + if (!$transaction->setArchived($aUserData['id'], $transaction->insert_id)) + $log->logError('Failed to mark transactions for user #' . $aUserData['id'] . ' prior to #' . $transaction->insert_id . ' as archived'); // 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"); - } + if (!$notification->sendNotification($aUserData['id'], 'auto_payout', $aMailData)) + $log->logError('Failed to send notification email to users address: ' . $aMailData['email']); } else { - verbose("FAILED\n"); + $log->logError('Failed to add new Debit_AP transaction in database for user ' . $user->getUserName($aUserData['id'])); } - - } else { - verbose("SKIPPED\n"); } } } else { - verbose("No user has configured their AP > 0\n"); + $log->logDebug(" no user has configured their AP > 0"); } -// Mark this job as inactive -$setting->setValue('auto_payout_active', 0); - +// Cron cleanup and monitoring +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/blockupdate.php b/cronjobs/blockupdate.php index 65779a3a..bd489c04 100755 --- a/cronjobs/blockupdate.php +++ b/cronjobs/blockupdate.php @@ -19,37 +19,44 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); if ( $bitcoin->can_connect() !== true ) { - verbose("Failed to connect to RPC server\n"); + $log->logFatal("Failed to connect to RPC server\n"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Fetch all unconfirmed blocks $aAllBlocks = $block->getAllUnconfirmed($config['confirmations']); -verbose("ID\tBlockhash\tConfirmations\t\n"); +$log->logInfo("ID\tHeight\tBlockhash\tConfirmations"); foreach ($aAllBlocks as $iIndex => $aBlock) { $aBlockInfo = $bitcoin->query('getblock', $aBlock['blockhash']); // Fetch this blocks transaction details to find orphan blocks $aTxDetails = $bitcoin->query('gettransaction', $aBlockInfo['tx'][0]); - verbose($aBlock['id'] . "\t" . $aBlock['blockhash'] . "\t" . $aBlock['confirmations'] . " -> " . $aBlockInfo['confirmations'] . "\t"); + $log->logInfo($aBlock['id'] . "\t" . $aBlock['height'] . "\t" . $aBlock['blockhash'] . "\t" . $aBlock['confirmations'] . " -> " . $aBlockInfo['confirmations']); if ($aTxDetails['details'][0]['category'] == 'orphan') { // We have an orphaned block, we need to invalidate all transactions for this one - if ($transaction->setOrphan($aBlock['id']) && $block->setConfirmations($aBlock['id'], -1)) { - verbose("ORPHAN\n"); + if ($block->setConfirmations($aBlock['id'], -1)) { + $log->logInfo(" Block marked as orphan"); } else { - verbose("ORPHAN_ERR"); + $log->logError(" Block became orphaned but unable to update database entries"); } continue; } if ($aBlock['confirmations'] == $aBlockInfo['confirmations']) { - verbose("SKIPPED\n"); - } else if ($block->setConfirmations($aBlock['id'], $aBlockInfo['confirmations'])) { - verbose("UPDATED\n"); - } else { - verbose("ERROR\n"); + $log->logDebug(' No update needed'); + } else if (!$block->setConfirmations($aBlock['id'], $aBlockInfo['confirmations'])) { + $log->logError(' Failed to update block confirmations'); } } + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/cron_end.inc.php b/cronjobs/cron_end.inc.php new file mode 100644 index 00000000..03a548d2 --- /dev/null +++ b/cronjobs/cron_end.inc.php @@ -0,0 +1,28 @@ +setStatus($cron_name . "_message", "message", "OK"); +$monitoring->setStatus($cron_name . "_status", "okerror", 0); +$monitoring->setStatus($cron_name . "_runtime", "time", microtime(true) - $cron_start[$cron_name]); +$monitoring->setStatus($cron_name . "_endtime", "date", time()); +// Mark cron as running for monitoring +$monitoring->setStatus($cron_name . '_active', "yesno", 0); +?> diff --git a/cronjobs/etc/logrotate.conf b/cronjobs/etc/logrotate.conf new file mode 100644 index 00000000..341b89e7 --- /dev/null +++ b/cronjobs/etc/logrotate.conf @@ -0,0 +1,6 @@ +"./logs/*.txt" { + copytruncate + rotate 7 + compress + daily +} diff --git a/cronjobs/findblock.php b/cronjobs/findblock.php index e68d9ff4..114d117b 100755 --- a/cronjobs/findblock.php +++ b/cronjobs/findblock.php @@ -19,114 +19,126 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); // Fetch our last block found from the DB as a starting point $aLastBlock = @$block->getLast(); $strLastBlockHash = $aLastBlock['blockhash']; -if (!$strLastBlockHash) { - $strLastBlockHash = ''; -} +if (!$strLastBlockHash) $strLastBlockHash = ''; // Fetch all transactions since our last block if ( $bitcoin->can_connect() === true ){ $aTransactions = $bitcoin->query('listsinceblock', $strLastBlockHash); } else { - verbose("Aborted: " . $bitcoin->can_connect() . "\n"); + $log->logFatal('Unable to conenct to RPC server backend'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Nothing to do so bail out if (empty($aTransactions['transactions'])) { - verbose("No new RPC transactions since last block\n"); + $log->logDebug('No new RPC transactions since last block'); } else { // Table header - verbose("Blockhash\t\tHeight\tAmount\tConfirmations\tDiff\t\tTime\t\t\tStatus\n"); + $log->logInfo("Blockhash\t\tHeight\tAmount\tConfirmations\tDiff\t\tTime"); // Let us add those blocks as unaccounted foreach ($aTransactions['transactions'] as $iIndex => $aData) { if ( $aData['category'] == 'generate' || $aData['category'] == 'immature' ) { $aBlockInfo = $bitcoin->query('getblock', $aData['blockhash']); + $config['reward_type'] == 'block' ? $aData['amount'] = $aData['amount'] : $aData['amount'] = $config['reward']; $aData['height'] = $aBlockInfo['height']; $aData['difficulty'] = $aBlockInfo['difficulty']; - verbose(substr($aData['blockhash'], 0, 15) . "...\t" . + $log->logInfo(substr($aData['blockhash'], 0, 15) . "...\t" . $aData['height'] . "\t" . $aData['amount'] . "\t" . $aData['confirmations'] . "\t\t" . $aData['difficulty'] . "\t" . - strftime("%Y-%m-%d %H:%M:%S", $aData['time']) . "\t"); - if ( $block->addBlock($aData) ) { - verbose("Added\n"); - } else { - verbose("Failed" . "\n"); + strftime("%Y-%m-%d %H:%M:%S", $aData['time'])); + if ( ! empty($aBlockInfo['flags']) && preg_match('/proof-of-stake/', $aBlockInfo['flags']) ) { + $log->logInfo("Block above with height " . $aData['height'] . " not added to database, proof-of-stake block!"); + continue; + } + if (!$block->addBlock($aData) ) { + $log->logFatal('Unable to add this block to database: ' . $aData['height']); } } } } -verbose("\n"); // Now with our blocks added we can scan for their upstream shares -$aAllBlocks = $block->getAllUnaccounted('ASC'); +$aAllBlocks = $block->getAllUnsetShareId('ASC'); if (empty($aAllBlocks)) { - verbose("No new unaccounted blocks found\n"); + $log->logDebug('No new blocks without share_id found in database'); } else { // Loop through our unaccounted blocks - verbose("\nBlock ID\tBlock Height\tShare ID\tShares\tFinder\t\t\tStatus\n"); + $log->logInfo("Block ID\t\tHeight\tAmount\tShare ID\tShares\tFinder\tType"); foreach ($aAllBlocks as $iIndex => $aBlock) { if (empty($aBlock['share_id'])) { // Fetch this blocks upstream ID - if ($share->setUpstream($block->getLastUpstreamId())) { + $aBlockInfo = $bitcoin->query('getblock', $aBlock['blockhash']); + if ($share->setUpstream($aBlockInfo, $block->getLastUpstreamId())) { $iCurrentUpstreamId = $share->getUpstreamId(); $iAccountId = $user->getUserId($share->getUpstreamFinder()); } else { - verbose("\nUnable to fetch blocks upstream share. Aborting!\n"); - verbose($share->getError() . "\n"); + $log->logFatal('Unable to fetch blocks upstream share, aborted:' . $share->getError()); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to fetch blocks " . $aBlock['height'] . " upstream share: " . $share->getError()); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); 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"); + $log->logInfo('Unable to find highest share ID found so far, if this is your first block, this is normal.'); } $iRoundShares = $share->getRoundShares($iPreviousShareId, $iCurrentUpstreamId); // Store new information - $strStatus = "OK"; if (!$block->setShareId($aBlock['id'], $iCurrentUpstreamId)) - $strStatus = "Share ID Failed"; + $log->logError('Failed to update share ID in database for block ' . $aBlock['height']); if (!$block->setFinder($aBlock['id'], $iAccountId)) - $strStatus = "Finder Failed"; + $log->logError('Failed to update finder account ID in database for block ' . $aBlock['height']); if (!$block->setShares($aBlock['id'], $iRoundShares)) - $strStatus = "Shares Failed"; + $log->logError('Failed to update share count in database for block ' . $aBlock['height']); if ($config['block_bonus'] > 0 && !$transaction->addTransaction($iAccountId, $config['block_bonus'], 'Bonus', $aBlock['id'])) { - $strStatus = "Bonus Failed"; + $log->logError('Failed to create Bonus transaction in database for user ' . $user->getUserName($iAccountId) . ' for block ' . $aBlock['height']); } - verbose( + $log->logInfo( $aBlock['id'] . "\t\t" . $aBlock['height'] . "\t\t" + . $aBlock['amount'] . "\t" . $iCurrentUpstreamId . "\t\t" . $iRoundShares . "\t" - . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t\t" - . $strStatus - . "\n" + . "[$iAccountId] " . $user->getUserName($iAccountId) . "\t" + . $share->share_type ); - // 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); + if ($setting->getValue('disable_notifications') != 1) { + // 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; + if (!$notification->sendNotification($aData['account_id'], 'new_block', $aMailData)) + $log->logError('Failed to notify user of new found block: ' . $user->getUserName($aData['account_id'])); + } } } } } } -?> +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/logrotate.sh b/cronjobs/logrotate.sh new file mode 100755 index 00000000..78f901ff --- /dev/null +++ b/cronjobs/logrotate.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Find scripts path +if [[ -L $0 ]]; then + CRONHOME=$( dirname $( readlink $0 ) ) +else + CRONHOME=$( dirname $0 ) +fi + +cd $CRONHOME +logrotate etc/logrotate.conf diff --git a/cronjobs/logs/README.md b/cronjobs/logs/README.md new file mode 100644 index 00000000..864999fd --- /dev/null +++ b/cronjobs/logs/README.md @@ -0,0 +1 @@ +Logging directory for cronjobs. diff --git a/cronjobs/manual_payout.php b/cronjobs/manual_payout.php new file mode 100755 index 00000000..befd720a --- /dev/null +++ b/cronjobs/manual_payout.php @@ -0,0 +1,102 @@ +#!/usr/bin/php +getValue('disable_mp') == 1) { + $log->logInfo(" auto payout disabled via admin panel"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Auto-Payout disabled"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + $monitoring->setStatus($cron_name . "_endtime", "date", time()); + exit(0); +} + +if ($bitcoin->can_connect() !== true) { + $log->logFatal(" unable to connect to RPC server, exiting"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); +} + +// Fetch outstanding payout requests +$aPayouts = $oPayout->getUnprocessedPayouts(); + +if (count($aPayouts) > 0) { + $log->logInfo("\tAccount ID\tUsername\tBalance\t\tCoin Address"); + foreach ($aPayouts as $aData) { + $aBalance = $transaction->getBalance($aData['account_id']); + $dBalance = $aBalance['confirmed']; + $aData['coin_address'] = $user->getCoinAddress($aData['account_id']); + $aData['username'] = $user->getUserName($aData['account_id']); + if ($dBalance > $config['txfee']) { + $log->logInfo("\t" . $aData['account_id'] . "\t\t" . $aData['username'] . "\t" . $dBalance . "\t\t" . $aData['coin_address']); + try { + $aStatus = $bitcoin->validateaddress($aData['coin_address']); + if (!$aStatus['isvalid']) { + $log->logError('Failed to verify this users coin address, skipping payout'); + continue; + } + } catch (BitcoinClientException $e) { + $log->logError('Failed to verify this users coin address, skipping payout'); + continue; + } + try { + $bitcoin->sendtoaddress($aData['coin_address'], $dBalance - $config['txfee']); + } catch (BitcoinClientException $e) { + $log->logError('Failed to send requested balance to coin address, please check payout process'); + continue; + } + // To ensure we don't run this transaction again, lets mark it completed + if (!$oPayout->setProcessed($aData['id'])) { + $log->logFatal('unable to mark transactions ' . $aData['id'] . ' as processed.'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable set payout as processed"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + + if ($transaction->addTransaction($aData['account_id'], $dBalance - $config['txfee'], 'Debit_MP', NULL, $aData['coin_address']) && $transaction->addTransaction($aData['account_id'], $config['txfee'], 'TXFee', NULL, $aData['coin_address'])) { + // Mark all older transactions as archived + if (!$transaction->setArchived($aData['account_id'], $transaction->insert_id)) + $log->logError('Failed to mark transactions for #' . $aData['account_id'] . ' prior to #' . $transaction->insert_id . ' as archived'); + // Notify user via mail + $aMailData['email'] = $user->getUserEmail($user->getUserName($aData['account_id'])); + $aMailData['subject'] = 'Manual Payout Completed'; + $aMailData['amount'] = $dBalance; + $aMailData['payout_id'] = $aData['id']; + if (!$notification->sendNotification($aData['account_id'], 'manual_payout', $aMailData)) + $log->logError('Failed to send notification email to users address: ' . $aMailData['email']); + } else { + $log->logError('Failed to add new Debit_MP transaction in database for user ' . $user->getUserName($aData['account_id'])); + } + } + + } +} + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/notifications.php b/cronjobs/notifications.php index ec61002a..8e3ac98a 100755 --- a/cronjobs/notifications.php +++ b/cronjobs/notifications.php @@ -19,38 +19,61 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); +if ($setting->getValue('disable_notifications') == 1) { + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Cron disabled by admin"); + $monitoring->setStatus($cron_name . "_status", "okerror", 0); + exit(0); +} + +$log->logDebug(" IDLE Worker Notifications ..."); // Find all IDLE workers $aWorkers = $worker->getAllIdleWorkers(); if (empty($aWorkers)) { - verbose("No idle workers found\n"); + $log->logDebug(" no idle workers found\n"); } else { + $log->logInfo(" found " . count($aWorkers) . " IDLE workers\n"); 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']); + $log->logInfo(" " . $aWorker['username'] . "..."); if (!$notification->sendNotification($aWorker['account_id'], 'idle_worker', $aData)) - verbose($notification->getError() . "\n"); + $log->logError(" Failed sending notifications: " . $notification->getError() . "\n"); } } + +$log->logDebug(" Reset IDLE Worker Notifications ..."); // We notified, lets check which recovered $aNotifications = $notification->getAllActive('idle_worker'); if (!empty($aNotifications)) { + $log->logInfo(" found " . count($aNotifications) . " active notification(s)\n"); foreach ($aNotifications as $aNotification) { $aData = json_decode($aNotification['data'], true); $aWorker = $worker->getWorker($aData['id']); - if ($aWorker['active'] == 1) { + $log->logInfo(" " . $aWorker['username'] . " ..."); + if ($aWorker['hashrate'] > 0) { if ($notification->setInactive($aNotification['id'])) { - verbose("Marked notification " . $aNotification['id'] . " as inactive\n"); + $log->logInfo(" updated #" . $aNotification['id'] . " for " . $aWorker['username'] . " as inactive\n"); } else { - verbose("Failed to set notification inactive for " . $aWorker['username'] . "\n"); + $log->logInfo(" failed to update #" . $aNotification['id'] . " for " . $aWorker['username'] . "\n"); } + } else { + $log->logInfo(" still inactive\n"); } } +} else { + $log->logDebug(" no active IDLE worker notifications\n"); } + +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/pplns_payout.php b/cronjobs/pplns_payout.php new file mode 100755 index 00000000..f5d6f24c --- /dev/null +++ b/cronjobs/pplns_payout.php @@ -0,0 +1,198 @@ +#!/usr/bin/php +logInfo("Please activate this cron in configuration via payout_system = pplns"); + exit(0); +} + +// Fetch all unaccounted blocks +$aAllBlocks = $block->getAllUnaccounted('ASC'); +if (empty($aAllBlocks)) { + $log->logDebug("No new unaccounted blocks found"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No new unaccounted blocks"); + $monitoring->setStatus($cron_name . "_status", "okerror", 0); + exit(0); +} + +$count = 0; +foreach ($aAllBlocks as $iIndex => $aBlock) { + // We support some dynamic share targets but fall back to our fixed value + // Re-calculate after each run due to re-targets in this loop + if ($config['pplns']['shares']['type'] == 'blockavg' && $block->getBlockCount() > 0) { + $pplns_target = round($block->getAvgBlockShares($aBlock['height'], $config['pplns']['blockavg']['blockcount'])); + } else { + $pplns_target = $config['pplns']['shares']['default']; + } + + if (!$aBlock['accounted']) { + $iPreviousShareId = @$aAllBlocks[$iIndex - 1]['share_id'] ? $aAllBlocks[$iIndex - 1]['share_id'] : 0; + $iCurrentUpstreamId = $aBlock['share_id']; + if (!is_numeric($iCurrentUpstreamId)) { + $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Block " . $aBlock['height'] . " has no share_id associated with it"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + $iRoundShares = $share->getRoundShares($iPreviousShareId, $aBlock['share_id']); + $iNewRoundShares = 0; + $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; + $aRoundAccountShares = $share->getSharesForAccounts($iPreviousShareId, $aBlock['share_id']); + + if ($iRoundShares >= $pplns_target) { + $log->logDebug("Matching or exceeding PPLNS target of $pplns_target with $iRoundShares"); + $iMinimumShareId = $share->getMinimumShareId($pplns_target, $aBlock['share_id']); + // We need to go one ID lower due to `id >` or we won't match if minimum share ID == $aBlock['share_id'] + $aAccountShares = $share->getSharesForAccounts($iMinimumShareId - 1, $aBlock['share_id']); + if (empty($aAccountShares)) { + $log->logFatal("No shares found for this block, aborted! Block Height : " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No shares found for this block: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + foreach($aAccountShares as $key => $aData) { + $iNewRoundShares += $aData['valid']; + } + $log->logInfo('Adjusting round target to PPLNS target ' . $iNewRoundShares); + $iRoundShares = $iNewRoundShares; + } else { + $log->logDebug("Not able to match PPLNS target of $pplns_target with $iRoundShares"); + // We need to fill up with archived shares + // Grab the full current round shares since we didn't match target + $aAccountShares = $aRoundAccountShares; + if (empty($aAccountShares)) { + $log->logFatal("No shares found for this block, aborted! Block height: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No shares found for this block: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + + // Grab only the most recent shares from Archive that fill the missing shares + $log->logInfo('Fetching ' . ($pplns_target - $iRoundShares) . ' additional shares from archive'); + if (!$aArchiveShares = $share->getArchiveShares($pplns_target - $iRoundShares)) { + $log->logError('Failed to fetch shares from archive, setting target to round total'); + $pplns_target = $iRoundShares; + } else { + // Add archived shares to users current shares, if we have any in archive + if (is_array($aArchiveShares)) { + $log->logDebug('Found shares in archive to match PPLNS target, calculating per-user shares'); + foreach($aAccountShares as $key => $aData) { + if (array_key_exists($aData['username'], $aArchiveShares)) { + $log->logDebug('Found user ' . $aData['username'] . ' in archived shares'); + $log->logDebug(' valid : ' . $aAccountShares[$key]['valid'] . ' + ' . $aArchiveShares[$aData['username']]['valid'] . ' = ' . ($aAccountShares[$key]['valid'] + $aArchiveShares[$aData['username']]['valid']) ); + $log->logDebug(' invalid : ' . $aAccountShares[$key]['invalid'] . ' + ' . $aArchiveShares[$aData['username']]['invalid'] . ' = ' . ($aAccountShares[$key]['invalid'] + $aArchiveShares[$aData['username']]['invalid']) ); + $aAccountShares[$key]['valid'] += $aArchiveShares[$aData['username']]['valid']; + $aAccountShares[$key]['invalid'] += $aArchiveShares[$aData['username']]['invalid']; + } + } + } + // We tried to fill up to PPLNS target, now we need to check the actual shares to properly payout users + foreach($aAccountShares as $key => $aData) { + $iNewRoundShares += $aData['valid']; + } + } + } + + // We filled from archive but still are not able to match PPLNS target, re-adjust + if ($iRoundShares < $iNewRoundShares) { + $log->logInfo('Adjusting round target to ' . $iNewRoundShares); + $iRoundShares = $iNewRoundShares; + } + + // Table header for account shares + $log->logInfo("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee"); + + // Loop through all accounts that have found shares for this round + foreach ($aAccountShares as $key => $aData) { + // Payout based on PPLNS target shares, proportional payout for all users + $aData['percentage'] = round(( 100 / $iRoundShares) * $aData['valid'], 8); + $aData['payout'] = round(( $aData['percentage'] / 100 ) * $dReward, 8); + // Defaults + $aData['fee' ] = 0; + $aData['donation'] = 0; + + if ($config['fees'] > 0 && $aData['no_fees'] == 0) + $aData['fee'] = round($config['fees'] / 100 * $aData['payout'], 8); + // Calculate donation amount, fees not included + $aData['donation'] = round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8); + + // Verbose output of this users calculations + $log->logInfo($aData['id'] . "\t" . + $aData['username'] . "\t" . + $aData['valid'] . "\t" . + $aData['invalid'] . "\t" . + number_format($aData['percentage'], 8) . "\t" . + number_format($aData['payout'], 8) . "\t" . + number_format($aData['donation'], 8) . "\t" . + number_format($aData['fee'], 8)); + + // Add full round share statistics, not just PPLNS + foreach ($aRoundAccountShares as $key => $aRoundData) { + if ($aRoundData['username'] == $aData['username']) + if (!$statistics->updateShareStatistics($aRoundData, $aBlock['id'])) + $log->logError('Failed to update share statistics for ' . $aData['username']); + } + + // Add new credit transaction + if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit', $aBlock['id'])) + $log->logFatal('Failed to insert new Credit transaction to database for ' . $aData['username']); + // Add new fee debit for this block + if ($aData['fee'] > 0 && $config['fees'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['fee'], 'Fee', $aBlock['id'])) + $log->logFatal('Failed to insert new Fee transaction to database for ' . $aData['username']); + // Add new donation debit + if ($aData['donation'] > 0) + if (!$transaction->addTransaction($aData['id'], $aData['donation'], 'Donation', $aBlock['id'])) + $log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']); + } + + // Move counted shares to archive before this blockhash upstream share + if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId)) + $log->logError('Failed to copy shares to archive table'); + // Delete all accounted shares + if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { + $log->logFatal("Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!"); + exit(1); + } + // Mark this block as accounted for + if (!$block->setAccounted($aBlock['id'])) { + $log->logFatal("Failed to mark block as accounted! Aborting!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to mark block " . $aBlock['height'] . " as accounted"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } + } +} + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/pps_payout.php b/cronjobs/pps_payout.php index 459384d2..7c3515fb 100755 --- a/cronjobs/pps_payout.php +++ b/cronjobs/pps_payout.php @@ -19,26 +19,54 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); // Check if we are set as the payout system if ($config['payout_system'] != 'pps') { - verbose("Please activate this cron in configuration via payout_system = pps\n"); + $log->logInfo("Please activate this cron in configuration via payout_system = pps\n"); exit(0); } // Fetch all transactions since our last block if ( $bitcoin->can_connect() === true ){ $dDifficulty = $bitcoin->getdifficulty(); + if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) + $dDifficulty = $dDifficulty['proof-of-work']; } else { - verbose("Aborted: " . $bitcoin->can_connect() . "\n"); + $log->logFatal("Aborted: " . $bitcoin->can_connect() . "\n"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Unable to connect to RPC server"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } -// Value per share calculation -$pps_value = number_format(round(50 / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12); +// We support some dynamic reward targets but fall back to our fixed value +// Re-calculate after each run due to re-targets in this loop +if ($config['pps']['reward']['type'] == 'blockavg' && $block->getBlockCount() > 0) { + $pps_reward = round($block->getAvgBlockReward($config['pps']['blockavg']['blockcount'])); + $log->logInfo("PPS reward using block average, amount: " . $pps_reward . "\tdifficulty: " . $dDifficulty); +} else { + if ($config['pps']['reward']['type'] == 'block') { + if ($aLastBlock = $block->getLast()) { + $pps_reward = $aLastBlock['amount']; + $log->logInfo("PPS reward using last block, amount: " . $pps_reward . "\tdifficulty: " . $dDifficulty); + } else { + $pps_reward = $config['pps']['reward']['default']; + $log->logInfo("PPS reward using default, amount: " . $pps_reward . "\tdifficulty: " . $dDifficulty); + } + } else { + $pps_reward = $config['pps']['reward']['default']; + $log->logInfo("PPS reward fixed default, amount: " . $pps_reward . "\tdifficulty: " . $dDifficulty); + } +} + +// Per-share value to be paid out to users +$pps_value = round($pps_reward / (pow(2,32) * $dDifficulty) * pow(2, $config['pps_target']), 12); // Find our last share accounted and last inserted share for PPS calculations $iPreviousShareId = $setting->getValue('pps_last_share_id'); @@ -47,81 +75,90 @@ $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"); +$log->logInfo("ID\tUsername\tInvalid\tValid\t\tPPS Value\t\tPayout\t\tDonation\tFee"); 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); + $aData['payout'] = round($aData['valid'] * $pps_value, 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); + if ($config['fees'] > 0 && $aData['no_fees'] == 0) + $aData['fee'] = round($config['fees'] / 100 * $aData['payout'], 8); // Calculate donation amount - $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 8); + $aData['donation'] = round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8); - verbose($aData['id'] . "\t" . + $log->logInfo($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"); + number_format($pps_value, 12) . "\t=\t" . + number_format($aData['payout'], 8) . "\t" . + number_format($aData['donation'], 8) . "\t" . + number_format($aData['fee'], 8)); - $strStatus = "OK"; // Add new credit transaction if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit_PPS')) - $strStatus = "Transaction Failed"; + $log->logError('Failed to add Credit_PPS transaction in database'); // 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"; + $log->logError('Failed to add Fee_PPS transaction in database'); // Add new donation debit if ($aData['donation'] > 0) if (!$transaction->addTransaction($aData['id'], $aData['donation'], 'Donation_PPS')) - $strStatus = "Donation Failed"; - verbose($strStatus . "\n"); + $log->logError('Failed to add Donation_PPS transaction in database'); } // 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"); -} +if (empty($aAllBlocks)) $log->logDebug("No new unaccounted blocks found"); // 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; + if (!is_numeric($aBlock['share_id'])) { + $log->logFatal("Block " . $aBlock['height'] . " has no share_id associated with it, not going to continue"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Block " . $aBlock['height'] . " has no share_id associated with it"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); + } // 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"); + $log->logError("Failed to update stats for this block on : " . $aData['username']); } // Move shares to archive - if ($config['archive_shares'] && $aBlock['share_id'] < $iLastShareId) { + if ($aBlock['share_id'] < $iLastShareId) { if (!$share->moveArchive($aBlock['share_id'], $aBlock['id'], @$iLastBlockShare)) - verbose("Archving failed\n"); + $log->logError("Archving failed"); } // 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"); + $log->logFatal("Failed to delete accounted shares from " . $aBlock['share_id'] . " to " . $iLastBlockShare . ", aborting!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to delete accounted shares from " . $aBlock['share_id'] . " to " . $iLastBlockShare); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Mark this block as accounted for if (!$block->setAccounted($aBlock['id'])) { - verbose("\nERROR : Failed to mark block as accounted! Aborting!\n"); + $log->logFatal("Failed to mark block as accounted! Aborting!"); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to mark block " . $aBlock['height'] . " as accounted"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } } + +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/proportional_payout.php b/cronjobs/proportional_payout.php index 61092006..e6e76113 100755 --- a/cronjobs/proportional_payout.php +++ b/cronjobs/proportional_payout.php @@ -19,93 +19,108 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // 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"); + $log->logInfo("Please activate this cron in configuration via payout_system = prop"); exit(0); } // Fetch all unaccounted blocks $aAllBlocks = $block->getAllUnaccounted('ASC'); if (empty($aAllBlocks)) { - verbose("No new unaccounted blocks found\n"); + $log->logDebug('No new unaccounted blocks found in database'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No new unaccounted blocks"); + $monitoring->setStatus($cron_name . "_status", "okerror", 0); exit(0); } $count = 0; +// Table header for account shares +$log->logInfo("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee"); foreach ($aAllBlocks as $iIndex => $aBlock) { if (!$aBlock['accounted']) { $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']); + $config['reward_type'] == 'block' ? $dReward = $aBlock['amount'] : $dReward = $config['reward']; if (empty($aAccountShares)) { - verbose("\nNo shares found for this block\n\n"); - sleep(2); - continue; + $log->logFatal('No shares found for this block, aborted: ' . $aBlock['height']); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "No shares found for this block, aborted: " . $aBlock['height']); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); } - // Table header for account shares - verbose("ID\tUsername\tValid\tInvalid\tPercentage\tPayout\t\tDonation\tFee\t\tStatus\n"); - // Loop through all accounts that have found shares for this round foreach ($aAccountShares as $key => $aData) { // Payout based on shares, PPS system - $aData['percentage'] = number_format(round(( 100 / $iRoundShares ) * $aData['valid'], 8), 8); - $aData['payout'] = number_format(round(( $aData['percentage'] / 100 ) * $config['reward'], 8), 8); + $aData['percentage'] = round(( 100 / $iRoundShares ) * $aData['valid'], 8); + $aData['payout'] = round(( $aData['percentage'] / 100 ) * $dReward, 8); // Defaults $aData['fee' ] = 0; $aData['donation'] = 0; - if ($config['fees'] > 0) - $aData['fee'] = number_format(round($config['fees'] / 100 * $aData['payout'], 8), 8); + if ($config['fees'] > 0 && $aData['no_fees'] == 0) + $aData['fee'] = round($config['fees'] / 100 * $aData['payout'], 8); // Calculate donation amount, fees not included - $aData['donation'] = number_format(round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8), 8); + $aData['donation'] = round($user->getDonatePercent($user->getUserId($aData['username'])) / 100 * ( $aData['payout'] - $aData['fee']), 8); // Verbose output of this users calculations - verbose($aData['id'] . "\t" . - $aData['username'] . "\t" . - $aData['valid'] . "\t" . - $aData['invalid'] . "\t" . - $aData['percentage'] . "\t" . - $aData['payout'] . "\t" . - $aData['donation'] . "\t" . - $aData['fee'] . "\t"); + $log->logInfo($aData['id'] . "\t" . + $aData['username'] . "\t" . + $aData['valid'] . "\t" . + $aData['invalid'] . "\t" . + number_format($aData['percentage'], 8) . "\t" . + number_format($aData['payout'], 8) . "\t" . + number_format($aData['donation'], 8) . "\t" . + number_format($aData['fee']), 8); - $strStatus = "OK"; // Update user share statistics if (!$statistics->updateShareStatistics($aData, $aBlock['id'])) - $strStatus = "Stats Failed"; + $log->logFatal('Failed to update share statistics for ' . $aData['username']); // Add new credit transaction if (!$transaction->addTransaction($aData['id'], $aData['payout'], 'Credit', $aBlock['id'])) - $strStatus = "Transaction Failed"; + $log->logFatal('Failed to insert new Credit transaction to database for ' . $aData['username']); // Add new fee debit for this block if ($aData['fee'] > 0 && $config['fees'] > 0) if (!$transaction->addTransaction($aData['id'], $aData['fee'], 'Fee', $aBlock['id'])) - $strStatus = "Fee Failed"; + $log->logFatal('Failed to insert new Fee transaction to database for ' . $aData['username']); // Add new donation debit if ($aData['donation'] > 0) if (!$transaction->addTransaction($aData['id'], $aData['donation'], 'Donation', $aBlock['id'])) - $strStatus = "Donation Failed"; - verbose("\t$strStatus\n"); + $log->logFatal('Failed to insert new Donation transaction to database for ' . $aData['username']); } // Move counted shares to archive before this blockhash upstream share - if ($config['archive_shares']) $share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId); + if (!$share->moveArchive($iCurrentUpstreamId, $aBlock['id'], $iPreviousShareId)) + $log->logError('Failed to copy shares to archive'); // Delete all accounted shares if (!$share->deleteAccountedShares($iCurrentUpstreamId, $iPreviousShareId)) { - verbose("\nERROR : Failed to delete accounted shares from $iPreviousShareId to $iCurrentUpstreamId, aborting!\n"); + $log->logFatal('Failed to delete accounted shares from ' . $iPreviousShareId . ' to ' . $iCurrentUpstreamId . ', aborted'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to delete accounted shares from " . $iPreviousShareId . " to " . $iCurrentUpstreamId); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); exit(1); } // Mark this block as accounted for if (!$block->setAccounted($aBlock['id'])) { - verbose("\nERROR : Failed to mark block as accounted! Aborting!\n"); + $log->logFatal('Failed to mark block as accounted! Aborted.'); + $monitoring->setStatus($cron_name . "_active", "yesno", 0); + $monitoring->setStatus($cron_name . "_message", "message", "Failed to mark block " . $aBlock['height'] . " as accounted"); + $monitoring->setStatus($cron_name . "_status", "okerror", 1); + exit(1); } - - verbose("------------------------------------------------------------------------\n\n"); } } + +require_once('cron_end.inc.php'); +?> diff --git a/cronjobs/run-crons.sh b/cronjobs/run-crons.sh index c9dfd949..c4877c8f 100755 --- a/cronjobs/run-crons.sh +++ b/cronjobs/run-crons.sh @@ -9,27 +9,64 @@ # PHP Detections, if this fails hard code it PHP_BIN=$( which php ) -# Path to PID file, needs to be writable by user running this -PIDFILE='/tmp/mmcfe-ng-cron.pid' - -# Location of our cronjobs, assume current directory -CRONHOME='.' - # List of cruns to execute -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" +CRONS="findblock.php proportional_payout.php pplns_payout.php pps_payout.php blockupdate.php manual_payout.php auto_payout.php tickerupdate.php notifications.php statistics.php archive_cleanup.php" # Output additional runtime information VERBOSE="0" +# Base path for PIDFILE, (full path). +BASEPATH="/tmp" + +# Subfolder for PIDFILE, so it's path will be unique in a multipool server. +# Path relative to BASEPATH. +# Eg. SUBFOLDER="LTC" +SUBFOLDER="" + ################################################################ # # # You probably don't need to change anything beyond this point # # # ################################################################ +# My own name +ME=$( basename $0 ) + +# Overwrite some settings via command line arguments +while getopts "hvp:d:" opt; do + case "$opt" in + h|\?) + echo "Usage: $0 [-v] [-p PHP_BINARY] [-d SUBFOLDER]"; + exit 0 + ;; + v) VERBOSE=1 ;; + p) PHP_BIN=$OPTARG ;; + d) SUBFOLDER=$OPTARG ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + esac +done + +# Path to PID file, needs to be writable by user running this +PIDFILE="${BASEPATH}/${SUBFOLDER}/${ME}.pid" +# Clean PIDFILE path +PIDFILE=$(readlink -m "$PIDFILE") + +# Create folders recursively if necessary +if ! $(mkdir -p $( dirname $PIDFILE)); then + echo "Error creating PIDFILE path: $( dirname $PIDFILE )" + exit 1 +fi + +# Find scripts path +if [[ -L $0 ]]; then + CRONHOME=$( dirname $( readlink $0 ) ) +else + CRONHOME=$( dirname $0 ) +fi + # Change working director to CRONHOME if ! cd $CRONHOME 2>/dev/null; then echo "Unable to change to working directory \$CRONHOME: $CRONHOME" @@ -66,8 +103,8 @@ fi echo $PID > $PIDFILE for cron in $CRONS; do - [[ $VERBOSE == 1 ]] && echo "Running $cron, see output below for details" - $PHP_BIN $cron $CRONARGS + [[ $VERBOSE == 1 ]] && echo "Running $cron, check logfile for details" + $PHP_BIN $cron done # Remove pidfile diff --git a/cronjobs/shared.inc.php b/cronjobs/shared.inc.php index a0b1dc5d..50692508 100644 --- a/cronjobs/shared.inc.php +++ b/cronjobs/shared.inc.php @@ -22,6 +22,16 @@ limitations under the License. // We need to find our include files so set this properly define("BASEPATH", "../public/"); +/***************************************************** + * No need to change beyond this point * + *****************************************************/ + +// Used in autoloading of API class, adding it to stop PHP warnings +$dStartTime = microtime(true); + +// Our cron name +$cron_name = basename($_SERVER['PHP_SELF'], '.php'); + // Our security check define("SECURITY", 1); @@ -31,16 +41,15 @@ require_once(BASEPATH . 'include/config/global.inc.php'); // We include all needed files here, even though our templates could load them themself require_once(INCLUDE_DIR . '/autoloader.inc.php'); -// Parse command line -$options = getopt("v"); -if (array_key_exists('v', $options)) { - define("VERBOSE", true); -} else { - define("VERBOSE", false); -} +// Load 3rd party logging library for running crons +$log = new KLogger ( 'logs/' . $cron_name . '.txt' , KLogger::INFO ); +$log->LogDebug('Starting ' . $cron_name); -// Command line cron functions only -function verbose($msg) { - if (VERBOSE) echo $msg; -} +// Load the start time for later runtime calculations for monitoring +$cron_start[$cron_name] = microtime(true); +// Mark cron as running for monitoring +$log->logDebug('Marking cronjob as running for monitoring'); +$monitoring->setStatus($cron_name . '_active', 'yesno', 1); +$monitoring->setStatus($cron_name . '_starttime', 'date', time()); +?> diff --git a/cronjobs/statistics.php b/cronjobs/statistics.php index d4fe65e0..9228095b 100755 --- a/cronjobs/statistics.php +++ b/cronjobs/statistics.php @@ -19,31 +19,35 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); -// Fetch all cachable values but disable fetching from cache -$statistics->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"); - } -} +$start = microtime(true); +if ( ! $aAllUserShares = $statistics->getAllUserShares() ) + $log->logError('getAllUserShares update failed'); +$log->logInfo("getAllUserShares " . number_format(microtime(true) - $start, 2) . " seconds"); + +$start = microtime(true); +if (!$statistics->getTopContributors('hashes')) + $log->logError("getTopContributors hashes update failed"); +$log->logInfo("getTopContributors hashes " . number_format(microtime(true) - $start, 2) . " seconds"); + +$start = microtime(true); +if (!$statistics->getCurrentHashrate()) + $log->logError("getCurrentHashrate update failed"); +$log->logInfo("getCurrentHashrate " . number_format(microtime(true) - $start, 2) . " seconds"); + +/* +// Admin specific statistics, we cache the global query due to slowness +$start = microtime(true); +if (!$statistics->getAllUserStats('%')) + $log->logError("getAllUserStats update failed"); +$log->logInfo("getAllUserStats " . number_format(microtime(true) - $start, 2) . " seconds"); +*/ + +require_once('cron_end.inc.php'); ?> diff --git a/cronjobs/tickerupdate.php b/cronjobs/tickerupdate.php index 7850ade9..6afff97d 100755 --- a/cronjobs/tickerupdate.php +++ b/cronjobs/tickerupdate.php @@ -19,18 +19,22 @@ limitations under the License. */ +// Change to working directory +chdir(dirname(__FILE__)); + // Include all settings and classes require_once('shared.inc.php'); // Include additional file not set in autoloader -require_once(BASEPATH . CLASS_DIR . '/tools.class.php'); +require_once(CLASS_DIR . '/tools.class.php'); -verbose("Running updates\n"); -if ($aData = $tools->getApi($config['price']['url'], $config['price']['target'])) { - if (!$setting->setValue('price', $aData['ticker']['last'])) - verbose("ERR Table update failed"); +if ($price = $tools->getPrice()) { + $log->logInfo("Price update: found $price as price"); + if (!$setting->setValue('price', $price)) + $log->logError("unable to update value in settings table"); } else { - verbose("ERR Failed download JSON data from " . $config['price']['url'].$config['price']['target'] . "\n"); + $log->logFatal("failed to fetch API data: " . $tools->getError()); } +require_once('cron_end.inc.php'); ?> diff --git a/public/include/autoloader.inc.php b/public/include/autoloader.inc.php index a78d4a62..8950f103 100644 --- a/public/include/autoloader.inc.php +++ b/public/include/autoloader.inc.php @@ -1,18 +1,56 @@ isMobile() && $setting->getValue('website_mobile_theme')) { + // Set to mobile theme + $setting->getValue('website_mobile_theme') ? $theme = $setting->getValue('website_mobile_theme') : $theme = 'mobile'; +} else { + // Use configured theme, fallback to default theme + $setting->getValue('website_theme') ? $theme = $setting->getValue('website_theme') : $theme = 'mpos'; +} +define('THEME', $theme); + +// Load smarty now that we have our theme defined require_once(INCLUDE_DIR . '/smarty.inc.php'); -// Load classes that need the above as dependencies + +// Load everything else in proper order +require_once(CLASS_DIR . '/mail.class.php'); +require_once(CLASS_DIR . '/tokentype.class.php'); +require_once(CLASS_DIR . '/token.class.php'); +require_once(CLASS_DIR . '/payout.class.php'); require_once(CLASS_DIR . '/block.class.php'); + +// We require the block class to properly grab the round ID +require_once(CLASS_DIR . '/statscache.class.php'); + +require_once(CLASS_DIR . '/bitcoin.class.php'); +require_once(CLASS_DIR . '/bitcoinwrapper.class.php'); +require_once(CLASS_DIR . '/monitoring.class.php'); require_once(CLASS_DIR . '/user.class.php'); +require_once(CLASS_DIR . '/invitation.class.php'); require_once(CLASS_DIR . '/share.class.php'); require_once(CLASS_DIR . '/worker.class.php'); require_once(CLASS_DIR . '/statistics.class.php'); +require_once(CLASS_DIR . '/roundstats.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'); +require_once(CLASS_DIR . '/news.class.php'); +require_once(CLASS_DIR . '/api.class.php'); +require_once(INCLUDE_DIR . '/lib/Michelf/Markdown.php'); +require_once(INCLUDE_DIR . '/lib/scrypt.php'); + + +?> diff --git a/public/include/classes/api.class.php b/public/include/classes/api.class.php new file mode 100644 index 00000000..26b2a95d --- /dev/null +++ b/public/include/classes/api.class.php @@ -0,0 +1,66 @@ +dStartTime = $dStartTime; + } + function isActive($error=true) { + if (!$this->setting->getValue('disable_api')) { + return true; + } else { + if ($error == true) { + header('HTTP/1.1 501 Not implemented'); + die('501 Not implemented'); + } + } + } + + /** + * Create API json object from input array + * @param data Array data to create JSON for + * @param force bool Enforce a JSON object + * @return string JSON object + **/ + function get_json($data, $force=false) { + return json_encode( + array( $_REQUEST['action'] => array( + 'version' => $this->api_version, + 'runtime' => (microtime(true) - $this->dStartTime) * 1000, + 'data' => $data + )), $force ? JSON_FORCE_OBJECT : 0 + ); + } + + /** + * Check user access level to the API call + **/ + function checkAccess($user_id, $get_id=NULL) { + if ( ! $this->user->isAdmin($user_id) && (!empty($get_id) && $get_id != $user_id)) { + // User is NOT admin and tries to access an ID that is not their own + header("HTTP/1.1 401 Unauthorized"); + die("Access denied"); + } else if ($this->user->isAdmin($user_id) && !empty($get_id)) { + // User is an admin and tries to fetch another users data + $id = $get_id; + // Is it a username or a user ID + ctype_digit($_REQUEST['id']) ? $id = $get_id : $id = $this->user->getUserId($get_id); + } else { + $id = $user_id; + } + return $id; + } +} + +$api = new Api(); +$api->setConfig($config); +$api->setUser($user); +$api->setSetting($setting); +$api->setStartTime($dStartTime); diff --git a/public/include/classes/base.class.php b/public/include/classes/base.class.php new file mode 100644 index 00000000..2aae51ce --- /dev/null +++ b/public/include/classes/base.class.php @@ -0,0 +1,122 @@ +debug = $debug; + } + public function setMysql($mysqli) { + $this->mysqli = $mysqli; + } + public function setMail($mail) { + $this->mail = $mail; + } + public function setSmarty($smarty) { + $this->smarty = $smarty; + } + public function setUser($user) { + $this->user = $user; + } + public function setConfig($config) { + $this->config = $config; + } + public function setToken($token) { + $this->token = $token; + } + public function setBlock($block) { + $this->block = $block; + } + public function setSetting($setting) { + $this->setting = $setting; + } + public function setBitcoin($bitcoin) { + $this->bitcoin = $bitcoin; + } + public function setTokenType($tokentype) { + $this->tokentype = $tokentype; + } + public function setErrorMessage($msg) { + $this->sError = $msg; + } + public function getError() { + return $this->sError; + } + + /** + * Get a single row from the table + * @param value string Value to search for + * @param search Return column to search for + * @param field string Search column + * @param type string Type of value + * @return array Return result + **/ + protected function getSingle($value, $search='id', $field='id', $type="i") { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT $search FROM $this->table WHERE $field = ? LIMIT 1"); + if ($this->checkStmt($stmt)) { + $stmt->bind_param($type, $value); + $stmt->execute(); + $stmt->bind_result($retval); + $stmt->fetch(); + $stmt->close(); + return $retval; + } + return false; + } + + 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; + } + /** + * Update a single row in a table + * @param userID int Account ID + * @param field string Field to update + * @return bool + **/ + protected 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 may need to generate our bind_param list + **/ + public function addParam($type, &$value) { + $this->values[] = $value; + $this->types .= $type; + } + public function getParam() { + $array = array_merge(array($this->types), $this->values); + // Clear the data + $this->values = NULL; + $this->types = NULL; + // See here why we need this: http://stackoverflow.com/questions/16120822/mysqli-bind-param-expected-to-be-a-reference-value-given + if (strnatcmp(phpversion(),'5.3') >= 0) { + $refs = array(); + foreach($array as $key => $value) + $refs[$key] = &$array[$key]; + return $refs; + } + return $array; + } +} +?> diff --git a/public/include/classes/bitcoinwrapper.class.php b/public/include/classes/bitcoinwrapper.class.php index eb6d55e0..6881babc 100644 --- a/public/include/classes/bitcoinwrapper.class.php +++ b/public/include/classes/bitcoinwrapper.class.php @@ -25,19 +25,40 @@ class BitcoinWrapper extends BitcoinClient { public function getblockcount() { $this->oDebug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; - return $this->memcache->setCache(__FUNCTION__, parent::getblockcount()); + return $this->memcache->setCache(__FUNCTION__, parent::getblockcount(), 30); } public function getdifficulty() { $this->oDebug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; - return $this->memcache->setCache(__FUNCTION__, parent::getdifficulty()); + $data = parent::getdifficulty(); + // Check for PoS/PoW coins + if (is_array($data) && array_key_exists('proof-of-work', $data)) + $data = $data['proof-of-work']; + return $this->memcache->setCache(__FUNCTION__, $data, 30); } public function getestimatedtime($iCurrentPoolHashrate) { $this->oDebug->append("STA " . __METHOD__, 4); if ($iCurrentPoolHashrate == 0) return 0; if ($data = $this->memcache->get(__FUNCTION__)) return $data; - $dDifficulty = parent::getdifficulty(); - return $this->memcache->setCache(__FUNCTION__, $dDifficulty * pow(2,32) / $iCurrentPoolHashrate); + $dDifficulty = $this->getdifficulty(); + return $this->memcache->setCache(__FUNCTION__, $dDifficulty * pow(2,32) / $iCurrentPoolHashrate, 30); + } + public function getnetworkhashps() { + $this->oDebug->append("STA " . __METHOD__, 4); + if ($data = $this->memcache->get(__FUNCTION__)) return $data; + try { + $dNetworkHashrate = $this->query('getmininginfo'); + if (is_array($dNetworkHashrate) && array_key_exists('networkhashps', $dNetworkHashrate)) { + $dNetworkHashrate = $dNetworkHashrate['networkhashps']; + } else if (is_array($dNetworkHashrate) && array_key_exists('hashespersec', $dNetworkHashrate)) { + $dNetworkHashrate = $dNetworkHashrate['hashespersec']; + } else if (is_array($dNetworkHashrate) && array_key_exists('netmhashps', $dNetworkHashrate)) { + $dNetworkHashrate = $dNetworkHashrate['netmhashps'] * 1000 * 1000; + } + } catch (Exception $e) { + return false; + } + return $this->memcache->setCache(__FUNCTION__, $dNetworkHashrate, 30); } } diff --git a/public/include/classes/block.class.php b/public/include/classes/block.class.php index 743ce67e..027d06c5 100644 --- a/public/include/classes/block.class.php +++ b/public/include/classes/block.class.php @@ -10,9 +10,10 @@ class Block { // This defines each block public $height, $blockhash, $confirmations, $time, $accounted; - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli, $config) { $this->debug = $debug; $this->mysqli = $mysqli; + $this->config = $config; $this->debug->append("Instantiated Block class", 2); } @@ -43,6 +44,18 @@ class Block { return false; } + /** + * Get a specific block, by block height + * @param height int Block Height + * @return data array Block information from DB + **/ + public function getBlock($height) { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE height = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $height) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } + /** * Get our last, highest share ID inserted for a block * @param none @@ -55,6 +68,18 @@ class Block { return false; } + /** + * Fetch all blocks without a share ID + * @param order string Sort order, default ASC + * @return data array Array with database fields as keys + **/ + public function getAllUnsetShareId($order='ASC') { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE ISNULL(share_id) ORDER BY height $order"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + return false; + } + /** * Fetch all unaccounted blocks * @param order string Sort order, default ASC @@ -62,12 +87,44 @@ class Block { **/ public function getAllUnaccounted($order='ASC') { $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE accounted = 0 ORDER BY height $order"); - if ($this->checkStmt($stmt)) { - $stmt->execute(); - $result = $stmt->get_result(); - $stmt->close(); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); - } + return false; + } + + /** + * Get total amount of blocks in our table + * @param noone + * @return data int Count of rows + **/ + public function getBlockCount() { + $stmt = $this->mysqli->prepare("SELECT COUNT(id) AS blocks FROM $this->table"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return (int)$result->fetch_object()->blocks; + return false; + } + + /** + * Fetch our average share count for the past N blocks + * @param limit int Maximum blocks to check + * @return data float Float value of average shares + **/ + public function getAvgBlockShares($height, $limit=1) { + $stmt = $this->mysqli->prepare("SELECT AVG(x.shares) AS average FROM (SELECT shares FROM $this->table WHERE height <= ? ORDER BY height DESC LIMIT ?) AS x"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $height, $limit) && $stmt->execute() && $result = $stmt->get_result()) + return (float)$result->fetch_object()->average; + return false; + } + + /** + * Fetch our average rewards for the past N blocks + * @param limit int Maximum blocks to check + * @return data float Float value of average shares + **/ + public function getAvgBlockReward($limit=1) { + $stmt = $this->mysqli->prepare("SELECT AVG(x.amount) AS average FROM (SELECT amount FROM $this->table ORDER BY height DESC LIMIT ?) AS x"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $limit) && $stmt->execute() && $result = $stmt->get_result()) + return (float)$result->fetch_object()->average; return false; } @@ -76,15 +133,10 @@ class Block { * @param confirmations int Required confirmations to consider block confirmed * @return data array Array with database fields as keys **/ - public function getAllUnconfirmed($confirmations='120') { - $stmt = $this->mysqli->prepare("SELECT id, blockhash, confirmations FROM $this->table WHERE confirmations < ? AND confirmations > -1"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param("i", $confirmations); - $stmt->execute(); - $result = $stmt->get_result(); - $stmt->close(); + public function getAllUnconfirmed($confirmations=120) { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE confirmations < ? AND confirmations > -1"); + if ($this->checkStmt($stmt) && $stmt->bind_param("i", $confirmations) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); - } return false; } @@ -228,4 +280,4 @@ class Block { } // Automatically load our class for furhter usage -$block = new Block($debug, $mysqli, SALT); +$block = new Block($debug, $mysqli, $config); diff --git a/public/include/classes/invitation.class.php b/public/include/classes/invitation.class.php new file mode 100644 index 00000000..822dfef3 --- /dev/null +++ b/public/include/classes/invitation.class.php @@ -0,0 +1,146 @@ +debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE account_id = ?"); + if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + $this->setErrorMessage('Unable to fetch invitiations send from your account'); + $this->debug->append('Failed to fetch invitations from database: ' . $this->mysqli->errro); + return false; + } + + /** + * Count invitations sent by an account_id + * @param account_id integer Account ID + * @return mixes Integer on success, boolean on failure + **/ + public function getCountInvitations($account_id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT count(id) AS total FROM $this->table WHERE account_id = ?"); + if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute() && $stmt->bind_result($total) && $stmt->fetch()) + return $total; + $this->setErrorMessage('Unable to fetch invitiations send from your account'); + $this->debug->append('Failed to fetch invitations from database: ' . $this->mysqli->errro); + return false; + } + + /** + * Get a specific invitation by email address + * Used to ensure no invitation was already sent + * @param strEmail string Email address to check for + * @return bool boolean true of ralse + **/ + public function getByEmail($strEmail) { + $this->debug->append("STA " . __METHOD__, 4); + return $this->getSingle($strEmail, 'id', 'email', 's'); + } + + /** + * Get a specific token by token ID + * Used to match an invitation against a token + * @param token_id integer Token ID stored in invitation + * @return data mixed Invitation ID on success, false on error + **/ + public function getByTokenId($token_id) { + $this->debug->append("STA " . __METHOD__, 4); + return $this->getSingle($token_id, 'id', 'token_id'); + } + + /** + * Set an invitation as activated by the invitee + * @param token_id integer Token to activate + * @return bool boolean true or false + **/ + public function setActivated($token_id) { + if (!$iInvitationId = $this->getByTokenId($token_id)) { + $this->setErrorMessage('Unable to convert token ID to invitation ID'); + return false; + } + $field = array('name' => 'is_activated', 'type' => 'i', 'value' => 1); + return $this->updateSingle($iInvitationId, $field); + } + + /** + * Insert a new invitation to the database + * @param account_id integer Account ID to bind the invitation to + * @param email string Email address the invite was sent to + * @param token_id integer Token ID used during invitation + * @return bool boolean True of false + **/ + public function createInvitation($account_id, $email, $token_id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("INSERT INTO $this->table ( account_id, email, token_id ) VALUES ( ?, ?, ?)"); + if ($stmt && $stmt->bind_param('isi', $account_id, $email, $token_id) && $stmt->execute()) + return true; + return false; + } + /** + * Send an invitation out to a user + * Uses the mail class to send mails + * @param account_id integer Sending account ID + * @param aData array Data array including mail information + * @return bool boolean True or false + **/ + public function sendInvitation($account_id, $aData) { + $this->debug->append("STA " . __METHOD__, 4); + // Check data input + if (empty($aData['email']) || !filter_var($aData['email'], FILTER_VALIDATE_EMAIL)) { + $this->setErrorMessage( 'Invalid e-mail address' ); + return false; + } + if (preg_match('/[^a-z_\.\!\?\-0-9 ]/i', $aData['message'])) { + $this->setErrorMessage('Message may only contain alphanumeric characters'); + return false; + } + // Ensure this invitation does not exist yet nor do we have an account with that email + if ($this->user->getEmail($aData['email'])) { + $this->setErrorMessage('This email is already registered as an account'); + return false; + } + if ($this->getByEmail($aData['email'])) { + $this->setErrorMessage('A pending invitation for this address already exists'); + return false; + } + if (!$aData['token'] = $this->token->createToken('invitation', $account_id)) { + $this->setErrorMessage('Unable to generate invitation token: ' . $this->token->getError()); + return false; + } + $aData['username'] = $this->user->getUserName($account_id); + $aData['subject'] = 'Pending Invitation'; + if ($this->mail->sendMail('invitations/body', $aData)) { + $aToken = $this->token->getToken($aData['token']); + if (!$this->createInvitation($account_id, $aData['email'], $aToken['id'])) { + $this->setErrorMessage('Unable to create invitation record'); + return false; + } + return true; + } else { + $this->setErrorMessage('Unable to send email to recipient'); + } + $this->setErrorMessage('Unable to send invitation'); + return false; + } +} + +// Instantiate class +$invitation = new invitation(); +$invitation->setDebug($debug); +$invitation->setMysql($mysqli); +$invitation->setMail($mail); +$invitation->setUser($user); +$invitation->setToken($oToken); +$invitation->setConfig($config); + +?> diff --git a/public/include/classes/mail.class.php b/public/include/classes/mail.class.php index e34423c5..ea00cef6 100644 --- a/public/include/classes/mail.class.php +++ b/public/include/classes/mail.class.php @@ -4,30 +4,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); -class Mail { - private $sError = ''; - - public function setDebug($debug) { - $this->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; - } +class Mail extends Base { function checkStmt($bState) { $this->debug->append("STA " . __METHOD__, 4); if ($bState ===! true) { @@ -38,22 +15,59 @@ class Mail { return true; } + /** + * Mail form contact site admin + * @param senderName string senderName + * @param senderEmail string senderEmail + * @param senderSubject string senderSubject + * @param senderMessage string senderMessage + * @param email string config Email address + * @param subject string header subject + * @return bool + **/ + public function contactform($senderName, $senderEmail, $senderSubject, $senderMessage) { + $this->debug->append("STA " . __METHOD__, 4); + if (preg_match('/[^a-z_\.\!\?\-0-9\\s ]/i', $senderName)) { + $this->setErrorMessage('Username may only contain alphanumeric characters'); + return false; + } + if (empty($senderEmail) || !filter_var($senderEmail, FILTER_VALIDATE_EMAIL)) { + $this->setErrorMessage( 'Invalid e-mail address' ); + return false; + } + if (preg_match('/[^a-z_\.\!\?\-0-9\\s ]/i', $senderSubject)) { + $this->setErrorMessage('Subject may only contain alphanumeric characters'); + return false; + } + if (strlen(strip_tags($senderMessage)) < strlen($senderMessage)) { + $this->setErrorMessage('Your message may only contain alphanumeric characters'); + return false; + } + $aData['senderName'] = $senderName; + $aData['senderEmail'] = $senderEmail; + $aData['senderSubject'] = $senderSubject; + $aData['senderMessage'] = $senderMessage; + $aData['email'] = $this->setting->getValue('website_email'); + $aData['subject'] = 'Contact From'; + if ($this->sendMail('contactform/body', $aData)) { + return true; + } else { + $this->setErrorMessage( 'Unable to send email' ); + return false; + } + return false; + } + public function sendMail($template, $aData) { - $this->smarty->assign('WEBSITENAME', $this->config['website']['name']); + $this->smarty->assign('WEBSITENAME', $this->setting->getValue('website_name')); $this->smarty->assign('SUBJECT', $aData['subject']); $this->smarty->assign('DATA', $aData); - $headers = 'From: Website Administration <' . $this->config['website']['email'] . ">\n"; + $headers = 'From: Website Administration <' . $this->setting->getValue('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; - } + if (mail($aData['email'], $this->smarty->fetch(BASEPATH . 'templates/mail/subject.tpl'), $this->smarty->fetch(BASEPATH . 'templates/mail/' . $template . '.tpl'), $headers)) + return true; + $this->setErrorMessage('Unable to send mail'); return false; } } @@ -64,4 +78,5 @@ $mail->setDebug($debug); $mail->setMysql($mysqli); $mail->setSmarty($smarty); $mail->setConfig($config); +$mail->setSetting($setting); ?> diff --git a/public/include/classes/monitoring.class.php b/public/include/classes/monitoring.class.php new file mode 100644 index 00000000..ff69007f --- /dev/null +++ b/public/include/classes/monitoring.class.php @@ -0,0 +1,49 @@ +debug = $debug; + $this->mysqli = $mysqli; + $this->table = 'monitoring'; + } + + /** + * Fetch a value from our table + * @param name string Setting name + * @return value string Value + **/ + public function getStatus($name) { + $query = $this->mysqli->prepare("SELECT * FROM $this->table WHERE name = ? LIMIT 1"); + if ($query && $query->bind_param('s', $name) && $query->execute() && $result = $query->get_result()) { + return $result->fetch_assoc(); + } else { + $this->debug->append("Failed to fetch variable $name from $this->table"); + return false; + } + return $value; + } + + /** + * Insert or update a setting + * @param name string Name of the variable + * @param value string Variable value + * @return bool + **/ + public function setStatus($name, $type, $value) { + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (name, type, value) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE value = ? + "); + if ($stmt && $stmt->bind_param('ssss', $name, $type, $value, $value) && $stmt->execute()) + return true; + $this->debug->append("Failed to set $name to $value"); + return false; + } +} + +$monitoring = new Monitoring($debug, $mysqli); diff --git a/public/include/classes/news.class.php b/public/include/classes/news.class.php new file mode 100644 index 00000000..3f3f8631 --- /dev/null +++ b/public/include/classes/news.class.php @@ -0,0 +1,102 @@ +debug->append("STA " . __METHOD__, 5); + return $this->getSingle($id, 'active', 'id'); + } + + public function toggleActive($id) { + $this->debug->append("STA " . __METHOD__, 5); + $field = array('name' => 'active', 'type' => 'i', 'value' => !$this->getActive($id)); + return $this->updateSingle($id, $field); + } + + /** + * Get all active news + **/ + public function getAllActive() { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT n.*, a.username AS author FROM $this->table AS n LEFT JOIN " . $this->user->getTableName() . " AS a ON a.id = n.account_id WHERE active = 1 ORDER BY time DESC"); + if ($stmt && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + // Catchall + return false; + } + + /** + * Get all news + **/ + public function getAll() { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT n.*, a.username AS author FROM $this->table AS n LEFT JOIN " . $this->user->getTableName() . " AS a ON a.id = n.account_id ORDER BY time DESC"); + if ($stmt && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + // Catchall + return false; + } + + /** + * Get a specific news entry + **/ + public function getEntry($id) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE id = ?"); + if ($stmt && $stmt->bind_param('i', $id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + // Catchall + return false; + } + + /** + * Update a news entry + **/ + public function updateNews($id, $header, $content, $active=0) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare("UPDATE $this->table SET content = ?, header = ?, active = ? WHERE id = ?"); + if ($stmt && $stmt->bind_param('ssii', $content, $header, $active, $id) && $stmt->execute() && $stmt->affected_rows == 1) + return true; + $this->setErrorMessage("Failed to update news entry $id"); + return false; + } + + public function deleteNews($id) { + $this->debug->append("STA " . __METHOD__, 4); + if (!is_int($id)) return false; + $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $id) && $stmt->execute() && $stmt->affected_rows == 1) + return true; + $this->setErrorMessage("Failed to delete news entry $id"); + return false; + } + + /** + * Add a new mews entry to the table + * @param type string Type of the notification + * @return bool + **/ + public function addNews($account_id, $aData, $active=false) { + $this->debug->append("STA " . __METHOD__, 4); + if (empty($aData['header'])) return false; + if (empty($aData['content'])) return false; + if (!is_int($account_id)) return false; + $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, header, content, active) VALUES (?,?,?,?)"); + if ($stmt && $stmt->bind_param('issi', $account_id, $aData['header'], $aData['content'], $active) && $stmt->execute()) + return true; + $this->debug->append("Failed to add news: " . $this->mysqli->error); + $this->setErrorMessage("Unable to add new news: " . $this->mysqli->error); + return false; + } +} + +$news = new News(); +$news->setDebug($debug); +$news->setMysql($mysqli); +$news->setUser($user); +?> diff --git a/public/include/classes/notification.class.php b/public/include/classes/notification.class.php index b38bac2e..ac61d105 100644 --- a/public/include/classes/notification.class.php +++ b/public/include/classes/notification.class.php @@ -17,21 +17,6 @@ class Notification extends Mail { 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 @@ -99,12 +84,16 @@ class Notification extends Mail { $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']; + if ($result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $aData[$row['type']] = $row['active']; + } + return $aData; } - return $aData; } // Catchall + $this->setErrorMessage('Unable to fetch notification settings'); + $this->debug->append('Failed fetching notification settings for ' . $account_id . ': ' . $this->mysqli->error); return false; } @@ -173,11 +162,17 @@ class Notification extends Mail { // 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)) + if ($stmt->close() && $this->sendMail('notifications/' . $strType, $aMailData) && $this->addNotification($account_id, $strType, $aMailData)) { return true; + } else { + $this->setErrorMessage('SendMail call failed: ' . $this->mail->getError()); + return false; + } } else { $this->setErrorMessage('User disabled ' . $strType . ' notifications'); + return false; } + $this->setErrorMessage('Error sending mail notification'); return false; } } @@ -187,5 +182,5 @@ $notification->setDebug($debug); $notification->setMysql($mysqli); $notification->setSmarty($smarty); $notification->setConfig($config); - +$notification->setSetting($setting); ?> diff --git a/public/include/classes/payout.class.php b/public/include/classes/payout.class.php new file mode 100644 index 00000000..65f353bd --- /dev/null +++ b/public/include/classes/payout.class.php @@ -0,0 +1,63 @@ +mysqli->prepare("SELECT id FROM $this->table WHERE completed = 0 AND account_id = ? LIMIT 1"); + if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute( )&& $stmt->store_result() && $stmt->num_rows > 0) + return true; + return false; + } + + /** + * Get all new, unprocessed payout requests + * @param none + * @return data Associative array with DB Fields + **/ + public function getUnprocessedPayouts() { + $stmt = $this->mysqli->prepare("SELECT * FROM $this->table WHERE completed = 0"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + return false; + } + + /** + * Insert a new payout request + * @param account_id Account ID + * @return data mixed Inserted ID or false + **/ + public function createPayout($account_id=NULL) { + $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id) VALUES (?)"); + if ($stmt && $stmt->bind_param('i', $account_id) && $stmt->execute()) { + return $stmt->insert_id; + } + $this->setErrorMessage('Unable to create new payout request'); + $this->debug->append('Failed to create new payout request in database: ' . $this->mysqli->error); + return false; + } + + /** + * Mark a payout as processed + * @param id int Payout ID + * @return boolean bool True or False + **/ + public function setProcessed($id) { + $stmt = $this->mysqli->prepare("UPDATE $this->table SET completed = 1 WHERE id = ?"); + if ($stmt && $stmt->bind_param('i', $id) && $stmt->execute()) + return true; + return false; + } +} + +$oPayout = new Payout(); +$oPayout->setDebug($debug); +$oPayout->setMysql($mysqli); diff --git a/public/include/classes/roundstats.class.php b/public/include/classes/roundstats.class.php new file mode 100644 index 00000000..6b187973 --- /dev/null +++ b/public/include/classes/roundstats.class.php @@ -0,0 +1,160 @@ +debug = $debug; + $this->mysqli = $mysqli; + $this->config = $config; + $this->debug->append("Instantiated RoundStats class", 2); + } + + // get and set methods + private function setErrorMessage($msg) { + $this->sError = $msg; + } + public function getError() { + return $this->sError; + } + + /** + * Get next block for round stats + **/ + public function getNextBlock($iHeight=0) { + $stmt = $this->mysqli->prepare(" + SELECT height + FROM $this->tableBlocks + WHERE height > ? + ORDER BY height ASC + LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $iHeight) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->height; + return false; + } + + /** + * Get prev block for round stats + **/ + public function getPreviousBlock($iHeight=0) { + $stmt = $this->mysqli->prepare(" + SELECT height + FROM $this->tableBlocks + WHERE height < ? + ORDER BY height DESC + LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $iHeight) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->height; + return false; + } + + /** + * Get details for block height + * @param height int Block Height + * @return data array Block information from DB + **/ + public function getDetailsForBlockHeight($iHeight=0, $isAdmin=0) { + $stmt = $this->mysqli->prepare(" + SELECT + b.id, height, blockhash, amount, confirmations, difficulty, FROM_UNIXTIME(time) as time, shares, + IF(a.is_anonymous, IF( ? , a.username, 'anonymous'), a.username) AS finder + FROM $this->tableBlocks as b + LEFT JOIN $this->tableUsers AS a ON b.account_id = a.id + WHERE b.height = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $isAdmin, $iHeight) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } + + /** + * Get shares statistics for round block height + * @param height int Block Height + * @return data array Block information from DB + **/ + public function getRoundStatsForAccounts($iHeight=0, $isAdmin=0) { + $stmt = $this->mysqli->prepare(" + SELECT + IF(a.is_anonymous, IF( ? , a.username, 'anonymous'), a.username) AS username, + s.valid, + s.invalid + FROM $this->tableStats AS s + LEFT JOIN $this->tableBlocks AS b ON s.block_id = b.id + LEFT JOIN $this->tableUsers AS a ON a.id = s.account_id + WHERE b.height = ? + GROUP BY username ASC + ORDER BY valid DESC + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $isAdmin, $iHeight) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + return false; + } + + /** + * Get all transactions for round block height for admin + * @param height int Block Height + * @return data array Block round transactions + **/ + public function getAllRoundTransactions($iHeight=0, $admin) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT + t.id AS id, + IF(a.is_anonymous, IF( ? , a.username, 'anonymous'), a.username) AS username, + t.type AS type, + t.amount AS amount + FROM $this->tableTrans AS t + LEFT JOIN $this->tableBlocks AS b ON t.block_id = b.id + LEFT JOIN $this->tableUsers AS a ON t.account_id = a.id + WHERE b.height = ? + ORDER BY id ASC"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $admin, $iHeight) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + $this->debug->append('Unable to fetch transactions'); + return false; + } + + /** + * Get transactions for round block height user id + * @param height int Block Height + * @param id int user id + * @return data array Block round transactions for user id + **/ + public function getUserRoundTransactions($iHeight=0, $id=0) { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT + t.id AS id, + a.username AS username, + t.type AS type, + t.amount AS amount + FROM $this->tableTrans AS t + LEFT JOIN $this->tableBlocks AS b ON t.block_id = b.id + LEFT JOIN $this->tableUsers AS a ON t.account_id = a.id + WHERE b.height = ? AND a.id = ? + ORDER BY id ASC"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $iHeight, $id) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + $this->debug->append('Unable to fetch transactions'); + return false; + } + + private function checkStmt($bState) { + if ($bState ===! true) { + $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); + $this->setErrorMessage('Internal application Error'); + return false; + } + return true; + } + +} + +$roundstats = new RoundStats($debug, $mysqli, $config); diff --git a/public/include/classes/setting.class.php b/public/include/classes/setting.class.php index f2c1cbea..56a0d460 100644 --- a/public/include/classes/setting.class.php +++ b/public/include/classes/setting.class.php @@ -5,10 +5,9 @@ if (!defined('SECURITY')) die('Hacking attempt'); class Setting { - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli) { $this->debug = $debug; $this->mysqli = $mysqli; - $this->salt = $salt; $this->table = 'settings'; } @@ -47,9 +46,8 @@ class Setting { if ($stmt && $stmt->bind_param('sss', $name, $value, $value) && $stmt->execute()) return true; $this->debug->append("Failed to set $name to $value"); - echo $this->mysqli->error; return false; } } -$setting = new Setting($debug, $mysqli, SALT); +$setting = new Setting($debug, $mysqli); diff --git a/public/include/classes/share.class.php b/public/include/classes/share.class.php index 85935a2d..705c41e1 100644 --- a/public/include/classes/share.class.php +++ b/public/include/classes/share.class.php @@ -11,11 +11,14 @@ class Share { private $oUpstream; private $iLastUpstreamId; // This defines each share - public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time; + public $rem_host, $username, $our_result, $upstream_result, $reason, $solution, $time, $difficulty; - public function __construct($debug, $mysqli, $salt) { + public function __construct($debug, $mysqli, $user, $block, $config) { $this->debug = $debug; $this->mysqli = $mysqli; + $this->user = $user; + $this->config = $config; + $this->block = $block; $this->debug->append("Instantiated Share class", 2); } @@ -67,10 +70,10 @@ class Share { **/ public function getRoundShares($previous_upstream=0, $current_upstream) { $stmt = $this->mysqli->prepare("SELECT - count(id) as total + ROUND(IFNULL(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS total FROM $this->table WHERE our_result = 'Y' - AND id BETWEEN ? AND ? + AND id > ? AND id <= ? "); if ($this->checkStmt($stmt)) { $stmt->bind_param('ii', $previous_upstream, $current_upstream); @@ -86,47 +89,106 @@ class Share { * Fetch all shares grouped by accounts to count share per account * @param previous_upstream int Previous found share accepted by upstream to limit results * @param current_upstream int Current upstream accepted share + * @param limit int Limit to this amount of shares for PPLNS * @return data array username, valid and invalid shares from account **/ public function getSharesForAccounts($previous_upstream=0, $current_upstream) { - $stmt = $this->mysqli->prepare("SELECT - a.id, - validT.account AS username, - sum(validT.valid) as valid, - IFNULL(sum(invalidT.invalid),0) as invalid - FROM - ( - SELECT DISTINCT - SUBSTRING_INDEX( `username` , '.', 1 ) as account, - COUNT(id) AS valid - FROM $this->table - WHERE id BETWEEN ? AND ? - AND our_result = 'Y' - GROUP BY account - ) validT - LEFT JOIN - ( - SELECT DISTINCT - SUBSTRING_INDEX( `username` , '.', 1 ) as account, - COUNT(id) AS invalid - FROM $this->table - WHERE id BETWEEN ? AND ? - AND our_result = 'N' - GROUP BY account - ) invalidT - ON validT.account = invalidT.account - INNER JOIN accounts a ON a.username = validT.account - GROUP BY a.username DESC"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param('iiii', $previous_upstream, $current_upstream, $previous_upstream, $current_upstream); - $stmt->execute(); - $result = $stmt->get_result(); - $stmt->close(); + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as username, + a.no_fees AS no_fees, + ROUND(IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS valid, + ROUND(IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS invalid + FROM $this->table AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) + WHERE s.id > ? AND s.id <= ? + GROUP BY username DESC + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); + return false; + } + + /** + * Fetch the highest available share ID + **/ + function getMaxShareId() { + $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; + return false; + } + + /** + * Fetch the highest available share ID from archive + **/ + function getMaxArchiveShareId() { + $stmt = $this->mysqli->prepare(" + SELECT MAX(share_id) AS share_id FROM $this->tableArchive + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->share_id; + return false; + } + + /** + * We need a certain amount of valid archived shares + * param left int Left/lowest share ID + * param right int Right/highest share ID + * return array data Returns an array with usernames as keys for easy access + **/ + function getArchiveShares($iCount) { + $iMinId = $this->getMinArchiveShareId($iCount); + $iMaxId = $this->getMaxArchiveShareId(); + $stmt = $this->mysqli->prepare(" + SELECT + a.id, + SUBSTRING_INDEX( s.username , '.', 1 ) as account, + a.no_fees AS no_fees, + ROUND(IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS valid, + ROUND(IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 8) AS invalid + FROM $this->tableArchive AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON a.username = SUBSTRING_INDEX( s.username , '.', 1 ) + WHERE s.share_id > ? AND s.share_id <= ? + GROUP BY account DESC"); + if ($this->checkStmt($stmt) && $stmt->bind_param("ii", $iMinId, $iMaxId) && $stmt->execute() && $result = $stmt->get_result()) { + $aData = NULL; + while ($row = $result->fetch_assoc()) { + $aData[$row['account']] = $row; + } + if (is_array($aData)) return $aData; } return false; } + /** + * We keep shares only up to a certain point + * This can be configured by the user. + * @return return bool true or false + **/ + public function purgeArchive() { + if ($this->config['payout_system'] == 'pplns') { + // Fetch our last block so we can go back configured rounds + $aLastBlock = $this->block->getLast(); + // Fetch the block we need to find the share_id + $aBlock = $this->block->getBlock($aLastBlock['height'] - $this->config['archive']['maxrounds']); + // Now that we know our block, remove those shares + $stmt = $this->mysqli->prepare("DELETE FROM $this->tableArchive WHERE block_id < ? AND time < DATE_SUB(now(), INTERVAL ? MINUTE)"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $aBlock['id'], $this->config['archive']['maxage']) && $stmt->execute()) + return true; + } else { + // We are not running pplns, so we just need to keep shares of the past minutes + $stmt = $this->mysqli->prepare("DELETE FROM $this->tableArchive WHERE time < DATE_SUB(now(), INTERVAL ? MINUTE)"); + if ($this->checkStmt($stmt) && $stmt->bind_param('i', $this->config['archive']['maxage']) && $stmt->execute()) + return true; + } + // Catchall + return false; + } + /** * Move accounted shares to archive table, this step is optional * @param previous_upstream int Previous found share accepted by upstream to limit results @@ -135,10 +197,11 @@ class Share { * @return bool **/ public function moveArchive($current_upstream, $block_id, $previous_upstream=0) { - $archive_stmt = $this->mysqli->prepare("INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time) - SELECT id, username, our_result, upstream_result, ?, time - FROM $this->table - WHERE id BETWEEN ? AND ?"); + $archive_stmt = $this->mysqli->prepare(" + INSERT INTO $this->tableArchive (share_id, username, our_result, upstream_result, block_id, time, difficulty) + SELECT id, username, our_result, upstream_result, ?, time, IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS difficulty + FROM $this->table + WHERE id > ? AND id <= ?"); if ($this->checkStmt($archive_stmt) && $archive_stmt->bind_param('iii', $block_id, $previous_upstream, $current_upstream) && $archive_stmt->execute()) { $archive_stmt->close(); return true; @@ -148,7 +211,7 @@ class Share { } public function deleteAccountedShares($current_upstream, $previous_upstream=0) { - $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id BETWEEN ? AND ?"); + $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE id > ? AND id <= ?"); if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $previous_upstream, $current_upstream) && $stmt->execute()) return true; // Catchall @@ -177,16 +240,76 @@ class Share { * @param last int Skips all shares up to last to find new share * @return bool **/ - public function setUpstream($last=0) { + public function setUpstream($aBlock, $last=0) { + // Many use stratum, so we create our stratum check first + $version = pack("I*", sprintf('%08d', $aBlock['version'])); + $previousblockhash = pack("H*", swapEndian($aBlock['previousblockhash'])); + $merkleroot = pack("H*", swapEndian($aBlock['merkleroot']) ); + $time = pack("I*", $aBlock['time']); + $bits = pack("H*", swapEndian($aBlock['bits'])); + $nonce = pack("I*", $aBlock['nonce']); + $header_bin = $version . $previousblockhash . $merkleroot . $time . $bits . $nonce; + $header_hex = implode(unpack("H*", $header_bin)); + + // Stratum supported blockhash solution entry + $stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id FROM $this->table WHERE solution = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('s', $aBlock['hash']) && $stmt->execute() && $result = $stmt->get_result()) { + $this->oUpstream = $result->fetch_object(); + $this->share_type = 'startum_blockhash'; + if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) + return true; + } + + // Stratum scrypt hash check + $scrypt_hash = swapEndian(bin2hex(Scrypt::calc($header_bin, $header_bin, 1024, 1, 1, 32))); + $stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id FROM $this->table WHERE solution = ? LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('s', $scrypt_hash) && $stmt->execute() && $result = $stmt->get_result()) { + $this->oUpstream = $result->fetch_object(); + $this->share_type = 'startum_solution'; + if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) + return true; + } + + // Failed to fetch via startum solution, try pushpoold + // Fallback to pushpoold solution type + $ppheader = sprintf('%08d', $aBlock['version']) . word_reverse($aBlock['previousblockhash']) . word_reverse($aBlock['merkleroot']) . dechex($aBlock['time']) . $aBlock['bits'] . dechex($aBlock['nonce']); + $stmt = $this->mysqli->prepare("SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id FROM $this->table WHERE solution LIKE CONCAT(?, '%') LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('s', $ppheader) && $stmt->execute() && $result = $stmt->get_result()) { + $this->oUpstream = $result->fetch_object(); + $this->share_type = 'pp_solution'; + if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) + return true; + } + + // Still no match, try upstream result with timerange $stmt = $this->mysqli->prepare(" SELECT SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id FROM $this->table WHERE upstream_result = 'Y' AND id > ? + AND UNIX_TIMESTAMP(time) >= ? + AND UNIX_TIMESTAMP(time) <= ( ? + 60 ) ORDER BY id ASC LIMIT 1"); - if ($this->checkStmt($stmt) && $stmt->bind_param('i', $last) && $stmt->execute() && $result = $stmt->get_result()) { + if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $last, $aBlock['time'], $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) { $this->oUpstream = $result->fetch_object(); + $this->share_type = 'upstream_share'; + if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) + return true; + } + + // We failed again, now we take ANY result matching the timestamp + $stmt = $this->mysqli->prepare(" + SELECT + SUBSTRING_INDEX( `username` , '.', 1 ) AS account, id + FROM $this->table + WHERE our_result = 'Y' + AND id > ? + AND UNIX_TIMESTAMP(time) >= ? + ORDER BY id ASC LIMIT 1"); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $last, $aBlock['time']) && $stmt->execute() && $result = $stmt->get_result()) { + $this->oUpstream = $result->fetch_object(); + $this->share_type = 'any_share'; if (!empty($this->oUpstream->account) && is_int($this->oUpstream->id)) return true; } @@ -194,6 +317,48 @@ class Share { return false; } + /** + * Fetch the lowest needed share ID from shares + **/ + function getMinimumShareId($iCount, $current_upstream) { + // We don't use baseline here to be more accurate + $iCount = $iCount * pow(2, ($this->config['difficulty'] - 16)); + $stmt = $this->mysqli->prepare(" + SELECT MIN(b.id) AS id FROM + ( + SELECT id, @total := @total + IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty) AS total + FROM $this->table, (SELECT @total := 0) AS a + WHERE our_result = 'Y' + AND id <= ? AND @total < ? + ORDER BY id DESC + ) AS b + WHERE total <= ? + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('iii', $current_upstream, $iCount, $iCount) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->id; + return false; + } + + /** + * Fetch the lowest needed share ID from archive + **/ + function getMinArchiveShareId($iCount) { + $stmt = $this->mysqli->prepare(" + SELECT MIN(b.share_id) AS share_id FROM + ( + SELECT share_id, @total := @total + (IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty) / POW(2, (" . $this->config['difficulty'] . " - 16))) AS total + FROM $this->tableArchive, (SELECT @total := 0) AS a + WHERE our_result = 'Y' + AND @total < ? + ORDER BY share_id DESC + ) AS b + WHERE total <= ? + "); + if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $iCount, $iCount) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_object()->share_id; + return false; + } + /** * Helper function **/ @@ -207,4 +372,4 @@ class Share { } } -$share = new Share($debug, $mysqli, SALT); +$share = new Share($debug, $mysqli, $user, $block, $config); diff --git a/public/include/classes/statistics.class.php b/public/include/classes/statistics.class.php index a30aba1b..0950b632 100644 --- a/public/include/classes/statistics.class.php +++ b/public/include/classes/statistics.class.php @@ -61,7 +61,11 @@ class Statistics { $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $limit)) return $data; $stmt = $this->mysqli->prepare(" - SELECT b.*, a.username as finder + SELECT + b.*, + a.username AS finder, + a.is_anonymous AS is_anonymous, + ROUND((difficulty * 65535) / POW(2, (" . $this->config['difficulty'] . " -16)), 0) AS estshares FROM " . $this->block->getTableName() . " AS b LEFT JOIN " . $this->user->getTableName() . " AS a ON b.account_id = a.id @@ -95,14 +99,25 @@ class Statistics { * @param none * @return data object Return our hashrateas an object **/ - public function getCurrentHashrate() { + public function getCurrentHashrate($interval=600) { $this->debug->append("STA " . __METHOD__, 4); if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" - 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) - "); + SELECT + ( + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536 / ? / 1000), 0) AS hashrate + FROM " . $this->share->getTableName() . " + WHERE time > DATE_SUB(now(), INTERVAL ? SECOND) + ) + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536 / ? / 1000), 0) AS hashrate + FROM " . $this->share->getArchiveTableName() . " + WHERE time > DATE_SUB(now(), INTERVAL ? SECOND) + ) + ) AS hashrate + FROM DUAL"); // Catchall - if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__, $result->fetch_object()->hashrate); + if ($this->checkStmt($stmt) && $stmt->bind_param('iiii', $interval, $interval, $interval, $interval) && $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); return false; } @@ -112,13 +127,24 @@ class Statistics { * @param none * @return data object Our share rate in shares per second **/ - public function getCurrentShareRate() { + public function getCurrentShareRate($interval=600) { $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__)) return $data; $stmt = $this->mysqli->prepare(" - 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); + SELECT + ( + ( + SELECT ROUND(COUNT(id) / ?, 2) AS sharerate + FROM " . $this->share->getTableName() . " + WHERE time > DATE_SUB(now(), INTERVAL ? SECOND) + ) + ( + SELECT ROUND(COUNT(id) / ?, 2) AS sharerate + FROM " . $this->share->getArchiveTableName() . " + WHERE time > DATE_SUB(now(), INTERVAL ? SECOND) + ) + ) AS sharerate + FROM DUAL"); + if ($this->checkStmt($stmt) && $stmt->bind_param('iiii', $interval, $interval, $interval, $interval) && $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); return false; @@ -131,24 +157,83 @@ class Statistics { **/ public function getRoundShares() { $this->debug->append("STA " . __METHOD__, 4); - if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__)) return $data; + // Try the statistics cron cache, then function cache, then fallback to SQL + if ($data = $this->memcache->get(STATISTICS_ALL_USER_SHARES)) { + $this->debug->append("Found data in statistics cache", 2); + $total = array('valid' => 0, 'invalid' => 0); + foreach ($data['data'] as $aUser) { + $total['valid'] += $aUser['valid']; + $total['invalid'] += $aUser['invalid']; + } + return $total; + } + if ($data = $this->memcache->get(STATISTICS_ROUND_SHARES)) { + $this->debug->append("Found data in local cache", 2); + return $data; + } $stmt = $this->mysqli->prepare(" SELECT - ( SELECT IFNULL(count(id), 0) + ROUND(IFNULL(SUM(IF(our_result='Y', IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS valid, + ROUND(IFNULL(SUM(IF(our_result='N', IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS invalid FROM " . $this->share->getTableName() . " - WHERE UNIX_TIMESTAMP(time) >IFNULL((SELECT MAX(time) FROM blocks),0) - AND our_result = 'Y' ) as valid, - ( SELECT IFNULL(count(id), 0) - FROM " . $this->share->getTableName() . " - WHERE UNIX_TIMESTAMP(time) >IFNULL((SELECT MAX(time) FROM blocks),0) - AND our_result = 'N' ) as invalid"); + WHERE UNIX_TIMESTAMP(time) > IFNULL((SELECT MAX(time) FROM " . $this->block->getTableName() . "), 0)"); if ( $this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result() ) - return $this->memcache->setCache(__FUNCTION__, $result->fetch_assoc()); + return $this->memcache->setCache(STATISTICS_ROUND_SHARES, $result->fetch_assoc()); // Catchall $this->debug->append("Failed to fetch round shares: " . $this->mysqli->error); return false; } + /** + * Get amount of shares for a all users + * Used in statistics cron to refresh memcache data + * @param account_id int User ID + * @return data array invalid and valid share counts + **/ + public function getAllUserShares() { + $this->debug->append("STA " . __METHOD__, 4); + if (! $data = $this->memcache->get(STATISTICS_ALL_USER_SHARES)) { + $data['share_id'] = 0; + $data['data'] = array(); + } + $stmt = $this->mysqli->prepare(" + SELECT + ROUND(IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS valid, + ROUND(IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS invalid, + u.id AS id, + u.donate_percent AS donate_percent, + u.is_anonymous AS is_anonymous, + u.username AS username + FROM " . $this->share->getTableName() . " AS s, + " . $this->user->getTableName() . " AS u + WHERE u.username = SUBSTRING_INDEX( s.username, '.', 1 ) + AND UNIX_TIMESTAMP(s.time) > IFNULL( + ( + SELECT MAX(b.time) + FROM " . $this->block->getTableName() . " AS b + ) ,0 ) + AND s.id > ? + GROUP BY u.id"); + if ($stmt && $stmt->bind_param('i', $data['share_id']) && $stmt->execute() && $result = $stmt->get_result()) { + $data_new = array(); + while ($row = $result->fetch_assoc()) { + if (! array_key_exists($row['id'], $data['data'])) { + $data['data'][$row['id']] = $row; + } else { + $data['data'][$row['id']]['valid'] += $row['valid']; + $data['data'][$row['id']]['invalid'] += $row['invalid']; + $data['data'][$row['id']]['donate_percent'] = $row['donate_percent']; + $data['data'][$row['id']]['is_anonymous'] = $row['is_anonymous']; + } + } + $data['share_id'] = $this->share->getMaxShareId(); + return $this->memcache->setCache(STATISTICS_ALL_USER_SHARES, $data); + } + // Catchall + $this->debug->append("Unable to fetch all users round shares: " . $this->mysqli->error); + return false; + } + /** * Get amount of shares for a specific user * @param account_id int User ID @@ -156,28 +241,20 @@ class Statistics { **/ public function getUserShares($account_id) { $this->debug->append("STA " . __METHOD__, 4); - if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; + // Dual-caching, try statistics cron first, then fallback to local, then fallbock to SQL + if ($data = $this->memcache->get(STATISTICS_ALL_USER_SHARES)) return @$data['data'][$account_id]; + if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT - ( - SELECT COUNT(s.id) - FROM " . $this->share->getTableName() . " AS s, - " . $this->user->getTableName() . " AS u - WHERE u.username = SUBSTRING_INDEX( s.username, '.', 1 ) - AND UNIX_TIMESTAMP(s.time) >IFNULL((SELECT MAX(b.time) FROM " . $this->block->getTableName() . " AS b),0) - AND our_result = 'Y' - AND u.id = ? - ) AS valid, - ( - SELECT COUNT(s.id) - FROM " . $this->share->getTableName() . " AS s, - " . $this->user->getTableName() . " AS u - WHERE u.username = SUBSTRING_INDEX( s.username, '.', 1 ) - AND UNIX_TIMESTAMP(s.time) >IFNULL((SELECT MAX(b.time) FROM " . $this->block->getTableName() . " AS b),0) - AND our_result = 'N' - AND u.id = ? - ) AS invalid"); - if ($stmt && $stmt->bind_param("ii", $account_id, $account_id) && $stmt->execute() && $result = $stmt->get_result()) + ROUND(IFNULL(SUM(IF(our_result='Y', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS valid, + ROUND(IFNULL(SUM(IF(our_result='N', IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty), 0)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS invalid + FROM " . $this->share->getTableName() . " AS s, + " . $this->user->getTableName() . " AS u + WHERE + u.username = SUBSTRING_INDEX( s.username, '.', 1 ) + AND UNIX_TIMESTAMP(s.time) >IFNULL((SELECT MAX(b.time) FROM " . $this->block->getTableName() . " AS b),0) + AND u.id = ?"); + if ($stmt && $stmt->bind_param("i", $account_id) && $stmt->execute() && $result = $stmt->get_result()) return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_assoc()); // Catchall $this->debug->append("Unable to fetch user round shares: " . $this->mysqli->error); @@ -196,18 +273,18 @@ class Statistics { a.id AS id, a.is_admin as is_admin, a.is_locked as is_locked, + a.no_fees as no_fees, a.username AS username, a.donate_percent AS donate_percent, a.email AS email, - COUNT(s.id) AS shares + ROUND(IFNULL(SUM(IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) 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 - "); + 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)); } @@ -218,17 +295,28 @@ class Statistics { * @param account_id integer User ID * @return data integer Current Hashrate in khash/s **/ - public function getUserHashrate($account_id) { + public function getUserHashrate($account_id, $interval=600) { $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 ROUND(COUNT(s.id) * POW(2, " . $this->config['difficulty'] . ")/600/1000) AS hashrate - 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() ) + SELECT + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536 / ? / 1000), 0) AS hashrate + 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 ? SECOND) + AND u.id = ? + ) + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536 / ? / 1000), 0) AS hashrate + FROM " . $this->share->getArchiveTableName() . " AS s, + " . $this->user->getTableName() . " AS u + WHERE u.username = SUBSTRING_INDEX( s.username, '.', 1 ) + AND s.time > DATE_SUB(now(), INTERVAL ? SECOND) + AND u.id = ? + ) AS hashrate + FROM DUAL"); + if ($this->checkStmt($stmt) && $stmt->bind_param("iiiiii", $interval, $interval, $account_id, $interval, $interval, $account_id) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__ . $account_id, $result->fetch_object()->hashrate); // Catchall $this->debug->append("Failed to fetch hashrate: " . $this->mysqli->error); @@ -240,17 +328,30 @@ class Statistics { * @param account_id integer User ID * @return data integer Current Sharerate in shares/s **/ - public function getUserSharerate($account_id) { + public function getUserSharerate($account_id, $interval=600) { $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 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() ) + SELECT + ( + ( + SELECT COUNT(s.id) / ? 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 ? SECOND) + AND u.id = ? + ) + ( + SELECT COUNT(s.id) / ? AS sharerate + FROM " . $this->share->getArchiveTableName() . " AS s, + " . $this->user->getTableName() . " AS u + WHERE u.username = SUBSTRING_INDEX( s.username, '.', 1 ) + AND s.time > DATE_SUB(now(), INTERVAL ? SECOND) + AND u.id = ? + ) + ) AS sharerate + FROM DUAL"); + if ($this->checkStmt($stmt) && $stmt->bind_param("iiiiii", $interval, $interval, $account_id, $interval, $interval, $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); @@ -266,11 +367,11 @@ class Statistics { $this->debug->append("STA " . __METHOD__, 4); if ($data = $this->memcache->get(__FUNCTION__ . $worker_id)) return $data; $stmt = $this->mysqli->prepare(" - SELECT ROUND(COUNT(s.id) * POW(2,21)/600/1000) AS hashrate + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536 / 600 / 1000), 0) AS hashrate 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 s.time > DATE_SUB(now(), INTERVAL 600 SECOND) AND u.id = ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $account_id) && $stmt->execute() && $result = $stmt->get_result() ) return $this->memcache->setCache(__FUNCTION__ . $worker_id, $result->fetch_object()->hashrate); @@ -290,11 +391,32 @@ class Statistics { if ($this->getGetCache() && $data = $this->memcache->get(__FUNCTION__ . $type . $limit)) return $data; switch ($type) { case 'shares': + if ($data = $this->memcache->get(STATISTICS_ALL_USER_SHARES)) { + // Use global cache to build data + $max = 0; + foreach($data['data'] as $key => $aUser) { + $shares[$key] = $aUser['valid']; + $username[$key] = $aUser['username']; + } + array_multisort($shares, SORT_DESC, $username, SORT_ASC, $data['data']); + foreach ($data['data'] as $key => $aUser) { + $data_new[$key]['shares'] = $aUser['valid']; + $data_new[$key]['account'] = $aUser['username']; + $data_new[$key]['donate_percent'] = $aUser['donate_percent']; + $data_new[$key]['is_anonymous'] = $aUser['is_anonymous']; + } + return $data_new; + } + // No cached data, fallback to SQL and cache in local cache $stmt = $this->mysqli->prepare(" SELECT - COUNT(id) AS shares, - SUBSTRING_INDEX( username, '.', 1 ) AS account - FROM " . $this->share->getTableName() . " + a.donate_percent AS donate_percent, + a.is_anonymous AS is_anonymous, + ROUND(IFNULL(SUM(IF(s.difficulty=0, POW(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)), 0) / POW(2, (" . $this->config['difficulty'] . " - 16)), 0) AS shares, + SUBSTRING_INDEX( s.username, '.', 1 ) AS account + FROM " . $this->share->getTableName() . " AS s + LEFT JOIN " . $this->user->getTableName() . " AS a + ON SUBSTRING_INDEX( s.username, '.', 1 ) = a.username WHERE our_result = 'Y' GROUP BY account ORDER BY shares DESC @@ -307,17 +429,24 @@ class Statistics { case 'hashes': $stmt = $this->mysqli->prepare(" - SELECT - ROUND(COUNT(id) * POW(2," . $this->config['difficulty'] . ")/600/1000,2) AS hashrate, - SUBSTRING_INDEX( username, '.', 1 ) AS account - FROM " . $this->share->getTableName() . " - WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) - AND our_result = 'Y' + SELECT + a.donate_percent AS donate_percent, + a.is_anonymous AS is_anonymous, + IFNULL(ROUND(SUM(t1.difficulty) * 65536/600/1000, 2), 0) AS hashrate, + SUBSTRING_INDEX( t1.username, '.', 1 ) AS account + FROM + ( + SELECT IFNULL(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty), 0) AS difficulty, username FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) AND our_result = 'Y' + UNION ALL + SELECT IFNULL(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty), 0) AS difficulty, username FROM " . $this->share->getArchiveTableName() ." WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE) AND our_result = 'Y' + ) AS t1 + LEFT JOIN " . $this->user->getTableName() . " AS a + ON SUBSTRING_INDEX( t1.username, '.', 1 ) = a.username GROUP BY account ORDER BY hashrate DESC LIMIT ?"); if ($this->checkStmt($stmt) && $stmt->bind_param("i", $limit) && $stmt->execute() && $result = $stmt->get_result()) return $this->memcache->setCache(__FUNCTION__ . $type . $limit, $result->fetch_all(MYSQLI_ASSOC)); - $this->debug->append("Fetching shares failed: "); + $this->debug->append("Fetching shares failed: " . $this->mysqli->error); return false; break; } @@ -333,7 +462,7 @@ class Statistics { if ($data = $this->memcache->get(__FUNCTION__ . $account_id)) return $data; $stmt = $this->mysqli->prepare(" SELECT - ROUND(COUNT(s.id) * POW(2, " . $this->config['difficulty'] . ") / 3600 / 1000) AS hashrate, + IFNULL(ROUND(SUM(IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)) * 65536/3600/1000), 0) AS hashrate, HOUR(s.time) AS hour FROM " . $this->share->getTableName() . " AS s, accounts AS a WHERE time < NOW() - INTERVAL 1 HOUR @@ -341,19 +470,29 @@ class Statistics { AND a.username = SUBSTRING_INDEX( s.username, '.', 1 ) AND a.id = ? 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']; - } + UNION ALL + SELECT + IFNULL(ROUND(SUM(IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)) * 65536/3600/1000), 0) 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()) { + $iStartHour = date('G'); + // Initilize array + for ($i = 0; $i < 24; $i++) $aData[($iStartHour + $i) % 24] = 0; + // Fill data + 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 @@ -364,18 +503,27 @@ class Statistics { 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, + IFNULL(ROUND(SUM(IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)) * 65536/3600/1000), 0) 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) - "); + UNION ALL + SELECT + IFNULL(ROUND(SUM(IF(s.difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), s.difficulty)) * 65536/3600/1000), 0) AS hashrate, + HOUR(s.time) AS hour + FROM " . $this->share->getArchiveTableName() . " 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); + $iStartHour = date('G'); + // Initilize array + for ($i = 0; $i < 24; $i++) $aData[($iStartHour + $i) % 24] = 0; + // Fill data + while ($row = $result->fetch_assoc()) $aData[$row['hour']] = (int) $row['hashrate']; + return $this->memcache->setCache(__FUNCTION__, $aData); } // Catchall $this->debug->append("Failed to fetch hourly hashrate: " . $this->mysqli->error); diff --git a/public/include/classes/statscache.class.php b/public/include/classes/statscache.class.php index e84386da..ff2f7da9 100644 --- a/public/include/classes/statscache.class.php +++ b/public/include/classes/statscache.class.php @@ -9,12 +9,24 @@ if (!defined('SECURITY')) * Can be enabled or disabled through site configuration * Also sets a default time if no time is passed to it to enforce caching **/ -class StatsCache extends Memcached { +class StatsCache { + private $cache, $round; + public function __construct($config, $debug) { $this->config = $config; $this->debug = $debug; - if (! $config['memcache']['enabled'] ) $this->debug->append("Not storing any values in memcache"); - return parent::__construct(); + if (! $config['memcache']['enabled'] ) { + $this->debug->append("Not storing any values in memcache"); + } else { + $this->cache = new Memcached(); + } + } + + public function setRound($round_id) { + $this->round = $round_id; + } + public function getRound() { + return $this->round; } /** @@ -25,8 +37,8 @@ class StatsCache extends Memcached { if (! $this->config['memcache']['enabled']) return false; if (empty($expiration)) $expiration = $this->config['memcache']['expiration'] + rand( -$this->config['memcache']['splay'], $this->config['memcache']['splay']); - $this->debug->append("Storing " . $this->config['memcache']['keyprefix'] . "$key with expiration $expiration", 3); - return parent::set($this->config['memcache']['keyprefix'] . $key, $value, $expiration); + $this->debug->append("Storing " . $this->getRound() . '_' . $this->config['memcache']['keyprefix'] . "$key with expiration $expiration", 3); + return $this->cache->set($this->getRound() . '_' . $this->config['memcache']['keyprefix'] . $key, $value, $expiration); } /** @@ -35,8 +47,8 @@ class StatsCache extends Memcached { **/ public function get($key, $cache_cb = NULL, &$cas_token = NULL) { if (! $this->config['memcache']['enabled']) return false; - $this->debug->append("Trying to fetch key " . $this->config['memcache']['keyprefix'] . "$key from cache", 3); - if ($data = parent::get($this->config['memcache']['keyprefix'].$key)) { + $this->debug->append("Trying to fetch key " . $this->getRound() . '_' . $this->config['memcache']['keyprefix'] . "$key from cache", 3); + if ($data = $this->cache->get($this->getRound() . '_' . $this->config['memcache']['keyprefix'].$key)) { $this->debug->append("Found key in cache", 3); return $data; } else { @@ -56,7 +68,22 @@ class StatsCache extends Memcached { return $data; } + /** + * This method is invoked if the called method was not realised in this class + **/ + public function __call($name, $arguments) { + if (! $this->config['memcache']['enabled']) return false; + //Invoke method $name of $this->cache class with array of $arguments + return call_user_func_array(array($this->cache, $name), $arguments); + } } $memcache = new StatsCache($config, $debug); $memcache->addServer($config['memcache']['host'], $config['memcache']['port']); +// Now we can set our additional key prefix +if ($aTmpBlock = $block->getLast()) { + $iRoundId = $aTmpBlock['id']; +} else { + $iRoundId = 0; +} +$memcache->setRound($iRoundId); diff --git a/public/include/classes/token.class.php b/public/include/classes/token.class.php new file mode 100644 index 00000000..c65da13c --- /dev/null +++ b/public/include/classes/token.class.php @@ -0,0 +1,60 @@ +mysqli->prepare("SELECT * FROM $this->table WHERE token = ? LIMIT 1"); + if ($stmt && $stmt->bind_param('s', $strToken) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_assoc(); + return false; + } + + /** + * Insert a new token + * @param name string Name of the variable + * @param value string Variable value + * @return mixed Token string on success, false on failure + **/ + public function createToken($strType, $account_id=NULL) { + $strToken = hash('sha256', $account_id.$strType.microtime()); + if (!$iToken_id = $this->tokentype->getTypeId($strType)) { + $this->setErrorMessage('Invalid token type: ' . $strType); + return false; + } + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (token, type, account_id) + VALUES (?, ?, ?) + "); + if ($stmt && $stmt->bind_param('sii', $strToken, $iToken_id, $account_id) && $stmt->execute()) + return $strToken; + $this->setErrorMessage('Unable to create new token'); + $this->debug->append('Failed to create new token in database: ' . $this->mysqli->error); + return false; + } + + /** + * Delete a used token + * @param token string Token name + * @return bool + **/ + public function deleteToken($token) { + $stmt = $this->mysqli->prepare("DELETE FROM $this->table WHERE token = ? LIMIT 1"); + if ($stmt && $stmt->bind_param('s', $token) && $stmt->execute()) + return true; + return false; + } +} + +$oToken = new Token(); +$oToken->setDebug($debug); +$oToken->setMysql($mysqli); +$oToken->setTokenType($tokentype); diff --git a/public/include/classes/tokentype.class.php b/public/include/classes/tokentype.class.php new file mode 100644 index 00000000..d33356cb --- /dev/null +++ b/public/include/classes/tokentype.class.php @@ -0,0 +1,21 @@ +getSingle($strName, 'id', 'name', 's'); + } +} + +$tokentype = new Token_Type(); +$tokentype->setDebug($debug); +$tokentype->setMysql($mysqli); diff --git a/public/include/classes/tools.class.php b/public/include/classes/tools.class.php index e97b3746..dcfa1959 100644 --- a/public/include/classes/tools.class.php +++ b/public/include/classes/tools.class.php @@ -9,11 +9,7 @@ if (!defined('SECURITY')) * Implements some common cron tasks outside * the scope of our web application **/ -class Tools { - public function __construct($debug) { - $this->debug = $debug; - } - +class Tools extends Base { /** * Fetch JSON data from an API * @param url string API URL @@ -21,13 +17,13 @@ class Tools { * @param auth array Optional authentication data to be sent with * @return dec array JSON decoded PHP array **/ - public function getApi($url, $target, $auth=NULL) { + private function getApi($url, $target, $auth=NULL) { static $ch = null; static $ch = null; if (is_null($ch)) { $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; BTCE PHP client; '.php_uname('s').'; PHP/'.phpversion().')'); + curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; PHP client; '.php_uname('s').'; PHP/'.phpversion().')'); } curl_setopt($ch, CURLOPT_URL, $url . $target); // curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); @@ -41,6 +37,54 @@ class Tools { if (!$dec) throw new Exception('Invalid data received, please make sure connection is working and requested API exists'); return $dec; } + + /** + * Detect the API to properly extract information + * @param url string API URL + * @return data string API type + **/ + private function getApiType($url) { + if (preg_match('/coinchoose.com/', $url)) { + return 'coinchose'; + } else if (preg_match('/btc-e.com/', $url)) { + return 'btce'; + } else if (preg_match('/cryptsy.com/', $url)) { + return 'cryptsy'; + } + $this->setErrorMessage("API URL unknown"); + return false; + } + + /** + * Extract price information from API data + **/ + public function getPrice() { + $aData = $this->getApi($this->config['price']['url'], $this->config['price']['target']); + $strCurrency = $this->config['currency']; + // Check the API type for configured URL + if (!$strApiType = $this->getApiType($this->config['price']['url'])) + return false; + // Extract price depending on API type + switch ($strApiType) { + case 'coinchose': + foreach ($aData as $aItem) { + if($strCurrency == $aItem[0]) + return $aItem['price']; + } + break; + case 'btce': + return $aData['ticker']['last']; + break; + case 'cryptsy': + return $aData['return']['markets'][$strCurrency]['lasttradeprice']; + break; + } + // Catchall, we have no data extractor for this API url + $this->setErrorMessage("Undefined API to getPrice() on URL " . $this->config['price']['url']); + return false; + } } -$tools = new Tools($debug); +$tools = new Tools(); +$tools->setDebug($debug); +$tools->setConfig($config); diff --git a/public/include/classes/transaction.class.php b/public/include/classes/transaction.class.php index 38e1b429..53d111e3 100644 --- a/public/include/classes/transaction.class.php +++ b/public/include/classes/transaction.class.php @@ -4,29 +4,13 @@ if (!defined('SECURITY')) die('Hacking attempt'); -class Transaction { - private $sError = ''; - private $table = 'transactions'; - private $tableBlocks = 'blocks'; - - public function __construct($debug, $mysqli, $config, $block) { - $this->debug = $debug; - $this->mysqli = $mysqli; - $this->config = $config; - $this->block = $block; - $this->debug->append("Instantiated Transaction class", 2); - } - - // get and set methods - private function setErrorMessage($msg) { - $this->sError = $msg; - } - public function getError() { - return $this->sError; - } +class Transaction extends Base { + private $sError = '', $table = 'transactions'; + public $num_rows = 0, $insert_id = 0; /** * Add a new transaction to our class table + * We also store the inserted ID in case the user needs it * @param account_id int Account ID to book transaction for * @param amount float Coin amount * @param type string Transaction type [Credit, Debit_AP, Debit_MP, Fee, Donation, Orphan_Credit, Orphan_Fee, Orphan_Donation] @@ -36,84 +20,199 @@ class Transaction { **/ public function addTransaction($account_id, $amount, $type='Credit', $block_id=NULL, $coin_address=NULL) { $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, amount, block_id, type, coin_address) VALUES (?, ?, ?, ?, ?)"); - if ($this->checkStmt($stmt)) { - $stmt->bind_param("idiss", $account_id, $amount, $block_id, $type, $coin_address); - if ($stmt->execute()) { - $this->setErrorMessage("Failed to store transaction"); - $stmt->close(); - return true; + if ($this->checkStmt($stmt) && $stmt->bind_param("idiss", $account_id, $amount, $block_id, $type, $coin_address) && $stmt->execute()) { + $this->insert_id = $stmt->insert_id; + return true; + } + $this->setErrorMessage("Failed to store transaction"); + return false; + } + + /* + * Mark transactions of a user as archived + * @param account_id int Account ID + * @param txid int Transaction ID to start from + * @param bool boolean True or False + **/ + public function setArchived($account_id, $txid) { + $stmt = $this->mysqli->prepare(" + UPDATE $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b + ON b.id = t.block_id + SET t.archived = 1 + WHERE ( t.account_id = ? AND t.id <= ? AND b.confirmations >= ? ) + OR ( t.account_id = ? AND t.id <= ? AND t.type IN ( 'Credit_PPS', 'Donation_PPS', 'Fee_PPS', 'TXFee', 'Debit_MP', 'Debit_AP' ) )"); + if ($this->checkStmt($stmt) && $stmt->bind_param('iiiii', $account_id, $txid, $this->config['confirmations'], $account_id, $txid) && $stmt->execute()) + return true; + return false; + } + + /** + * Fetch a transaction summary by type with total amounts + * @param account_id int Account ID, NULL for all + * @return data array type and total + **/ + public function getTransactionSummary($account_id=NULL) { + $sql = "SELECT SUM(t.amount) AS total, t.type AS type FROM $this->table AS t"; + if (!empty($account_id)) { + $sql .= " WHERE t.account_id = ? "; + $this->addParam('i', $account_id); + } + $sql .= " GROUP BY t.type"; + $stmt = $this->mysqli->prepare($sql); + if (!empty($account_id)) { + if (!($this->checkStmt($stmt) && call_user_func_array( array($stmt, 'bind_param'), $this->getParam()) && $stmt->execute())) + return false; + $result = $stmt->get_result(); + } else { + if (!($this->checkStmt($stmt) && $stmt->execute())) + return false; + $result = $stmt->get_result(); + } + if ($result) { + $aData = NULL; + while ($row = $result->fetch_assoc()) { + $aData[$row['type']] = $row['total']; } + return $aData; } return false; } - /** - * Sometimes transactions become orphans when a block associated to them is orphaned - * Updates the transaction types to Orphan_ - * @param block_id int Orphaned block ID - * @return bool - **/ - public function setOrphan($block_id) { - $this->debug->append("STA " . __METHOD__, 4); - $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; - } - /** * Get all transactions from start for account_id - * @param account_id int Account ID * @param start int Starting point, id of transaction + * @param filter array Filter to limit transactions + * @param limit int Only display this many transactions + * @param account_id int Account ID * @return data array Database fields as defined in SELECT **/ - public function getTransactions($account_id, $start=0) { + public function getTransactions($start=0, $filter=NULL, $limit=30, $account_id=NULL) { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare(" + $sql = " SELECT + SQL_CALC_FOUND_ROWS t.id AS id, + a.username as username, t.type AS type, t.amount AS amount, t.coin_address AS coin_address, t.timestamp AS timestamp, b.height AS height, + b.blockhash AS blockhash, b.confirmations AS confirmations - FROM transactions AS t - LEFT JOIN blocks AS b ON t.block_id = b.id - WHERE t.account_id = ? - ORDER BY id DESC"); - if ($this->checkStmt($stmt)) { - if(!$stmt->bind_param('i', $account_id)) return false; - $stmt->execute(); - $result = $stmt->get_result(); + FROM $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b ON t.block_id = b.id + LEFT JOIN " . $this->user->getTableName() . " AS a ON t.account_id = a.id"; + if (!empty($account_id)) { + $sql .= " WHERE ( t.account_id = ? ) "; + $this->addParam('i', $account_id); + } + if (is_array($filter)) { + $aFilter = array(); + foreach ($filter as $key => $value) { + if (!empty($value)) { + switch ($key) { + case 'type': + $aFilter[] = "( t.type = ? )"; + $this->addParam('s', $value); + break; + case 'status': + switch ($value) { + case 'Confirmed': + if (empty($filter['type']) || ($filter['type'] != 'Debit_AP' && $filter['type'] != 'Debit_MP' && $filter['type'] != 'TXFee' && $filter['type'] != 'Credit_PPS' && $filter['type'] != 'Fee_PPS' && $filter['type'] != 'Donation_PPS')) { + $aFilter[] = "( b.confirmations >= " . $this->config['confirmations'] . " OR ISNULL(b.confirmations) )"; + } + break; + case 'Unconfirmed': + $aFilter[] = "( b.confirmations < " . $this->config['confirmations'] . " AND b.confirmations >= 0 )"; + break; + case 'Orphan': + $aFilter[] = "( b.confirmations = -1 )"; + break; + } + break; + case 'account': + $aFilter[] = "( LOWER(a.username) = LOWER(?) )"; + $this->addParam('s', $value); + break; + case 'address': + $aFilter[] = "( t.coin_address = ? )"; + $this->addParam('s', $value); + break; + } + } + } + if (!empty($aFilter)) { + empty($account_id) ? $sql .= " WHERE " : $sql .= " AND "; + $sql .= implode(' AND ', $aFilter); + } + } + $sql .= " + ORDER BY id DESC + LIMIT ?,? + "; + // Add some other params to query + $this->addParam('i', $start); + $this->addParam('i', $limit); + $stmt = $this->mysqli->prepare($sql); + if ($this->checkStmt($stmt) && call_user_func_array( array($stmt, 'bind_param'), $this->getParam()) && $stmt->execute() && $result = $stmt->get_result()) { + // Fetch matching row count + $num_rows = $this->mysqli->prepare("SELECT FOUND_ROWS() AS num_rows"); + if ($num_rows->execute() && $row_count = $num_rows->get_result()->fetch_object()->num_rows) + $this->num_rows = $row_count; return $result->fetch_all(MYSQLI_ASSOC); } - $this->debug->append('Unable to fetch transactions'); + $this->debug->append('Failed to fetch transactions: ' . $this->mysqli->error); return false; } - private function checkStmt($bState) { - if ($bState ===! true) { - $this->debug->append("Failed to prepare statement: " . $this->mysqli->error); - $this->setErrorMessage('Internal application Error'); - return false; + /** + * Get all different transaction types + * @return mixed array/bool Return types on succes, false on failure + **/ + public function getTypes() { + $stmt = $this->mysqli->prepare("SELECT DISTINCT type FROM $this->table"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) { + $aData = array('' => ''); + while ($row = $result->fetch_assoc()) { + $aData[$row['type']] = $row['type']; + } + return $aData; } - return true; + $this->debug->append('Failed to fetch transaction types: ' . $this->mysqli->error); + return false; + } + + /** + * Get all donation transactions + * Used on donors page + * return data array Donors and amounts + **/ + public function getDonations() { + $this->debug->append("STA " . __METHOD__, 4); + $stmt = $this->mysqli->prepare(" + SELECT + SUM(t.amount) AS donation, + a.username AS username, + a.is_anonymous AS is_anonymous, + a.donate_percent AS donate_percent + FROM $this->table AS t + LEFT JOIN " . $this->user->getTableName() . " AS a + ON t.account_id = a.id + LEFT JOIN blocks AS b + ON t.block_id = b.id + WHERE + ( + ( t.type = 'Donation' AND b.confirmations >= " . $this->config['confirmations'] . " ) OR + t.type = 'Donation_PPS' + ) + GROUP BY a.username + "); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) + return $result->fetch_all(MYSQLI_ASSOC); + $this->debug->append("Failed to fetch website donors: " . $this->mysqli->error); + return false; } /** @@ -125,31 +224,16 @@ class Transaction { public function getLockedBalance() { $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 - 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 IN ('Credit','Bonus') AND b.confirmations >= ? ) OR - ( t.type = 'Credit_PPS' ) - ) - ) AS t1, - ( - SELECT sum(t.amount) AS debit - FROM $this->table AS t - WHERE t.type IN ('Debit_MP', 'Debit_AP') - ) AS t2, - ( - 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 >= ? ) OR - t.type IN ('Donation_PPS','Fee_PPS','TXFee') - ) - ) AS t3"); + SELECT + ROUND(( + SUM( IF( ( t.type IN ('Credit','Bonus') AND b.confirmations >= ? ) OR t.type = 'Credit_PPS', t.amount, 0 ) ) - + SUM( IF( t.type IN ('Debit_MP', 'Debit_AP'), t.amount, 0 ) ) - + SUM( IF( ( t.type IN ('Donation','Fee') AND b.confirmations >= ? ) OR ( t.type IN ('Donation_PPS', 'Fee_PPS', 'TXFee') ), t.amount, 0 ) ) + ), 8) AS balance + FROM $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b + ON t.block_id = b.id + WHERE archived = 0"); if ($this->checkStmt($stmt) && $stmt->bind_param('ii', $this->config['confirmations'], $this->config['confirmations']) && $stmt->execute() && $stmt->bind_result($dBalance) && $stmt->fetch()) return $dBalance; // Catchall @@ -159,7 +243,7 @@ class Transaction { } /** - * Get an accounts total balance + * Get an accounts total balance, ignore archived entries * @param account_id int Account ID * @return data float Credit - Debit - Fees - Donation **/ @@ -167,68 +251,35 @@ class Transaction { $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 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 IN ('Credit','Bonus') AND b.confirmations >= ? ) OR - ( t.type = 'Credit_PPS' ) - ) - AND t.account_id = ? - ) AS t1, - ( - SELECT sum(t.amount) AS debit - FROM $this->table AS t - WHERE t.type IN ('Debit_MP', 'Debit_AP') - AND t.account_id = ? - ) AS t2, - ( - 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 >= ? ) OR - ( t.type IN ('Donation_PPS', 'Fee_PPS', 'TXFee') ) - ) - AND t.account_id = ? - ) 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 + IFNULL(ROUND(( + SUM( IF( ( t.type IN ('Credit','Bonus') AND b.confirmations >= ? ) OR t.type = 'Credit_PPS', t.amount, 0 ) ) - + SUM( IF( t.type IN ('Debit_MP', 'Debit_AP'), t.amount, 0 ) ) - + SUM( IF( ( t.type IN ('Donation','Fee') AND b.confirmations >= ? ) OR ( t.type IN ('Donation_PPS', 'Fee_PPS', 'TXFee') ), t.amount, 0 ) ) + ), 8), 0) AS confirmed, + IFNULL(ROUND(( + SUM( IF( t.type IN ('Credit','Bonus') AND b.confirmations < ? AND b.confirmations >= 0, t.amount, 0 ) ) - + SUM( IF( t.type IN ('Donation','Fee') AND b.confirmations < ? AND b.confirmations >= 0, t.amount, 0 ) ) + ), 8), 0) AS unconfirmed, + IFNULL(ROUND(( + SUM( IF( t.type IN ('Credit','Bonus') AND b.confirmations = -1, t.amount, 0) ) - + SUM( IF( t.type IN ('Donation','Fee') AND b.confirmations = -1, t.amount, 0) ) + ), 8), 0) AS orphaned + FROM $this->table AS t + LEFT JOIN " . $this->block->getTableName() . " AS b + ON t.block_id = b.id + WHERE t.account_id = ? + AND archived = 0 "); - if ($this->checkStmt($stmt)) { - $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(); + if ($this->checkStmt($stmt) && $stmt->bind_param("iiiii", $this->config['confirmations'], $this->config['confirmations'], $this->config['confirmations'], $this->config['confirmations'], $account_id) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_assoc(); - } + $this->debug->append('Failed to fetch users balance: ' . $this->mysqli->error); return false; } } -$transaction = new Transaction($debug, $mysqli, $config, $block); +$transaction = new Transaction(); +$transaction->setDebug($debug); +$transaction->setMysql($mysqli); +$transaction->setConfig($config); +$transaction->setBlock($block); +$transaction->setUser($user); diff --git a/public/include/classes/user.class.php b/public/include/classes/user.class.php index 49de7b5f..e01b24af 100644 --- a/public/include/classes/user.class.php +++ b/public/include/classes/user.class.php @@ -9,7 +9,6 @@ class User { private $userID = false; private $table = 'accounts'; private $user = array(); - private $tableAccountBalance = 'accountBalance'; public function __construct($debug, $mysqli, $salt, $config) { $this->debug = $debug; @@ -20,38 +19,59 @@ class User { } // get and set methods + public function setMail($mail) { + $this->mail = $mail; + } + public function setToken($token) { + $this->token = $token; + } + public function setBitcoin($bitcoin) { + $this->bitcoin = $bitcoin; + } + public function setSetting($setting) { + $this->setting = $setting; + } private function setErrorMessage($msg) { $this->sError = $msg; } public function getError() { return $this->sError; } + private function getHash($string) { + return hash('sha256', $string.$this->salt); + } public function getUserName($id) { return $this->getSingle($id, 'username', 'id'); } + public function getUserNameByEmail($email) { + return $this->getSingle($email, 'username', 'email', 's'); + } public function getUserId($username) { return $this->getSingle($username, 'id', 'username', 's'); } public function getUserEmail($username) { return $this->getSingle($username, 'email', 'username', 's'); } + public function getUserNoFee($id) { + return $this->getSingle($id, 'no_fees', 'id'); + } 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 getEmail($email) { + return $this->getSingle($email, 'email', 'email', 's'); + } public function getUserFailed($id) { return $this->getSingle($id, 'failed_logins', 'id'); } - public function getIdFromToken($token) { - return $this->getSingle($token, 'id', 'token', 's'); + public function isNoFee($id) { + return $this->getUserNoFee($id); } public function isLocked($id) { return $this->getUserLocked($id); @@ -59,6 +79,10 @@ class User { public function isAdmin($id) { return $this->getUserAdmin($id); } + public function changeNoFee($id) { + $field = array('name' => 'no_fees', 'type' => 'i', 'value' => !$this->isNoFee($id)); + return $this->updateSingle($id, $field); + } public function changeLocked($id) { $field = array('name' => 'is_locked', 'type' => 'i', 'value' => !$this->isLocked($id)); return $this->updateSingle($id, $field); @@ -67,11 +91,7 @@ class User { $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) { + public function setUserFailed($id, $value) { $field = array( 'name' => 'failed_logins', 'type' => 'i', 'value' => $value); return $this->updateSingle($id, $field); } @@ -105,15 +125,25 @@ 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 (empty($username) || empty($password)) { + $this->setErrorMessage("Invalid username or password."); + return false; + } + if (filter_var($username, FILTER_VALIDATE_EMAIL)) { + $this->debug->append("Username is an e-mail", 2); + if (!$username = $this->getUserNameByEmail($username)) { + $this->setErrorMessage("Invalid username or password."); + return false; + } + } if ($this->isLocked($this->getUserId($username))) { $this->setErrorMessage("Account is locked. Please contact site support."); return false; } - if ( $this->checkUserPassword($username, $password)) { + if ($this->checkUserPassword($username, $password)) { $this->createSession($username); - $this->setUserFailed($this->getUserId($username), 0); - $this->setUserIp($this->getUserId($username), $_SERVER['REMOTE_ADDR']); - return true; + if ($this->setUserIp($this->getUserId($username), $_SERVER['REMOTE_ADDR'])) + return true; } $this->setErrorMessage("Invalid username or password"); if ($id = $this->getUserId($username)) @@ -132,7 +162,7 @@ class User { $this->debug->append("STA " . __METHOD__, 4); $this->debug->append("Confirming PIN for $userId and pin $pin", 2); $stmt = $this->mysqli->prepare("SELECT pin FROM $this->table WHERE id=? AND pin=? LIMIT 1"); - $pin_hash = hash('sha256', $pin.$this->salt); + $pin_hash = $this->getHash($pin); $stmt->bind_param('is', $userId, $pin_hash); $stmt->execute(); $stmt->bind_result($row_pin); @@ -181,7 +211,6 @@ class User { return $result->fetch_all(MYSQLI_ASSOC); } $this->debug->append("Unable to fetch users with AP set"); - echo $this->mysqli->error; return false; } @@ -251,8 +280,8 @@ class User { $this->setErrorMessage( 'New password is too short, please use more than 8 chars' ); return false; } - $current = hash('sha256', $current.$this->salt); - $new = hash('sha256', $new1.$this->salt); + $current = $this->getHash($current); + $new = $this->getHash($new1); $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ? WHERE ( id = ? AND pass = ? )"); if ($this->checkStmt($stmt)) { $stmt->bind_param('sis', $new, $userID, $current); @@ -274,19 +303,25 @@ class User { * @param donat float donation % of income * @return bool **/ - public function updateAccount($userID, $address, $threshold, $donate, $email) { + public function updateAccount($userID, $address, $threshold, $donate, $email, $is_anonymous) { $this->debug->append("STA " . __METHOD__, 4); $bUser = false; // number validation checks - if ($threshold < $this->config['ap_threshold']['min'] && $threshold != 0) { + if (!is_numeric($threshold)) { + $this->setErrorMessage('Invalid input for auto-payout'); + return false; + } else 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) { + if (!is_numeric($donate)) { + $this->setErrorMessage('Invalid input for donation'); + return false; + } else if ($donate < 0) { $this->setErrorMessage('Donation below allowed 0% limit'); return false; } else if ($donate > 100) { @@ -297,13 +332,29 @@ class User { $this->setErrorMessage('Invalid email address'); return false; } +/* if ($this->bitcoin->can_connect() === true && !empty($address)) { + try { + $aStatus = $this->bitcoin->validateaddress($address); + if (!$aStatus['isvalid']) { + $this->setErrorMessage('Invalid coin address'); + return false; + } + } catch (BitcoinClientException $e) { + $this->setErrorMessage('Unable to verify coin address'); + return false; + } + } else { + $this->setErrorMessage('Unable to connect to RPC server for coin address validation'); + 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))); // 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()) + $stmt = $this->mysqli->prepare("UPDATE $this->table SET coin_address = ?, ap_threshold = ?, donate_percent = ?, email = ?, is_anonymous = ? WHERE id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('sddsii', $address, $threshold, $donate, $email, $is_anonymous, $userID) && $stmt->execute()) return true; // Catchall $this->setErrorMessage('Failed to update your account'); @@ -336,9 +387,10 @@ class User { private function checkUserPassword($username, $password) { $this->debug->append("STA " . __METHOD__, 4); $user = array(); + $password_hash = $this->getHash($password); $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->bind_param('ss', $username, $password_hash); $stmt->execute(); $stmt->bind_result($row_username, $row_id, $row_admin); $stmt->fetch(); @@ -369,12 +421,24 @@ class User { * @param none * @return true **/ - public function logoutUser() { + public function logoutUser($from="") { $this->debug->append("STA " . __METHOD__, 4); + // Unset all of the session variables + $_SESSION = array(); + // As we're killing the sesison, also kill the cookie! + if (ini_get("session.use_cookies")) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"]); + } + // Destroy the session. session_destroy(); + // Enforce generation of a new Session ID and delete the old session_regenerate_id(true); - // Enforce a page reload - header("Location: index.php"); + // Enforce a page reload and point towards login with referrer included, if supplied + $location = @$_SERVER['HTTPS'] ? 'https' . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF'] : 'http' . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['PHP_SELF']; + if (!empty($from)) $location .= '?page=login&to=' . urlencode($from); + // if (!headers_sent()) header('Location: ' . $location); + exit(''); } /** @@ -396,7 +460,7 @@ class User { $this->debug->append("Fetching user information for user id: $userID"); $stmt = $this->mysqli->prepare(" SELECT - id, username, pin, api_key, is_admin, email, + id, username, pin, api_key, is_admin, is_anonymous, email, no_fees, IFNULL(donate_percent, '0') as donate_percent, coin_address, ap_threshold FROM $this->table WHERE id = ? LIMIT 0,1"); @@ -424,8 +488,20 @@ class User { * @param email2 string Email confirmation * @return bool **/ - public function register($username, $password1, $password2, $pin, $email1='', $email2='') { + public function register($username, $password1, $password2, $pin, $email1='', $email2='', $strToken='') { $this->debug->append("STA " . __METHOD__, 4); + if (strlen($username > 40)) { + $this->setErrorMessage('Username exceeding character limit'); + return false; + } + if (preg_match('/[^a-z_\-0-9]/i', $username)) { + $this->setErrorMessage('Username may only contain alphanumeric characters'); + return false; + } + if ($this->getEmail($email1)) { + $this->setErrorMessage( 'This e-mail address is already taken' ); + return false; + } if (strlen($password1) < 8) { $this->setErrorMessage( 'Password is too short, minimum of 8 characters required' ); return false; @@ -446,28 +522,70 @@ class User { $this->setErrorMessage( 'Invalid PIN' ); return false; } - $apikey = hash("sha256",$username.$salt); - if ($this->mysqli->query("SELECT id FROM $this->table LIMIT 1")->num_rows > 0) { - $stmt = $this->mysqli->prepare(" - INSERT INTO $this->table (username, pass, email, pin, api_key) - VALUES (?, ?, ?, ?, ?) - "); - } else { - $stmt = $this->mysqli->prepare(" - INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin) - VALUES (?, ?, ?, ?, ?, 1) - "); - } - if ($this->checkStmt($stmt)) { - $stmt->bind_param('sssss', $username, hash("sha256", $password1.$this->salt), $email1, hash("sha256", $pin.$this->salt), $apikey); - if (!$stmt->execute()) { - $this->setErrorMessage( 'Unable to register' ); - if ($stmt->sqlstate == '23000') $this->setErrorMessage( 'Username already exists' ); - echo $this->mysqli->error; + if (isset($strToken) && !empty($strToken)) { + $aToken = $this->token->getToken($strToken); + // Circle dependency, so we create our own object here + $invitation = new Invitation(); + $invitation->setMysql($this->mysqli); + $invitation->setDebug($this->debug); + $invitation->setUser($this); + $invitation->setConfig($this->config); + if (!$invitation->setActivated($aToken['id'])) { + $this->setErrorMessage('Unable to activate your invitation'); return false; } - $stmt->close(); - return true; + if (!$this->token->deleteToken($strToken)) { + $this->setErrorMessage('Unable to remove used token'); + return false; + } + } + if ($this->mysqli->query("SELECT id FROM $this->table LIMIT 1")->num_rows > 0) { + ! $this->setting->getValue('accounts_confirm_email_disabled') ? $is_locked = 1 : $is_locked = 0; + $is_admin = 0; + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (username, pass, email, pin, api_key, is_locked) + VALUES (?, ?, ?, ?, ?, ?) + "); + } else { + $is_locked = 0; + $is_admin = 1; + $stmt = $this->mysqli->prepare(" + INSERT INTO $this->table (username, pass, email, pin, api_key, is_admin, is_locked) + VALUES (?, ?, ?, ?, ?, 1, ?) + "); + } + + // Create hashed strings using original string and salt + $password_hash = $this->getHash($password1); + $pin_hash = $this->getHash($pin); + $apikey_hash = $this->getHash($username); + $username_clean = strip_tags($username); + + if ($this->checkStmt($stmt) && $stmt->bind_param('sssssi', $username_clean, $password_hash, $email1, $pin_hash, $apikey_hash, $is_locked) && $stmt->execute()) { + if (! $this->setting->getValue('accounts_confirm_email_enabled') && $is_admin != 1) { + if ($token = $this->token->createToken('confirm_email', $stmt->insert_id)) { + $aData['username'] = $username_clean; + $aData['token'] = $token; + $aData['email'] = $email1; + $aData['subject'] = 'E-Mail verification'; + if (!$this->mail->sendMail('register/confirm_email', $aData)) { + $this->setErrorMessage('Unable to request email confirmation: ' . $this->mail->getError()); + return false; + } + return true; + } else { + $this->setErrorMessage('Failed to create confirmation token'); + $this->debug->append('Unable to create confirm_email token: ' . $this->token->getError()); + return false; + } + } else { + return true; + } + } else { + $this->setErrorMessage( 'Unable to register' ); + $this->debug->append('Failed to insert user into DB: ' . $this->mysqli->error); + if ($stmt->sqlstate == '23000') $this->setErrorMessage( 'Username or email already registered' ); + return false; } return false; } @@ -479,9 +597,9 @@ class User { * @param new2 string New password verification * @return bool **/ - public function useToken($token, $new1, $new2) { + public function resetPassword($token, $new1, $new2) { $this->debug->append("STA " . __METHOD__, 4); - if ($id = $this->getIdFromToken($token)) { + if ($aToken = $this->token->getToken($token)) { if ($new1 !== $new2) { $this->setErrorMessage( 'New passwords do not match' ); return false; @@ -490,51 +608,47 @@ class User { $this->setErrorMessage( 'New password is too short, please use more than 8 chars' ); return false; } - $new = hash('sha256', $new1.$this->salt); - $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ?, token = NULL WHERE id = ? AND token = ?"); - if ($this->checkStmt($stmt) && $stmt->bind_param('sis', $new, $id, $token) && $stmt->execute() && $stmt->affected_rows === 1) { - return true; + $new_hash = $this->getHash($new1); + $stmt = $this->mysqli->prepare("UPDATE $this->table SET pass = ? WHERE id = ?"); + if ($this->checkStmt($stmt) && $stmt->bind_param('si', $new_hash, $aToken['account_id']) && $stmt->execute() && $stmt->affected_rows === 1) { + if ($this->token->deleteToken($aToken['token'])) { + return true; + } else { + $this->setErrorMessage('Unable to invalidate used token'); + } + } else { + $this->setErrorMessage('Unable to set new password'); } } else { - $this->setErrorMessage("Unable find user for your token"); - return false; + $this->setErrorMessage('Invalid token'); } + $this->debug->append('Failed to update password:' . $this->mysqli->error); return false; } /** * Reset a password by sending a password reset mail * @param username string Username to reset password for - * @param smarty object Smarty object for mail templating * @return bool **/ - public function resetPassword($username, $smarty) { + public function initResetPassword($username) { $this->debug->append("STA " . __METHOD__, 4); // Fetch the users mail address - if (!$email = $this->getUserEmail($username)) { + if (empty($username)) { + $this->serErrorMessage("Username must not be empty"); + return false; + } + if (!$aData['email'] = $this->getUserEmail($username)) { $this->setErrorMessage("Unable to find a mail address for user $username"); return false; } - if (!$this->setUserToken($this->getUserId($username))) { - $this->setErrorMessage("Unable to setup token for password reset"); + if (!$aData['token'] = $this->token->createToken('password_reset', $this->getUserId($username))) { + $this->setErrorMessage('Unable to setup token for password reset'); return false; } - // Send password reset link - if (!$token = $this->getUserToken($this->getUserId($username))) { - $this->setErrorMessage("Unable fetch token for password reset"); - return false; - } - $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"; - $headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n"; - if (mail($email, - $smarty->fetch('templates/mail/subject.tpl'), - $smarty->fetch('templates/mail/body.tpl'), - $headers)) { + $aData['username'] = $username; + $aData['subject'] = 'Password Reset Request'; + if ($this->mail->sendMail('password/reset', $aData)) { return true; } else { $this->setErrorMessage("Unable to send mail to your address"); @@ -550,15 +664,21 @@ class User { * @param none * @return bool **/ - public function isAuthenticated() { + public function isAuthenticated($logout=true) { $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; + if (@$_SESSION['AUTHENTICATED'] == true && + !$this->isLocked($_SESSION['USERDATA']['id']) && + $this->getUserIp($_SESSION['USERDATA']['id']) == $_SERVER['REMOTE_ADDR'] + ) return true; // Catchall - $this->logoutUser(); + if ($logout == true) $this->logoutUser($_SERVER['REQUEST_URI']); return false; } } // Make our class available automatically $user = new User($debug, $mysqli, SALT, $config); +$user->setMail($mail); +$user->setToken($oToken); +$user->setBitcoin($bitcoin); +$user->setSetting($setting); diff --git a/public/include/classes/worker.class.php b/public/include/classes/worker.class.php index 6a9f6524..122d287d 100644 --- a/public/include/classes/worker.class.php +++ b/public/include/classes/worker.class.php @@ -42,14 +42,22 @@ class Worker { **/ public function updateWorkers($account_id, $data) { $this->debug->append("STA " . __METHOD__, 4); + if (!is_array($data)) { + $this->setErrorMessage('No workers to update'); + return false; + } $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 = ?, 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()) ) + if ('' === $value['username'] || '' === $value['password']) { $iFailed++; + } else { + // Prefix the WebUser to Worker name + $value['username'] = "$username." . $value['username']; + $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++; + } } if ($iFailed == 0) return true; @@ -67,14 +75,15 @@ class Worker { $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"); - + FROM " . $this->table . " AS w + WHERE monitor = 1 + AND ( + SELECT IFNULL(SUM(IF(our_result = 'Y', 1, 0)), 0) FROM " . $this->share->getTableName() . " WHERE username = w.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; } @@ -87,15 +96,38 @@ class Worker { $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 + ( SELECT COUNT(id) FROM " . $this->share->getTableName() . " WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS count_all, + ( SELECT COUNT(id) FROM " . $this->share->getArchiveTableName() . " WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS count_all_archive, + ( + SELECT + IFNULL(IF(our_result='Y', ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536/600/1000), 0), 0) AS hashrate + FROM " . $this->share->getTableName() . " + WHERE + username = w.username + AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) + ( + SELECT + IFNULL(IF(our_result='Y', ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536/600/1000), 0), 0) AS hashrate + FROM " . $this->share->getArchiveTableName() . " + WHERE + username = w.username + AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) AS hashrate, + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) / count_all, 2), 0) + FROM " . $this->share->getTableName() . " + WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) / count_all_archive, 2), 0) + FROM " . $this->share->getArchiveTableName() . " + WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) AS difficulty + FROM $this->table AS w 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; } @@ -108,9 +140,33 @@ class Worker { $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 + ( SELECT COUNT(id) FROM " . $this->share->getTableName() . " WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS count_all, + ( SELECT COUNT(id) FROM " . $this->share->getArchiveTableName() . " WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE)) AS count_all_archive, + ( + SELECT + IFNULL(IF(our_result='Y', ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536/600/1000), 0), 0) AS hashrate + FROM " . $this->share->getTableName() . " + WHERE + username = w.username + AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) + ( + SELECT + IFNULL(IF(our_result='Y', ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) * 65536/600/1000), 0), 0) AS hashrate + FROM " . $this->share->getArchiveTableName() . " + WHERE + username = w.username + AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) AS hashrate, + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) / count_all, 2), 0) + FROM " . $this->share->getTableName() . " + WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) + ( + SELECT IFNULL(ROUND(SUM(IF(difficulty=0, pow(2, (" . $this->config['difficulty'] . " - 16)), difficulty)) / count_all_archive, 2), 0) + FROM " . $this->share->getArchiveTableName() . " + WHERE username = w.username AND time > DATE_SUB(now(), INTERVAL 10 MINUTE) + ) AS difficulty + FROM $this->table AS w WHERE account_id = ?"); if ($this->checkStmt($stmt) && $stmt->bind_param('i', $account_id) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_all(MYSQLI_ASSOC); @@ -127,15 +183,9 @@ class Worker { **/ public function getCountAllActiveWorkers() { $this->debug->append("STA " . __METHOD__, 4); - $stmt = $this->mysqli->prepare("SELECT COUNT(DISTINCT username) AS total FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE)"); - if ($this->checkStmt($stmt)) { - if (!$stmt->execute()) { - return false; - } - $result = $stmt->get_result(); - $stmt->close(); + $stmt = $this->mysqli->prepare("SELECT IFNULL(IF(our_result='Y', COUNT(DISTINCT username), 0), 0) AS total FROM " . $this->share->getTableName() . " WHERE time > DATE_SUB(now(), INTERVAL 10 MINUTE)"); + if ($this->checkStmt($stmt) && $stmt->execute() && $result = $stmt->get_result()) return $result->fetch_object()->total; - } return false; } @@ -150,6 +200,10 @@ class Worker { **/ public function addWorker($account_id, $workerName, $workerPassword) { $this->debug->append("STA " . __METHOD__, 4); + if ('' === $workerName || '' === $workerPassword) { + $this->setErrorMessage('Worker name and/or password may not be empty'); + return false; + } $username = $this->user->getUserName($account_id); $workerName = "$username.$workerName"; $stmt = $this->mysqli->prepare("INSERT INTO $this->table (account_id, username, password) VALUES(?, ?, ?)"); @@ -177,7 +231,6 @@ class Worker { if ($this->checkStmt($stmt)) { $stmt->bind_param('ii', $account_id, $id); if ($stmt->execute() && $stmt->affected_rows == 1) { - $stmt->close; return true; } else { $this->setErrorMessage( 'Unable to delete worker' ); diff --git a/public/include/config/admin_settings.inc.php b/public/include/config/admin_settings.inc.php new file mode 100644 index 00000000..e85b9480 --- /dev/null +++ b/public/include/config/admin_settings.inc.php @@ -0,0 +1,253 @@ + 'Maintenance Mode', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'maintenance', 'value' => $setting->getValue('maintenance'), + 'tooltip' => 'Enable or Disable maintenance mode. Only admins can still login.' +); +$aSettings['website'][] = array( + 'display' => 'Message of the Day', 'type' => 'text', + 'size' => 25, + 'default' => '', + 'name' => 'system_motd', 'value' => $setting->getValue('system_motd'), + 'tooltip' => 'Display a message of the day as information popup if set.' +); +$aSettings['website'][] = array( + 'display' => 'Website Name', 'type' => 'text', + 'size' => 25, + 'default' => 'The Pool', + 'name' => 'website_name', 'value' => $setting->getValue('website_name'), + 'tooltip' => 'The name of you pool page, displayed in the header of the page.' +); +$aSettings['website'][] = array( + 'display' => 'Website Title', 'type' => 'text', + 'size' => 25, + 'default' => 'The Pool - Mining Evolved', + 'name' => 'website_title', 'value' => $setting->getValue('website_title'), + 'tooltip' => 'The title of you pool page, displayed in the browser window header.' +); +$aSettings['website'][] = array( + 'display' => 'Website Slogan', 'type' => 'text', + 'size' => 25, + 'default' => 'Resistance is Futile', + 'name' => 'website_slogan', 'value' => $setting->getValue('website_slogan'), + 'tooltip' => 'The slogan of you pool page, displayed in the browser window header.' +); +$aSettings['website'][] = array( + 'display' => 'Website e-mail', 'type' => 'text', + 'size' => 25, + 'default' => 'test@example.com', + 'name' => 'website_email', 'value' => $setting->getValue('website_email'), + 'tooltip' => 'The email address for your pool, used in mail templates and notifications.' +); +$aSettings['website'][] = array( + 'display' => 'Website theme', 'type' => 'select', + 'options' => $aThemes, + 'default' => 'mpos', + 'name' => 'website_theme', 'value' => $setting->getValue('website_theme'), + 'tooltip' => 'The default theme used on your pool.' +); +$aSettings['website'][] = array( + 'display' => 'Website mobile theme', 'type' => 'select', + 'options' => $aThemes, + 'default' => 'mobile', + 'name' => 'website_mobile_theme', 'value' => $setting->getValue('website_mobile_theme'), + 'tooltip' => 'The mobile theme used for your pool.' +); +$aSettings['website'][] = array( + 'display' => 'Blockexplorer URL', 'type' => 'text', + 'size' => 50, + 'default' => 'http://explorer.litecoin.net/block/', + 'name' => 'website_blockexplorer_url', 'value' => $setting->getValue('website_blockexplorer_url'), + 'tooltip' => 'URL to the blockexplorer website for your blockchain. Will append the blockhash to the URL. Leave empty to disabled this.' +); +$aSettings['website'][] = array( + 'display' => 'Disable Blockexplorer', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'website_blockexplorer_disabled', 'value' => $setting->getValue('website_blockexplorer_disabled'), + 'tooltip' => 'Enabled or disable the blockexplorer URL feature. Will remove any links using the blockexplorer URL.' +); +$aSettings['website'][] = array( + 'display' => 'Chaininfo URL', 'type' => 'text', + 'size' => 50, + 'default' => 'http://allchains.info', + 'name' => 'website_chaininfo_url', 'value' => $setting->getValue('website_chaininfo_url'), + 'tooltip' => 'URL to the chaininfo website for your blockchain. Leave empty to disabled this.' +); +$aSettings['website'][] = array( + 'display' => 'Disable Chaininfo', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'website_chaininfo_disabled', 'value' => $setting->getValue('website_chaininfo_disabled'), + 'tooltip' => 'Enabled or disable the chainfo URL feature. Will remove any links using the chaininfo URL.' +); +$aSettings['wallet'][] = array( + 'display' => 'Cold Coins', 'type' => 'text', + 'size' => 6, + 'default' => 0, + 'name' => 'wallet_cold_coins', 'value' => $setting->getValue('wallet_cold_coins'), + 'tooltip' => 'Amount of coins held in a pools cold wallet.' +); +$aSettings['statistics'][] = array( + 'display' => 'Ajax Refresh Interval', 'type' => 'select', + 'options' => array('5' => '5', '10' => '10', '15' => '15', '30' => '30', '60' => '60' ), + 'default' => 10, + 'name' => 'statistics_ajax_refresh_interval', 'value' => $setting->getValue('statistics_ajax_refresh_interval'), + 'tooltip' => 'How often to refresh data via ajax in seconds.' +); +$aSettings['statistics'][] = array( + 'display' => 'Ajax Data Interval', 'type' => 'select', + 'options' => array('60' => '1', '300' => '5', '600' => '10'), + 'default' => 300, + 'name' => 'statistics_ajax_data_interval', 'value' => $setting->getValue('statistics_ajax_data_interval'), + 'tooltip' => 'Time in minutes, interval for hashrate and sharerate calculations. Higher intervals allow for better accuracy at a higer server load.' +); +$aSettings['statistics'][] = array( + 'display' => 'Block Statistics Count', 'type' => 'text', + 'size' => 25, + 'default' => 20, + 'name' => 'statistics_block_count', 'value' => $setting->getValue('statistics_block_count'), + 'tooltip' => 'Blocks to fetch for the block statistics page.' +); +$aSettings['statistics'][] = array( + 'display' => 'Pool Hashrate Modifier', 'type' => 'select', + 'options' => array( '1' => 'KH/s', '0.001' => 'MH/s', '0.000001' => 'GH/s' ), + 'default' => '1', + 'name' => 'statistics_pool_hashrate_modifier', 'value' => $setting->getValue('statistics_pool_hashrate_modifier'), + 'tooltip' => 'Auto-adjust displayed pool hashrates to a certain limit.' +); +$aSettings['statistics'][] = array( + 'display' => 'Network Hashrate Modifier', 'type' => 'select', + 'options' => array( '1' => 'KH/s', '0.001' => 'MH/s', '0.000001' => 'GH/s' ), + 'default' => '1', + 'name' => 'statistics_network_hashrate_modifier', 'value' => $setting->getValue('statistics_network_hashrate_modifier'), + 'tooltip' => 'Auto-adjust displayed network hashrates to a certain limit.' +); +$aSettings['statistics'][] = array( + 'display' => 'Personal Hashrate Modifier', 'type' => 'select', + 'options' => array( '1' => 'KH/s', '0.001' => 'MH/s', '0.000001' => 'GH/s' ), + 'default' => '1', + 'name' => 'statistics_personal_hashrate_modifier', 'value' => $setting->getValue('statistics_personal_hashrate_modifier'), + 'tooltip' => 'Auto-adjust displayed personal hashrates to a certain limit.' +); +$aSettings['acl'][] = array( + 'display' => 'Pool Statistics', 'type' => 'select', + 'options' => array( 0 => 'Private', 1 => 'Public'), + 'default' => 1, + 'name' => 'acl_pool_statistics', 'value' => $setting->getValue('acl_pool_statistics'), + 'tooltip' => 'Make the pool statistics page private (users only) or public.' +); +$aSettings['acl'][] = array( + 'display' => 'Block Statistics', 'type' => 'select', + 'options' => array( 0 => 'Private', 1 => 'Public'), + 'default' => 1, + 'name' => 'acl_block_statistics', 'value' => $setting->getValue('acl_block_statistics'), + 'tooltip' => 'Make the block statistics page private (users only) or public.' +); +$aSettings['acl'][] = array( + 'display' => 'Round Statistics', 'type' => 'select', + 'options' => array( 0 => 'Private', 1 => 'Public'), + 'default' => 1, + 'name' => 'acl_round_statistics', 'value' => $setting->getValue('acl_round_statistics'), + 'tooltip' => 'Make the round statistics page private (users only) or public.' +); +$aSettings['acl'][] = array( + 'display' => 'Round Transactions', 'type' => 'select', + 'options' => array( 0 => 'Admins', 1 => 'Public'), + 'default' => 0, + 'name' => 'acl_round_transactions', 'value' => $setting->getValue('acl_round_transactions'), + 'tooltip' => 'Display all transactions regardless of admin status.' +); +$aSettings['system'][] = array( + 'display' => 'Disable e-mail confirmations', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'accounts_confirm_email_disabled', 'value' => $setting->getValue('accounts_confirm_email_disabled'), + 'tooltip' => 'Should users supply a valid e-mail address upon registration. Requires them to confirm the address before accounts are activated.' +); +$aSettings['system'][] = array( + 'display' => 'Disable registrations', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'lock_registration', 'value' => $setting->getValue('lock_registration'), + 'tooltip' => 'Enable or Disable registrations. Useful to create an invitation only pool.' +); +$aSettings['system'][] = array( + 'display' => 'Disable Invitations', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'disable_invitations', 'value' => $setting->getValue('disable_invitations'), + 'tooltip' => 'Enable or Disable invitations. Users will not be able to invite new users via email if disabled.' +); +$aSettings['system'][] = array( + 'display' => 'Disable Manual Payouts', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'disable_mp', 'value' => $setting->getValue('disable_mp'), + 'tooltip' => 'Enable or Disable the manual payout processing. Users will not be able to withdraw any funds if disabled.' +); +$aSettings['system'][] = array( + 'display' => 'Disable Automatic Payouts', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'disable_ap', 'value' => $setting->getValue('disable_ap'), + 'tooltip' => 'Enable or Disable the automatic payout processing. Users exceeding their thresholds will not be paid out if disabled.' +); +$aSettings['system'][] = array( + 'display' => 'Disable notifications', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'disable_notifications', 'value' => $setting->getValue('disable_notifications'), + 'tooltip' => 'Enable or Disable system notifications. This includes new found blocks, monitoring and all other notifications.' +); +$aSettings['system'][] = array( + 'display' => 'Disable API', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'disable_api', 'value' => $setting->getValue('disable_api'), + 'tooltip' => 'Enable or Disable the pool wide API functions. See API reference on Github for details.' +); +$aSettings['system'][] = array( + 'display' => 'Disable Contactform', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'disable_contactform', 'value' => $setting->getValue('disable_contactform'), + 'tooltip' => 'Enable or Disable Contactform. Users will not be able to use the contact form.' +); +$aSettings['recaptcha'][] = array( + 'display' => 'Enable re-Captcha', 'type' => 'select', + 'options' => array( 0 => 'No', 1 => 'Yes' ), + 'default' => 0, + 'name' => 'recaptcha_enabled', 'value' => $setting->getValue('recaptcha_enabled'), + 'tooltip' => 'Enable or Disable re-Captcha. This will require user input on registraion and other forms.' +); +$aSettings['recaptcha'][] = array( + 'display' => 're-Captcha Private Key', 'type' => 'text', + 'size' => 25, + 'default' => 'YOUR_PRIVATE_KEY', + 'name' => 'recaptcha_private_key', 'value' => $setting->getValue('recaptcha_private_key'), + 'tooltip' => '.' +); +$aSettings['recaptcha'][] = array( + 'display' => 're-Captcha Public Key', 'type' => 'text', + 'size' => 25, + 'default' => 'YOUR_PUBLIC_KEY', + 'name' => 'recaptcha_public_key', 'value' => $setting->getValue('recaptcha_public_key'), + 'tooltip' => 'Your public key as given by your re-Captcha account.' +); + +?> diff --git a/public/include/config/global.inc.dist.php b/public/include/config/global.inc.dist.php index 29a46658..941f57b0 100644 --- a/public/include/config/global.inc.dist.php +++ b/public/include/config/global.inc.dist.php @@ -1,10 +1,6 @@ array( - 'url' => 'https://btc-e.com/api/2', - '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 - 'fees' => 0, - 'difficulty' => '20', // Target difficulty for this pool as set in pushpoold json - 'reward' => '50', // Reward for finding blocks, fixed value but changes someday - 'confirmations' => '120', // Confirmations per block needed to credit transactions - 'memcache' => array( - 'enabled' => true, - 'host' => 'localhost', // Memcache Host - 'port' => 11211, // Memcache Port - 'keyprefix' => 'mmcfe_ng_', // Prefix for all keys - 'expiration'=> '90', // Cache time - 'splay' => '15' // Splay time - ), - 'wallet' => array( - 'type' => 'http', // http or https are supported - 'host' => 'localhost:9332', - 'username' => 'litecoinrpc', - 'password' => 'somepass' - ), - 'cashout' => array( - 'min_balance' => 0.0 // Minimal balance to cash out - ), - 'cookie' => array( - 'path' => '/', - 'name' => 'POOLERCOOKIE', - 'domain' => '' - ), - 'cache' => 0, // 1 to enable smarty cache in templates/cache - 'db' => array( - 'host' => 'localhost', - 'user' => 'someuser', - 'pass' => 'somepass', - 'port' => '3306', - 'name' => 'litecoin', - ), -); +/** + * Database configuration + * + * A MySQL database backend is required for MPOS. + * Also ensure the database structure is imported! + * The SQL file should be included in this project under the `sql` directory + * + * Default: + * host = 'localhost' + * port = 3306 + * user = 'someuser' + * pass = 'somepass' + * name = 'mpos' + **/ +$config['db']['host'] = 'localhost'; +$config['db']['user'] = 'someuser'; +$config['db']['pass'] = 'somepass'; +$config['db']['port'] = 3306; +$config['db']['name'] = 'mpos'; + +/** + * Local wallet RPC configuration + * + * MPOS uses the RPC backend to fetch transactions, blocks + * and various other things. They need to match your coind RPC + * configuration. + * + * Default: + * type = 'http' + * host = 'localhost:19334' + * username = 'testnet' + * password = 'testnet' + **/ +$config['wallet']['type'] = 'http'; +$config['wallet']['host'] = 'localhost:19334'; +$config['wallet']['username'] = 'testnet'; +$config['wallet']['password'] = 'testnet'; + +/** + * API configuration to fetch prices for set currency + * + * Explanation: + * MPOS will try to fetch the current exchange rates + * from this API URL/target. Currently btc-e and coinchoose + * are supported in MPOS. If you want to remove the trade + * header just set currency to an empty string. + * + * Default (btc-e.com): + * url = `https://btc-e.com` + * target = `/api/2/ltc_usd/ticker` + * currency = `USD` + * + * Optional (coinchoose.com): + * url = `http://www.coinchoose.com` + * target = `/api.php` + * currency = `BTC` + * + * Optional (cryptsy.com): + * url = `http://pubapi.cryptsy.com` + * currency = `BTC` + * target = `/api.php?method=marketdata` + **/ +$config['price']['url'] = 'https://btc-e.com'; +$config['price']['target'] = '/api/2/ltc_usd/ticker'; +$config['price']['currency'] = 'USD'; + + +/** + * Automatic payout thresholds + * + * These values define the min and max settings + * that can be entered by a user. + * Defaults: + * `min` = `1` + * `max` = `250` + **/ +$config['ap_threshold']['min'] = 1; +$config['ap_threshold']['max'] = 250; + + +/** + * Account specific settings + * + * Explanation + * Invitations will allow your users to invite new members to join the pool. + * After sending a mail to the invited user, they can register using the token + * created. Invitations can be enabled and disabled through the admin panel. + * Sent invitations are listed on the account invitations page. + * + * You can limit the number of registrations send per account via configuration + * variable. + * + * Options: + * count : Maximum invitations a user is able to send + * + * Defaults: + * count : 5 + **/ +$config['accounts']['invitations']['count'] = 5; + +// Currency system used in this pool, default: `LTC` +$config['currency'] = 'LTC'; + +/** + * Default transaction fee to apply to user transactions + * + * Explanation + * The coin daemon applies transcation fees to young coins. + * Since we are unable to find out what the exact fee was we set + * a default value here which is applied to both manual and auto payouts. + * If this is not set, no fee is applied in the transactions history but + * the user might still see them when the coins arrive. + * + * Default: + * txfee = 0.1 + **/ +$config['txfee'] = 0.1; + +// Payout a block bonus to block finders, default: 0 (disabled) +// This bonus is paid by the pool operator, it is not deducted from the block payout! +$config['block_bonus'] = 0; + + +/** + * Payout sytem in use + * + * This will modify some templates and activate the + * appropriate crons. Only ONE payout system at a time + * is supported! + * + * Available options: + * prop: Proportional payout system + * pps : Pay Per Share payout system + * pplns : Pay Per Last N Shares payout system + * + * Default: + * prop +**/ +$config['payout_system'] = 'prop'; + +/** + * Archiving configuration for debugging + * + * Explanation: + * By default, we don't need to archive for a long time. PPLNS and Hashrate + * calculations rely on this archive, but all shares past a certain point can + * safely be deleted. + * + * To ensure we have enough shares on stack for PPLNS, this + * is set to the past 10 rounds. Even with lucky ones in between those should + * fit the PPLNS target. On top of that, even if we have more than 10 rounds, + * we still keep the last maxage shares to ensure we can calculate hashrates. + * Both conditions need to be met in order for shares to be purged from archive. + * + * Proportional mode will only keep the past 24 hours. These are required for + * hashrate calculations to work past a round, hence 24 hours was selected as + * the default. You may want to increase the time for debugging, then add any + * integer reflecting minutes of shares to keep. + * + * Availabe Options: + * maxrounds : PPLNS, keep shares for maxrounds + * maxage : PROP and PPLNS, delete shares older than maxage minutes + * + * Default: + * maxrounds = 10 + * maxage = 60 * 24 (24h) + **/ +$config['archive']['maxrounds'] = 10; +$config['archive']['maxage'] = 60 * 24; + +// Pool fees applied to users in percent, default: 0 (disabled) +$config['fees'] = 0; + +/** + * PPLNS requires some settings to run properly. First we need to define + * a default shares count that is applied if we don't have a proper type set. + * Different dynamic types can be applied, or you can run a fixed scheme. + * + * Explanation + * + * PPLNS can run on two different payouts: fixed and blockavg. Each one + * defines a different PPLNS target. + * + * Fixed means we will be looking at the shares setup in the default + * setting. There is no automatic adjustments to the PPLNS target, + * all users will be paid out proportionally to that target. + * + * Blockavg will look at the last blockcount blocks shares and take + * the average as the PPLNS target. This will be automatically adjusted + * when difficulty changes and more blocks are available. This keeps the + * target dynamic but still traceable. + * + * If you use the fixed type it will use $config['pplns']['shares']['default'] + * for target calculations, if you use blockavg type it will use + * $config['pplns']['blockavg']['blockcount'] blocks average for target + * calculations. + * + * default : Default target shares for PPLNS + * type : Payout type used in PPLNS + * blockcount : Amount of blocks to check for avg shares + * + * Available Options: + * default : amount of shares, integeger + * type : blockavg or fixed + * blockcount : amount of blocks, any integer + * + * Defaults: + * default = 4000000 + * type = `blockavg` + * blockcount = 10 + **/ +$config['pplns']['shares']['default'] = 4000000; +$config['pplns']['shares']['type'] = 'blockavg'; +$config['pplns']['blockavg']['blockcount'] = 10; + +// Pool target difficulty as set in pushpoold configuration file +// Please also read this for stratum: https://github.com/TheSerapher/php-mpos/wiki/FAQ +$config['difficulty'] = 20; + + +/** + * This defines how rewards are paid to users. + * + * Explanation: + * + * Proportional + PPLNS Payout System + * When running a pool on fixed mode, each block will be paid + * out as defined in `reward`. If you wish to pass transaction + * fees inside discovered blocks on to user, set this to `block`. + * This is really helpful for altcoins with dynamic block values! + * + * PPS Payout System + * If set to `fixed`, all PPS values are based on the `reward` setting. + * If you set it to `block` you will calculate the current round based + * on the previous block value. The idea is to pass the block of the + * last round on to the users. If no previous block is found, PPS value + * will fall back to the fixed value set in `reward`. Ensure you don't + * overpay users in the first round! + * + * Available options: + * reward_type: + * fixed : Fixed value according to `reward` setting + * block : Dynamic value based on block amount + * reward: + * float value : Any value of your choice but should reflect base block values + * + * Default: + * reward_type = `fixed` + * reward = 50 + * + **/ +$config['reward_type'] = 'fixed'; +$config['reward'] = 50; + +// Confirmations per block required to credit transactions, default: 120 +$config['confirmations'] = 120; +// Confirmations per block required in network to confirm its transactions, default: 120 +$config['network_confirmations'] = 120; + + /** + * Available pps options: + * reward_type: + * fixed : Fixed value according to `reward` setting + * blockavg : Dynamic value based on average of x number of block rewards + * block : Dynamic value based on LAST block amount + * reward: + * float value : Any value of your choice but should reflect base block values + * blockcount : amount of blocks to average, any integer + * Default: + * pps_reward_type = `fixed` default $config['pps']['reward']['default'] + * reward = 50 + * + **/ +$config['pps']['reward']['default'] = 50; +$config['pps']['reward']['type'] = 'blockavg'; +$config['pps']['blockavg']['blockcount'] = 10; + +// pps base payout target, default 16 = difficulty 1 shares for vardiff +// (1/(65536 * difficulty) * reward) = (reward / (pow(2,32) * difficulty) * pow(2, 16)) +$config['pps_target'] = 16; // do not change unless you know what it does + +/** + * Memcache configuration + * + * To disable memcache set option $config['memcache']['enabled'] = false + * After disable memcache installation of memcache is not required. + * + * Please note that a memcache is greatly increasing performance + * when combined with the `statistics.php` cronjob. Disabling this + * is not recommended in a live environment! + * + * Explanations + * enabled : Disable (false) memcache for debugging or enable (true) it + * host : Host IP or hostname + * port : memcache port + * keyprefix : Must be changed for multiple MPOS instances on one host + * expiration : Default expiration time in seconds of all cached keys. + * Increase if caches expire too fast. + * splay : Default randomizer for expiration times. + * This will spread expired keys across `splay` seconds. + * + * Default: + * enabled = `true` + * host = `localhost` + * port = 11211 + * keyprefix = `mpos_` + * expiration = 90 + * splay = 15 + **/ +$config['memcache']['enabled'] = true; +$config['memcache']['host'] = 'localhost'; +$config['memcache']['port'] = 11211; +$config['memcache']['keyprefix'] = 'mpos_'; +$config['memcache']['expiration'] = 90; +$config['memcache']['splay'] = 15; + + +/** + * Cookie configiration + * + * You can configure the cookie behaviour to secure your cookies more than the PHP defaults + * + * For multiple installations of MPOS on the same domain you must change the cookie path. + * + * Explanation: + * duration: + * the amount of time, in seconds, that a cookie should persist in the users browser. + * 0 = until closed; 1440 = 24 minutes. Check your php.ini 'session.gc_maxlifetime' value + * and ensure that it is at least the duration specified here. + * + * domain: + * the only domain name that may access this cookie in the browser + * + * path: + * the highest path on the domain that can access this cookie; i.e. if running two pools + * from a single domain you might set the path /ltc/ and /ftc/ to separate user session + * cookies between the two. + * + * httponly: + * marks the cookie as accessible only through the HTTP protocol. The cookie can't be + * accessed by scripting languages, such as JavaScript. This can help to reduce identity + * theft through XSS attacks in most browsers. + * + * secure: + * marks the cookie as accessible only through the HTTPS protocol. If you have a SSL + * certificate installed on your domain name then this will stop a user accidently + * accessing the site over a HTTP connection, without SSL, exposing their session cookie. + * + * Default: + * duration = '1440' + * domain = '' + * path = '/' + * httponly = true + * secure = false + **/ +$config['cookie']['duration'] = '1440'; +$config['cookie']['domain'] = ''; +$config['cookie']['path'] = '/'; +$config['cookie']['httponly'] = true; +$config['cookie']['secure'] = false; + +/** + * Enable or disable the Smarty cache + * + * Explanation: + * Smarty implements a file based cache for all HTML output generated + * from dynamic scripts. It can be enabled to cache the HTML data on disk, + * future request are served from those cache files. + * + * This may or may not work as expected, in general Memcache is used to cache + * all data so rendering the page should not take too long anyway. + * + * You can test this out and enable (1) this setting but it's not guaranteed to + * work with MPOS. + * + * Ensure that the folder `templates/cache` is writable by the webserver! + * + * cache = Enable/Disable the cache + * cache_lifetime = Time to keep files in seconds before updating them + * + * Options: + * cache: + * 0 = disabled + * 1 = enabled + * cache_lifetime: + * time in seconds + * + * Defaults: + * cache = 0, disabled + * cache_lifetime = 30 seconds + **/ +$config['smarty']['cache'] = 0; +$config['smarty']['cache_lifetime'] = 30; ?> diff --git a/public/include/config/memcache_keys.inc.php b/public/include/config/memcache_keys.inc.php new file mode 100644 index 00000000..61538ebd --- /dev/null +++ b/public/include/config/memcache_keys.inc.php @@ -0,0 +1,5 @@ + diff --git a/public/include/lib/KLogger.php b/public/include/lib/KLogger.php new file mode 100755 index 00000000..9f53788e --- /dev/null +++ b/public/include/lib/KLogger.php @@ -0,0 +1,148 @@ + + * Date : July 26, 2008 + * Comments : Originally written for use with wpSearch + * Website : http://codefury.net + * Version : 1.0 + * + * Usage: + * $log = new KLogger ( "log.txt" , KLogger::INFO ); + * $log->LogInfo("Returned a million search results"); //Prints to the log file + * $log->LogFATAL("Oh dear."); //Prints to the log file + * $log->LogDebug("x = 5"); //Prints nothing due to priority setting + */ + + class KLogger + { + + const DEBUG = 1; // Most Verbose + const INFO = 2; // ... + const WARN = 3; // ... + const ERROR = 4; // ... + const FATAL = 5; // Least Verbose + const OFF = 6; // Nothing at all. + + const LOG_OPEN = 1; + const OPEN_FAILED = 2; + const LOG_CLOSED = 3; + + /* Public members: Not so much of an example of encapsulation, but that's okay. */ + public $Log_Status = KLogger::LOG_CLOSED; + public $DateFormat = "Y-m-d G:i:s"; + public $MessageQueue; + + private $log_file; + private $priority = KLogger::INFO; + + private $file_handle; + + public function __construct( $filepath , $priority ) + { + if ( $priority == KLogger::OFF ) return; + + $this->log_file = $filepath; + $this->MessageQueue = array(); + $this->priority = $priority; + + if ( file_exists( $this->log_file ) ) + { + if ( !is_writable($this->log_file) ) + { + $this->Log_Status = KLogger::OPEN_FAILED; + $this->MessageQueue[] = "The file exists, but could not be opened for writing. Check that appropriate permissions have been set."; + return; + } + } + + if ( $this->file_handle = fopen( $this->log_file , "a" ) ) + { + $this->Log_Status = KLogger::LOG_OPEN; + $this->MessageQueue[] = "The log file was opened successfully."; + } + else + { + $this->Log_Status = KLogger::OPEN_FAILED; + $this->MessageQueue[] = "The file could not be opened. Check permissions."; + } + + return; + } + + public function __destruct() + { + if ( $this->file_handle ) + fclose( $this->file_handle ); + } + + public function LogInfo($line) + { + $this->Log( $line , KLogger::INFO ); + } + + public function LogDebug($line) + { + $this->Log( $line , KLogger::DEBUG ); + } + + public function LogWarn($line) + { + $this->Log( $line , KLogger::WARN ); + } + + public function LogError($line) + { + $this->Log( $line , KLogger::ERROR ); + } + + public function LogFatal($line) + { + $this->Log( $line , KLogger::FATAL ); + } + + public function Log($line, $priority) + { + if ( $this->priority <= $priority ) + { + $status = $this->getTimeLine( $priority ); + $this->WriteFreeFormLine ( "$status $line \n" ); + } + } + + public function WriteFreeFormLine( $line ) + { + if ( $this->Log_Status == KLogger::LOG_OPEN && $this->priority != KLogger::OFF ) + { + if (fwrite( $this->file_handle , $line ) === false) { + $this->MessageQueue[] = "The file could not be written to. Check that appropriate permissions have been set."; + } + } + } + + private function getTimeLine( $level ) + { + $time = date( $this->DateFormat ); + + switch( $level ) + { + case KLogger::INFO: + return "$time - INFO -->"; + case KLogger::WARN: + return "$time - WARN -->"; + case KLogger::DEBUG: + return "$time - DEBUG -->"; + case KLogger::ERROR: + return "$time - ERROR -->"; + case KLogger::FATAL: + return "$time - FATAL -->"; + default: + return "$time - LOG -->"; + } + } + + } + + +?> \ No newline at end of file diff --git a/public/include/lib/Michelf/Markdown.php b/public/include/lib/Michelf/Markdown.php new file mode 100644 index 00000000..ba45246c --- /dev/null +++ b/public/include/lib/Michelf/Markdown.php @@ -0,0 +1,3094 @@ + +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# +namespace Michelf; + + +# +# Markdown Parser Class +# + +class Markdown { + + ### Version ### + + const MARKDOWNLIB_VERSION = "1.3"; + + ### Simple Function Interface ### + + public static function defaultTransform($text) { + # + # Initialize the parser and return the result of its transform method. + # This will work fine for derived classes too. + # + # Take parser class on which this function was called. + $parser_class = \get_called_class(); + + # try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class]; + + # create the parser it not already set + if (!$parser) + $parser = new $parser_class; + + # Transform text using parser. + return $parser->transform($text); + } + + ### Configuration Variables ### + + # Change to ">" for HTML output. + public $empty_element_suffix = " />"; + public $tab_width = 4; + + # Change to `true` to disallow markup or entities. + public $no_markup = false; + public $no_entities = false; + + # Predefined urls and titles for reference links and images. + public $predef_urls = array(); + public $predef_titles = array(); + + + ### Parser Implementation ### + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + protected $nested_brackets_depth = 6; + protected $nested_brackets_re; + + protected $nested_url_parenthesis_depth = 4; + protected $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + protected $escape_chars = '\`*_{}[]()>#+-.!'; + protected $escape_chars_re; + + + public function __construct() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + # Internal hashes used during transformation. + protected $urls = array(); + protected $titles = array(); + protected $html_hashes = array(); + + # Status flag to avoid invalid nesting. + protected $in_anchor = false; + + + protected function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $this->in_anchor = false; + } + + protected function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + + public function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + protected $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + + protected function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; # String that will replace the block + } + + + protected function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + #

+ #
+ # tags for inner block must be indented. + #
+ #
+ # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
` and stop at the first `
`. + $text = preg_replace_callback('{(?> + (?> + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n` to `\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for
. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions ( + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array(&$this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + protected function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + + protected function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + + protected function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + + protected $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + protected function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + protected function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + + protected function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("empty_element_suffix")."\n", + $text); + } + + + protected $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + protected function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + + protected function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array(&$this, '_doHardBreaks_callback'), $text); + } + protected function _doHardBreaks_callback($matches) { + return $this->hashPart("empty_element_suffix\n"); + } + + + protected function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + + protected function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + protected function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + protected function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + protected function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + } + + return $text; + } + protected function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . ""); + return "\n". $result ."\n\n"; + } + + protected $list_level = 0; + + protected function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array(&$this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "
  • " . $item . "
  • \n"; + } + + + protected function doCodeBlocks($text) { + # + # Process Markdown `
    ` blocks.
    +	#
    +		$text = preg_replace_callback('{
    +				(?:\n\n|\A\n?)
    +				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    +				  (?>
    +					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
    +					.*\n+
    +				  )+
    +				)
    +				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    +			}xm',
    +			array(&$this, '_doCodeBlocks_callback'), $text);
    +
    +		return $text;
    +	}
    +	protected function _doCodeBlocks_callback($matches) {
    +		$codeblock = $matches[1];
    +
    +		$codeblock = $this->outdent($codeblock);
    +		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    +
    +		# trim leading newlines and trailing newlines
    +		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    +
    +		$codeblock = "
    $codeblock\n
    "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + + protected function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("$code"); + } + + + protected $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + protected function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration search for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array(&$this, '_doBlockQuotes_callback'), $text); + + return $text; + } + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with
     content, 
    +		# so we need to fix that:
    +		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', + array(&$this, '_doBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; + } + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + + protected function formParagraphs($text) { + # + # Params: + # $text - string to process with html

    tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "

    ", $value); + $value .= "

    "; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 =
    tag +//
    ]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (
    ) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + + protected function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + + protected function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text);; + } + # Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', + array(&$this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array(&$this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeAttribute($matches[1]); + $link = "$url"; + return $this->hashPart($link); + } + protected function _doAutoLinks_email_callback($matches) { + $address = $matches[1]; + $link = $this->encodeEmailAddress($address); + return $this->hashPart($link); + } + + + protected function encodeEmailAddress($addr) { + # + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + #

    foo@exampl + # e.com

    + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. + # + $addr = "mailto:" . $addr; + $chars = preg_split('/(? $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + if ($r > 90 && $char != '@') /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $addr = implode('', $chars); + $text = implode('', array_slice($chars, 7)); # text without `mailto:` + $addr = "$text"; + + return $addr; + } + + + protected function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?no_markup ? '' : ' + | + # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + # closing tag + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + + protected function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + + protected function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + protected $utf8_strlen = 'mb_strlen'; + + protected function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array(&$this, '_detab_callback'), $text); + + return $text; + } + protected function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + protected function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + + protected function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array(&$this, '_unhash_callback'), $text); + } + protected function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + + +# +# Temporary Markdown Extra Parser Implementation Class +# +# NOTE: DON'T USE THIS CLASS +# Currently the implementation of of Extra resides here in this temporary class. +# This makes it easier to propagate the changes between the three different +# packaging styles of PHP Markdown. When this issue is resolved, this +# MarkdownExtra_TmpImpl class here will disappear and \Michelf\MarkdownExtra +# will contain the code. So please use \Michelf\MarkdownExtra and ignore this +# one. +# + +class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { + + ### Configuration Variables ### + + # Prefix for footnote ids. + public $fn_id_prefix = ""; + + # Optional title attribute for footnote links and backlinks. + public $fn_link_title = ""; + public $fn_backlink_title = ""; + + # Optional class attribute for footnote links and backlinks. + public $fn_link_class = "footnote-ref"; + public $fn_backlink_class = "footnote-backref"; + + # Class name for table cell alignment (%% replaced left/center/right) + # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' + # If empty, the align attribute is used instead of a class name. + public $table_align_class_tmpl = ''; + + # Optional class prefix for fenced code block. + public $code_class_prefix = ""; + # Class attribute for code blocks goes on the `code` tag; + # setting this to true will put attributes on the `pre` tag instead. + public $code_attr_on_pre = false; + + # Predefined abbreviations. + public $predef_abbr = array(); + + + ### Parser Implementation ### + + public function __construct() { + # + # Constructor function. Initialize the parser object. + # + # Add extra escapable characters before parent constructor + # initialize the table. + $this->escape_chars .= ':|'; + + # Insert extra document, block, and span transformations. + # Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + parent::__construct(); + } + + + # Extra variables used during extra transformations. + protected $footnotes = array(); + protected $footnotes_ordered = array(); + protected $footnotes_ref_count = array(); + protected $footnotes_numbers = array(); + protected $abbr_desciptions = array(); + protected $abbr_word_re = ''; + + # Give the current footnote number. + protected $footnote_counter = 1; + + + protected function setup() { + # + # Setting up Extra-specific variables. + # + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + protected function teardown() { + # + # Clearing Extra-specific variables. + # + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + ### Extra Attribute Parser ### + + # Expression to use to catch attributes (includes the braces) + protected $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}'; + # Expression to use when parsing in a context when no capture is desired + protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}'; + + protected function doExtraAttributes($tag_name, $attr) { + # + # Parse attributes caught by the $this->id_class_attr_catch_re expression + # and return the HTML-formatted list of attributes. + # + # Currently supported attributes are .class and #id. + # + if (empty($attr)) return ""; + + # Split on components + preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches); + $elements = $matches[0]; + + # handle classes and ids (only first id taken into account) + $classes = array(); + $id = false; + foreach ($elements as $element) { + if ($element{0} == '.') { + $classes[] = substr($element, 1); + } else if ($element{0} == '#') { + if ($id === false) $id = substr($element, 1); + } + } + + # compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$id.'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'.implode(" ", $classes).'"'; + } + return $attr_str; + } + + + protected function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; # String that will replace the block + } + + + ### HTML Block Parser ### + + # Tags that are always treated as block tags: + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption'; + + # Tags treated as block tags only if the opening tag is alone on its line: + protected $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + # Tags where markdown="1" default to span mode: + protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + # Tags which must not have their contents modified, no matter where + # they appear: + protected $clean_tags_re = 'script|math|svg'; + + # Tags that do not need to be closed. + protected $auto_close_tags_re = 'hr|img|param|source|track'; + + + protected function hashHTMLBlocks($text) { + # + # Hashify HTML Blocks and "clean tags". + # + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded. + # + # This works by calling _HashHTMLBlocks_InMarkdown, which then calls + # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + # These two functions are calling each other. It's recursive! + # + if ($this->no_markup) return $text; + + # + # Call the HTML-in-Markdown hasher. + # + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + # + # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + # + # * $indent is the number of space to be ignored when checking for code + # blocks. This is important because if we don't take the indent into + # account, something like this (which looks right) won't work as expected: + # + #

    + #
    + # Hello World. <-- Is this a Markdown code block or text? + #
    <-- Is this a Markdown code block or a real tag? + #
    + # + # If you don't like this, just don't indent the tag on which + # you apply the markdown="1" attribute. + # + # * If $enclosing_tag_re is not empty, stops at the first unmatched closing + # tag with that name. Nested tags supported. + # + # * If $span is true, text inside must treated as span. So any double + # newline will be replaced by a single newline so that it does not create + # paragraphs. + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + # Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + # Tag name. + '.$this->block_tags_re.' | + '.$this->context_block_tags_re.' | + '.$this->clean_tags_re.' | + (?!\s)'.$enclosing_tag_re.' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + | + # Code span marker + `+ + '. ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{'.($indent+4).'}[^\n]* \n + (?> + (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,'.($indent+3).'}~{3,} + [ ]* + (?: + \.?[-_:a-zA-Z0-9]+ # standalone class name + | + '.$this->id_class_attr_nocatch_re.' # extra attributes + )? + [ ]* + \n + ' : '' ). ' # End (if not is span). + ) + }xs'; + + + $depth = 0; # Current depth inside the tag tree. + $parsed = ""; # Parsed text that will be returned. + + # + # Loop through every tag until we find the closing tag of the parent + # or loop until reaching the end of text if no parent tag specified. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + # If in Markdown span mode, add a empty-string span-level hash + # after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "$void\n"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; # Text before current tag. + + # If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + $tag_re = preg_quote($tag); # For use in a regular expression. + + # + # Check for: Code span marker + # + if ($tag{0} == "`") { + # Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text, + $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # No end marker: just skip it. + $parsed .= $tag; + } + } + # + # Check for: Indented code block. + # + else if ($tag{0} == "\n" || $tag{0} == " ") { + # Indented code block: pass it unchanged, will be handled + # later. + $parsed .= $tag; + } + # + # Check for: Opening Block level tag or + # Opening Context Block tag (like ins and del) + # used as a block tag (tag is alone on it's line). + # + else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || + ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + # Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + # Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + # + # Check for: Clean tag (like script, math) + # HTML Comments, processing instructions. + # + else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Need to parse tag and following text using the HTML parser. + # (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + # + # Check for: Tag with same name as enclosing tag. + # + else if ($enclosing_tag_re !== '' && + # Same name as enclosing tag. + preg_match('{^= 0); + + return array($parsed, $text); + } + protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + # + # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + # + # * Calls $hash_method to convert any blocks. + # * Stops when the first opening tag closes. + # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + # (it is not inside clean tags) + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + # Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + ) + }xs'; + + $original_text = $text; # Save original text in case of faliure. + + $depth = 0; # Current depth inside the tag tree. + $block_text = ""; # Temporary text holder for current text. + $parsed = ""; # Parsed text that will be returned. + + # + # Get the name of the starting tag. + # (This pattern makes $base_tag_name_re safe without quoting.) + # + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + # + # Loop through every tag until we find the corresponding closing tag. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + # + # End of $text reached with unbalenced tag(s). + # In that case, we return original text unchanged and pass the + # first character as filtered to prevent an infinite loop in the + # parent function. + # + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; # Text before current tag. + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + + # + # Check for: Auto-close tag (like
    ) + # Comments and Processing Instructions. + # + if (preg_match('{^auto_close_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + # + # Increase/decrease nested tag count. Only do so if + # the tag's name match base tag's. + # + if (preg_match('{^mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); + + # Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + # End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + # Get enclosing tag name for the ParseMarkdown function. + # (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + # Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + # Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + # Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + # Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + # + # Hash last block text that wasn't processed inside the loop. + # + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + + protected function hashClean($text) { + # + # Called whenever a tag must be hashed when a function inserts a "clean" tag + # in $text, it passes through this function and is automaticaly escaped, + # blocking invalid nested overlap. + # + return $this->hashPart($text, 'C'); + } + + + protected function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + + protected function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + protected function doHeaders($text) { + # + # Redefined to add id and class attribute support. + # + # Setext-style headers: + # Header 1 {#header1} + # ======== + # + # Header 2 {#header2 .class1 .class2} + # -------- + # + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 {#header1} + # ## Header 2 {#header2} + # ## Header 2 with closing hashes ## {#header3.class1.class2} + # ... + # ###### Header 6 {.class2} + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + protected function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + return $matches[0]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + protected function doTables($text) { + # + # Form HTML tables. + # + $less_than_tab = $this->tab_width - 1; + # + # Find tables with leading pipe. + # + # | Header 1 | Header 2 + # | -------- | -------- + # | Cell 1 | Cell 2 + # | Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_doTable_leadingPipe_callback'), $text); + + # + # Find tables without leading pipe. + # + # Header 1 | Header 2 + # -------- | -------- + # Cell 1 | Cell 2 + # Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_DoTable_callback'), $text); + + return $text; + } + protected function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove leading pipe for each row. + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + protected function _doTable_makeAlignAttr($alignname) + { + if (empty($this->table_align_class_tmpl)) + return " align=\"$alignname\""; + + $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); + return " class=\"$classname\""; + } + protected function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + # Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('right'); + else if (preg_match('/^ *:-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('center'); + else if (preg_match('/^ *:-+ *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('left'); + else + $attr[$n] = ''; + } + + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + # Write column headers. + $text = "\n"; + $text .= "\n"; + $text .= "\n"; + foreach ($headers as $n => $header) + $text .= " ".$this->runSpanGamut(trim($header))."\n"; + $text .= "\n"; + $text .= "\n"; + + # Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "\n"; + foreach ($rows as $row) { + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + # Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "\n"; + foreach ($row_cells as $n => $cell) + $text .= " ".$this->runSpanGamut(trim($cell))."\n"; + $text .= "\n"; + } + $text .= "\n"; + $text .= "
    "; + + return $this->hashBlock($text) . "\n"; + } + + + protected function doDefLists($text) { + # + # Form HTML definition lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,'.$less_than_tab.'} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + '.$whole_list_re.' + }mx', + array(&$this, '_doDefLists_callback'), $text); + + return $text; + } + protected function _doDefLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + # Turn double returns into triple returns, so that we can make a + # paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "
    \n" . $result . "\n
    "; + return $this->hashBlock($result) . "\n\n"; + } + + + protected function processDefListItems($list_str) { + # + # Process the contents of a single definition list, splitting it + # into individual term and definition list items. + # + $less_than_tab = $this->tab_width - 1; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + # Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,'.$less_than_tab.'} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array(&$this, '_processDefListItems_callback_dt'), $list_str); + + # Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,'.$less_than_tab.'} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,'.$less_than_tab.'} \:[ ] | +
    | \z + ) + ) + }xm', + array(&$this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + protected function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n
    " . $term . "
    "; + } + return $text . "\n"; + } + protected function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + # Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n
    " . $def . "
    \n"; + } + + + protected function doFencedCodeBlocks($text) { + # + # Adding the fenced code block syntax to regular Markdown: + # + # ~~~ + # Code block + # ~~~ + # + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + ~{3,} # Marker: three tilde or more. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + | + '.$this->id_class_attr_catch_re.' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* \n + }xm', + array(&$this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + protected function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + $codeblock = preg_replace_callback('/^\n+/', + array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock); + + if ($classname != "") { + if ($classname{0} == '.') + $classname = substr($classname, 1); + $attr_str = ' class="'.$this->code_class_prefix.$classname.'"'; + } else { + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs); + } + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "$codeblock
    "; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + protected function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("empty_element_suffix", + strlen($matches[0])); + } + + + # + # Redefining emphasis markers so that emphasis by underscore does not + # work in the middle of a word. + # + protected $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + # Check if this should be enclosed in a paragraph. + # Clean tag hashes & block tag hashes are left alone. + $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "

    $value

    "; + } + $grafs[$key] = $value; + } + + # Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + # Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + ### Footnotes + + protected function stripFootnotes($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array(&$this, '_stripFootnotes_callback'), + $text); + return $text; + } + protected function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; # String that will replace the block + } + + + protected function doFootnotes($text) { + # + # Replace footnote references in $text [^id] with a special text-token + # which will be replaced by the actual footnote marker in appendFootnotes. + # + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + + protected function appendFootnotes($text) { + # + # Append footnote list to text. + # + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "
    \n"; + $text .= "empty_element_suffix ."\n"; + $text .= "
      \n\n"; + + $attr = " rev=\"footnote\""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; # Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + # Prepare backlink, multiple backlinks if multiple references + $backlink = ""; + for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { + $backlink .= " "; + } + # Add backlink to last paragraph; create new paragraph if needed. + if (preg_match('{

      $}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink

      "; + } else { + $footnote .= "\n\n

      $backlink

      "; + } + + $text .= "
    1. \n"; + $text .= $footnote . "\n"; + $text .= "
    2. \n\n"; + } + + $text .= "
    \n"; + $text .= "
    "; + } + return $text; + } + protected function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + # Create footnote marker only if it has a corresponding footnote *and* + # the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + # Transfer footnote content to the ordered list and give it its + # number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = ""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "". + "$num". + ""; + } + + return "[^".$matches[1]."]"; + } + + + ### Abbreviations ### + + protected function stripAbbreviations($text) { + # + # Strips abbreviations from text, stores titles in hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array(&$this, '_stripAbbreviations_callback'), + $text); + return $text; + } + protected function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; # String that will replace the block + } + + + protected function doAbbreviations($text) { + # + # Find defined abbreviations in text and wrap them in elements. + # + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{'. + '(?abbr_word_re.')'. + '(?![\w\x1A])'. + '}', + array(&$this, '_doAbbreviations_callback'), $text); + } + return $text; + } + protected function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("$abbr"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("$abbr"); + } + } else { + return $matches[0]; + } + } + +} +?> diff --git a/public/include/lib/Michelf/MarkdownExtra.php b/public/include/lib/Michelf/MarkdownExtra.php new file mode 100644 index 00000000..267bf16d --- /dev/null +++ b/public/include/lib/Michelf/MarkdownExtra.php @@ -0,0 +1,40 @@ + +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# +namespace Michelf; + + +# Just force Michelf/Markdown.php to load. This is needed to load +# the temporary implementation class. See below for details. +\Michelf\Markdown::MARKDOWNLIB_VERSION; + +# +# Markdown Extra Parser Class +# +# Note: Currently the implementation resides in the temporary class +# \Michelf\MarkdownExtra_TmpImpl (in the same file as \Michelf\Markdown). +# This makes it easier to propagate the changes between the three different +# packaging styles of PHP Markdown. Once this issue is resolved, the +# _MarkdownExtra_TmpImpl will disappear and this one will contain the code. +# + +class MarkdownExtra extends \Michelf\_MarkdownExtra_TmpImpl { + + ### Parser Implementation ### + + # Temporarily, the implemenation is in the _MarkdownExtra_TmpImpl class. + # See note above. + +} + + +?> \ No newline at end of file diff --git a/public/include/lib/Mobile_Detect.php b/public/include/lib/Mobile_Detect.php new file mode 100644 index 00000000..0d739d8b --- /dev/null +++ b/public/include/lib/Mobile_Detect.php @@ -0,0 +1,814 @@ + + * Victor Stanciu (until v. 1.0) + * @license MIT License https://github.com/serbanghita/Mobile-Detect/blob/master/LICENSE.txt + * @link Official page: http://mobiledetect.net + * GitHub Repository: https://github.com/serbanghita/Mobile-Detect + * Google Code Old Page: http://code.google.com/p/php-mobile-detect/ + * @version 2.6.2 + */ + +class Mobile_Detect { + + protected $scriptVersion = '2.6.2'; + + // External info. + protected $userAgent = null; + protected $httpHeaders; + + // Arrays holding all detection rules. + protected $mobileDetectionRules = null; + protected $mobileDetectionRulesExtended = null; + // Type of detection to use. + protected $detectionType = 'mobile'; // mobile, extended @todo: refactor this. + + // List of mobile devices (phones) + protected $phoneDevices = array( + 'iPhone' => '\biPhone.*Mobile|\biPod|\biTunes', + 'BlackBerry' => 'BlackBerry|\bBB10\b|rim[0-9]+', + 'HTC' => 'HTC|HTC.*(Sensation|Evo|Vision|Explorer|6800|8100|8900|A7272|S510e|C110e|Legend|Desire|T8282)|APX515CKT|Qtek9090|APA9292KT|HD_mini|Sensation.*Z710e|PG86100|Z715e|Desire.*(A8181|HD)|ADR6200|ADR6425|001HT|Inspire 4G|Android.*\bEVO\b', + 'Nexus' => 'Nexus One|Nexus S|Galaxy.*Nexus|Android.*Nexus.*Mobile', + // @todo: Is 'Dell Streak' a tablet or a phone? ;) + 'Dell' => 'Dell.*Streak|Dell.*Aero|Dell.*Venue|DELL.*Venue Pro|Dell Flash|Dell Smoke|Dell Mini 3iX|XCD28|XCD35|\b001DL\b|\b101DL\b|\bGS01\b', + 'Motorola' => 'Motorola|\bDroid\b.*Build|DROIDX|Android.*Xoom|HRI39|MOT-|A1260|A1680|A555|A853|A855|A953|A955|A956|Motorola.*ELECTRIFY|Motorola.*i1|i867|i940|MB200|MB300|MB501|MB502|MB508|MB511|MB520|MB525|MB526|MB611|MB612|MB632|MB810|MB855|MB860|MB861|MB865|MB870|ME501|ME502|ME511|ME525|ME600|ME632|ME722|ME811|ME860|ME863|ME865|MT620|MT710|MT716|MT720|MT810|MT870|MT917|Motorola.*TITANIUM|WX435|WX445|XT300|XT301|XT311|XT316|XT317|XT319|XT320|XT390|XT502|XT530|XT531|XT532|XT535|XT603|XT610|XT611|XT615|XT681|XT701|XT702|XT711|XT720|XT800|XT806|XT860|XT862|XT875|XT882|XT883|XT894|XT909|XT910|XT912|XT928', + 'Samsung' => 'Samsung|SGH-I337|BGT-S5230|GT-B2100|GT-B2700|GT-B2710|GT-B3210|GT-B3310|GT-B3410|GT-B3730|GT-B3740|GT-B5510|GT-B5512|GT-B5722|GT-B6520|GT-B7300|GT-B7320|GT-B7330|GT-B7350|GT-B7510|GT-B7722|GT-B7800|GT-C3010|GT-C3011|GT-C3060|GT-C3200|GT-C3212|GT-C3212I|GT-C3262|GT-C3222|GT-C3300|GT-C3300K|GT-C3303|GT-C3303K|GT-C3310|GT-C3322|GT-C3330|GT-C3350|GT-C3500|GT-C3510|GT-C3530|GT-C3630|GT-C3780|GT-C5010|GT-C5212|GT-C6620|GT-C6625|GT-C6712|GT-E1050|GT-E1070|GT-E1075|GT-E1080|GT-E1081|GT-E1085|GT-E1087|GT-E1100|GT-E1107|GT-E1110|GT-E1120|GT-E1125|GT-E1130|GT-E1160|GT-E1170|GT-E1175|GT-E1180|GT-E1182|GT-E1200|GT-E1210|GT-E1225|GT-E1230|GT-E1390|GT-E2100|GT-E2120|GT-E2121|GT-E2152|GT-E2220|GT-E2222|GT-E2230|GT-E2232|GT-E2250|GT-E2370|GT-E2550|GT-E2652|GT-E3210|GT-E3213|GT-I5500|GT-I5503|GT-I5700|GT-I5800|GT-I5801|GT-I6410|GT-I6420|GT-I7110|GT-I7410|GT-I7500|GT-I8000|GT-I8150|GT-I8160|GT-I8320|GT-I8330|GT-I8350|GT-I8530|GT-I8700|GT-I8703|GT-I8910|GT-I9000|GT-I9001|GT-I9003|GT-I9010|GT-I9020|GT-I9023|GT-I9070|GT-I9100|GT-I9103|GT-I9220|GT-I9250|GT-I9300|GT-I9505|GT-M3510|GT-M5650|GT-M7500|GT-M7600|GT-M7603|GT-M8800|GT-M8910|GT-N7000|GT-S3110|GT-S3310|GT-S3350|GT-S3353|GT-S3370|GT-S3650|GT-S3653|GT-S3770|GT-S3850|GT-S5210|GT-S5220|GT-S5229|GT-S5230|GT-S5233|GT-S5250|GT-S5253|GT-S5260|GT-S5263|GT-S5270|GT-S5300|GT-S5330|GT-S5350|GT-S5360|GT-S5363|GT-S5369|GT-S5380|GT-S5380D|GT-S5560|GT-S5570|GT-S5600|GT-S5603|GT-S5610|GT-S5620|GT-S5660|GT-S5670|GT-S5690|GT-S5750|GT-S5780|GT-S5830|GT-S5839|GT-S6102|GT-S6500|GT-S7070|GT-S7200|GT-S7220|GT-S7230|GT-S7233|GT-S7250|GT-S7500|GT-S7530|GT-S7550|GT-S7562|GT-S8000|GT-S8003|GT-S8500|GT-S8530|GT-S8600|SCH-A310|SCH-A530|SCH-A570|SCH-A610|SCH-A630|SCH-A650|SCH-A790|SCH-A795|SCH-A850|SCH-A870|SCH-A890|SCH-A930|SCH-A950|SCH-A970|SCH-A990|SCH-I100|SCH-I110|SCH-I400|SCH-I405|SCH-I500|SCH-I510|SCH-I515|SCH-I600|SCH-I730|SCH-I760|SCH-I770|SCH-I830|SCH-I910|SCH-I920|SCH-LC11|SCH-N150|SCH-N300|SCH-R100|SCH-R300|SCH-R351|SCH-R400|SCH-R410|SCH-T300|SCH-U310|SCH-U320|SCH-U350|SCH-U360|SCH-U365|SCH-U370|SCH-U380|SCH-U410|SCH-U430|SCH-U450|SCH-U460|SCH-U470|SCH-U490|SCH-U540|SCH-U550|SCH-U620|SCH-U640|SCH-U650|SCH-U660|SCH-U700|SCH-U740|SCH-U750|SCH-U810|SCH-U820|SCH-U900|SCH-U940|SCH-U960|SCS-26UC|SGH-A107|SGH-A117|SGH-A127|SGH-A137|SGH-A157|SGH-A167|SGH-A177|SGH-A187|SGH-A197|SGH-A227|SGH-A237|SGH-A257|SGH-A437|SGH-A517|SGH-A597|SGH-A637|SGH-A657|SGH-A667|SGH-A687|SGH-A697|SGH-A707|SGH-A717|SGH-A727|SGH-A737|SGH-A747|SGH-A767|SGH-A777|SGH-A797|SGH-A817|SGH-A827|SGH-A837|SGH-A847|SGH-A867|SGH-A877|SGH-A887|SGH-A897|SGH-A927|SGH-B100|SGH-B130|SGH-B200|SGH-B220|SGH-C100|SGH-C110|SGH-C120|SGH-C130|SGH-C140|SGH-C160|SGH-C170|SGH-C180|SGH-C200|SGH-C207|SGH-C210|SGH-C225|SGH-C230|SGH-C417|SGH-C450|SGH-D307|SGH-D347|SGH-D357|SGH-D407|SGH-D415|SGH-D780|SGH-D807|SGH-D980|SGH-E105|SGH-E200|SGH-E315|SGH-E316|SGH-E317|SGH-E335|SGH-E590|SGH-E635|SGH-E715|SGH-E890|SGH-F300|SGH-F480|SGH-I200|SGH-I300|SGH-I320|SGH-I550|SGH-I577|SGH-I600|SGH-I607|SGH-I617|SGH-I627|SGH-I637|SGH-I677|SGH-I700|SGH-I717|SGH-I727|SGH-i747M|SGH-I777|SGH-I780|SGH-I827|SGH-I847|SGH-I857|SGH-I896|SGH-I897|SGH-I900|SGH-I907|SGH-I917|SGH-I927|SGH-I937|SGH-I997|SGH-J150|SGH-J200|SGH-L170|SGH-L700|SGH-M110|SGH-M150|SGH-M200|SGH-N105|SGH-N500|SGH-N600|SGH-N620|SGH-N625|SGH-N700|SGH-N710|SGH-P107|SGH-P207|SGH-P300|SGH-P310|SGH-P520|SGH-P735|SGH-P777|SGH-Q105|SGH-R210|SGH-R220|SGH-R225|SGH-S105|SGH-S307|SGH-T109|SGH-T119|SGH-T139|SGH-T209|SGH-T219|SGH-T229|SGH-T239|SGH-T249|SGH-T259|SGH-T309|SGH-T319|SGH-T329|SGH-T339|SGH-T349|SGH-T359|SGH-T369|SGH-T379|SGH-T409|SGH-T429|SGH-T439|SGH-T459|SGH-T469|SGH-T479|SGH-T499|SGH-T509|SGH-T519|SGH-T539|SGH-T559|SGH-T589|SGH-T609|SGH-T619|SGH-T629|SGH-T639|SGH-T659|SGH-T669|SGH-T679|SGH-T709|SGH-T719|SGH-T729|SGH-T739|SGH-T746|SGH-T749|SGH-T759|SGH-T769|SGH-T809|SGH-T819|SGH-T839|SGH-T919|SGH-T929|SGH-T939|SGH-T959|SGH-T989|SGH-U100|SGH-U200|SGH-U800|SGH-V205|SGH-V206|SGH-X100|SGH-X105|SGH-X120|SGH-X140|SGH-X426|SGH-X427|SGH-X475|SGH-X495|SGH-X497|SGH-X507|SGH-X600|SGH-X610|SGH-X620|SGH-X630|SGH-X700|SGH-X820|SGH-X890|SGH-Z130|SGH-Z150|SGH-Z170|SGH-ZX10|SGH-ZX20|SHW-M110|SPH-A120|SPH-A400|SPH-A420|SPH-A460|SPH-A500|SPH-A560|SPH-A600|SPH-A620|SPH-A660|SPH-A700|SPH-A740|SPH-A760|SPH-A790|SPH-A800|SPH-A820|SPH-A840|SPH-A880|SPH-A900|SPH-A940|SPH-A960|SPH-D600|SPH-D700|SPH-D710|SPH-D720|SPH-I300|SPH-I325|SPH-I330|SPH-I350|SPH-I500|SPH-I600|SPH-I700|SPH-L700|SPH-M100|SPH-M220|SPH-M240|SPH-M300|SPH-M305|SPH-M320|SPH-M330|SPH-M350|SPH-M360|SPH-M370|SPH-M380|SPH-M510|SPH-M540|SPH-M550|SPH-M560|SPH-M570|SPH-M580|SPH-M610|SPH-M620|SPH-M630|SPH-M800|SPH-M810|SPH-M850|SPH-M900|SPH-M910|SPH-M920|SPH-M930|SPH-N100|SPH-N200|SPH-N240|SPH-N300|SPH-N400|SPH-Z400|SWC-E100|SCH-i909|GT-N7100|SCH-I535', + 'LG' => '\bLG\b;|(LG|LG-)?(C800|C900|E400|E610|E900|E-900|F160|F180K|F180L|F180S|730|855|L160|LS840|LS970|LU6200|MS690|MS695|MS770|MS840|MS870|MS910|P500|P700|P705|VM696|AS680|AS695|AX840|C729|E970|GS505|272|C395|E739BK|E960|L55C|L75C|LS696|LS860|P769BK|P350|P870|UN272|US730|VS840|VS950|LN272|LN510|LS670|LS855|LW690|MN270|MN510|P509|P769|P930|UN200|UN270|UN510|UN610|US670|US740|US760|UX265|UX840|VN271|VN530|VS660|VS700|VS740|VS750|VS910|VS920|VS930|VX9200|VX11000|AX840A|LW770|P506|P925|P999)', + 'Sony' => 'sony|SonyEricsson|SonyEricssonLT15iv|LT18i|E10i', + 'Asus' => 'Asus.*Galaxy', + 'Palm' => 'PalmSource|Palm', // avantgo|blazer|elaine|hiptop|plucker|xiino ; @todo - complete the regex. + 'Vertu' => 'Vertu|Vertu.*Ltd|Vertu.*Ascent|Vertu.*Ayxta|Vertu.*Constellation(F|Quest)?|Vertu.*Monika|Vertu.*Signature', // Just for fun ;) + // @ref: http://www.pantech.co.kr/en/prod/prodList.do?gbrand=VEGA (PANTECH) + // Most of the VEGA devices are legacy. PANTECH seem to be newer devices based on Android. + 'Pantech' => 'PANTECH|IM-A850S|IM-A840S|IM-A830L|IM-A830K|IM-A830S|IM-A820L|IM-A810K|IM-A810S|IM-A800S|IM-T100K|IM-A725L|IM-A780L|IM-A775C|IM-A770K|IM-A760S|IM-A750K|IM-A740S|IM-A730S|IM-A720L|IM-A710K|IM-A690L|IM-A690S|IM-A650S|IM-A630K|IM-A600S|VEGA PTL21|PT003|P8010|ADR910L|P6030|P6020|P9070|P4100|P9060|P5000|CDM8992|TXT8045|ADR8995|IS11PT|P2030|P6010|P8000|PT002|IS06|CDM8999|P9050|PT001|TXT8040|P2020|P9020|P2000|P7040|P7000|C790', + // @ref: http://www.fly-phone.com/devices/smartphones/ ; Included only smartphones. + 'Fly' => 'IQ230|IQ444|IQ450|IQ440|IQ442|IQ441|IQ245|IQ256|IQ236|IQ255|IQ235|IQ245|IQ275|IQ240|IQ285|IQ280|IQ270|IQ260|IQ250', + // Added simvalley mobile just for fun. They have some interesting devices. + // @ref: http://www.simvalley.fr/telephonie---gps-_22_telephonie-mobile_telephones_.html + 'SimValley' => '\b(SP-80|XT-930|SX-340|XT-930|SX-310|SP-360|SP60|SPT-800|SP-120|SPT-800|SP-140|SPX-5|SPX-8|SP-100|SPX-8|SPX-12)\b', + // @Tapatalk is a mobile app; @ref: http://support.tapatalk.com/threads/smf-2-0-2-os-and-browser-detection-plugin-and-tapatalk.15565/#post-79039 + 'GenericPhone' => 'Tapatalk|PDA;|PPC;|SAGEM|mmp|pocket|psp|symbian|Smartphone|smartfon|treo|up.browser|up.link|vodafone|wap|nokia|Series40|Series60|S60|SonyEricsson|N900|MAUI.*WAP.*Browser|LG-P500' + ); + // List of tablet devices. + protected $tabletDevices = array( + 'iPad' => 'iPad|iPad.*Mobile', // @todo: check for mobile friendly emails topic. + 'NexusTablet' => '^.*Android.*Nexus(((?:(?!Mobile))|(?:(\s(7|10).+))).)*$', + 'SamsungTablet' => 'SAMSUNG.*Tablet|Galaxy.*Tab|SC-01C|GT-P1000|GT-P1003|GT-P1010|GT-P3105|GT-P6210|GT-P6800|GT-P6810|GT-P7100|GT-P7300|GT-P7310|GT-P7500|GT-P7510|SCH-I800|SCH-I815|SCH-I905|SGH-I957|SGH-I987|SGH-T849|SGH-T859|SGH-T869|SPH-P100|GT-P3100|GT-P3108|GT-P3110|GT-P5100|GT-P5110|GT-P6200|GT-P7320|GT-P7511|GT-N8000|GT-P8510|SGH-I497|SPH-P500|SGH-T779|SCH-I705|SCH-I915|GT-N8013|GT-P3113|GT-P5113|GT-P8110|GT-N8010|GT-N8005|GT-N8020|GT-P1013|GT-P6201|GT-P7501|GT-N5100|GT-N5110|SHV-E140K|SHV-E140L|SHV-E140S|SHV-E150S|SHV-E230K|SHV-E230L|SHV-E230S|SHW-M180K|SHW-M180L|SHW-M180S|SHW-M180W|SHW-M300W|SHW-M305W|SHW-M380K|SHW-M380S|SHW-M380W|SHW-M430W|SHW-M480K|SHW-M480S|SHW-M480W|SHW-M485W|SHW-M486W|SHW-M500W|GT-I9228|SCH-P739|SCH-I925', + // @reference: http://www.labnol.org/software/kindle-user-agent-string/20378/ + 'Kindle' => 'Kindle|Silk.*Accelerated', + // Only the Surface tablets with Windows RT are considered mobile. + // @ref: http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx + 'SurfaceTablet' => 'Windows NT [0-9.]+; ARM;', + 'AsusTablet' => 'Transformer|TF101', + 'BlackBerryTablet' => 'PlayBook|RIM Tablet', + 'HTCtablet' => 'HTC Flyer|HTC Jetstream|HTC-P715a|HTC EVO View 4G|PG41200', + 'MotorolaTablet' => 'xoom|sholest|MZ615|MZ605|MZ505|MZ601|MZ602|MZ603|MZ604|MZ606|MZ607|MZ608|MZ609|MZ615|MZ616|MZ617', + 'NookTablet' => 'Android.*Nook|NookColor|nook browser|BNRV200|BNRV200A|BNTV250|BNTV250A|LogicPD Zoom2', + // @ref: http://www.acer.ro/ac/ro/RO/content/drivers + // @ref: http://www.packardbell.co.uk/pb/en/GB/content/download (Packard Bell is part of Acer) + 'AcerTablet' => 'Android.*\b(A100|A101|A110|A200|A210|A211|A500|A501|A510|A511|A700|A701|W500|W500P|W501|W501P|W510|W511|W700|G100|G100W|B1-A71)\b', + // @ref: http://eu.computers.toshiba-europe.com/innovation/family/Tablets/1098744/banner_id/tablet_footerlink/ + // @ref: http://us.toshiba.com/tablets/tablet-finder + // @ref: http://www.toshiba.co.jp/regza/tablet/ + 'ToshibaTablet' => 'Android.*(AT100|AT105|AT200|AT205|AT270|AT275|AT300|AT305|AT1S5|AT500|AT570|AT700|AT830)|TOSHIBA.*FOLIO', + // @ref: http://www.nttdocomo.co.jp/english/service/developer/smart_phone/technical_info/spec/index.html + 'LGTablet' => '\bL-06C|LG-V900|LG-V909\b', + // Prestigio Tablets http://www.prestigio.com/support + 'PrestigioTablet' => 'PMP3170B|PMP3270B|PMP3470B|PMP7170B|PMP3370B|PMP3570C|PMP5870C|PMP3670B|PMP5570C|PMP5770D|PMP3970B|PMP3870C|PMP5580C|PMP5880D|PMP5780D|PMP5588C|PMP7280C|PMP7280|PMP7880D|PMP5597D|PMP5597|PMP7100D|PER3464|PER3274|PER3574|PER3884|PER5274|PER5474', + 'YarvikTablet' => 'Android.*(TAB210|TAB211|TAB224|TAB250|TAB260|TAB264|TAB310|TAB360|TAB364|TAB410|TAB411|TAB420|TAB424|TAB450|TAB460|TAB461|TAB464|TAB465|TAB467|TAB468)', + 'MedionTablet' => 'Android.*\bOYO\b|LIFE.*(P9212|P9514|P9516|S9512)|LIFETAB', + 'ArnovaTablet' => 'AN10G2|AN7bG3|AN7fG3|AN8G3|AN8cG3|AN7G3|AN9G3|AN7dG3|AN7dG3ST|AN7dG3ChildPad|AN10bG3|AN10bG3DT', + // @reference: http://wiki.archosfans.com/index.php?title=Main_Page + 'ArchosTablet' => 'Android.*ARCHOS|\b101G9\b|\b80G9\b', + // @reference: http://en.wikipedia.org/wiki/NOVO7 + 'AinolTablet' => 'NOVO7|Novo7Aurora|Novo7Basic|NOVO7PALADIN', + // @todo: inspect http://esupport.sony.com/US/p/select-system.pl?DIRECTOR=DRIVER + // @ref: Readers http://www.atsuhiro-me.net/ebook/sony-reader/sony-reader-web-browser + // @ref: http://www.sony.jp/support/tablet/ + 'SonyTablet' => 'Sony.*Tablet|Xperia Tablet|Sony Tablet S|SO-03E|SGPT12|SGPT121|SGPT122|SGPT123|SGPT111|SGPT112|SGPT113|SGPT211|SGPT213|SGP311|SGP312|SGP321|EBRD1101|EBRD1102|EBRD1201', + // @ref: db + http://www.cube-tablet.com/buy-products.html + 'CubeTablet' => 'Android.*(K8GT|U9GT|U10GT|U16GT|U17GT|U18GT|U19GT|U20GT|U23GT|U30GT)|CUBE U8GT', + // @ref: http://www.cobyusa.com/?p=pcat&pcat_id=3001 + 'CobyTablet' => 'MID1042|MID1045|MID1125|MID1126|MID7012|MID7014|MID7034|MID7035|MID7036|MID7042|MID7048|MID7127|MID8042|MID8048|MID8127|MID9042|MID9740|MID9742|MID7022|MID7010', + // @ref: http://pdadb.net/index.php?m=pdalist&list=SMiT (NoName Chinese Tablets) + // @ref: http://www.imp3.net/14/show.php?itemid=20454 + 'SMiTTablet' => 'Android.*(\bMID\b|MID-560|MTV-T1200|MTV-PND531|MTV-P1101|MTV-PND530)', + // @ref: http://www.rock-chips.com/index.php?do=prod&pid=2 + 'RockChipTablet' => 'Android.*(RK2818|RK2808A|RK2918|RK3066)|RK2738|RK2808A', + // @ref: http://www.telstra.com.au/home-phone/thub-2/ + 'TelstraTablet' => 'T-Hub2', + // @ref: http://www.fly-phone.com/devices/tablets/ ; http://www.fly-phone.com/service/ + 'FlyTablet' => 'IQ310|Fly Vision', + // @ref: http://www.bqreaders.com/gb/tablets-prices-sale.html + 'bqTablet' => 'bq.*(Elcano|Curie|Edison|Maxwell|Kepler|Pascal|Tesla|Hypatia|Platon|Newton|Livingstone|Cervantes|Avant)', + // @ref: http://www.huaweidevice.com/worldwide/productFamily.do?method=index&directoryId=5011&treeId=3290 + // @ref: http://www.huaweidevice.com/worldwide/downloadCenter.do?method=index&directoryId=3372&treeId=0&tb=1&type=software (including legacy tablets) + 'HuaweiTablet' => 'MediaPad|IDEOS S7|S7-201c|S7-202u|S7-101|S7-103|S7-104|S7-105|S7-106|S7-201|S7-Slim', + // Nec or Medias Tab + 'NecTablet' => '\bN-06D|\bN-08D', + // Pantech Tablets: http://www.pantechusa.com/phones/ + 'PantechTablet' => 'Pantech.*P4100', + // Broncho Tablets: http://www.broncho.cn/ (hard to find) + 'BronchoTablet' => 'Broncho.*(N701|N708|N802|a710)', + // @ref: http://versusuk.com/support.html + 'VersusTablet' => 'TOUCHPAD.*[78910]', + // @ref: http://www.zync.in/index.php/our-products/tablet-phablets + 'ZyncTablet' => 'z1000|Z99 2G|z99|z930|z999|z990|z909|Z919|z900', + // @ref: http://www.positivoinformatica.com.br/www/pessoal/tablet-ypy/ + 'PositivoTablet' => 'TB07STA|TB10STA|TB07FTA|TB10FTA', + // @ref: https://www.nabitablet.com/ + 'NabiTablet' => 'Android.*\bNabi', + 'KoboTablet' => 'Kobo Touch|\bK080\b|\bVox\b Build|\bArc\b Build', + // French Danew Tablets http://www.danew.com/produits-tablette.php + 'DanewTablet' => 'DSlide.*\b(700|701R|702|703R|704|802|970|971|972|973|974|1010|1012)\b', + // Texet Tablets and Readers http://www.texet.ru/tablet/ + 'TexetTablet' => 'NaviPad|TB-772A|TM-7045|TM-7055|TM-9750|TM-7016|TM-7024|TM-7026|TM-7041|TM-7043|TM-7047|TM-8041|TM-9741|TM-9747|TM-9748|TM-9751|TM-7022|TM-7021|TM-7020|TM-7011|TM-7010|TM-7023|TM-7025|TM-7037W|TM-7038W|TM-7027W|TM-9720|TM-9725|TM-9737W|TM-1020|TM-9738W|TM-9740|TM-9743W|TB-807A|TB-771A|TB-727A|TB-725A|TB-719A|TB-823A|TB-805A|TB-723A|TB-715A|TB-707A|TB-705A|TB-709A|TB-711A|TB-890HD|TB-880HD|TB-790HD|TB-780HD|TB-770HD|TB-721HD|TB-710HD|TB-434HD|TB-860HD|TB-840HD|TB-760HD|TB-750HD|TB-740HD|TB-730HD|TB-722HD|TB-720HD|TB-700HD|TB-500HD|TB-470HD|TB-431HD|TB-430HD|TB-506|TB-504|TB-446|TB-436|TB-416|TB-146SE|TB-126SE', + // @note: Avoid detecting 'PLAYSTATION 3' as mobile. + 'PlaystationTablet' => 'Playstation.*(Portable|Vita)', + // @ref: http://www.galapad.net/product.html + 'GalapadTablet' => 'Android.*\bG1\b', + 'GenericTablet' => 'Android.*\b97D\b|Tablet(?!.*PC)|ViewPad7|MID7015|BNTV250A|LogicPD Zoom2|\bA7EB\b|CatNova8|A1_07|CT704|CT1002|\bM721\b|hp-tablet|rk30sdk', + ); + // List of mobile Operating Systems. + protected $operatingSystems = array( + 'AndroidOS' => 'Android', + 'BlackBerryOS' => 'blackberry|\bBB10\b|rim tablet os', + 'PalmOS' => 'PalmOS|avantgo|blazer|elaine|hiptop|palm|plucker|xiino', + 'SymbianOS' => 'Symbian|SymbOS|Series60|Series40|SYB-[0-9]+|\bS60\b', + // @reference: http://en.wikipedia.org/wiki/Windows_Mobile + 'WindowsMobileOS' => 'Windows CE.*(PPC|Smartphone|Mobile|[0-9]{3}x[0-9]{3})|Window Mobile|Windows Phone [0-9.]+|WCE;', + // @reference: http://en.wikipedia.org/wiki/Windows_Phone + // http://wifeng.cn/?r=blog&a=view&id=106 + // http://nicksnettravels.builttoroam.com/post/2011/01/10/Bogus-Windows-Phone-7-User-Agent-String.aspx + 'WindowsPhoneOS' => 'Windows Phone OS|XBLWP7|ZuneWP7', + 'iOS' => '\biPhone.*Mobile|\biPod|\biPad', + // http://en.wikipedia.org/wiki/MeeGo + // @todo: research MeeGo in UAs + 'MeeGoOS' => 'MeeGo', + // http://en.wikipedia.org/wiki/Maemo + // @todo: research Maemo in UAs + 'MaemoOS' => 'Maemo', + 'JavaOS' => 'J2ME/|Java/|\bMIDP\b|\bCLDC\b', + 'webOS' => 'webOS|hpwOS', + 'badaOS' => '\bBada\b', + 'BREWOS' => 'BREW', + ); + // List of mobile User Agents. + protected $userAgents = array( + // @reference: https://developers.google.com/chrome/mobile/docs/user-agent + 'Chrome' => '\bCrMo\b|CriOS|Android.*Chrome/[.0-9]* (Mobile)?', + 'Dolfin' => '\bDolfin\b', + 'Opera' => 'Opera.*Mini|Opera.*Mobi|Android.*Opera|Mobile.*OPR/[0-9.]+', + 'Skyfire' => 'Skyfire', + 'IE' => 'IEMobile|MSIEMobile', + 'Firefox' => 'fennec|firefox.*maemo|(Mobile|Tablet).*Firefox|Firefox.*Mobile', + 'Bolt' => 'bolt', + 'TeaShark' => 'teashark', + 'Blazer' => 'Blazer', + // @reference: http://developer.apple.com/library/safari/#documentation/AppleApplications/Reference/SafariWebContent/OptimizingforSafarioniPhone/OptimizingforSafarioniPhone.html#//apple_ref/doc/uid/TP40006517-SW3 + 'Safari' => 'Version.*Mobile.*Safari|Safari.*Mobile', + // @ref: http://en.wikipedia.org/wiki/Midori_(web_browser) + //'Midori' => 'midori', + 'Tizen' => 'Tizen', + 'UCBrowser' => 'UC.*Browser|UCWEB', + // @ref: https://github.com/serbanghita/Mobile-Detect/issues/7 + 'DiigoBrowser' => 'DiigoBrowser', + // http://www.puffinbrowser.com/index.php + 'Puffin' => 'Puffin', + // @ref: http://mercury-browser.com/index.html + 'Mercury' => '\bMercury\b', + // @reference: http://en.wikipedia.org/wiki/Minimo + // http://en.wikipedia.org/wiki/Vision_Mobile_Browser + 'GenericBrowser' => 'NokiaBrowser|OviBrowser|OneBrowser|TwonkyBeamBrowser|SEMC.*Browser|FlyFlow|Minimo|NetFront|Novarra-Vision' + ); + // Utilities. + protected $utilities = array( + // Experimental. When a mobile device wants to switch to 'Desktop Mode'. + // @ref: http://scottcate.com/technology/windows-phone-8-ie10-desktop-or-mobile/ + // @ref: https://github.com/serbanghita/Mobile-Detect/issues/57#issuecomment-15024011 + 'DesktopMode' => 'WPDesktop', + 'TV' => 'SonyDTV115', // experimental + 'WebKit' => '(webkit)[ /]([\w.]+)', + 'Bot' => 'Googlebot|DoCoMo|YandexBot|bingbot|ia_archiver|AhrefsBot|Ezooms|GSLFbot|WBSearchBot|Twitterbot|TweetmemeBot|Twikle|PaperLiBot|Wotbox|UnwindFetchor|facebookexternalhit', + 'MobileBot' => 'Googlebot-Mobile|DoCoMo|YahooSeeker/M1A1-R2D2', + ); + // Properties list. + // @reference: http://user-agent-string.info/list-of-ua#Mobile Browser + const VER = '([\w._\+]+)'; + protected $properties = array( + + // Build + 'Mobile' => 'Mobile/[VER]', + 'Build' => 'Build/[VER]', + 'Version' => 'Version/[VER]', + 'VendorID' => 'VendorID/[VER]', + + // Devices + 'iPad' => 'iPad.*CPU[a-z ]+[VER]', + 'iPhone' => 'iPhone.*CPU[a-z ]+[VER]', + 'iPod' => 'iPod.*CPU[a-z ]+[VER]', + //'BlackBerry' => array('BlackBerry[VER]', 'BlackBerry [VER];'), + 'Kindle' => 'Kindle/[VER]', + + // Browser + 'Chrome' => array('Chrome/[VER]', 'CriOS/[VER]', 'CrMo/[VER]'), + 'Dolfin' => 'Dolfin/[VER]', + // @reference: https://developer.mozilla.org/en-US/docs/User_Agent_Strings_Reference + 'Firefox' => 'Firefox/[VER]', + 'Fennec' => 'Fennec/[VER]', + // @reference: http://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx + 'IE' => array('IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];'), + // http://en.wikipedia.org/wiki/NetFront + 'NetFront' => 'NetFront/[VER]', + 'NokiaBrowser' => 'NokiaBrowser/[VER]', + 'Opera' => array( ' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]' ), + 'UC Browser' => 'UC Browser[VER]', + // @note: Safari 7534.48.3 is actually Version 5.1. + // @note: On BlackBerry the Version is overwriten by the OS. + 'Safari' => array( 'Version/[VER]', 'Safari/[VER]' ), + 'Skyfire' => 'Skyfire/[VER]', + 'Tizen' => 'Tizen/[VER]', + 'Webkit' => 'webkit[ /][VER]', + + // Engine + 'Gecko' => 'Gecko/[VER]', + 'Trident' => 'Trident/[VER]', + 'Presto' => 'Presto/[VER]', + + // OS + 'iOS' => ' \bOS\b [VER] ', + 'Android' => 'Android [VER]', + 'BlackBerry' => array('BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'), + 'BREW' => 'BREW [VER]', + 'Java' => 'Java/[VER]', + // @reference: http://windowsteamblog.com/windows_phone/b/wpdev/archive/2011/08/29/introducing-the-ie9-on-windows-phone-mango-user-agent-string.aspx + // @reference: http://en.wikipedia.org/wiki/Windows_NT#Releases + 'Windows Phone OS' => array( 'Windows Phone OS [VER]', 'Windows Phone [VER]'), + 'Windows Phone' => 'Windows Phone [VER]', + 'Windows CE' => 'Windows CE/[VER]', + // http://social.msdn.microsoft.com/Forums/en-US/windowsdeveloperpreviewgeneral/thread/6be392da-4d2f-41b4-8354-8dcee20c85cd + 'Windows NT' => 'Windows NT [VER]', + 'Symbian' => array('SymbianOS/[VER]', 'Symbian/[VER]'), + 'webOS' => array('webOS/[VER]', 'hpwOS/[VER];'), + + + ); + + function __construct(){ + + $this->setHttpHeaders(); + $this->setUserAgent(); + + $this->setMobileDetectionRules(); + $this->setMobileDetectionRulesExtended(); + + } + + + /** + * Get the current script version. + * This is useful for the demo.php file, + * so people can check on what version they are testing + * for mobile devices. + */ + public function getScriptVersion(){ + + return $this->scriptVersion; + + } + + public function setHttpHeaders($httpHeaders = null){ + + if(!empty($httpHeaders)){ + $this->httpHeaders = $httpHeaders; + } else { + foreach($_SERVER as $key => $value){ + if(substr($key,0,5)=='HTTP_'){ + $this->httpHeaders[$key] = $value; + } + } + } + + } + + public function getHttpHeaders(){ + + return $this->httpHeaders; + + } + + public function setUserAgent($userAgent = null){ + + if(!empty($userAgent)){ + $this->userAgent = $userAgent; + } else { + $this->userAgent = isset($this->httpHeaders['HTTP_USER_AGENT']) ? $this->httpHeaders['HTTP_USER_AGENT'] : null; + + if(empty($this->userAgent)){ + $this->userAgent = isset($this->httpHeaders['HTTP_X_DEVICE_USER_AGENT']) ? $this->httpHeaders['HTTP_X_DEVICE_USER_AGENT'] : null; + } + // Header can occur on devices using Opera Mini (can expose the real device type). Let's concatenate it (we need this extra info in the regexes). + if(!empty($this->httpHeaders['HTTP_X_OPERAMINI_PHONE_UA'])){ + $this->userAgent .= ' '.$this->httpHeaders['HTTP_X_OPERAMINI_PHONE_UA']; + } + } + + } + + public function getUserAgent(){ + + return $this->userAgent; + + } + + function setDetectionType($type = null){ + + $this->detectionType = (!empty($type) ? $type : 'mobile'); + + } + + public function getPhoneDevices(){ + + return $this->phoneDevices; + + } + + public function getTabletDevices(){ + + return $this->tabletDevices; + + } + + /** + * Method sets the mobile detection rules. + * + * This method is used for the magic methods $detect->is*() + */ + public function setMobileDetectionRules(){ + // Merge all rules together. + $this->mobileDetectionRules = array_merge( + $this->phoneDevices, + $this->tabletDevices, + $this->operatingSystems, + $this->userAgents + ); + + } + + /** + * Method sets the mobile detection rules + utilities. + * The reason this is separate is because utilities rules + * don't necessary imply mobile. + * + * This method is used inside the new $detect->is('stuff') method. + * + * @return bool + */ + public function setMobileDetectionRulesExtended(){ + + // Merge all rules together. + $this->mobileDetectionRulesExtended = array_merge( + $this->phoneDevices, + $this->tabletDevices, + $this->operatingSystems, + $this->userAgents, + $this->utilities + ); + + } + + /** + * @return array + */ + public function getRules() + { + + if($this->detectionType=='extended'){ + return $this->mobileDetectionRulesExtended; + } else { + return $this->mobileDetectionRules; + } + + } + +/** +* Check the HTTP headers for signs of mobile. +* This is the fastest mobile check possible; it's used +* inside isMobile() method. +* @return boolean +*/ + public function checkHttpHeadersForMobile(){ + + if( + isset($this->httpHeaders['HTTP_ACCEPT']) && + (strpos($this->httpHeaders['HTTP_ACCEPT'], 'application/x-obml2d') !== false || // Opera Mini; @reference: http://dev.opera.com/articles/view/opera-binary-markup-language/ + strpos($this->httpHeaders['HTTP_ACCEPT'], 'application/vnd.rim.html') !== false || // BlackBerry devices. + strpos($this->httpHeaders['HTTP_ACCEPT'], 'text/vnd.wap.wml') !== false || + strpos($this->httpHeaders['HTTP_ACCEPT'], 'application/vnd.wap.xhtml+xml') !== false) || + isset($this->httpHeaders['HTTP_X_WAP_PROFILE']) || // @todo: validate + isset($this->httpHeaders['HTTP_X_WAP_CLIENTID']) || + isset($this->httpHeaders['HTTP_WAP_CONNECTION']) || + isset($this->httpHeaders['HTTP_PROFILE']) || + isset($this->httpHeaders['HTTP_X_OPERAMINI_PHONE_UA']) || // Reported by Nokia devices (eg. C3) + isset($this->httpHeaders['HTTP_X_NOKIA_IPADDRESS']) || + isset($this->httpHeaders['HTTP_X_NOKIA_GATEWAY_ID']) || + isset($this->httpHeaders['HTTP_X_ORANGE_ID']) || + isset($this->httpHeaders['HTTP_X_VODAFONE_3GPDPCONTEXT']) || + isset($this->httpHeaders['HTTP_X_HUAWEI_USERID']) || + isset($this->httpHeaders['HTTP_UA_OS']) || // Reported by Windows Smartphones. + isset($this->httpHeaders['HTTP_X_MOBILE_GATEWAY']) || // Reported by Verizon, Vodafone proxy system. + isset($this->httpHeaders['HTTP_X_ATT_DEVICEID']) || // Seend this on HTC Sensation. @ref: SensationXE_Beats_Z715e + //HTTP_X_NETWORK_TYPE = WIFI + ( isset($this->httpHeaders['HTTP_UA_CPU']) && + $this->httpHeaders['HTTP_UA_CPU'] == 'ARM' // Seen this on a HTC. + ) + ){ + + return true; + + } + + return false; + + } + + /** + * Magic overloading method. + * + * @method boolean is[...]() + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call($name, $arguments) + { + + $this->setDetectionType('mobile'); + + $key = substr($name, 2); + return $this->matchUAAgainstKey($key); + + } + + /** + * Find a detection rule that matches the current User-agent. + * + * @param null $userAgent deprecated + * @return boolean + */ + private function matchDetectionRulesAgainstUA($userAgent = null){ + + // Begin general search. + foreach($this->getRules() as $_regex){ + if(empty($_regex)){ continue; } + if( $this->match($_regex, $userAgent) ){ + //var_dump( $_regex ); + return true; + } + } + + return false; + + } + + /** + * Search for a certain key in the rules array. + * If the key is found the try to match the corresponding + * regex agains the User-agent. + * + * @param string $key + * @param null $userAgent deprecated + * @return mixed + */ + private function matchUAAgainstKey($key, $userAgent = null){ + + // Make the keys lowercase so we can match: isIphone(), isiPhone(), isiphone(), etc. + $key = strtolower($key); + $_rules = array_change_key_case($this->getRules()); + + if(array_key_exists($key, $_rules)){ + if(empty($_rules[$key])){ return null; } + return $this->match($_rules[$key], $userAgent); + } + + return false; + + } + + /** + * Check if the device is mobile. + * Returns true if any type of mobile device detected, including special ones + * @param null $userAgent deprecated + * @param null $httpHeaders deprecated + * @return bool + */ + public function isMobile($userAgent = null, $httpHeaders = null) { + + if($httpHeaders){ $this->setHttpHeaders($httpHeaders); } + if($userAgent){ $this->setUserAgent($userAgent); } + + $this->setDetectionType('mobile'); + + if ($this->checkHttpHeadersForMobile()) { + return true; + } else { + return $this->matchDetectionRulesAgainstUA(); + } + + } + + /** + * Check if the device is a tablet. + * Return true if any type of tablet device is detected. + * + * @param null $userAgent deprecated + * @param null $httpHeaders deprecated + * @return bool + */ + public function isTablet($userAgent = null, $httpHeaders = null) { + + $this->setDetectionType('mobile'); + + foreach($this->tabletDevices as $_regex){ + if($this->match($_regex, $userAgent)){ + return true; + } + } + + return false; + + } + + /** + * This method checks for a certain property in the + * userAgent. + * @todo: The httpHeaders part is not yet used. + * + * @param $key + * @param string $userAgent deprecated + * @param string $httpHeaders deprecated + * @return bool|int|null + */ + public function is($key, $userAgent = null, $httpHeaders = null){ + + + // Set the UA and HTTP headers only if needed (eg. batch mode). + if($httpHeaders) $this->setHttpHeaders($httpHeaders); + if($userAgent) $this->setUserAgent($userAgent); + + $this->setDetectionType('extended'); + + return $this->matchUAAgainstKey($key); + + } + + public function getOperatingSystems(){ + + return $this->operatingSystems; + + } + + /** + * Some detection rules are relative (not standard), + * because of the diversity of devices, vendors and + * their conventions in representing the User-Agent or + * the HTTP headers. + * + * This method will be used to check custom regexes against + * the User-Agent string. + * + * @param $regex + * @param string $userAgent + * @return bool + * + * @todo: search in the HTTP headers too. + */ + function match($regex, $userAgent=null){ + + // Escape the special character which is the delimiter. + $regex = str_replace('/', '\/', $regex); + + return (bool)preg_match('/'.$regex.'/is', (!empty($userAgent) ? $userAgent : $this->userAgent)); + + } + + /** + * Get the properties array. + * @return array + */ + function getProperties(){ + + return $this->properties; + + } + + /** + * Prepare the version number. + * + * @param $ver + * @return int + */ + function prepareVersionNo($ver){ + + $ver = str_replace(array('_', ' ', '/'), array('.', '.', '.'), $ver); + $arrVer = explode('.', $ver, 2); + if(isset($arrVer[1])){ + $arrVer[1] = @str_replace('.', '', $arrVer[1]); // @todo: treat strings versions. + } + $ver = (float)implode('.', $arrVer); + + return $ver; + + } + + /** + * Check the version of the given property in the User-Agent. + * Will return a float number. (eg. 2_0 will return 2.0, 4.3.1 will return 4.31) + * + * @param string $propertyName + * @return mixed $version + */ + function version($propertyName, $type = 'text'){ + + if(empty($propertyName)){ return false; } + if( !in_array($type, array('text', 'float')) ){ $type = 'text'; } + + $properties = $this->getProperties(); + + // Check if the property exists in the properties array. + if( array_key_exists($propertyName, $properties) ){ + + // Prepare the pattern to be matched. + // Make sure we always deal with an array (string is converted). + $properties[$propertyName] = (array)$properties[$propertyName]; + + foreach($properties[$propertyName] as $propertyMatchString){ + + $propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString); + + // Escape the special character which is the delimiter. + $propertyPattern = str_replace('/', '\/', $propertyPattern); + + // Identify and extract the version. + preg_match('/'.$propertyPattern.'/is', $this->userAgent, $match); + + if(!empty($match[1])){ + $version = ( $type == 'float' ? $this->prepareVersionNo($match[1]) : $match[1] ); + return $version; + } + + } + + } + + return false; + + } + + function mobileGrade(){ + + $isMobile = $this->isMobile(); + + if( + // Apple iOS 3.2-5.1 - Tested on the original iPad (4.3 / 5.0), iPad 2 (4.3), iPad 3 (5.1), original iPhone (3.1), iPhone 3 (3.2), 3GS (4.3), 4 (4.3 / 5.0), and 4S (5.1) + $this->version('iPad')>=4.3 || + $this->version('iPhone')>=3.1 || + $this->version('iPod')>=3.1 || + + // Android 2.1-2.3 - Tested on the HTC Incredible (2.2), original Droid (2.2), HTC Aria (2.1), Google Nexus S (2.3). Functional on 1.5 & 1.6 but performance may be sluggish, tested on Google G1 (1.5) + // Android 3.1 (Honeycomb) - Tested on the Samsung Galaxy Tab 10.1 and Motorola XOOM + // Android 4.0 (ICS) - Tested on a Galaxy Nexus. Note: transition performance can be poor on upgraded devices + // Android 4.1 (Jelly Bean) - Tested on a Galaxy Nexus and Galaxy 7 + ( $this->version('Android')>2.1 && $this->is('Webkit') ) || + + // Windows Phone 7-7.5 - Tested on the HTC Surround (7.0) HTC Trophy (7.5), LG-E900 (7.5), Nokia Lumia 800 + $this->version('Windows Phone OS')>=7.0 || + + // Blackberry 7 - Tested on BlackBerryยฎ Torch 9810 + // Blackberry 6.0 - Tested on the Torch 9800 and Style 9670 + $this->version('BlackBerry')>=6.0 || + // Blackberry Playbook (1.0-2.0) - Tested on PlayBook + $this->match('Playbook.*Tablet') || + + // Palm WebOS (1.4-2.0) - Tested on the Palm Pixi (1.4), Pre (1.4), Pre 2 (2.0) + ( $this->version('webOS')>=1.4 && $this->match('Palm|Pre|Pixi') ) || + // Palm WebOS 3.0 - Tested on HP TouchPad + $this->match('hp.*TouchPad') || + + // Firefox Mobile (12 Beta) - Tested on Android 2.3 device + ( $this->is('Firefox') && $this->version('Firefox')>=12 ) || + + // Chrome for Android - Tested on Android 4.0, 4.1 device + ( $this->is('Chrome') && $this->is('AndroidOS') && $this->version('Android')>=4.0 ) || + + // Skyfire 4.1 - Tested on Android 2.3 device + ( $this->is('Skyfire') && $this->version('Skyfire')>=4.1 && $this->is('AndroidOS') && $this->version('Android')>=2.3 ) || + + // Opera Mobile 11.5-12: Tested on Android 2.3 + ( $this->is('Opera') && $this->version('Opera Mobi')>11 && $this->is('AndroidOS') ) || + + // Meego 1.2 - Tested on Nokia 950 and N9 + $this->is('MeeGoOS') || + + // Tizen (pre-release) - Tested on early hardware + $this->is('Tizen') || + + // Samsung Bada 2.0 - Tested on a Samsung Wave 3, Dolphin browser + // @todo: more tests here! + $this->is('Dolfin') && $this->version('Bada')>=2.0 || + + // UC Browser - Tested on Android 2.3 device + ( ($this->is('UC Browser') || $this->is('Dolfin')) && $this->version('Android')>=2.3 ) || + + // Kindle 3 and Fire - Tested on the built-in WebKit browser for each + ( $this->match('Kindle Fire') || + $this->is('Kindle') && $this->version('Kindle')>=3.0 ) || + + // Nook Color 1.4.1 - Tested on original Nook Color, not Nook Tablet + $this->is('AndroidOS') && $this->is('NookTablet') || + + // Chrome Desktop 11-21 - Tested on OS X 10.7 and Windows 7 + $this->version('Chrome')>=11 && !$isMobile || + + // Safari Desktop 4-5 - Tested on OS X 10.7 and Windows 7 + $this->version('Safari')>=5.0 && !$isMobile || + + // Firefox Desktop 4-13 - Tested on OS X 10.7 and Windows 7 + $this->version('Firefox')>=4.0 && !$isMobile || + + // Internet Explorer 7-9 - Tested on Windows XP, Vista and 7 + $this->version('MSIE')>=7.0 && !$isMobile || + + // Opera Desktop 10-12 - Tested on OS X 10.7 and Windows 7 + // @reference: http://my.opera.com/community/openweb/idopera/ + $this->version('Opera')>=10 && !$isMobile + + + ){ + return 'A'; + } + + if( + // Blackberry 5.0: Tested on the Storm 2 9550, Bold 9770 + $this->version('BlackBerry')>=5 && $this->version('BlackBerry')<6 || + + //Opera Mini (5.0-6.5) - Tested on iOS 3.2/4.3 and Android 2.3 + ( $this->version('Opera Mini')>=5.0 && $this->version('Opera Mini')<=6.5 && + ($this->version('Android')>=2.3 || $this->is('iOS')) ) || + + // Nokia Symbian^3 - Tested on Nokia N8 (Symbian^3), C7 (Symbian^3), also works on N97 (Symbian^1) + $this->match('NokiaN8|NokiaC7|N97.*Series60|Symbian/3') || + + // @todo: report this (tested on Nokia N71) + $this->version('Opera Mobi')>=11 && $this->is('SymbianOS') + + ){ + return 'B'; + } + + if( + // Blackberry 4.x - Tested on the Curve 8330 + $this->version('BlackBerry')<5.0 || + // Windows Mobile - Tested on the HTC Leo (WinMo 5.2) + $this->match('MSIEMobile|Windows CE.*Mobile') || $this->version('Windows Mobile')<=5.2 + + + ){ + + return 'C'; + + } + + // All older smartphone platforms and featurephones - Any device that doesn't support media queries will receive the basic, C grade experience + return 'C'; + + + } + + +} + +$detect = new Mobile_Detect; diff --git a/public/include/recaptchalib.php b/public/include/lib/recaptchalib.php similarity index 100% rename from public/include/recaptchalib.php rename to public/include/lib/recaptchalib.php diff --git a/public/include/lib/scrypt.php b/public/include/lib/scrypt.php new file mode 100644 index 00000000..3d501b7d --- /dev/null +++ b/public/include/lib/scrypt.php @@ -0,0 +1,536 @@ + 0 and a power of 2"); + } + if ($n > PHP_INT_MAX / 128 / $r) { + throw new Exception\InvalidArgumentException("Parameter n is too large"); + } + if ($r > PHP_INT_MAX / 128 / $p) { + throw new Exception\InvalidArgumentException("Parameter r is too large"); + } + + if (extension_loaded('Scrypt')) { + if ($length < 16) { + throw new Exception\InvalidArgumentException("Key length is too low, must be greater or equal to 16"); + } + return self::hex2bin(scrypt($password, $salt, $n, $r, $p, $length)); + } + + $b = Pbkdf2::calc('sha256', $password, $salt, 1, $p * 128 * $r); + + $s = ''; + for ($i = 0; $i < $p; $i++) { + $s .= self::scryptROMix(substr($b, $i * 128 * $r, 128 * $r), $n, $r); + } + + return Pbkdf2::calc('sha256', $password, $s, 1, $length); + } + + /** +* scryptROMix +* +* @param string $b +* @param integer $n +* @param integer $r +* @return string +* @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-4 +*/ + protected static function scryptROMix($b, $n, $r) + { + $x = $b; + $v = array(); + for ($i = 0; $i < $n; $i++) { + $v[$i] = $x; + $x = self::scryptBlockMix($x, $r); + } + for ($i = 0; $i < $n; $i++) { + $j = self::integerify($x) % $n; + $t = $x ^ $v[$j]; + $x = self::scryptBlockMix($t, $r); + } + return $x; + } + + /** +* scryptBlockMix +* +* @param string $b +* @param integer $r +* @return string +* @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-3 +*/ + protected static function scryptBlockMix($b, $r) + { + $x = substr($b, -64); + $even = ''; + $odd = ''; + $len = 2 * $r; + + for ($i = 0; $i < $len; $i++) { + if (PHP_INT_SIZE === 4) { + $x = self::salsa208Core32($x ^ substr($b, 64 * $i, 64)); + } else { + $x = self::salsa208Core64($x ^ substr($b, 64 * $i, 64)); + } + if ($i % 2 == 0) { + $even .= $x; + } else { + $odd .= $x; + } + } + return $even . $odd; + } + + /** +* Salsa 20/8 core (32 bit version) +* +* @param string $b +* @return string +* @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-2 +* @see http://cr.yp.to/salsa20.html +*/ + protected static function salsa208Core32($b) + { + $b32 = array(); + for ($i = 0; $i < 16; $i++) { + list(, $b32[$i]) = unpack("V", substr($b, $i * 4, 4)); + } + + $x = $b32; + for ($i = 0; $i < 8; $i += 2) { + $a = ($x[ 0] + $x[12]); + $x[ 4] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 4] + $x[ 0]); + $x[ 8] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 8] + $x[ 4]); + $x[12] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[12] + $x[ 8]); + $x[ 0] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[ 5] + $x[ 1]); + $x[ 9] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 9] + $x[ 5]); + $x[13] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[13] + $x[ 9]); + $x[ 1] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 1] + $x[13]); + $x[ 5] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[10] + $x[ 6]); + $x[14] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[14] + $x[10]); + $x[ 2] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 2] + $x[14]); + $x[ 6] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 6] + $x[ 2]); + $x[10] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[15] + $x[11]); + $x[ 3] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 3] + $x[15]); + $x[ 7] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 7] + $x[ 3]); + $x[11] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[11] + $x[ 7]); + $x[15] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[ 0] + $x[ 3]); + $x[ 1] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 1] + $x[ 0]); + $x[ 2] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 2] + $x[ 1]); + $x[ 3] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 3] + $x[ 2]); + $x[ 0] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[ 5] + $x[ 4]); + $x[ 6] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[ 6] + $x[ 5]); + $x[ 7] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 7] + $x[ 6]); + $x[ 4] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 4] + $x[ 7]); + $x[ 5] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[10] + $x[ 9]); + $x[11] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[11] + $x[10]); + $x[ 8] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[ 8] + $x[11]); + $x[ 9] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[ 9] + $x[ 8]); + $x[10] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + $a = ($x[15] + $x[14]); + $x[12] ^= ($a << 7) | ($a >> 25) & 0x7f; + $a = ($x[12] + $x[15]); + $x[13] ^= ($a << 9) | ($a >> 23) & 0x1ff; + $a = ($x[13] + $x[12]); + $x[14] ^= ($a << 13) | ($a >> 19) & 0x1fff; + $a = ($x[14] + $x[13]); + $x[15] ^= ($a << 18) | ($a >> 14) & 0x3ffff; + } + for ($i = 0; $i < 16; $i++) { + $b32[$i] = $b32[$i] + $x[$i]; + } + $result = ''; + for ($i = 0; $i < 16; $i++) { + $result .= pack("V", $b32[$i]); + } + + return $result; + } + + /** +* Salsa 20/8 core (64 bit version) +* +* @param string $b +* @return string +* @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-2 +* @see http://cr.yp.to/salsa20.html +*/ + protected static function salsa208Core64($b) + { + $b32 = array(); + for ($i = 0; $i < 16; $i++) { + list(, $b32[$i]) = unpack("V", substr($b, $i * 4, 4)); + } + + $x = $b32; + for ($i = 0; $i < 8; $i += 2) { + $a = ($x[ 0] + $x[12]) & 0xffffffff; + $x[ 4] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 4] + $x[ 0]) & 0xffffffff; + $x[ 8] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 8] + $x[ 4]) & 0xffffffff; + $x[12] ^= ($a << 13) | ($a >> 19); + $a = ($x[12] + $x[ 8]) & 0xffffffff; + $x[ 0] ^= ($a << 18) | ($a >> 14); + $a = ($x[ 5] + $x[ 1]) & 0xffffffff; + $x[ 9] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 9] + $x[ 5]) & 0xffffffff; + $x[13] ^= ($a << 9) | ($a >> 23); + $a = ($x[13] + $x[ 9]) & 0xffffffff; + $x[ 1] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 1] + $x[13]) & 0xffffffff; + $x[ 5] ^= ($a << 18) | ($a >> 14); + $a = ($x[10] + $x[ 6]) & 0xffffffff; + $x[14] ^= ($a << 7) | ($a >> 25); + $a = ($x[14] + $x[10]) & 0xffffffff; + $x[ 2] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 2] + $x[14]) & 0xffffffff; + $x[ 6] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 6] + $x[ 2]) & 0xffffffff; + $x[10] ^= ($a << 18) | ($a >> 14); + $a = ($x[15] + $x[11]) & 0xffffffff; + $x[ 3] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 3] + $x[15]) & 0xffffffff; + $x[ 7] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 7] + $x[ 3]) & 0xffffffff; + $x[11] ^= ($a << 13) | ($a >> 19); + $a = ($x[11] + $x[ 7]) & 0xffffffff; + $x[15] ^= ($a << 18) | ($a >> 14); + $a = ($x[ 0] + $x[ 3]) & 0xffffffff; + $x[ 1] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 1] + $x[ 0]) & 0xffffffff; + $x[ 2] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 2] + $x[ 1]) & 0xffffffff; + $x[ 3] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 3] + $x[ 2]) & 0xffffffff; + $x[ 0] ^= ($a << 18) | ($a >> 14); + $a = ($x[ 5] + $x[ 4]) & 0xffffffff; + $x[ 6] ^= ($a << 7) | ($a >> 25); + $a = ($x[ 6] + $x[ 5]) & 0xffffffff; + $x[ 7] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 7] + $x[ 6]) & 0xffffffff; + $x[ 4] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 4] + $x[ 7]) & 0xffffffff; + $x[ 5] ^= ($a << 18) | ($a >> 14); + $a = ($x[10] + $x[ 9]) & 0xffffffff; + $x[11] ^= ($a << 7) | ($a >> 25); + $a = ($x[11] + $x[10]) & 0xffffffff; + $x[ 8] ^= ($a << 9) | ($a >> 23); + $a = ($x[ 8] + $x[11]) & 0xffffffff; + $x[ 9] ^= ($a << 13) | ($a >> 19); + $a = ($x[ 9] + $x[ 8]) & 0xffffffff; + $x[10] ^= ($a << 18) | ($a >> 14); + $a = ($x[15] + $x[14]) & 0xffffffff; + $x[12] ^= ($a << 7) | ($a >> 25); + $a = ($x[12] + $x[15]) & 0xffffffff; + $x[13] ^= ($a << 9) | ($a >> 23); + $a = ($x[13] + $x[12]) & 0xffffffff; + $x[14] ^= ($a << 13) | ($a >> 19); + $a = ($x[14] + $x[13]) & 0xffffffff; + $x[15] ^= ($a << 18) | ($a >> 14); + } + for ($i = 0; $i < 16; $i++) { + $b32[$i] = ($b32[$i] + $x[$i]) & 0xffffffff; + } + $result = ''; + for ($i = 0; $i < 16; $i++) { + $result .= pack("V", $b32[$i]); + } + + return $result; + } + + /** +* Integerify +* +* Integerify (B[0] ... B[2 * r - 1]) is defined as the result +* of interpreting B[2 * r - 1] as a little-endian integer. +* Each block B is a string of 64 bytes. +* +* @param string $b +* @return integer +* @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-4 +*/ + protected static function integerify($b) + { + $v = 'v'; + if (PHP_INT_SIZE === 8) { + $v = 'V'; + } + list(,$n) = unpack($v, substr($b, -64)); + return $n; + } + + /** +* Convert hex string in a binary string +* +* @param string $hex +* @return string +*/ + protected static function hex2bin($hex) + { + if (version_compare(PHP_VERSION, '5.4') >= 0) { + return hex2bin($hex); + } + $len = strlen($hex); + $result = ''; + for ($i = 0; $i < $len; $i+=2) { + $result .= chr(hexdec($hex[$i] . $hex[$i+1])); + } + return $result; + } +} + + + +/** +* Zend Framework (http://framework.zend.com/) +* +* @link http://github.com/zendframework/zf2 for the canonical source repository +* @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com) +* @license http://framework.zend.com/license/new-bsd New BSD License +*/ + + + + + +/** +* PKCS #5 v2.0 standard RFC 2898 +*/ +class Pbkdf2 +{ + /** +* Generate the new key +* +* @param string $hash The hash algorithm to be used by HMAC +* @param string $password The source password/key +* @param string $salt +* @param integer $iterations The number of iterations +* @param integer $length The output size +* @throws Exception\InvalidArgumentException +* @return string +*/ + public static function calc($hash, $password, $salt, $iterations, $length) + { + if (!Hmac::isSupported($hash)) { + throw new Exception\InvalidArgumentException("The hash algorithm $hash is not supported by " . __CLASS__); + } + + $num = ceil($length / Hmac::getOutputSize($hash, Hmac::OUTPUT_BINARY)); + $result = ''; + for ($block = 1; $block <= $num; $block++) { + $hmac = hash_hmac($hash, $salt . pack('N', $block), $password, Hmac::OUTPUT_BINARY); + $mix = $hmac; + for ($i = 1; $i < $iterations; $i++) { + $hmac = hash_hmac($hash, $hmac, $password, Hmac::OUTPUT_BINARY); + $mix ^= $hmac; + } + $result .= $mix; + } + return substr($result, 0, $length); + } +} + + + +/** +* Zend Framework (http://framework.zend.com/) +* +* @link http://github.com/zendframework/zf2 for the canonical source repository +* @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com) +* @license http://framework.zend.com/license/new-bsd New BSD License +*/ + + + +/** +* PHP implementation of the RFC 2104 Hash based Message Authentication Code +*/ +class Hmac +{ + const OUTPUT_STRING = false; + const OUTPUT_BINARY = true; + + /** +* Last algorithm supported +* +* @var string|null +*/ + protected static $lastAlgorithmSupported; + + /** +* Performs a HMAC computation given relevant details such as Key, Hashing +* algorithm, the data to compute MAC of, and an output format of String, +* or Binary. +* +* @param string $key +* @param string $hash +* @param string $data +* @param bool $output +* @throws Exception\InvalidArgumentException +* @return string +*/ + public static function compute($key, $hash, $data, $output = self::OUTPUT_STRING) + { + + if (empty($key)) { + throw new Exception\InvalidArgumentException('Provided key is null or empty'); + } + + if (!$hash || ($hash !== static::$lastAlgorithmSupported && !static::isSupported($hash))) { + throw new Exception\InvalidArgumentException( + "Hash algorithm is not supported on this PHP installation; provided '{$hash}'" + ); + } + + return hash_hmac($hash, $data, $key, $output); + } + + /** +* Get the output size according to the hash algorithm and the output format +* +* @param string $hash +* @param bool $output +* @return integer +*/ + public static function getOutputSize($hash, $output = self::OUTPUT_STRING) + { + return strlen(static::compute('key', $hash, 'data', $output)); + } + + /** +* Get the supported algorithm +* +* @return array +*/ + public static function getSupportedAlgorithms() + { + return hash_algos(); + } + + /** +* Is the hash algorithm supported? +* +* @param string $algorithm +* @return bool +*/ + public static function isSupported($algorithm) + { + if ($algorithm === static::$lastAlgorithmSupported) { + return true; + } + + if (in_array(strtolower($algorithm), hash_algos(), true)) { + static::$lastAlgorithmSupported = $algorithm; + return true; + } + + return false; + } + + /** +* Clear the cache of last algorithm supported +*/ + public static function clearLastAlgorithmCache() + { + static::$lastAlgorithmSupported = null; + } +} + function swapEndian($input){ + $output = ""; + for($i=0;$i< strlen($input);$i+=2){ + $output .= substr($input, -($i+2), 2); + + + } + return $output; + + + } + + +/*for($i=0;$i < 200;$i++){ + $value = Scrypt::calc($i, $i, 1024, 1, 1, 32); + echo "scrypt ".$i." hash:". bin2hex($value)."
    "; +}*/ +/* +$i = pack("H*", "01000000f615f7ce3b4fc6b8f61e8f89aedb1d0852507650533a9e3b10b9bbcc30639f279fcaa86746e1ef52d3edb3c4ad8259920d509bd073605c9bf1d59983752a6b06b817bb4ea78e011d012d59d4"); + +$value = Scrypt::calc($i, $i, 1024, 1, 1, 32); + echo "scrypt ".$i." hash:". bin2hex($value)."
    "; + print_r( swapEndian(bin2hex($value))); + */ + + +// Function used for pushpoold solution checks +function word_reverse($str) { + $ret = ''; + while (strlen($str) > 0) { + $ret .= substr($str, -8, 8); + $str = substr($str, 0, -8); + } + return $ret; +} + +?> diff --git a/public/include/pages/about/api.inc.php b/public/include/pages/about/api.inc.php new file mode 100644 index 00000000..d0eb55e7 --- /dev/null +++ b/public/include/pages/about/api.inc.php @@ -0,0 +1,8 @@ +assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/about/donors.inc.php b/public/include/pages/about/donors.inc.php new file mode 100644 index 00000000..e4ff8753 --- /dev/null +++ b/public/include/pages/about/donors.inc.php @@ -0,0 +1,11 @@ +getDonations(); + +// Tempalte specifics +$smarty->assign("DONORS", $aDonors); +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/account/confirm.inc.php b/public/include/pages/account/confirm.inc.php new file mode 100644 index 00000000..3611c5de --- /dev/null +++ b/public/include/pages/account/confirm.inc.php @@ -0,0 +1,17 @@ + 'Missing token', 'TYPE' => 'errormsg'); +} else if (!$aToken = $oToken->getToken($_GET['token'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to activate your account. Invalid token', 'TYPE' => 'errormsg'); +} else { + $user->changeLocked($aToken['account_id']); + $oToken->deleteToken($aToken['token']); + $_SESSION['POPUP'][] = array('CONTENT' => 'Account activated. Please login.'); +} +$smarty->assign('CONTENT', 'default.tpl'); +?> diff --git a/public/include/pages/account/edit.inc.php b/public/include/pages/account/edit.inc.php index 5b717cee..006f9862 100644 --- a/public/include/pages/account/edit.inc.php +++ b/public/include/pages/account/edit.inc.php @@ -10,57 +10,29 @@ if ($user->isAuthenticated()) { } 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'); + if ($setting->getValue('disable_mp') == 1) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Manual payouts are disabled.', 'TYPE' => 'info'); } else { - $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); + if (!$oPayout->isPayoutActive($_SESSION['USERDATA']['id'])) { + if ($iPayoutId = $oPayout->createPayout($_SESSION['USERDATA']['id'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Created new manual payout request with ID #' . $iPayoutId); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to create manual payout request.', 'TYPE' => 'errormsg'); } } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service', 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'You already have one active manual payout request.', 'TYPE' => 'errormsg'); } } else { - $_SESSION['POPUP'][] = array('CONTENT' => 'Insufficient funds, you need more than ' . $config['txfee'] . ' ' . $conifg['currency'] . ' to cover transaction fees', 'TYPE' => 'errormsg'); + $_SESSION['POPUP'][] = array('CONTENT' => 'Insufficient funds, you need more than ' . $config['txfee'] . ' ' . $config['currency'] . ' to cover transaction fees', 'TYPE' => 'errormsg'); } - $setting->setValue('manual_payout_active', 0); } break; case 'updateAccount': - if ($user->updateAccount($_SESSION['USERDATA']['id'], $_POST['paymentAddress'], $_POST['payoutThreshold'], $_POST['donatePercent'], $_POST['email'])) { + if ($user->updateAccount($_SESSION['USERDATA']['id'], $_POST['paymentAddress'], $_POST['payoutThreshold'], $_POST['donatePercent'], $_POST['email'], $_POST['is_anonymous'])) { $_SESSION['POPUP'][] = array('CONTENT' => 'Account details updated', 'TYPE' => 'success'); } else { $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to update your account: ' . $user->getError(), 'TYPE' => 'errormsg'); diff --git a/public/include/pages/account/invitations.inc.php b/public/include/pages/account/invitations.inc.php new file mode 100644 index 00000000..b12e2d4e --- /dev/null +++ b/public/include/pages/account/invitations.inc.php @@ -0,0 +1,25 @@ +isAuthenticated()) { + if (!$setting->getValue('disable_invitations')) { + if ($invitation->getCountInvitations($_SESSION['USERDATA']['id']) >= $config['accounts']['invitations']['count']) { + $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded the allowed invitations of ' . $config['accounts']['invitations']['count'], 'TYPE' => 'errormsg'); + } else if (isset($_POST['do']) && $_POST['do'] == 'sendInvitation') { + if ($invitation->sendInvitation($_SESSION['USERDATA']['id'], $_POST['data'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Invitation sent'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to send invitation to recipient: ' . $invitation->getError(), 'TYPE' => 'errormsg'); + } + } + $aInvitations = $invitation->getInvitations($_SESSION['USERDATA']['id']); + $smarty->assign('INVITATIONS', $aInvitations); + } else { + $aInvitations = array(); + $_SESSION['POPUP'][] = array('CONTENT' => 'Invitations are disabled', 'TYPE' => 'errormsg'); + } +} +$smarty->assign('CONTENT', 'default.tpl'); +?> diff --git a/public/include/pages/account/notifications.inc.php b/public/include/pages/account/notifications.inc.php index 3f013ed0..a454fe12 100644 --- a/public/include/pages/account/notifications.inc.php +++ b/public/include/pages/account/notifications.inc.php @@ -3,23 +3,28 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); if ($user->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'); + if ($setting->getValue('disable_notifications') == 1) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Notification system disabled by admin.', 'TYPE' => 'info'); + $smarty->assign('CONTENT', 'empty'); + } else { + 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'); } - - // 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/qrcode.inc.php b/public/include/pages/account/qrcode.inc.php new file mode 100644 index 00000000..a6543dc5 --- /dev/null +++ b/public/include/pages/account/qrcode.inc.php @@ -0,0 +1,6 @@ +isAuthenticated()) $smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/account/reset_failed.inc.php b/public/include/pages/account/reset_failed.inc.php new file mode 100644 index 00000000..ef30938f --- /dev/null +++ b/public/include/pages/account/reset_failed.inc.php @@ -0,0 +1,13 @@ +isAuthenticated()) { + // Reset failed login counter + $user->setUserFailed($_SESSION['USERDATA']['id'], 0); + header("Location: " . $_SERVER['HTTP_REFERER']); +} +// Somehow we still need to load this empty template +$smarty->assign("CONTENT", "empty"); +?> diff --git a/public/include/pages/account/transactions.inc.php b/public/include/pages/account/transactions.inc.php index db927ca7..a06eae61 100644 --- a/public/include/pages/account/transactions.inc.php +++ b/public/include/pages/account/transactions.inc.php @@ -3,9 +3,19 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); if ($user->isAuthenticated()) { - $aTransactions = $transaction->getTransactions($_SESSION['USERDATA']['id']); + $iLimit = 30; + empty($_REQUEST['start']) ? $start = 0 : $start = $_REQUEST['start']; + $aTransactions = $transaction->getTransactions($start, @$_REQUEST['filter'], $iLimit, $_SESSION['USERDATA']['id']); + $aTransactionSummary = $transaction->getTransactionSummary($_SESSION['USERDATA']['id']); + $iCountTransactions = $transaction->num_rows; + $aTransactionTypes = $transaction->getTypes(); if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); + $smarty->assign('LIMIT', $iLimit); $smarty->assign('TRANSACTIONS', $aTransactions); - $smarty->assign('CONTENT', 'default.tpl'); + $smarty->assign('SUMMARY', $aTransactionSummary); + $smarty->assign('TRANSACTIONTYPES', $aTransactionTypes); + $smarty->assign('TXSTATUS', array('' => '', 'Confirmed' => 'Confirmed', 'Unconfirmed' => 'Unconfirmed', 'Orphan' => 'Orphan')); + $smarty->assign('COUNTTRANSACTIONS', $iCountTransactions); } +$smarty->assign('CONTENT', 'default.tpl'); ?> diff --git a/public/include/pages/account/workers.inc.php b/public/include/pages/account/workers.inc.php index ccdae2b8..cb0711b1 100644 --- a/public/include/pages/account/workers.inc.php +++ b/public/include/pages/account/workers.inc.php @@ -1,5 +1,4 @@ isAuthenticated()) { } break; case 'update': - if ($worker->updateWorkers($_SESSION['USERDATA']['id'], $_POST['data'])) { + if ($worker->updateWorkers($_SESSION['USERDATA']['id'], @$_POST['data'])) { $_SESSION['POPUP'][] = array('CONTENT' => 'Worker updated'); } else { $_SESSION['POPUP'][] = array('CONTENT' => $worker->getError(), 'TYPE' => 'errormsg'); diff --git a/public/include/pages/admin/monitoring.inc.php b/public/include/pages/admin/monitoring.inc.php new file mode 100644 index 00000000..b46908db --- /dev/null +++ b/public/include/pages/admin/monitoring.inc.php @@ -0,0 +1,43 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +// Default crons to monitor +$aCrons = array('statistics','auto_payout','manual_payout','archive_cleanup','blockupdate','findblock','notifications','tickerupdate'); + +// Special cases, only add them if activated +switch ($config['payout_system']) { +case 'pplns': + $aCrons[] = $config['payout_system'] . '_payout'; + break; +case 'pps': + $aCrons[] = $config['payout_system'] . '_payout'; + break; +case 'prop': + $aCrons[] = 'proportional_payout'; + break; +} + +// Data array for template +foreach ($aCrons as $strCron) { + $aCronStatus[$strCron] = array( + 'exit' => $monitoring->getStatus($strCron . '_status'), + 'active' => $monitoring->getStatus($strCron . '_active'), + 'runtime' => $monitoring->getStatus($strCron . '_runtime'), + 'starttime' => $monitoring->getStatus($strCron . '_starttime'), + 'endtime' => $monitoring->getStatus($strCron . '_endtime'), + 'message' => $monitoring->getStatus($strCron . '_message'), + ); +} +$smarty->assign("CRONSTATUS", $aCronStatus); + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/admin/news.inc.php b/public/include/pages/admin/news.inc.php new file mode 100644 index 00000000..df0bb2fc --- /dev/null +++ b/public/include/pages/admin/news.inc.php @@ -0,0 +1,43 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +// Include markdown library +use \Michelf\Markdown; + +if (@$_REQUEST['do'] == 'toggle_active') + if ($news->toggleActive($_REQUEST['id'])) + $_SESSION['POPUP'][] = array('CONTENT' => 'News entry changed', 'TYPE' => 'success'); + +if (@$_REQUEST['do'] == 'add') { + if ($news->addNews($_SESSION['USERDATA']['id'], $_POST['data'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'News entry added', 'TYPE' => 'success'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to add new entry: ' . $news->getError(), 'TYPE' => 'errormsg'); + } +} + +if (@$_REQUEST['do'] == 'delete') { + if ($news->deleteNews((int)$_REQUEST['id'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Succesfully removed news entry', 'TYPE' => 'success'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'Failed to delete entry: ' . $news->getError(), 'TYPE' => 'errormsg'); + } +} + +// Fetch all news +$aNews = $news->getAll(); +foreach ($aNews as $key => $aData) { + // Transform Markdown content to HTML + $aNews[$key]['content'] = Markdown::defaultTransform($aData['content']); +} +$smarty->assign("NEWS", $aNews); +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/admin/news_edit.inc.php b/public/include/pages/admin/news_edit.inc.php new file mode 100644 index 00000000..81cfcb56 --- /dev/null +++ b/public/include/pages/admin/news_edit.inc.php @@ -0,0 +1,27 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +// Include markdown library +use \Michelf\Markdown; + +if (@$_REQUEST['do'] == 'save') { + if ($news->updateNews($_REQUEST['id'], $_REQUEST['header'], $_REQUEST['content'], $_REQUEST['active'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'News updated', 'TYPE' => 'success'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'News update failed: ' . $news->getError(), 'TYPE' => 'errormsg'); + } +} + +// Fetch news entry +$aNews = $news->getEntry($_REQUEST['id']); +$smarty->assign("NEWS", $aNews); +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/admin/settings.inc.php b/public/include/pages/admin/settings.inc.php new file mode 100644 index 00000000..e5dc124e --- /dev/null +++ b/public/include/pages/admin/settings.inc.php @@ -0,0 +1,27 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +if (@$_REQUEST['do'] == 'save' && !empty($_REQUEST['data'])) { + foreach($_REQUEST['data'] as $var => $value) { + $setting->setValue($var, $value); + } + $_SESSION['POPUP'][] = array('CONTENT' => 'Settings updated'); +} + +// Load our available settings from configuration +require_once(INCLUDE_DIR . '/config/admin_settings.inc.php'); + +// Load onto the template +$smarty->assign("SETTINGS", $aSettings); + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); +?> diff --git a/public/include/pages/admin/transactions.inc.php b/public/include/pages/admin/transactions.inc.php new file mode 100644 index 00000000..5b544da6 --- /dev/null +++ b/public/include/pages/admin/transactions.inc.php @@ -0,0 +1,41 @@ +isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { + header("HTTP/1.1 404 Page not found"); + die("404 Page not found"); +} + +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $iLimit = 30; + $debug->append('No cached version available, fetching from backend', 3); + empty($_REQUEST['start']) ? $start = 0 : $start = $_REQUEST['start']; + $aTransactions = $transaction->getTransactions($start, @$_REQUEST['filter'], $iLimit); + $aTransactionSummary = $transaction->getTransactionSummary(); + $iCountTransactions = $transaction->num_rows; + $aTransactionTypes = $transaction->getTypes(); + if (!$aTransactions) $_SESSION['POPUP'][] = array('CONTENT' => 'Could not find any transaction', 'TYPE' => 'errormsg'); + $smarty->assign('LIMIT', $iLimit); + $smarty->assign('TRANSACTIONS', $aTransactions); + $smarty->assign('SUMMARY', $aTransactionSummary); + $smarty->assign('TRANSACTIONTYPES', $aTransactionTypes); + $smarty->assign('TXSTATUS', array('' => '', 'Confirmed' => 'Confirmed', 'Unconfirmed' => 'Unconfirmed', 'Orphan' => 'Orphan')); + $smarty->assign('COUNTTRANSACTIONS', $iCountTransactions); +} else { + $debug->append('Using cached page', 3); +} + +// Gernerate the GET URL for filters +if (isset($_REQUEST['filter'])) { + $strFilters = ''; + foreach (@$_REQUEST['filter'] as $filter => $value) { + $filter = "filter[$filter]"; + $strFilters .= "&$filter=$value"; + } + $smarty->assign('FILTERS', $strFilters); +} +$smarty->assign('CONTENT', 'default.tpl'); +?> diff --git a/public/include/pages/admin/user.inc.php b/public/include/pages/admin/user.inc.php index 9bb9ecee..bc562bbf 100644 --- a/public/include/pages/admin/user.inc.php +++ b/public/include/pages/admin/user.inc.php @@ -11,16 +11,19 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { $aRoundShares = $statistics->getRoundShares(); -// Change account lock -if (@$_POST['do'] == 'lock') { +switch (@$_POST['do']) { +case 'lock': $supress_master = 1; $user->changeLocked($_POST['account_id']); -} - -// Change account admin -if (@$_POST['do'] == 'admin') { + break; +case 'fee': + $supress_master = 1; + $user->changeNoFee($_POST['account_id']); + break; +case 'admin': $supress_master = 1; $user->changeAdmin($_POST['account_id']); + break; } if (@$_POST['query']) { @@ -34,10 +37,17 @@ if (@$_POST['query']) { $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); + if ($aUser['shares'] > 0) { + $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); + } else { + $aUser['payout']['est_block'] = 0; + $aUser['payout']['est_fee'] = 0; + $aUser['payout']['est_donation'] = 0; + $aUser['payout']['est_payout'] = 0; + } $aUsers[$iKey] = $aUser; } // Assign our variables diff --git a/public/include/pages/admin/wallet.inc.php b/public/include/pages/admin/wallet.inc.php index 45ff5af4..1e2d2659 100644 --- a/public/include/pages/admin/wallet.inc.php +++ b/public/include/pages/admin/wallet.inc.php @@ -9,16 +9,44 @@ if (!$user->isAuthenticated() || !$user->isAdmin($_SESSION['USERDATA']['id'])) { die("404 Page not found"); } -if ($bitcoin->can_connect() === true){ - $dBalance = $bitcoin->query('getbalance'); +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + if ($bitcoin->can_connect() === true){ + $dBalance = $bitcoin->query('getbalance'); + $dGetInfo = $bitcoin->query('getinfo'); + if (is_array($dGetInfo) && array_key_exists('newmint', $dGetInfo)) { + $dNewmint = $dGetInfo['newmint']; + } else { + $dNewmint = -1; + } + } else { + $dBalance = 0; + $dNewmint = -1; + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to wallet RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + } + // Fetch unconfirmed amount from blocks table + empty($config['network_confirmations']) ? $confirmations = 120 : $confirmations = $config['network_confirmations']; + $aBlocksUnconfirmed = $block->getAllUnconfirmed($confirmations); + $dBlocksUnconfirmedBalance = 0; + if (!empty($aBlocksUnconfirmed)) + foreach ($aBlocksUnconfirmed as $aData) $dBlocksUnconfirmedBalance += $aData['amount']; + + // Fetch locked balance from transactions + $dLockedBalance = $transaction->getLockedBalance(); + + // Cold wallet balance + if (! $dColdCoins = $setting->getValue('wallet_cold_coins')) $dColdCoins = 0; } else { - $dBalance = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $debug->append('Using cached page', 3); } +$smarty->assign("UNCONFIRMED", $dBlocksUnconfirmedBalance); $smarty->assign("BALANCE", $dBalance); -$smarty->assign("LOCKED", $transaction->getLockedBalance()); +$smarty->assign("COLDCOINS", $dColdCoins); +$smarty->assign("LOCKED", $dLockedBalance); +$smarty->assign("NEWMINT", $dNewmint); // Tempalte specifics $smarty->assign("CONTENT", "default.tpl"); + ?> diff --git a/public/include/pages/api.inc.php b/public/include/pages/api.inc.php index c2e64a60..c85ada42 100644 --- a/public/include/pages/api.inc.php +++ b/public/include/pages/api.inc.php @@ -1,8 +1,10 @@ isActive(); // Check for valid API key $id = $user->checkApiKey($_REQUEST['api_key']); diff --git a/public/include/pages/api/getblockcount.inc.php b/public/include/pages/api/getblockcount.inc.php index 2cbd06a5..978a7142 100644 --- a/public/include/pages/api/getblockcount.inc.php +++ b/public/include/pages/api/getblockcount.inc.php @@ -1,23 +1,22 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); if ($bitcoin->can_connect() === true){ - if (!$iBlock = $memcache->get('iBlock')) { - $iBlock = $bitcoin->query('getblockcount'); - $memcache->set('iBlock', $iBlock); - } + $iBlock = $bitcoin->getblockcount(); } else { $iBlock = 0; } // Output JSON format -echo json_encode(array('getblockcount' => $iBlock)); +echo $api->get_json($iBlock); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getblocksfound.inc.php b/public/include/pages/api/getblocksfound.inc.php index 00883dad..f4770278 100644 --- a/public/include/pages/api/getblocksfound.inc.php +++ b/public/include/pages/api/getblocksfound.inc.php @@ -1,19 +1,19 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); -// Set a sane limit, overwrite with URL parameter -$iLimit = 10; -if (@$_REQUEST['limit']) - $iLimit = $_REQUEST['limit']; +// Check how many blocks to fetch +$setting->getValue('statistics_block_count') ? $iLimit = $setting->getValue('statistics_block_count') : $iLimit = 20; // Output JSON format -echo json_encode(array('getblocksfound' => $statistics->getBlocksFound($iLimit))); +echo $api->get_json($statistics->getBlocksFound($iLimit)); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getcurrentworkers.inc.php b/public/include/pages/api/getcurrentworkers.inc.php index 4e26cc1c..837b75ef 100644 --- a/public/include/pages/api/getcurrentworkers.inc.php +++ b/public/include/pages/api/getcurrentworkers.inc.php @@ -1,14 +1,16 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); // Output JSON format -echo json_encode(array('getcurrentworkers' => $worker->getCountAllActiveWorkers())); +echo $api->get_json($worker->getCountAllActiveWorkers()); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getdashboarddata.inc.php b/public/include/pages/api/getdashboarddata.inc.php new file mode 100644 index 00000000..9eac5d46 --- /dev/null +++ b/public/include/pages/api/getdashboarddata.inc.php @@ -0,0 +1,57 @@ +isActive(); + +// Check user token and access level permissions +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Fetch RPC information +if ($bitcoin->can_connect() === true) { + $dNetworkHashrate = $bitcoin->getnetworkhashps(); + $dDifficulty = $bitcoin->getdifficulty(); + $iBlock = $bitcoin->getblockcount(); +} else { + $dNetworkHashrate = 0; + $dDifficulty = 1; + $iBlock = 0; +} + +// Some settings +if ( ! $interval = $setting->getValue('statistics_ajax_data_interval')) $interval = 300; +if ( ! $dPoolHashrateModifier = $setting->getValue('statistics_pool_hashrate_modifier') ) $dPoolHashrateModifier = 1; +if ( ! $dPersonalHashrateModifier = $setting->getValue('statistics_personal_hashrate_modifier') ) $dPersonalHashrateModifier = 1; +if ( ! $dNetworkHashrateModifier = $setting->getValue('statistics_network_hashrate_modifier') ) $dNetworkHashrateModifier = 1; + +// Fetch raw data +$statistics->setGetCache(false); +$dPoolHashrate = $statistics->getCurrentHashrate($interval); +if ($dPoolHashrate > $dNetworkHashrate) $dNetworkHashrate = $dPoolHashrate; +$dPersonalHashrate = $statistics->getUserHashrate($user_id, $interval); +$dPersonalSharerate = $statistics->getUserSharerate($user_id, $interval); +$statistics->setGetCache(true); + +// Use caches for this one +$aUserRoundShares = $statistics->getUserShares($user_id); +$aRoundShares = $statistics->getRoundShares(); + +// Apply pool modifiers +$dPersonalHashrateAdjusted = $dPersonalHashrate * $dPersonalHashrateModifier; +$dPoolHashrateAdjusted = $dPoolHashrate * $dPoolHashrateModifier; +$dNetworkHashrateAdjusted = $dNetworkHashrate / 1000 * $dNetworkHashrateModifier; + +// Output JSON format +$data = array( + 'raw' => array( 'personal' => array( 'hashrate' => $dPersonalHashrate ), 'pool' => array( 'hashrate' => $dPoolHashrate ), 'network' => array( 'hashrate' => $dNetworkHashrate / 1000 ) ), + 'personal' => array ( 'hashrate' => $dPersonalHashrateAdjusted, 'sharerate' => $dPersonalSharerate, 'shares' => $aUserRoundShares, 'balance' => $transaction->getBalance($user_id)), + 'pool' => array( 'hashrate' => $dPoolHashrateAdjusted, 'shares' => $aRoundShares ), + 'network' => array( 'hashrate' => $dNetworkHashrateAdjusted, 'difficulty' => $dDifficulty, 'block' => $iBlock ), +); +echo $api->get_json($data); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getdifficulty.inc.php b/public/include/pages/api/getdifficulty.inc.php index 9d2aa7a2..95805548 100644 --- a/public/include/pages/api/getdifficulty.inc.php +++ b/public/include/pages/api/getdifficulty.inc.php @@ -1,24 +1,19 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); -// Fetch data from litecoind -if ($bitcoin->can_connect() === true){ - if (!$dDifficulty = $memcache->get('dDifficulty')) { - $memcache->set('dDifficulty', $dDifficulty); - $dDifficulty = $bitcoin->query('getdifficulty'); - } -} else { - $iDifficulty = 1; -} +// Fetch data from wallet +$bitcoin->can_connect() === true ? $dDifficulty = $bitcoin->getdifficulty() : $iDifficulty = 1; // Output JSON format -echo json_encode(array('getdifficulty' => $dDifficulty)); +echo $api->get_json($dDifficulty); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getestimatedtime.inc.php b/public/include/pages/api/getestimatedtime.inc.php index a48393fa..91ed811d 100644 --- a/public/include/pages/api/getestimatedtime.inc.php +++ b/public/include/pages/api/getestimatedtime.inc.php @@ -1,17 +1,20 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); // Estimated time to find the next block $iCurrentPoolHashrate = $statistics->getCurrentHashrate() * 1000; +$bitcoin->can_connect() === true ? $dEstimatedTime = $bitcoin->getestimatedtime($iCurrentPoolHashrate) : $dEstimatedTime = 0; // Output JSON format -echo json_encode(array('getestimatedtime' => $bitcoin->getestimatedtime($iCurrentPoolHashrate))); +echo $api->get_json($dEstimatedTime); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/gethourlyhashrates.inc.php b/public/include/pages/api/gethourlyhashrates.inc.php new file mode 100644 index 00000000..54112325 --- /dev/null +++ b/public/include/pages/api/gethourlyhashrates.inc.php @@ -0,0 +1,22 @@ +isActive(); + +// Check user token +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Output JSON format +$data = array( + 'mine' => $statistics->getHourlyHashrateByAccount($id), + 'pool' => $statistics->getHourlyHashrateByPool() +); + +echo $api->json($data); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getpoolhashrate.inc.php b/public/include/pages/api/getpoolhashrate.inc.php index 6f6763ec..a5985d44 100644 --- a/public/include/pages/api/getpoolhashrate.inc.php +++ b/public/include/pages/api/getpoolhashrate.inc.php @@ -1,14 +1,23 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Fetch settings +if ( ! $interval = $setting->getValue('statistics_ajax_data_interval')) $interval = 300; // Output JSON format -echo json_encode(array('getpoolhashrate' => $statistics->getCurrentHashrate())); +$statistics->setGetCache(false); +$dPoolHashrate = $statistics->getCurrentHashrate($interval); +$statistics->setGetCache(true); + +echo $api->get_json($dPoolHashrate); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getpoolsharerate.inc.php b/public/include/pages/api/getpoolsharerate.inc.php index 8e9117f1..a55654be 100644 --- a/public/include/pages/api/getpoolsharerate.inc.php +++ b/public/include/pages/api/getpoolsharerate.inc.php @@ -1,11 +1,16 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Fetch settings +if ( ! $interval = $setting->getValue('statistics_ajax_data_interval')) $interval = 300; // Output JSON format echo json_encode(array('getpoolsharerate' => $statistics->getCurrentShareRate())); diff --git a/public/include/pages/api/getpoolstatus.inc.php b/public/include/pages/api/getpoolstatus.inc.php new file mode 100644 index 00000000..00aab39c --- /dev/null +++ b/public/include/pages/api/getpoolstatus.inc.php @@ -0,0 +1,67 @@ +isActive(); + +// Check user token +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Fetch last block information +$aLastBlock = $block->getLast(); + +// Efficiency +$aShares = $statistics->getRoundShares(); +$aShares['valid'] > 0 ? $dEfficiency = round((100 - (100 / $aShares['valid'] * $aShares['invalid'])), 2) : $dEfficiency = 0; + +// Fetch RPC data +if ($bitcoin->can_connect() === true){ + $dDifficulty = $bitcoin->getdifficulty(); + $iBlock = $bitcoin->getblockcount(); + $dNetworkHashrate = $bitcoin->getnetworkhashps(); +} else { + $dDifficulty = 1; + $iBlock = 0; + $dNetworkHashrate = 0; +} + +// Estimated time to find the next block +$iCurrentPoolHashrate = $statistics->getCurrentHashrate(); + +// Avoid confusion, ensure our nethash isn't higher than poolhash +if ($iCurrentPoolHashrate > $dNetworkHashrate) $dNetworkHashrate = $iCurrentPoolHashrate; + +// Time in seconds, not hours, using modifier in smarty to translate +$iCurrentPoolHashrate > 0 ? $iEstTime = $dDifficulty * pow(2,32) / ($iCurrentPoolHashrate * 1000) : $iEstTime = 0; +$iEstShares = (pow(2, 32 - $config['difficulty']) * $dDifficulty); + +// Time since last +$now = new DateTime( "now" ); +if (!empty($aLastBlock)) { + $dTimeSinceLast = ($now->getTimestamp() - $aLastBlock['time']); +} else { + $dTimeSinceLast = 0; +} + +// Output JSON format +$data = array( + 'hashrate' => $iCurrentPoolHashrate, + 'efficiency' => $dEfficiency, + 'workers' => $worker->getCountAllActiveWorkers(), + 'currentnetworkblock' => $iBlock, + 'nextnetworkblock' => $iBlock + 1, + 'lastblock' => $aLastBlock['height'], + 'networkdiff' => $dDifficulty, + 'esttime' => $iEstTime, + 'estshares' => $iEstShares, + 'timesincelast' => $dTimeSinceLast, + 'nethashrate' => $dNetworkHashrate +); + +echo $api->get_json($data); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/gettimesincelastblock.inc.php b/public/include/pages/api/gettimesincelastblock.inc.php index 532da6bd..c0de01f8 100644 --- a/public/include/pages/api/gettimesincelastblock.inc.php +++ b/public/include/pages/api/gettimesincelastblock.inc.php @@ -1,25 +1,23 @@ isActive(); // Check user token -$id = $user->checkApiKey($_REQUEST['api_key']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); // Fetch our last block found $aBlocksFoundData = $statistics->getBlocksFound(1); // Time since last block $now = new DateTime( "now" ); -if (!empty($aBlocksFoundData)) { - $dTimeSinceLast = ($now->getTimestamp() - $aBlocksFoundData[0]['time']); -} else { - $dTimeSinceLast = 0; -} +! empty($aBlocksFoundData) ? $dTimeSinceLast = ($now->getTimestamp() - $aBlocksFoundData[0]['time']) : $dTimeSinceLast = 0; // Output JSON format -echo json_encode(array('gettimesincelastblock' => $dTimeSinceLast)); +echo $api->get_json($dTimeSinceLast); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getuserbalance.inc.php b/public/include/pages/api/getuserbalance.inc.php new file mode 100644 index 00000000..d91c6865 --- /dev/null +++ b/public/include/pages/api/getuserbalance.inc.php @@ -0,0 +1,17 @@ +isActive(); + +// Check user token +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Output JSON format +echo $api->get_json($transaction->getBalance($user_id)); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getuserhashrate.inc.php b/public/include/pages/api/getuserhashrate.inc.php new file mode 100644 index 00000000..2edee628 --- /dev/null +++ b/public/include/pages/api/getuserhashrate.inc.php @@ -0,0 +1,25 @@ +isActive(); + +// Check user token +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Fetch some settings +if ( ! $interval = $setting->getValue('statistics_ajax_data_interval')) $interval = 300; + +// Gather un-cached data +$statistics->setGetCache(false); +$hashrate = $statistics->getUserHashrate($user_id, $interval); +$statistics->setGetCache(true); + +// Output JSON +echo $api->get_json($hashrate); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getusersharerate.inc.php b/public/include/pages/api/getusersharerate.inc.php new file mode 100644 index 00000000..23a4562d --- /dev/null +++ b/public/include/pages/api/getusersharerate.inc.php @@ -0,0 +1,25 @@ +isActive(); + +// Check user token +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); + +// Fetch settings +if ( ! $interval = $setting->getValue('statistics_ajax_data_interval')) $interval = 300; + +// Gather un-cached data +$statistics->setGetCache(false); +$sharerate = $statistics->getUserSharerate($user_id, $interval); +$statistics->setGetCache(true); + +// Output JSON format +echo $api->get_json($sharerate); + +// Supress master template +$supress_master = 1; +?> diff --git a/public/include/pages/api/getuserstatus.inc.php b/public/include/pages/api/getuserstatus.inc.php index c91ade94..8e0ef7ec 100644 --- a/public/include/pages/api/getuserstatus.inc.php +++ b/public/include/pages/api/getuserstatus.inc.php @@ -1,28 +1,22 @@ isActive(); // Check user token -$id = $user->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']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); // Output JSON format -echo json_encode(array('getuserstatus' => array( - 'username' => $username, - 'shares' => $statistics->getUserShares($id), - 'hashrate' => $statistics->getUserHashrate($id) -))); +$data = array( + 'username' => $user->getUsername($user_id), + 'shares' => $statistics->getUserShares($user_id), + 'hashrate' => $statistics->getUserHashrate($user_id), + 'sharerate' => $statistics->getUserSharerate($user_id) +); +echo $api->get_json($data); // Supress master template $supress_master = 1; diff --git a/public/include/pages/api/getuserworkers.inc.php b/public/include/pages/api/getuserworkers.inc.php index 23bdcf5d..a1f19605 100644 --- a/public/include/pages/api/getuserworkers.inc.php +++ b/public/include/pages/api/getuserworkers.inc.php @@ -1,23 +1,16 @@ isActive(); // Check user token -$id = $user->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']); +$user_id = $api->checkAccess($user->checkApiKey($_REQUEST['api_key']), @$_REQUEST['id']); // Output JSON format -echo json_encode(array('getuserworkers' => $worker->getWorkers($id))); +echo $api->get_json($worker->getWorkers($user_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 index 162e9134..f465d1a8 100644 --- a/public/include/pages/api/public.inc.php +++ b/public/include/pages/api/public.inc.php @@ -1,23 +1,27 @@ isActive(); // Fetch last block information $aLastBlock = $block->getLast(); $aShares = $statistics->getRoundShares(); +// RPC Calls +$bitcoin->can_connect() === true ? $dNetworkHashrate = $bitcoin->getnetworkhashps() : $dNetworkHashrate = 0; + +// Backwards compatible with the existing services echo json_encode( array( - 'pool_name' => $config['website']['name'], + 'pool_name' => $setting->getValue('website_name'), 'hashrate' => $statistics->getCurrentHashrate(), 'workers' => $worker->getCountAllActiveWorkers(), 'shares_this_round' => $aShares['valid'], 'last_block' => $aLastBlock['height'], - 'network_hashrate' => '0' + 'network_hashrate' => $dNetworkHashrate ) ); diff --git a/public/include/pages/contactform.inc.php b/public/include/pages/contactform.inc.php new file mode 100644 index 00000000..e4f947c4 --- /dev/null +++ b/public/include/pages/contactform.inc.php @@ -0,0 +1,18 @@ +getValue('disable_contactform')) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Contactform is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); + $smarty->assign("CONTENT", "disabled.tpl"); +} else { + if ($setting->getValue('recaptcha_enabled')) { + require_once(INCLUDE_DIR . '/lib/recaptchalib.php'); + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('recaptcha_public_key'))); + } + + // Tempalte specifics + $smarty->assign("CONTENT", "default.tpl"); +} +?> diff --git a/public/include/pages/contactform/contactform.inc.php b/public/include/pages/contactform/contactform.inc.php new file mode 100644 index 00000000..fbfab779 --- /dev/null +++ b/public/include/pages/contactform/contactform.inc.php @@ -0,0 +1,51 @@ +getValue('recaptcha_enabled')) { + // Load re-captcha specific data + require_once(INCLUDE_DIR . '/lib/recaptchalib.php'); + $rsp = recaptcha_check_answer ( + $setting->getValue('recaptcha_private_key'), + $_SERVER["REMOTE_ADDR"], + $_POST["recaptcha_challenge_field"], + $_POST["recaptcha_response_field"] + ); +} + +if ($setting->getValue('disable_contactform')) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Contactform is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); +} else { + // Check if recaptcha is enabled, process form data if valid + if($setting->getValue('recaptcha_enabled') && $_POST["recaptcha_response_field"] && $_POST["recaptcha_response_field"]!=''){ + if ($rsp->is_valid) { + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('recaptcha_public_key'))); + if ($mail->contactform($_POST['senderName'], $_POST['senderEmail'], $_POST['senderSubject'], $_POST['senderMessage'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Thanks for sending your message! We will get back to you shortly'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'There was a problem sending your message. Please try again.' . $user->getError(), 'TYPE' => 'errormsg'); + } + } else { + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('recaptcha_public_key'), $rsp->error)); + $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid Captcha, please try again. (' . $rsp->error . ')', 'TYPE' => 'errormsg'); + } + // Empty captcha + } else if ($setting->getValue('recaptcha_enabled')) { + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('recaptcha_public_key'), $rsp->error)); + $_SESSION['POPUP'][] = array('CONTENT' => 'Empty Captcha, please try again.', 'TYPE' => 'errormsg'); + // Captcha disabled + } else { + if ($mail->contactform($_POST['senderName'], $_POST['senderEmail'], $_POST['senderSubject'], $_POST['senderMessage'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Thanks for sending your message! We will get back to you shortly'); + } else { + $_SESSION['POPUP'][] = array('CONTENT' => 'There was a problem sending your message. Please try again. ' . $user->getError(), 'TYPE' => 'errormsg'); + } + } +} + +// Tempalte specifics +$smarty->assign("CONTENT", "default.tpl"); + +?> diff --git a/public/include/pages/dashboard.inc.php b/public/include/pages/dashboard.inc.php new file mode 100644 index 00000000..22a4edec --- /dev/null +++ b/public/include/pages/dashboard.inc.php @@ -0,0 +1,50 @@ +isAuthenticated()) { + if (! $interval = $setting->getValue('statistics_ajax_data_interval')) $interval = 300; + // Defaults to get rid of PHP Notice warnings + $dDifficulty = 1; + $aRoundShares = 1; + + // Only run these if the user is logged in + $aRoundShares = $statistics->getRoundShares(); + if ($bitcoin->can_connect() === true) { + $dDifficulty = $bitcoin->query('getdifficulty'); + if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) + $dDifficulty = $dDifficulty['proof-of-work']; + } + + // Always fetch this since we need for ministats header + $aRoundShares = $statistics->getRoundShares(); + if ($bitcoin->can_connect() === true) { + $dDifficulty = $bitcoin->query('getdifficulty'); + if (is_array($dDifficulty) && array_key_exists('proof-of-work', $dDifficulty)) + $dDifficulty = $dDifficulty['proof-of-work']; + try { $dNetworkHashrate = $bitcoin->query('getnetworkhashps') / 1000; } catch (Exception $e) { + // Maybe we are SHA + try { $dNetworkHashrate = $bitcoin->query('gethashespersec') / 1000; } catch (Exception $e) { + $dNetworkHashrate = 0; + } + $dNetworkHashrate = 0; + } + } else { + $dNetworkHashrate = 0; + } + + // Fetch some data + if (!$iCurrentActiveWorkers = $worker->getCountAllActiveWorkers()) $iCurrentActiveWorkers = 0; + $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); + $iCurrentPoolShareRate = $statistics->getCurrentShareRate(); + + // Avoid confusion, ensure our nethash isn't higher than poolhash + if ($iCurrentPoolHashrate > $dNetworkHashrate) $dNetworkHashrate = $iCurrentPoolHashrate; + + // Make it available in Smarty + $smarty->assign('INTERVAL', $interval / 60); + $smarty->assign('CONTENT', 'default.tpl'); +} + +?> diff --git a/public/include/pages/home.inc.php b/public/include/pages/home.inc.php index aecab054..349b197d 100644 --- a/public/include/pages/home.inc.php +++ b/public/include/pages/home.inc.php @@ -1,9 +1,26 @@ isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + // Fetch active news to display + $aNews = $news->getAllActive(); + if (is_array($aNews)) { + foreach ($aNews as $key => $aData) { + // Transform Markdown content to HTML + $aNews[$key]['content'] = Markdown::defaultTransform($aData['content']); + } + } + $smarty->assign("NEWS", $aNews); +} else { + $debug->append('Using cached page', 3); +} + +// Load news entries for Desktop site and unauthenticated users $smarty->assign("CONTENT", "default.tpl"); ?> diff --git a/public/include/pages/login.inc.php b/public/include/pages/login.inc.php index c157d720..0dcf6774 100644 --- a/public/include/pages/login.inc.php +++ b/public/include/pages/login.inc.php @@ -1,13 +1,19 @@ checkLogin($_POST['username'],$_POST['password']) ) { - header('Location: index.php?page=home'); -} else { +if ($setting->getValue('maintenance') && !$user->isAdmin($user->getUserId($_POST['username']))) { + $_SESSION['POPUP'][] = array('CONTENT' => 'You are not allowed to login during maintenace.', 'TYPE' => 'info'); +} else if ($user->checkLogin(@$_POST['username'], @$_POST['password']) ) { + empty($_POST['to']) ? $to = $_SERVER['PHP_SELF'] : $to = $_POST['to']; + $location = @$_SERVER['HTTPS'] === true ? 'https' : 'http' . '://' . $_SERVER['SERVER_NAME'] . $to; + if (!headers_sent()) header('Location: ' . $location); + exit(''); +} else if (@$_POST['username'] && @$_POST['password']) { $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to login: '. $user->getError(), 'TYPE' => 'errormsg'); } + +// Load login template $smarty->assign('CONTENT', 'default.tpl'); ?> diff --git a/public/include/pages/logout.inc.php b/public/include/pages/logout.inc.php index b10415f4..6a537c1d 100644 --- a/public/include/pages/logout.inc.php +++ b/public/include/pages/logout.inc.php @@ -6,5 +6,6 @@ if (!defined('SECURITY')) // This probably (?) never fails $user->logoutUser(); -header('Location: index.php?page=home'); +$smarty->assign("CONTENT", "default.tpl"); +// header('Location: index.php?page=home'); ?> diff --git a/public/include/pages/news.inc.php b/public/include/pages/news.inc.php index aecab054..127b1775 100644 --- a/public/include/pages/news.inc.php +++ b/public/include/pages/news.inc.php @@ -1,9 +1,21 @@ getAllActive(); +if (is_array($aNews)) { + foreach ($aNews as $key => $aData) { + // Transform Markdown content to HTML + $aNews[$key]['content'] = Markdown::defaultTransform($aData['content']); + } +} // Tempalte specifics +$smarty->assign("NEWS", $aNews); $smarty->assign("CONTENT", "default.tpl"); ?> diff --git a/public/include/pages/password/change.inc.php b/public/include/pages/password/change.inc.php index 8b5f4064..919632bd 100644 --- a/public/include/pages/password/change.inc.php +++ b/public/include/pages/password/change.inc.php @@ -4,14 +4,14 @@ if (!defined('SECURITY')) die('Hacking attempt'); -if ($_POST['do'] == 'useToken') { - if ($user->useToken($_POST['token'], $_POST['newPassword'], $_POST['newPassword2'])) { +if (isset($_POST['do']) && $_POST['do'] == 'resetPassword') { + if ($user->resetPassword($_POST['token'], $_POST['newPassword'], $_POST['newPassword2'])) { $_SESSION['POPUP'][] = array('CONTENT' => 'Password reset complete! Please login.'); } else { $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); } } - // Tempalte specifics $smarty->assign("CONTENT", "default.tpl"); + ?> diff --git a/public/include/pages/password/reset.inc.php b/public/include/pages/password/reset.inc.php index 796f5811..f7334fe8 100644 --- a/public/include/pages/password/reset.inc.php +++ b/public/include/pages/password/reset.inc.php @@ -5,7 +5,7 @@ if (!defined('SECURITY')) die('Hacking attempt'); // Process password reset request -if ($user->resetPassword($_POST['username'], $smarty)) { +if ($user->initResetPassword($_POST['username'], $smarty)) { $_SESSION['POPUP'][] = array('CONTENT' => 'Please check your mail account to finish your password reset'); } else { $_SESSION['POPUP'][] = array('CONTENT' => $user->getError(), 'TYPE' => 'errormsg'); diff --git a/public/include/pages/register.inc.php b/public/include/pages/register.inc.php index d0a1d713..681bcb53 100644 --- a/public/include/pages/register.inc.php +++ b/public/include/pages/register.inc.php @@ -3,15 +3,17 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if (!$config['website']['registration']) { +if ($setting->getValue('lock_registration') && $setting->getValue('disable_invitations')) { $_SESSION['POPUP'][] = array('CONTENT' => 'Account registration is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); $smarty->assign("CONTENT", "disabled.tpl"); +} else if ($setting->getValue('lock_registration') && !$setting->getValue('disable_invitations') && !isset($_GET['token'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Only invited users are allowed to register.', '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'])); + if ($setting->getValue('recaptcha_enabled')) { + require_once(INCLUDE_DIR . '/lib/recaptchalib.php'); + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('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 ce41630e..ecb2c028 100644 --- a/public/include/pages/register/register.inc.php +++ b/public/include/pages/register/register.inc.php @@ -2,44 +2,48 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if ($config['recaptcha']['enabled']) { +if ($setting->getValue('recaptcha_enabled')) { // Load re-captcha specific data - require_once(INCLUDE_DIR . '/recaptchalib.php'); + require_once(INCLUDE_DIR . '/lib/recaptchalib.php'); $rsp = recaptcha_check_answer ( - $config['recaptcha']['private_key'], + $setting->getValue('recaptcha_private_key'), $_SERVER["REMOTE_ADDR"], $_POST["recaptcha_challenge_field"], $_POST["recaptcha_response_field"] ); } -// 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'); +if ($setting->getValue('disable_invitations') && $setting->getValue('lock_registration')) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Account registration is currently disabled. Please try again later.', 'TYPE' => 'errormsg'); +} else if ($setting->getValue('lock_registration') && !$setting->getValue('disable_invitations') && !isset($_POST['token'])) { + $_SESSION['POPUP'][] = array('CONTENT' => 'Only invited users are allowed to register.', 'TYPE' => 'errormsg'); +} else { + // Check if recaptcha is enabled, process form data if valid + if($setting->getValue('recaptcha_enabled') && $_POST["recaptcha_response_field"] && $_POST["recaptcha_response_field"]!=''){ + if ($rsp->is_valid) { + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('recaptcha_public_key'))); + isset($_POST['token']) ? $token = $_POST['token'] : $token = ''; + if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $token)) { + ! $setting->getValue('accounts_confirm_email_disabled') ? $_SESSION['POPUP'][] = array('CONTENT' => 'Please check your mailbox to activate this account') : $_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($setting->getValue('recaptcha_public_key'), $rsp->error)); + $_SESSION['POPUP'][] = array('CONTENT' => 'Invalid Captcha, please try again. (' . $rsp->error . ')', 'TYPE' => 'errormsg'); + } + // Empty captcha + } else if ($setting->getValue('recaptcha_enabled')) { + $smarty->assign("RECAPTCHA", recaptcha_get_html($setting->getValue('recaptcha_public_key'), $rsp->error)); + $_SESSION['POPUP'][] = array('CONTENT' => 'Empty Captcha, please try again.', 'TYPE' => 'errormsg'); + // Captcha disabled + } else { + isset($_POST['token']) ? $token = $_POST['token'] : $token = ''; + if ($user->register($_POST['username'], $_POST['password1'], $_POST['password2'], $_POST['pin'], $_POST['email1'], $_POST['email2'], $token)) { + ! $setting->getValue('accounts_confirm_email_disabled') ? $_SESSION['POPUP'][] = array('CONTENT' => 'Please check your mailbox to activate this account') : $_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 { - 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'); } } diff --git a/public/include/pages/statistics.inc.php b/public/include/pages/statistics.inc.php index 195e3545..3f63e870 100644 --- a/public/include/pages/statistics.inc.php +++ b/public/include/pages/statistics.inc.php @@ -4,15 +4,21 @@ if (!defined('SECURITY')) die('Hacking attempt'); -if ($bitcoin->can_connect() === true){ - $dDifficulty = $bitcoin->query('getdifficulty'); - $iBlock = $bitcoin->query('getblockcount'); +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + if ($bitcoin->can_connect() === true){ + $dDifficulty = $bitcoin->getdifficulty(); + $iBlock = $bitcoin->getblockcount(); + } else { + $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("DIFFICULTY", $dDifficulty); } else { - $dDifficulty = 1; - $iBlock = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $debug->append('Using cached page', 3); } -$smarty->assign("CURRENTBLOCK", $iBlock); -$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 e83aa8a8..116d999d 100644 --- a/public/include/pages/statistics/blocks.inc.php +++ b/public/include/pages/statistics/blocks.inc.php @@ -2,16 +2,24 @@ // Make sure we are called from index.php if (!defined('SECURITY')) die('Hacking attempt'); -if (!$user->isAuthenticated()) header("Location: index.php?page=home"); // Grab the last blocks found -$iLimit = 30; -$aBlocksFoundData = $statistics->getBlocksFound($iLimit); -$aBlockData = $aBlocksFoundData[0]; +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + // Grab the last blocks found + $setting->getValue('statistics_block_count') ? $iLimit = $setting->getValue('statistics_block_count') : $iLimit = 20; + $aBlocksFoundData = $statistics->getBlocksFound($iLimit); -// Propagate content our template -$smarty->assign("BLOCKSFOUND", $aBlocksFoundData); -$smarty->assign("BLOCKLIMIT", $iLimit); + // Propagate content our template + $smarty->assign("BLOCKSFOUND", $aBlocksFoundData); + $smarty->assign("BLOCKLIMIT", $iLimit); +} else { + $debug->append('Using cached page', 3); +} -$smarty->assign("CONTENT", "default.tpl"); +if ($setting->getValue('acl_block_statistics')) { + $smarty->assign("CONTENT", "default.tpl"); +} else if ($user->isAuthenticated()) { + $smarty->assign("CONTENT", "default.tpl"); +} ?> diff --git a/public/include/pages/statistics/graphs.inc.php b/public/include/pages/statistics/graphs.inc.php index f7016835..575ce36d 100644 --- a/public/include/pages/statistics/graphs.inc.php +++ b/public/include/pages/statistics/graphs.inc.php @@ -1,16 +1,19 @@ isAuthenticated()) { - $aHourlyHashRates = $statistics->getHourlyHashrateByAccount($_SESSION['USERDATA']['id']); - $aPoolHourlyHashRates = $statistics->getHourlyHashrateByPool(); +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + if ($user->isAuthenticated()) { + $aHourlyHashRates = $statistics->getHourlyHashrateByAccount($_SESSION['USERDATA']['id']); + $aPoolHourlyHashRates = $statistics->getHourlyHashrateByPool(); + } + $smarty->assign("YOURHASHRATES", @$aHourlyHashRates); + $smarty->assign("POOLHASHRATES", @$aPoolHourlyHashRates); +} else { + $debug->append('Using cached page', 3); } -// 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 9650681e..5416b984 100644 --- a/public/include/pages/statistics/pool.inc.php +++ b/public/include/pages/statistics/pool.inc.php @@ -1,59 +1,73 @@ can_connect() === true){ - $dDifficulty = $bitcoin->getdifficulty(); - $iBlock = $bitcoin->getblockcount(); +if (!$smarty->isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + // Fetch data from wallet + if ($bitcoin->can_connect() === true){ + $dDifficulty = $bitcoin->getdifficulty(); + $iBlock = $bitcoin->getblockcount(); + is_int($iBlock) && $iBlock > 0 ? $sBlockHash = $bitcoin->query('getblockhash', $iBlock) : $sBlockHash = ''; + } else { + $dDifficulty = 1; + $iBlock = 0; + $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to wallet RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + } + + // Top share contributors + $aContributorsShares = $statistics->getTopContributors('shares', 15); + + // Top hash contributors + $aContributorsHashes = $statistics->getTopContributors('hashes', 15); + + // Grab the last 10 blocks found + $iLimit = 5; + $aBlocksFoundData = $statistics->getBlocksFound($iLimit); + count($aBlocksFoundData) > 0 ? $aBlockData = $aBlocksFoundData[0] : $aBlockData = array(); + + // Estimated time to find the next block + $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); + + // Time in seconds, not hours, using modifier in smarty to translate + $iCurrentPoolHashrate > 0 ? $iEstTime = $dDifficulty * pow(2,32) / ($iCurrentPoolHashrate * 1000) : $iEstTime = 0; + + // Time since last block + $now = new DateTime( "now" ); + if (!empty($aBlockData)) { + $dTimeSinceLast = ($now->getTimestamp() - $aBlockData['time']); + if ($dTimeSinceLast < 0) $dTimeSinceLast = 0; + } else { + $dTimeSinceLast = 0; + } + + // Propagate content our template + $smarty->assign("ESTTIME", $iEstTime); + $smarty->assign("TIMESINCELAST", $dTimeSinceLast); + $smarty->assign("BLOCKSFOUND", $aBlocksFoundData); + $smarty->assign("BLOCKLIMIT", $iLimit); + $smarty->assign("CONTRIBSHARES", $aContributorsShares); + $smarty->assign("CONTRIBHASHES", $aContributorsHashes); + $smarty->assign("CURRENTBLOCK", $iBlock); + $smarty->assign("CURRENTBLOCKHASH", @$sBlockHash); + if (count($aBlockData) > 0) { + $smarty->assign("LASTBLOCK", $aBlockData['height']); + $smarty->assign("LASTBLOCKHASH", $aBlockData['blockhash']); + } else { + $smarty->assign("LASTBLOCK", 0); + } + $smarty->assign("DIFFICULTY", $dDifficulty); + $smarty->assign("REWARD", $config['reward']); } else { - $dDifficulty = 1; - $iBlock = 0; - $_SESSION['POPUP'][] = array('CONTENT' => 'Unable to connect to litecoind RPC service: ' . $bitcoin->can_connect(), 'TYPE' => 'errormsg'); + $debug->append('Using cached page', 3); } -// Top share contributors -$aContributorsShares = $statistics->getTopContributors('shares', 15); - -// Top hash contributors -$aContributorsHashes = $statistics->getTopContributors('hashes', 15); - -// Grab the last 10 blocks found -$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); - -// Time since last block -$now = new DateTime( "now" ); -if (!empty($aBlockData)) { - $dTimeSinceLast = ($now->getTimestamp() - $aBlockData['time']); -} else { - $dTimeSinceLast = 0; -} - -// Propagate content our template -$smarty->assign("ESTTIME", $iEstTime); -$smarty->assign("TIMESINCELAST", $dTimeSinceLast); -$smarty->assign("BLOCKSFOUND", $aBlocksFoundData); -$smarty->assign("BLOCKLIMIT", $iLimit); -$smarty->assign("CONTRIBSHARES", $aContributorsShares); -$smarty->assign("CONTRIBHASHES", $aContributorsHashes); -$smarty->assign("CURRENTBLOCK", $iBlock); -$smarty->assign("LASTBLOCK", $aBlockData['height']); -$smarty->assign("DIFFICULTY", $dDifficulty); -$smarty->assign("REWARD", $config['reward']); - -if ($user->isAuthenticated()) { - $smarty->assign("CONTENT", "authenticated.tpl"); +// Public / private page detection +if ($setting->getValue('acl_pool_statistics')) { + $smarty->assign("CONTENT", "default.tpl"); +} else if ($user->isAuthenticated() && ! $setting->getValue('acl_pool_statistics')) { + $smarty->assign("CONTENT", "default.tpl"); } else { $smarty->assign("CONTENT", "../default.tpl"); } diff --git a/public/include/pages/statistics/round.inc.php b/public/include/pages/statistics/round.inc.php new file mode 100644 index 00000000..4c37f749 --- /dev/null +++ b/public/include/pages/statistics/round.inc.php @@ -0,0 +1,46 @@ +isCached('master.tpl', $smarty_cache_key)) { + $debug->append('No cached version available, fetching from backend', 3); + + if (@$_REQUEST['next'] && !empty($_REQUEST['height'])) { + $iKey = $roundstats->getNextBlock($_REQUEST['height']); + } else if (@$_REQUEST['prev'] && !empty($_REQUEST['height'])) { + $iKey = $roundstats->getPreviousBlock($_REQUEST['height']); + } else { + + if (empty($_REQUEST['height'])) { + $iBlock = $block->getLast(); + $iKey = $iBlock['height']; + } else { + $iKey = $_REQUEST['height']; + } + } + $aDetailsForBlockHeight = $roundstats->getDetailsForBlockHeight($iKey, $user->isAdmin(@$_SESSION['USERDATA']['id'])); + $aRoundShareStats = $roundstats->getRoundStatsForAccounts($iKey, $user->isAdmin(@$_SESSION['USERDATA']['id'])); + + if ($user->isAdmin(@$_SESSION['USERDATA']['id']) || $setting->getValue('acl_round_transactions')) { + $aUserRoundTransactions = $roundstats->getAllRoundTransactions($iKey, @$_SESSION['USERDATA']['id']); + } else { + $aUserRoundTransactions = $roundstats->getUserRoundTransactions($iKey, @$_SESSION['USERDATA']['id']); + } + + // Propagate content our template + $smarty->assign('BLOCKDETAILS', $aDetailsForBlockHeight); + $smarty->assign('ROUNDSHARES', $aRoundShareStats); + $smarty->assign("ROUNDTRANSACTIONS", $aUserRoundTransactions); +} else { + $debug->append('Using cached page', 3); +} + +if ($setting->getValue('acl_round_statistics')) { + $smarty->assign("CONTENT", "default.tpl"); +} else if ($user->isAuthenticated(false)) { + $smarty->assign("CONTENT", "default.tpl"); +} else { + $smarty->assign("CONTENT", "empty"); +} +?> diff --git a/public/include/smarty.inc.php b/public/include/smarty.inc.php index 8a320581..dfd1f4f6 100644 --- a/public/include/smarty.inc.php +++ b/public/include/smarty.inc.php @@ -18,8 +18,14 @@ $smarty = new Smarty; $debug->append('Define Smarty Paths', 3); $smarty->template_dir = BASEPATH . 'templates/' . THEME . '/'; $smarty->compile_dir = BASEPATH . 'templates/compile/'; +$smarty_cache_key = md5(serialize($_REQUEST) . serialize(@$_SESSION['USERDATA']['id'])); // Optional smarty caching, check Smarty documentation for details -$smarty->caching = $config['cache']; -$smarty->cache_dir = BASEPATH . "templates/cache"; +if ($config['smarty']['cache']) { + $debug->append('Enable smarty cache'); + $smarty->setCaching(Smarty::CACHING_LIFETIME_SAVED); + $smarty->cache_lifetime = $config['smarty']['cache_lifetime']; + $smarty->cache_dir = BASEPATH . "templates/cache"; + $smarty->use_sub_dirs = true; +} ?> diff --git a/public/include/smarty_globals.inc.php b/public/include/smarty_globals.inc.php index 923b7263..1cb7e21d 100644 --- a/public/include/smarty_globals.inc.php +++ b/public/include/smarty_globals.inc.php @@ -1,44 +1,74 @@ append('Global smarty variables', 3); +$debug->append('No cached page detected, loading smarty globals', 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 round shares +if (!$aRoundShares = $statistics->getRoundShares()) { + $aRoundShares = array('valid' => 0, 'invalid' => 0); } -// Fetch some data -$iCurrentActiveWorkers = $worker->getCountAllActiveWorkers(); +if ($bitcoin->can_connect() === true) { + $dDifficulty = $bitcoin->getdifficulty(); + $dNetworkHashrate = $bitcoin->getnetworkhashps(); +} else { + $dDifficulty = 1; + $dNetworkHashrate = 0; +} + +// Baseline pool hashrate for templates +if ( ! $dPoolHashrateModifier = $setting->getValue('statistics_pool_hashrate_modifier') ) $dPoolHashrateModifier = 1; $iCurrentPoolHashrate = $statistics->getCurrentHashrate(); + +// Avoid confusion, ensure our nethash isn't higher than poolhash +if ($iCurrentPoolHashrate > $dNetworkHashrate) $dNetworkHashrate = $iCurrentPoolHashrate; + +// Baseline network hashrate for templates +if ( ! $dPersonalHashrateModifier = $setting->getValue('statistics_personal_hashrate_modifier') ) $dPersonalHashrateModifier = 1; +if ( ! $dNetworkHashrateModifier = $setting->getValue('statistics_network_hashrate_modifier') ) $dNetworkHashrateModifier = 1; + +// Apply modifier now +$dNetworkHashrate = $dNetworkHashrate * $dNetworkHashrateModifier; +$iCurrentPoolHashrate = $iCurrentPoolHashrate * $dPoolHashrateModifier; + +// Share rate of the entire pool $iCurrentPoolShareRate = $statistics->getCurrentShareRate(); +// Active workers +if (!$iCurrentActiveWorkers = $worker->getCountAllActiveWorkers()) $iCurrentActiveWorkers = 0; + +// Some settings to propagate to template +if (! $statistics_ajax_refresh_interval = $setting->getValue('statistics_ajax_refresh_interval')) $statistics_ajax_refresh_interval = 10; + +// Small helper array +$aHashunits = array( '1' => 'KH/s', '0.001' => 'MH/s', '0.000001' => 'GH/s' ); + // Global data for Smarty $aGlobal = array( - 'slogan' => $config['website']['slogan'], - 'websitename' => $config['website']['name'], + 'hashunits' => array( 'pool' => $aHashunits[$dPoolHashrateModifier], 'network' => $aHashunits[$dNetworkHashrateModifier], 'personal' => $aHashunits[$dPersonalHashrateModifier]), + 'hashmods' => array( 'personal' => $dPersonalHashrateModifier ), 'hashrate' => $iCurrentPoolHashrate, + 'nethashrate' => $dNetworkHashrate, 'sharerate' => $iCurrentPoolShareRate, - 'ppsvalue' => number_format(round(50 / (pow(2,32) * $dDifficulty) * pow(2, $config['difficulty']), 12) ,12), 'workers' => $iCurrentActiveWorkers, 'roundshares' => $aRoundShares, 'fees' => $config['fees'], 'confirmations' => $config['confirmations'], 'reward' => $config['reward'], 'price' => $setting->getValue('price'), - 'blockexplorer' => $config['blockexplorer'], - 'chaininfo' => $config['chaininfo'], + 'disable_mp' => $setting->getValue('disable_mp'), 'config' => array( + 'accounts' => $config['accounts'], + 'disable_invitations' => $setting->getValue('disable_invitations'), + 'disable_notifications' => $setting->getValue('disable_notifications'), + 'statistics_ajax_refresh_interval' => $statistics_ajax_refresh_interval, 'price' => array( 'currency' => $config['price']['currency'] ), 'targetdiff' => $config['difficulty'], 'currency' => $config['currency'], @@ -51,6 +81,40 @@ $aGlobal = array( ) ); +// Website configurations +$aGlobal['website']['name'] = $setting->getValue('website_name'); +$aGlobal['website']['title'] = $setting->getValue('website_title'); +$aGlobal['website']['slogan'] = $setting->getValue('website_slogan'); +$aGlobal['website']['email'] = $setting->getValue('website_email'); +$aGlobal['website']['api']['disabled'] = $setting->getValue('disable_api'); +$aGlobal['website']['blockexplorer']['disabled'] = $setting->getValue('website_blockexplorer_disabled'); +$aGlobal['website']['chaininfo']['disabled'] = $setting->getValue('website_chaininfo_disabled'); +$setting->getValue('website_blockexplorer_url') ? $aGlobal['website']['blockexplorer']['url'] = $setting->getValue('website_blockexplorer_url') : $aGlobal['website']['blockexplorer']['url'] = 'http://explorer.litecoin.net/block/'; +$setting->getValue('website_chaininfo_url') ? $aGlobal['website']['chaininfo']['url'] = $setting->getValue('website_chaininfo_url') : $aGlobal['website']['chaininfo']['url'] = 'http://allchains.info'; + +// ACLs +$aGlobal['acl']['pool']['statistics'] = $setting->getValue('acl_pool_statistics'); +$aGlobal['acl']['block']['statistics'] = $setting->getValue('acl_block_statistics'); +$aGlobal['acl']['round']['statistics'] = $setting->getValue('acl_round_statistics'); + +// We support some dynamic reward targets but fall back to our fixed value +// Special calculations for PPS Values based on reward_type setting and/or available blocks +if ($config['pps']['reward']['type'] == 'blockavg' && $block->getBlockCount() > 0) { + $pps_reward = round($block->getAvgBlockReward($config['pps']['blockavg']['blockcount'])); +} else { + if ($config['pps']['reward']['type'] == 'block') { + if ($aLastBlock = $block->getLast()) { + $pps_reward = $aLastBlock['amount']; + } else { + $pps_reward = $config['pps']['reward']['default']; + } + } else { + $pps_reward = $config['pps']['reward']['default']; + } +} + +$aGlobal['ppsvalue'] = number_format(round($pps_reward / (pow(2,32) * $dDifficulty) * pow(2, $config['pps_target']), 12) ,12); + // We don't want these session infos cached if (@$_SESSION['USERDATA']['id']) { $aGlobal['userdata'] = $_SESSION['USERDATA']['id'] ? $user->getUserData($_SESSION['USERDATA']['id']) : array(); @@ -58,22 +122,50 @@ 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']['hashrate'] = $statistics->getUserHashrate($_SESSION['USERDATA']['id']) * $dPersonalHashrateModifier; $aGlobal['userdata']['sharerate'] = $statistics->getUserSharerate($_SESSION['USERDATA']['id']); switch ($config['payout_system']) { + case 'prop' || 'pplns': + // Some estimations + if (@$aRoundShares['valid'] > 0) { + $aGlobal['userdata']['est_block'] = round(( (int)$aGlobal['userdata']['shares']['valid'] / (int)$aRoundShares['valid'] ) * (float)$config['reward'], 8); + $aGlobal['userdata']['no_fees'] == 0 ? $aGlobal['userdata']['est_fee'] = round(((float)$config['fees'] / 100) * (float)$aGlobal['userdata']['est_block'], 8) : $aGlobal['userdata']['est_fee'] = 0; + $aGlobal['userdata']['est_donation'] = round((( (float)$aGlobal['userdata']['donate_percent'] / 100) * ((float)$aGlobal['userdata']['est_block'] - (float)$aGlobal['userdata']['est_fee'])), 8); + $aGlobal['userdata']['est_payout'] = round((float)$aGlobal['userdata']['est_block'] - (float)$aGlobal['userdata']['est_donation'] - (float)$aGlobal['userdata']['est_fee'], 8); + } else { + $aGlobal['userdata']['est_block'] = 0; + $aGlobal['userdata']['est_fee'] = 0; + $aGlobal['userdata']['est_donation'] = 0; + $aGlobal['userdata']['est_payout'] = 0; + } + case 'pplns': + $aGlobal['pplns']['target'] = $config['pplns']['shares']['default']; + if ($aLastBlock = $block->getLast()) { + if ($iAvgBlockShares = round($block->getAvgBlockShares($aLastBlock['height'], $config['pplns']['blockavg']['blockcount']))) { + $aGlobal['pplns']['target'] = $iAvgBlockShares; + } + } + break; 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; } + + // Site-wide notifications, based on user events + if ($aGlobal['userdata']['balance']['confirmed'] >= $config['ap_threshold']['max']) + $_SESSION['POPUP'][] = array('CONTENT' => 'You have exceeded the pools configured ' . $config['currency'] . ' warning threshold. Please initiate a transfer!', 'TYPE' => 'errormsg'); + if ($user->getUserFailed($_SESSION['USERDATA']['id']) > 0) + $_SESSION['POPUP'][] = array('CONTENT' => 'You have ' . $user->getUserFailed($_SESSION['USERDATA']['id']) . ' failed login attempts! Reset Counter', 'TYPE' => 'errormsg'); } +if ($setting->getValue('maintenance')) + $_SESSION['POPUP'][] = array('CONTENT' => 'This pool is currently in maintenance mode.', 'TYPE' => 'warning'); +if ($motd = $setting->getValue('system_motd')) + $_SESSION['POPUP'][] = array('CONTENT' => $motd, 'TYPE' => 'info'); + +// So we can display additional info +$smarty->assign('DEBUG', DEBUG); + // Make it available in Smarty $smarty->assign('PATH', 'site_assets/' . THEME); $smarty->assign('GLOBAL', $aGlobal); diff --git a/public/index.php b/public/index.php index e9100a57..f8ccb438 100644 --- a/public/index.php +++ b/public/index.php @@ -18,20 +18,23 @@ limitations under the License. */ +// Used for performance calculations +$dStartTime = microtime(true); + // This should be okay define("BASEPATH", "./"); // Our security check define("SECURITY", 1); -// Start a session -session_start(); -$session_id = session_id(); - // Include our configuration (holding defines for the requires) -if (!include_once(BASEPATH . 'include/config/global.inc.php')) { - die('Unable to load site configuration'); -} +if (!include_once(BASEPATH . 'include/config/global.inc.php')) die('Unable to load site configuration'); + +// Start a session +session_set_cookie_params(time()+$config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); +session_start(); +setcookie(session_name(),session_id(),time()+$config['cookie']['duration'], $config['cookie']['path'], $config['cookie']['domain'], $config['cookie']['secure'], $config['cookie']['httponly']); +$session_id = session_id(); // Load Classes, they name defines the $ variable used // We include all needed files here, even though our templates could load them themself @@ -76,15 +79,15 @@ $smarty->assign("PAGE", $page); $smarty->assign("ACTION", $action); // Now with all loaded and processed, setup some globals we need for smarty templates -require_once(INCLUDE_DIR . '/smarty_globals.inc.php'); +if ($page != 'api') require_once(INCLUDE_DIR . '/smarty_globals.inc.php'); -// Debguger +// Load debug information into template $debug->append("Loading debug information into template", 4); $smarty->assign('DebuggerInfo', $debug->getDebugInfo()); +$smarty->assign('RUNTIME', (microtime(true) - $dStartTime) * 1000); // Display our page -if (!@$supress_master) - $smarty->display("master.tpl", md5(serialize($_REQUEST))); +if (!@$supress_master) $smarty->display("master.tpl", $smarty_cache_key); // Unset any temporary values here unset($_SESSION['POPUP']); diff --git a/public/site_assets/mmcFE/css/style.css b/public/site_assets/mmcFE/css/style.css index 6243fbfc..e9284371 100644 --- a/public/site_assets/mmcFE/css/style.css +++ b/public/site_assets/mmcFE/css/style.css @@ -878,9 +878,9 @@ a:hover { } .block.withsidebar .block_content .sidebar { - width: 210px; + width: 230px; float: left; - font-size: 11px; + font-size: 10px; } .block.withsidebar .block_content .sidebar p { @@ -918,7 +918,7 @@ a:hover { } .block.withsidebar .block_content .sidebar_content { - padding: 15px 20px 15px 210px; + padding: 15px 20px 15px 230px; } /* Image list */ diff --git a/public/site_assets/mmcFE/js/custom.js b/public/site_assets/mmcFE/js/custom.js index 76e967c6..9d63724a 100644 --- a/public/site_assets/mmcFE/js/custom.js +++ b/public/site_assets/mmcFE/js/custom.js @@ -16,10 +16,14 @@ $(function () { var statsType = 'area'; } + // hack to statically set width as something is broken with div width calculation - anni + var chart_width = $(document).width() - 400; + 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', @@ -37,6 +41,7 @@ $(function () { } else { $(this).hide().visualize({ // 'bar', 'area', 'pie', 'line' + width: chart_width, type: statsType, height: '240px', colors: ['#6fb9e8', '#ec8526', '#9dc453', '#ddd74c'] diff --git a/public/site_assets/mmcFE/js/jquery-1.9.1.min.js b/public/site_assets/mmcFE/js/jquery-1.9.1.min.js new file mode 100644 index 00000000..006e9531 --- /dev/null +++ b/public/site_assets/mmcFE/js/jquery-1.9.1.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.9.1 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery.min.map +*/(function(e,t){var n,r,i=typeof t,o=e.document,a=e.location,s=e.jQuery,u=e.$,l={},c=[],p="1.9.1",f=c.concat,d=c.push,h=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,b=function(e,t){return new b.fn.init(e,t,r)},x=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^[\],:{}\s]*$/,E=/(?:^|:|,)(?:\s*\[)+/g,S=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,A=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,j=/^-ms-/,D=/-([\da-z])/gi,L=function(e,t){return t.toUpperCase()},H=function(e){(o.addEventListener||"load"===e.type||"complete"===o.readyState)&&(q(),b.ready())},q=function(){o.addEventListener?(o.removeEventListener("DOMContentLoaded",H,!1),e.removeEventListener("load",H,!1)):(o.detachEvent("onreadystatechange",H),e.detachEvent("onload",H))};b.fn=b.prototype={jquery:p,constructor:b,init:function(e,n,r){var i,a;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof b?n[0]:n,b.merge(this,b.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:o,!0)),C.test(i[1])&&b.isPlainObject(n))for(i in n)b.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(a=o.getElementById(i[2]),a&&a.parentNode){if(a.id!==i[2])return r.find(e);this.length=1,this[0]=a}return this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):b.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),b.makeArray(e,this))},selector:"",length:0,size:function(){return this.length},toArray:function(){return h.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=b.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return b.each(this,e,t)},ready:function(e){return b.ready.promise().done(e),this},slice:function(){return this.pushStack(h.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(b.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:d,sort:[].sort,splice:[].splice},b.fn.init.prototype=b.fn,b.extend=b.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},u=1,l=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},u=2),"object"==typeof s||b.isFunction(s)||(s={}),l===u&&(s=this,--u);l>u;u++)if(null!=(o=arguments[u]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(b.isPlainObject(r)||(n=b.isArray(r)))?(n?(n=!1,a=e&&b.isArray(e)?e:[]):a=e&&b.isPlainObject(e)?e:{},s[i]=b.extend(c,a,r)):r!==t&&(s[i]=r));return s},b.extend({noConflict:function(t){return e.$===b&&(e.$=u),t&&e.jQuery===b&&(e.jQuery=s),b},isReady:!1,readyWait:1,holdReady:function(e){e?b.readyWait++:b.ready(!0)},ready:function(e){if(e===!0?!--b.readyWait:!b.isReady){if(!o.body)return setTimeout(b.ready);b.isReady=!0,e!==!0&&--b.readyWait>0||(n.resolveWith(o,[b]),b.fn.trigger&&b(o).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===b.type(e)},isArray:Array.isArray||function(e){return"array"===b.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if(!e||"object"!==b.type(e)||e.nodeType||b.isWindow(e))return!1;try{if(e.constructor&&!y.call(e,"constructor")&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||y.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=b.buildFragment([e],t,i),i&&b(i).remove(),b.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=b.trim(n),n&&k.test(n.replace(S,"@").replace(A,"]").replace(E,"")))?Function("return "+n)():(b.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||b.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&b.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(j,"ms-").replace(D,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:v&&!v.call("\ufeff\u00a0")?function(e){return null==e?"":v.call(e)}:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?b.merge(n,"string"==typeof e?[e]:e):d.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(g)return g.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return f.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),b.isFunction(e)?(r=h.call(arguments,2),i=function(){return e.apply(n||this,r.concat(h.call(arguments)))},i.guid=e.guid=e.guid||b.guid++,i):t},access:function(e,n,r,i,o,a,s){var u=0,l=e.length,c=null==r;if("object"===b.type(r)){o=!0;for(u in r)b.access(e,n,u,r[u],!0,a,s)}else if(i!==t&&(o=!0,b.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(b(e),n)})),n))for(;l>u;u++)n(e[u],r,s?i:i.call(e[u],u,n(e[u],r)));return o?e:c?n.call(e):l?n(e[0],r):a},now:function(){return(new Date).getTime()}}),b.ready.promise=function(t){if(!n)if(n=b.Deferred(),"complete"===o.readyState)setTimeout(b.ready);else if(o.addEventListener)o.addEventListener("DOMContentLoaded",H,!1),e.addEventListener("load",H,!1);else{o.attachEvent("onreadystatechange",H),e.attachEvent("onload",H);var r=!1;try{r=null==e.frameElement&&o.documentElement}catch(i){}r&&r.doScroll&&function a(){if(!b.isReady){try{r.doScroll("left")}catch(e){return setTimeout(a,50)}q(),b.ready()}}()}return n.promise(t)},b.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=b.type(e);return b.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=b(o);var _={};function F(e){var t=_[e]={};return b.each(e.match(w)||[],function(e,n){t[n]=!0}),t}b.Callbacks=function(e){e="string"==typeof e?_[e]||F(e):b.extend({},e);var n,r,i,o,a,s,u=[],l=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=u.length,n=!0;u&&o>a;a++)if(u[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,u&&(l?l.length&&c(l.shift()):r?u=[]:p.disable())},p={add:function(){if(u){var t=u.length;(function i(t){b.each(t,function(t,n){var r=b.type(n);"function"===r?e.unique&&p.has(n)||u.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=u.length:r&&(s=t,c(r))}return this},remove:function(){return u&&b.each(arguments,function(e,t){var r;while((r=b.inArray(t,u,r))>-1)u.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?b.inArray(e,u)>-1:!(!u||!u.length)},empty:function(){return u=[],this},disable:function(){return u=l=r=t,this},disabled:function(){return!u},lock:function(){return l=t,r||p.disable(),this},locked:function(){return!l},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!u||i&&!l||(n?l.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},b.extend({Deferred:function(e){var t=[["resolve","done",b.Callbacks("once memory"),"resolved"],["reject","fail",b.Callbacks("once memory"),"rejected"],["notify","progress",b.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return b.Deferred(function(n){b.each(t,function(t,o){var a=o[0],s=b.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&b.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?b.extend(e,r):r}},i={};return r.pipe=r.then,b.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=h.call(arguments),r=n.length,i=1!==r||e&&b.isFunction(e.promise)?r:0,o=1===i?e:b.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?h.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,u,l;if(r>1)for(s=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&b.isFunction(n[t].promise)?n[t].promise().done(a(t,l,n)).fail(o.reject).progress(a(t,u,s)):--i;return i||o.resolveWith(l,n),o.promise()}}),b.support=function(){var t,n,r,a,s,u,l,c,p,f,d=o.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
    a",n=d.getElementsByTagName("*"),r=d.getElementsByTagName("a")[0],!n||!r||!n.length)return{};s=o.createElement("select"),l=s.appendChild(o.createElement("option")),a=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={getSetAttribute:"t"!==d.className,leadingWhitespace:3===d.firstChild.nodeType,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:"/a"===r.getAttribute("href"),opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:!!a.value,optSelected:l.selected,enctype:!!o.createElement("form").enctype,html5Clone:"<:nav>"!==o.createElement("nav").cloneNode(!0).outerHTML,boxModel:"CSS1Compat"===o.compatMode,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},a.checked=!0,t.noCloneChecked=a.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!l.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}a=o.createElement("input"),a.setAttribute("value",""),t.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),t.radioValue="t"===a.value,a.setAttribute("checked","t"),a.setAttribute("name","t"),u=o.createDocumentFragment(),u.appendChild(a),t.appendChecked=a.checked,t.checkClone=u.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;return d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip,b(function(){var n,r,a,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",u=o.getElementsByTagName("body")[0];u&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",u.appendChild(n).appendChild(d),d.innerHTML="
    t
    ",a=d.getElementsByTagName("td"),a[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===a[0].offsetHeight,a[0].style.display="",a[1].style.display="none",t.reliableHiddenOffsets=p&&0===a[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=4===d.offsetWidth,t.doesNotIncludeMarginInBodyOffset=1!==u.offsetTop,e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(o.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
    ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(u.style.zoom=1)),u.removeChild(n),n=d=a=r=null)}),n=s=u=l=r=a=null,t}();var O=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,B=/([A-Z])/g;function P(e,n,r,i){if(b.acceptData(e)){var o,a,s=b.expando,u="string"==typeof n,l=e.nodeType,p=l?b.cache:e,f=l?e[s]:e[s]&&s;if(f&&p[f]&&(i||p[f].data)||!u||r!==t)return f||(l?e[s]=f=c.pop()||b.guid++:f=s),p[f]||(p[f]={},l||(p[f].toJSON=b.noop)),("object"==typeof n||"function"==typeof n)&&(i?p[f]=b.extend(p[f],n):p[f].data=b.extend(p[f].data,n)),o=p[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[b.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[b.camelCase(n)])):a=o,a}}function R(e,t,n){if(b.acceptData(e)){var r,i,o,a=e.nodeType,s=a?b.cache:e,u=a?e[b.expando]:b.expando;if(s[u]){if(t&&(o=n?s[u]:s[u].data)){b.isArray(t)?t=t.concat(b.map(t,b.camelCase)):t in o?t=[t]:(t=b.camelCase(t),t=t in o?[t]:t.split(" "));for(r=0,i=t.length;i>r;r++)delete o[t[r]];if(!(n?$:b.isEmptyObject)(o))return}(n||(delete s[u].data,$(s[u])))&&(a?b.cleanData([e],!0):b.support.deleteExpando||s!=s.window?delete s[u]:s[u]=null)}}}b.extend({cache:{},expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?b.cache[e[b.expando]]:e[b.expando],!!e&&!$(e)},data:function(e,t,n){return P(e,t,n)},removeData:function(e,t){return R(e,t)},_data:function(e,t,n){return P(e,t,n,!0)},_removeData:function(e,t){return R(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&b.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),b.fn.extend({data:function(e,n){var r,i,o=this[0],a=0,s=null;if(e===t){if(this.length&&(s=b.data(o),1===o.nodeType&&!b._data(o,"parsedAttrs"))){for(r=o.attributes;r.length>a;a++)i=r[a].name,i.indexOf("data-")||(i=b.camelCase(i.slice(5)),W(o,i,s[i]));b._data(o,"parsedAttrs",!0)}return s}return"object"==typeof e?this.each(function(){b.data(this,e)}):b.access(this,function(n){return n===t?o?W(o,e,b.data(o,e)):null:(this.each(function(){b.data(this,e,n)}),t)},null,n,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){b.removeData(this,e)})}});function W(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(B,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:O.test(r)?b.parseJSON(r):r}catch(o){}b.data(e,n,r)}else r=t}return r}function $(e){var t;for(t in e)if(("data"!==t||!b.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}b.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=b._data(e,n),r&&(!i||b.isArray(r)?i=b._data(e,n,b.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=b.queue(e,t),r=n.length,i=n.shift(),o=b._queueHooks(e,t),a=function(){b.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),o.cur=i,i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return b._data(e,n)||b._data(e,n,{empty:b.Callbacks("once memory").add(function(){b._removeData(e,t+"queue"),b._removeData(e,n)})})}}),b.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?b.queue(this[0],e):n===t?this:this.each(function(){var t=b.queue(this,e,n);b._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&b.dequeue(this,e)})},dequeue:function(e){return this.each(function(){b.dequeue(this,e)})},delay:function(e,t){return e=b.fx?b.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=b.Deferred(),a=this,s=this.length,u=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=b._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(u));return u(),o.promise(n)}});var I,z,X=/[\t\r\n]/g,U=/\r/g,V=/^(?:input|select|textarea|button|object)$/i,Y=/^(?:a|area)$/i,J=/^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i,G=/^(?:checked|selected)$/i,Q=b.support.getSetAttribute,K=b.support.input;b.fn.extend({attr:function(e,t){return b.access(this,b.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){b.removeAttr(this,e)})},prop:function(e,t){return b.access(this,b.prop,e,t,arguments.length>1)},removeProp:function(e){return e=b.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,u="string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=b.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,u=0===arguments.length||"string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?b.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return b.isFunction(e)?this.each(function(n){b(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=b(this),u=t,l=e.match(w)||[];while(o=l[a++])u=r?u:!s.hasClass(o),s[u?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&b._data(this,"__className__",this.className),this.className=this.className||e===!1?"":b._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(X," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=b.isFunction(e),this.each(function(n){var o,a=b(this);1===this.nodeType&&(o=i?e.call(this,n,a.val()):e,null==o?o="":"number"==typeof o?o+="":b.isArray(o)&&(o=b.map(o,function(e){return null==e?"":e+""})),r=b.valHooks[this.type]||b.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=b.valHooks[o.type]||b.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(U,""):null==n?"":n)}}}),b.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,u=0>i?s:o?i:0;for(;s>u;u++)if(n=r[u],!(!n.selected&&u!==i||(b.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&b.nodeName(n.parentNode,"optgroup"))){if(t=b(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n=b.makeArray(t);return b(e).find("option").each(function(){this.selected=b.inArray(b(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attr:function(e,n,r){var o,a,s,u=e.nodeType;if(e&&3!==u&&8!==u&&2!==u)return typeof e.getAttribute===i?b.prop(e,n,r):(a=1!==u||!b.isXMLDoc(e),a&&(n=n.toLowerCase(),o=b.attrHooks[n]||(J.test(n)?z:I)),r===t?o&&a&&"get"in o&&null!==(s=o.get(e,n))?s:(typeof e.getAttribute!==i&&(s=e.getAttribute(n)),null==s?t:s):null!==r?o&&a&&"set"in o&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r):(b.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=b.propFix[n]||n,J.test(n)?!Q&&G.test(n)?e[b.camelCase("default-"+n)]=e[r]=!1:e[r]=!1:b.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!b.support.radioValue&&"radio"===t&&b.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!b.isXMLDoc(e),a&&(n=b.propFix[n]||n,o=b.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):V.test(e.nodeName)||Y.test(e.nodeName)&&e.href?0:t}}}}),z={get:function(e,n){var r=b.prop(e,n),i="boolean"==typeof r&&e.getAttribute(n),o="boolean"==typeof r?K&&Q?null!=i:G.test(n)?e[b.camelCase("default-"+n)]:!!i:e.getAttributeNode(n);return o&&o.value!==!1?n.toLowerCase():t},set:function(e,t,n){return t===!1?b.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&b.propFix[n]||n,n):e[b.camelCase("default-"+n)]=e[n]=!0,n}},K&&Q||(b.attrHooks.value={get:function(e,n){var r=e.getAttributeNode(n);return b.nodeName(e,"input")?e.defaultValue:r&&r.specified?r.value:t},set:function(e,n,r){return b.nodeName(e,"input")?(e.defaultValue=n,t):I&&I.set(e,n,r)}}),Q||(I=b.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&("id"===n||"name"===n||"coords"===n?""!==r.value:r.specified)?r.value:t},set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},b.attrHooks.contenteditable={get:I.get,set:function(e,t,n){I.set(e,""===t?!1:t,n)}},b.each(["width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}})})),b.support.hrefNormalized||(b.each(["href","src","width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return null==r?t:r}})}),b.each(["href","src"],function(e,t){b.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}})),b.support.style||(b.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),b.support.optSelected||(b.propHooks.selected=b.extend(b.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),b.support.enctype||(b.propFix.enctype="encoding"),b.support.checkOn||b.each(["radio","checkbox"],function(){b.valHooks[this]={get:function(e){return null===e.getAttribute("value")?"on":e.value}}}),b.each(["radio","checkbox"],function(){b.valHooks[this]=b.extend(b.valHooks[this],{set:function(e,n){return b.isArray(n)?e.checked=b.inArray(b(e).val(),n)>=0:t}})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}b.event={global:{},add:function(e,n,r,o,a){var s,u,l,c,p,f,d,h,g,m,y,v=b._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=b.guid++),(u=v.events)||(u=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof b===i||e&&b.event.triggered===e.type?t:b.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(w)||[""],l=n.length;while(l--)s=rt.exec(n[l])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),p=b.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=b.event.special[g]||{},d=b.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&b.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=u[g])||(h=u[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),b.event.global[g]=!0;e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,p,f,d,h,g,m=b.hasData(e)&&b._data(e);if(m&&(c=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(s=rt.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=b.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));u&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||b.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)b.event.remove(e,d+t[l],n,r,!0);b.isEmptyObject(c)&&(delete m.handle,b._removeData(e,"events"))}},trigger:function(n,r,i,a){var s,u,l,c,p,f,d,h=[i||o],g=y.call(n,"type")?n.type:n,m=y.call(n,"namespace")?n.namespace.split("."):[];if(l=f=i=i||o,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+b.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),u=0>g.indexOf(":")&&"on"+g,n=n[b.expando]?n:new b.Event(g,"object"==typeof n&&n),n.isTrigger=!0,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:b.makeArray(r,[n]),p=b.event.special[g]||{},a||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!a&&!p.noBubble&&!b.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(l=l.parentNode);l;l=l.parentNode)h.push(l),f=l;f===(i.ownerDocument||o)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((l=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(b._data(l,"events")||{})[n.type]&&b._data(l,"handle"),s&&s.apply(l,r),s=u&&l[u],s&&b.acceptData(l)&&s.apply&&s.apply(l,r)===!1&&n.preventDefault();if(n.type=g,!(a||n.isDefaultPrevented()||p._default&&p._default.apply(i.ownerDocument,r)!==!1||"click"===g&&b.nodeName(i,"a")||!b.acceptData(i)||!u||!i[g]||b.isWindow(i))){f=i[u],f&&(i[u]=null),b.event.triggered=g;try{i[g]()}catch(v){}b.event.triggered=t,f&&(i[u]=f)}return n.result}},dispatch:function(e){e=b.event.fix(e);var n,r,i,o,a,s=[],u=h.call(arguments),l=(b._data(this,"events")||{})[e.type]||[],c=b.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=b.event.handlers.call(this,e,l),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((b.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,u),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],u=n.delegateCount,l=e.target;if(u&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(o=[],a=0;u>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?b(r,this).index(l)>=0:b.find(r,this,null,[l]).length),o[r]&&o.push(i);o.length&&s.push({elem:l,handlers:o})}return n.length>u&&s.push({elem:this,handlers:n.slice(u)}),s},fix:function(e){if(e[b.expando])return e;var t,n,r,i=e.type,a=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new b.Event(a),t=r.length;while(t--)n=r[t],e[n]=a[n];return e.target||(e.target=a.srcElement||o),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,a):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,a,s=n.button,u=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||o,a=i.documentElement,r=i.body,e.pageX=n.clientX+(a&&a.scrollLeft||r&&r.scrollLeft||0)-(a&&a.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(a&&a.scrollTop||r&&r.scrollTop||0)-(a&&a.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&u&&(e.relatedTarget=u===e.target?n.toElement:u),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},click:{trigger:function(){return b.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t}},focus:{trigger:function(){if(this!==o.activeElement&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===o.activeElement&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=b.extend(new b.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?b.event.trigger(i,null,t):b.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},b.removeEvent=o.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},b.Event=function(e,n){return this instanceof b.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&b.extend(this,n),this.timeStamp=e&&e.timeStamp||b.now(),this[b.expando]=!0,t):new b.Event(e,n)},b.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},b.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){b.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj; +return(!i||i!==r&&!b.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),b.support.submitBubbles||(b.event.special.submit={setup:function(){return b.nodeName(this,"form")?!1:(b.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=b.nodeName(n,"input")||b.nodeName(n,"button")?n.form:t;r&&!b._data(r,"submitBubbles")&&(b.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),b._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&b.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return b.nodeName(this,"form")?!1:(b.event.remove(this,"._submit"),t)}}),b.support.changeBubbles||(b.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(b.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),b.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),b.event.simulate("change",this,e,!0)})),!1):(b.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!b._data(t,"changeBubbles")&&(b.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||b.event.simulate("change",this.parentNode,e,!0)}),b._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return b.event.remove(this,"._change"),!Z.test(this.nodeName)}}),b.support.focusinBubbles||b.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){b.event.simulate(t,e.target,b.event.fix(e),!0)};b.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),b.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return b().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=b.guid++)),this.each(function(){b.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,b(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){b.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){b.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?b.event.trigger(e,n,r,!0):t}}),function(e,t){var n,r,i,o,a,s,u,l,c,p,f,d,h,g,m,y,v,x="sizzle"+-new Date,w=e.document,T={},N=0,C=0,k=it(),E=it(),S=it(),A=typeof t,j=1<<31,D=[],L=D.pop,H=D.push,q=D.slice,M=D.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},_="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=F.replace("w","w#"),B="([*^$|!~]?=)",P="\\["+_+"*("+F+")"+_+"*(?:"+B+_+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+O+")|)|)"+_+"*\\]",R=":("+F+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+P.replace(3,8)+")*)|.*)\\)|)",W=RegExp("^"+_+"+|((?:^|[^\\\\])(?:\\\\.)*)"+_+"+$","g"),$=RegExp("^"+_+"*,"+_+"*"),I=RegExp("^"+_+"*([\\x20\\t\\r\\n\\f>+~])"+_+"*"),z=RegExp(R),X=RegExp("^"+O+"$"),U={ID:RegExp("^#("+F+")"),CLASS:RegExp("^\\.("+F+")"),NAME:RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:RegExp("^("+F.replace("w","w*")+")"),ATTR:RegExp("^"+P),PSEUDO:RegExp("^"+R),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+_+"*(even|odd|(([+-]|)(\\d*)n|)"+_+"*(?:([+-]|)"+_+"*(\\d+)|))"+_+"*\\)|)","i"),needsContext:RegExp("^"+_+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+_+"*((?:-\\d)?\\d*)"+_+"*\\)|)(?=[^-]|$)","i")},V=/[\x20\t\r\n\f]*[+~]/,Y=/^[^{]+\{\s*\[native code/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,G=/^(?:input|select|textarea|button)$/i,Q=/^h\d$/i,K=/'|\\/g,Z=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,et=/\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,tt=function(e,t){var n="0x"+t-65536;return n!==n?t:0>n?String.fromCharCode(n+65536):String.fromCharCode(55296|n>>10,56320|1023&n)};try{q.call(w.documentElement.childNodes,0)[0].nodeType}catch(nt){q=function(e){var t,n=[];while(t=this[e++])n.push(t);return n}}function rt(e){return Y.test(e+"")}function it(){var e,t=[];return e=function(n,r){return t.push(n+=" ")>i.cacheLength&&delete e[t.shift()],e[n]=r}}function ot(e){return e[x]=!0,e}function at(e){var t=p.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}}function st(e,t,n,r){var i,o,a,s,u,l,f,g,m,v;if((t?t.ownerDocument||t:w)!==p&&c(t),t=t||p,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(!d&&!r){if(i=J.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&y(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return H.apply(n,q.call(t.getElementsByTagName(e),0)),n;if((a=i[3])&&T.getByClassName&&t.getElementsByClassName)return H.apply(n,q.call(t.getElementsByClassName(a),0)),n}if(T.qsa&&!h.test(e)){if(f=!0,g=x,m=t,v=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){l=ft(e),(f=t.getAttribute("id"))?g=f.replace(K,"\\$&"):t.setAttribute("id",g),g="[id='"+g+"'] ",u=l.length;while(u--)l[u]=g+dt(l[u]);m=V.test(e)&&t.parentNode||t,v=l.join(",")}if(v)try{return H.apply(n,q.call(m.querySelectorAll(v),0)),n}catch(b){}finally{f||t.removeAttribute("id")}}}return wt(e.replace(W,"$1"),t,n,r)}a=st.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},c=st.setDocument=function(e){var n=e?e.ownerDocument||e:w;return n!==p&&9===n.nodeType&&n.documentElement?(p=n,f=n.documentElement,d=a(n),T.tagNameNoComments=at(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),T.attributes=at(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return"boolean"!==t&&"string"!==t}),T.getByClassName=at(function(e){return e.innerHTML="",e.getElementsByClassName&&e.getElementsByClassName("e").length?(e.lastChild.className="e",2===e.getElementsByClassName("e").length):!1}),T.getByName=at(function(e){e.id=x+0,e.innerHTML="
    ",f.insertBefore(e,f.firstChild);var t=n.getElementsByName&&n.getElementsByName(x).length===2+n.getElementsByName(x+0).length;return T.getIdNotName=!n.getElementById(x),f.removeChild(e),t}),i.attrHandle=at(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==A&&"#"===e.firstChild.getAttribute("href")})?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},T.getIdNotName?(i.find.ID=function(e,t){if(typeof t.getElementById!==A&&!d){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){return e.getAttribute("id")===t}}):(i.find.ID=function(e,n){if(typeof n.getElementById!==A&&!d){var r=n.getElementById(e);return r?r.id===e||typeof r.getAttributeNode!==A&&r.getAttributeNode("id").value===e?[r]:t:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){var n=typeof e.getAttributeNode!==A&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=T.tagNameNoComments?function(e,n){return typeof n.getElementsByTagName!==A?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.NAME=T.getByName&&function(e,n){return typeof n.getElementsByName!==A?n.getElementsByName(name):t},i.find.CLASS=T.getByClassName&&function(e,n){return typeof n.getElementsByClassName===A||d?t:n.getElementsByClassName(e)},g=[],h=[":focus"],(T.qsa=rt(n.querySelectorAll))&&(at(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+_+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||h.push(":checked")}),at(function(e){e.innerHTML="",e.querySelectorAll("[i^='']").length&&h.push("[*^$]="+_+"*(?:\"\"|'')"),e.querySelectorAll(":enabled").length||h.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),h.push(",.*:")})),(T.matchesSelector=rt(m=f.matchesSelector||f.mozMatchesSelector||f.webkitMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&at(function(e){T.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",R)}),h=RegExp(h.join("|")),g=RegExp(g.join("|")),y=rt(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},v=f.compareDocumentPosition?function(e,t){var r;return e===t?(u=!0,0):(r=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t))?1&r||e.parentNode&&11===e.parentNode.nodeType?e===n||y(w,e)?-1:t===n||y(w,t)?1:0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return u=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:0;if(o===a)return ut(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?ut(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},u=!1,[0,0].sort(v),T.detectDuplicates=u,p):p},st.matches=function(e,t){return st(e,null,null,t)},st.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Z,"='$1']"),!(!T.matchesSelector||d||g&&g.test(t)||h.test(t)))try{var n=m.call(e,t);if(n||T.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(r){}return st(t,p,null,[e]).length>0},st.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},st.attr=function(e,t){var n;return(e.ownerDocument||e)!==p&&c(e),d||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):d||T.attributes?e.getAttribute(t):((n=e.getAttributeNode(t))||e.getAttribute(t))&&e[t]===!0?t:n&&n.specified?n.value:null},st.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},st.uniqueSort=function(e){var t,n=[],r=1,i=0;if(u=!T.detectDuplicates,e.sort(v),u){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e};function ut(e,t){var n=t&&e,r=n&&(~t.sourceIndex||j)-(~e.sourceIndex||j);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function lt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ct(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function pt(e){return ot(function(t){return t=+t,ot(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}o=st.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=st.selectors={cacheLength:50,createPseudo:ot,match:U,find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(et,tt),e[3]=(e[4]||e[5]||"").replace(et,tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||st.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&st.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return U.CHILD.test(e[0])?null:(e[4]?e[2]=e[4]:n&&z.test(n)&&(t=ft(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){return"*"===e?function(){return!0}:(e=e.replace(et,tt).toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[e+" "];return t||(t=RegExp("(^|"+_+")"+e+"("+_+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==A&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=st.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!u&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[x]||(m[x]={}),l=c[e]||[],d=l[0]===N&&l[1],f=l[0]===N&&l[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[N,d,f];break}}else if(v&&(l=(t[x]||(t[x]={}))[e])&&l[0]===N)f=l[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[x]||(p[x]={}))[e]=[N,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||st.error("unsupported pseudo: "+e);return r[x]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?ot(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=M.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ot(function(e){var t=[],n=[],r=s(e.replace(W,"$1"));return r[x]?ot(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ot(function(e){return function(t){return st(e,t).length>0}}),contains:ot(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:ot(function(e){return X.test(e||"")||st.error("unsupported lang: "+e),e=e.replace(et,tt).toLowerCase(),function(t){var n;do if(n=d?t.getAttribute("xml:lang")||t.getAttribute("lang"):t.lang)return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return Q.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:pt(function(){return[0]}),last:pt(function(e,t){return[t-1]}),eq:pt(function(e,t,n){return[0>n?n+t:n]}),even:pt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:pt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:pt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:pt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[n]=lt(n);for(n in{submit:!0,reset:!0})i.pseudos[n]=ct(n);function ft(e,t){var n,r,o,a,s,u,l,c=E[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=i.preFilter;while(s){(!n||(r=$.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),u.push(o=[])),n=!1,(r=I.exec(s))&&(n=r.shift(),o.push({value:n,type:r[0].replace(W," ")}),s=s.slice(n.length));for(a in i.filter)!(r=U[a].exec(s))||l[a]&&!(r=l[a](r))||(n=r.shift(),o.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?st.error(e):E(e,u).slice(0)}function dt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function ht(e,t,n){var i=t.dir,o=n&&"parentNode"===i,a=C++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,s){var u,l,c,p=N+" "+a;if(s){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[x]||(t[x]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,s)||r,l[1]===!0)return!0}}function gt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function mt(e,t,n,r,i){var o,a=[],s=0,u=e.length,l=null!=t;for(;u>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),l&&t.push(s));return a}function yt(e,t,n,r,i,o){return r&&!r[x]&&(r=yt(r)),i&&!i[x]&&(i=yt(i,o)),ot(function(o,a,s,u){var l,c,p,f=[],d=[],h=a.length,g=o||xt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:mt(g,f,e,s,u),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,u),r){l=mt(y,d),r(l,[],s,u),c=l.length;while(c--)(p=l[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?M.call(o,p):f[c])>-1&&(o[l]=!(a[l]=p))}}else y=mt(y===a?y.splice(h,y.length):y),i?i(null,a,y,u):H.apply(a,y)})}function vt(e){var t,n,r,o=e.length,a=i.relative[e[0].type],s=a||i.relative[" "],u=a?1:0,c=ht(function(e){return e===t},s,!0),p=ht(function(e){return M.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>u;u++)if(n=i.relative[e[u].type])f=[ht(gt(f),n)];else{if(n=i.filter[e[u].type].apply(null,e[u].matches),n[x]){for(r=++u;o>r;r++)if(i.relative[e[r].type])break;return yt(u>1&>(f),u>1&&dt(e.slice(0,u-1)).replace(W,"$1"),n,r>u&&vt(e.slice(u,r)),o>r&&vt(e=e.slice(r)),o>r&&dt(e))}f.push(n)}return gt(f)}function bt(e,t){var n=0,o=t.length>0,a=e.length>0,s=function(s,u,c,f,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,T=l,C=s||a&&i.find.TAG("*",d&&u.parentNode||u),k=N+=null==T?1:Math.random()||.1;for(w&&(l=u!==p&&u,r=n);null!=(h=C[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,u,c)){f.push(h);break}w&&(N=k,r=++n)}o&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,o&&b!==v){g=0;while(m=t[g++])m(x,y,u,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=L.call(f));y=mt(y)}H.apply(f,y),w&&!s&&y.length>0&&v+t.length>1&&st.uniqueSort(f)}return w&&(N=k,l=T),x};return o?ot(s):s}s=st.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=ft(e)),n=t.length;while(n--)o=vt(t[n]),o[x]?r.push(o):i.push(o);o=S(e,bt(i,r))}return o};function xt(e,t,n){var r=0,i=t.length;for(;i>r;r++)st(e,t[r],n);return n}function wt(e,t,n,r){var o,a,u,l,c,p=ft(e);if(!r&&1===p.length){if(a=p[0]=p[0].slice(0),a.length>2&&"ID"===(u=a[0]).type&&9===t.nodeType&&!d&&i.relative[a[1].type]){if(t=i.find.ID(u.matches[0].replace(et,tt),t)[0],!t)return n;e=e.slice(a.shift().value.length)}o=U.needsContext.test(e)?0:a.length;while(o--){if(u=a[o],i.relative[l=u.type])break;if((c=i.find[l])&&(r=c(u.matches[0].replace(et,tt),V.test(a[0].type)&&t.parentNode||t))){if(a.splice(o,1),e=r.length&&dt(a),!e)return H.apply(n,q.call(r,0)),n;break}}}return s(e,p)(r,t,d,n,V.test(e)),n}i.pseudos.nth=i.pseudos.eq;function Tt(){}i.filters=Tt.prototype=i.pseudos,i.setFilters=new Tt,c(),st.attr=b.attr,b.find=st,b.expr=st.selectors,b.expr[":"]=b.expr.pseudos,b.unique=st.uniqueSort,b.text=st.getText,b.isXMLDoc=st.isXML,b.contains=st.contains}(e);var at=/Until$/,st=/^(?:parents|prev(?:Until|All))/,ut=/^.[^:#\[\.,]*$/,lt=b.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};b.fn.extend({find:function(e){var t,n,r,i=this.length;if("string"!=typeof e)return r=this,this.pushStack(b(e).filter(function(){for(t=0;i>t;t++)if(b.contains(r[t],this))return!0}));for(n=[],t=0;i>t;t++)b.find(e,this[t],n);return n=this.pushStack(i>1?b.unique(n):n),n.selector=(this.selector?this.selector+" ":"")+e,n},has:function(e){var t,n=b(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(b.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1))},filter:function(e){return this.pushStack(ft(this,e,!0))},is:function(e){return!!e&&("string"==typeof e?lt.test(e)?b(e,this.context).index(this[0])>=0:b.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,o=[],a=lt.test(e)||"string"!=typeof e?b(e,t||this.context):0;for(;i>r;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&11!==n.nodeType){if(a?a.index(n)>-1:b.find.matchesSelector(n,e)){o.push(n);break}n=n.parentNode}}return this.pushStack(o.length>1?b.unique(o):o)},index:function(e){return e?"string"==typeof e?b.inArray(this[0],b(e)):b.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?b(e,t):b.makeArray(e&&e.nodeType?[e]:e),r=b.merge(this.get(),n);return this.pushStack(b.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),b.fn.andSelf=b.fn.addBack;function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}b.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return b.dir(e,"parentNode")},parentsUntil:function(e,t,n){return b.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return b.dir(e,"nextSibling")},prevAll:function(e){return b.dir(e,"previousSibling")},nextUntil:function(e,t,n){return b.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return b.dir(e,"previousSibling",n)},siblings:function(e){return b.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return b.sibling(e.firstChild)},contents:function(e){return b.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:b.merge([],e.childNodes)}},function(e,t){b.fn[e]=function(n,r){var i=b.map(this,t,n);return at.test(e)||(r=n),r&&"string"==typeof r&&(i=b.filter(r,i)),i=this.length>1&&!ct[e]?b.unique(i):i,this.length>1&&st.test(e)&&(i=i.reverse()),this.pushStack(i)}}),b.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),1===t.length?b.find.matchesSelector(t[0],e)?[t[0]]:[]:b.find.matches(e,t)},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!b(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(t=t||0,b.isFunction(t))return b.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return b.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=b.grep(e,function(e){return 1===e.nodeType});if(ut.test(t))return b.filter(t,r,!n);t=b.filter(t,r)}return b.grep(e,function(e){return b.inArray(e,t)>=0===n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:b.support.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},jt=dt(o),Dt=jt.appendChild(o.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,b.fn.extend({text:function(e){return b.access(this,function(e){return e===t?b.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(b.isFunction(e))return this.each(function(t){b(this).wrapAll(e.call(this,t))});if(this[0]){var t=b(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return b.isFunction(e)?this.each(function(t){b(this).wrapInner(e.call(this,t))}):this.each(function(){var t=b(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=b.isFunction(e);return this.each(function(n){b(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){b.nodeName(this,"body")||b(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.insertBefore(e,this.firstChild)})},before:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=0;for(;null!=(n=this[r]);r++)(!e||b.filter(e,[n]).length>0)&&(t||1!==n.nodeType||b.cleanData(Ot(n)),n.parentNode&&(t&&b.contains(n.ownerDocument,n)&&Mt(Ot(n,"script")),n.parentNode.removeChild(n)));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&b.cleanData(Ot(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&b.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return b.clone(this,e,t)})},html:function(e){return b.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!b.support.htmlSerialize&&mt.test(e)||!b.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(b.cleanData(Ot(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){var t=b.isFunction(e);return t||"string"==typeof e||(e=b(e).not(this).detach()),this.domManip([e],!0,function(e){var t=this.nextSibling,n=this.parentNode;n&&(b(this).remove(),n.insertBefore(e,t))})},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=f.apply([],e);var i,o,a,s,u,l,c=0,p=this.length,d=this,h=p-1,g=e[0],m=b.isFunction(g);if(m||!(1>=p||"string"!=typeof g||b.support.checkClone)&&Ct.test(g))return this.each(function(i){var o=d.eq(i);m&&(e[0]=g.call(this,i,n?o.html():t)),o.domManip(e,n,r)});if(p&&(l=b.buildFragment(e,this[0].ownerDocument,!1,this),i=l.firstChild,1===l.childNodes.length&&(l=i),i)){for(n=n&&b.nodeName(i,"tr"),s=b.map(Ot(l,"script"),Ht),a=s.length;p>c;c++)o=l,c!==h&&(o=b.clone(o,!0,!0),a&&b.merge(s,Ot(o,"script"))),r.call(n&&b.nodeName(this[c],"table")?Lt(this[c],"tbody"):this[c],o,c);if(a)for(u=s[s.length-1].ownerDocument,b.map(s,qt),c=0;a>c;c++)o=s[c],kt.test(o.type||"")&&!b._data(o,"globalEval")&&b.contains(u,o)&&(o.src?b.ajax({url:o.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):b.globalEval((o.text||o.textContent||o.innerHTML||"").replace(St,"")));l=i=null}return this}});function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function Ht(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Mt(e,t){var n,r=0;for(;null!=(n=e[r]);r++)b._data(n,"globalEval",!t||b._data(t[r],"globalEval"))}function _t(e,t){if(1===t.nodeType&&b.hasData(e)){var n,r,i,o=b._data(e),a=b._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)b.event.add(t,n,s[n][r])}a.data&&(a.data=b.extend({},a.data))}}function Ft(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!b.support.noCloneEvent&&t[b.expando]){i=b._data(t);for(r in i.events)b.removeEvent(t,r,i.handle);t.removeAttribute(b.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),b.support.html5Clone&&e.innerHTML&&!b.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Nt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}b.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){b.fn[e]=function(e){var n,r=0,i=[],o=b(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),b(o[r])[t](n),d.apply(i,n.get());return this.pushStack(i)}});function Ot(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||b.nodeName(o,n)?s.push(o):b.merge(s,Ot(o,n));return n===t||n&&b.nodeName(e,n)?b.merge([e],s):s}function Bt(e){Nt.test(e.type)&&(e.defaultChecked=e.checked)}b.extend({clone:function(e,t,n){var r,i,o,a,s,u=b.contains(e.ownerDocument,e);if(b.support.html5Clone||b.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(b.support.noCloneEvent&&b.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||b.isXMLDoc(e)))for(r=Ot(o),s=Ot(e),a=0;null!=(i=s[a]);++a)r[a]&&Ft(i,r[a]);if(t)if(n)for(s=s||Ot(e),r=r||Ot(o),a=0;null!=(i=s[a]);a++)_t(i,r[a]);else _t(e,o);return r=Ot(o,"script"),r.length>0&&Mt(r,!u&&Ot(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,u,l,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===b.type(o))b.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),u=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[u]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!b.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!b.support.tbody){o="table"!==u||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)b.nodeName(l=o.childNodes[i],"tbody")&&!l.childNodes.length&&o.removeChild(l) +}b.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),b.support.appendChecked||b.grep(Ot(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===b.inArray(o,r))&&(a=b.contains(o.ownerDocument,o),s=Ot(f.appendChild(o),"script"),a&&Mt(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,u=b.expando,l=b.cache,p=b.support.deleteExpando,f=b.event.special;for(;null!=(n=e[s]);s++)if((t||b.acceptData(n))&&(o=n[u],a=o&&l[o])){if(a.events)for(r in a.events)f[r]?b.event.remove(n,r):b.removeEvent(n,r,a.handle);l[o]&&(delete l[o],p?delete n[u]:typeof n.removeAttribute!==i?n.removeAttribute(u):n[u]=null,c.push(o))}}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+x+")(.*)$","i"),Yt=RegExp("^("+x+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+x+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===b.css(e,"display")||!b.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=b._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=b._data(r,"olddisplay",un(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&b._data(r,"olddisplay",i?n:b.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}b.fn.extend({css:function(e,n){return b.access(this,function(e,n,r){var i,o,a={},s=0;if(b.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=b.css(e,n[s],!1,o);return a}return r!==t?b.style(e,n,r):b.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?b(this).show():b(this).hide()})}}),b.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":b.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,u=b.camelCase(n),l=e.style;if(n=b.cssProps[u]||(b.cssProps[u]=tn(l,u)),s=b.cssHooks[n]||b.cssHooks[u],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:l[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(b.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||b.cssNumber[u]||(r+="px"),b.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(l[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{l[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,u=b.camelCase(n);return n=b.cssProps[u]||(b.cssProps[u]=tn(e.style,u)),s=b.cssHooks[n]||b.cssHooks[u],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||b.isNumeric(o)?o||0:a):a},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s.getPropertyValue(n)||s[n]:t,l=e.style;return s&&(""!==u||b.contains(e.ownerDocument,e)||(u=b.style(e,n)),Yt.test(u)&&Ut.test(n)&&(i=l.width,o=l.minWidth,a=l.maxWidth,l.minWidth=l.maxWidth=l.width=u,u=s.width,l.width=i,l.minWidth=o,l.maxWidth=a)),u}):o.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s[n]:t,l=e.style;return null==u&&l&&l[n]&&(u=l[n]),Yt.test(u)&&!zt.test(n)&&(i=l.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),l.left="fontSize"===n?"1em":u,u=l.pixelLeft+"px",l.left=i,a&&(o.left=a)),""===u?"auto":u});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=b.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=b.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=b.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=b.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=b.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=b.support.boxSizing&&"border-box"===b.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(b.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function un(e){var t=o,n=Gt[e];return n||(n=ln(e,t),"none"!==n&&n||(Pt=(Pt||b("